Metadata-Version: 2.4
Name: custom-ffmpeg-v
Version: 0.1.2
Summary: A custom FFmpeg and FFprobe wrapper for precise frame extraction and video metadata
Author-email: kishan0822 <kishan@sportzengage.ai>
Project-URL: Homepage, https://github.com/example/custom-ffmpeg
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.21.0
Requires-Dist: opencv-python>=4.5.0
Requires-Dist: imageio-ffmpeg>=0.5.1

# Custom FFmpeg

A pure FFmpeg/FFprobe wrapper for precise frame extraction and video metadata.
Uses the **same system-level FFmpeg** as the ball detection pipeline — installed via `apt-get install ffmpeg` in Docker or your local system PATH.

## Features

| Feature | Description |
|---|---|
| **VFR support** | Per-frame PTS timestamps via `ffprobe` — accurate kinematic Δt for mobile VFR video |
| **LazyFrameLoader** | Extracts all frames to PNG on disk, returns file paths for random access |
| **StreamingFrameLoader** | Pipes raw BGR frames from FFmpeg stdout — zero disk I/O, ideal for YOLO inference |
| **`as_frame_generator()`** | Single-pass generator on disk PNGs yielding `(idx, timestamp, ndarray)` |
| **Reliable frame count** | `nb_frames` header → packet-count fallback for VFR/MKV/MOV containers |
| **Same FFmpeg as the pipeline** | Uses system FFmpeg (`apt-get install ffmpeg`) — identical binary to the ball detection Docker image |

---

## Prerequisites

**None.** FFmpeg and FFprobe are automatically downloaded and managed by [`imageio-ffmpeg`](https://github.com/imageio/imageio-ffmpeg), which ships pre-compiled static binaries for:

| Platform | Architecture |
|---|---|
| Linux | x86_64, arm64 |
| macOS | x86_64, arm64 (Apple Silicon) |
| Windows | x86_64 |

The correct binary for your OS is selected automatically at runtime. No `apt-get`, `brew`, or manual PATH configuration required.

---

## Installation

### From a local path

```bash
# Standard
pip install /path/to/scripts/custom_ffmpeg

# Editable (recommended while developing)
pip install -e /path/to/scripts/custom_ffmpeg
```

### From a Git repository

```bash
pip install git+https://github.com/yourusername/your-repo.git#subdirectory=scripts/custom_ffmpeg
```

---

## Loader Selection Guide

| Use case | Recommended loader |
|---|---|
| YOLO / single-pass inference | **`StreamingFrameLoader`** — zero disk I/O |
| Overlay rendering, random frame access | **`LazyFrameLoader`** — PNG on disk, index by `[i]` |
| Single-pass with disk PNGs already extracted | **`LazyFrameLoader.as_frame_generator()`** |

---

## Usage

### 1. `StreamingFrameLoader` — Zero-disk streaming for YOLO inference

Pipes raw `BGR24` frames directly from FFmpeg stdout as numpy arrays. No PNGs are written to disk.
Timestamps are pre-fetched via ffprobe before streaming begins so `loader.timestamps[i]` stays
in sync with YOLO result index `i`.

```python
from custom_ffmpeg import StreamingFrameLoader

loader = StreamingFrameLoader("sample_video.mp4")

print(f"FPS: {loader.fps}")
print(f"Resolution: {loader.width}x{loader.height}")
print(f"Total timestamps: {len(loader.timestamps)}")

# ── Use directly with YOLO ──────────────────────────────────────────────────
# from ultralytics import YOLO
# model = YOLO("yolov8n-pose.pt")
# results = model.track(source=loader.as_numpy_generator(), stream=True)
# for i, result in enumerate(results):
#     ts = loader.timestamps[i]   # exact PTS in seconds
#     dt = loader.get_dt(i)       # true Δt for kinematics

# ── Manual iteration (index + timestamp + ndarray) ─────────────────────────
for frame_idx, timestamp, frame in loader:
    print(f"Frame {frame_idx} @ {timestamp:.4f}s — shape: {frame.shape}")
```

---

### 2. `LazyFrameLoader` — Disk-backed random access

Extracts all frames to PNG files up front. `__getitem__` returns the absolute file path. Use
`as_frame_generator()` for a single-pass iteration that also returns numpy arrays.

```python
from custom_ffmpeg import LazyFrameLoader

loader = LazyFrameLoader("sample_video.mp4", output_dir="/tmp/my_frames")

print(f"Total frames: {len(loader)}")
print(f"FPS: {loader.fps}")

# ── Random access by index → returns file path ─────────────────────────────
frame_path = loader[0]
print(f"First frame: {frame_path}")

# Load with any library you choose
# import cv2; frame = cv2.imread(frame_path)
# from PIL import Image; frame = Image.open(frame_path)

# ── VFR-accurate time delta ─────────────────────────────────────────────────
dt = loader.get_dt(1)               # Δt between frame 0 and frame 1
ts = loader.get_timestamp(10)       # absolute PTS of frame 10 in seconds

# ── Single-pass YOLO integration ────────────────────────────────────────────
# results = model.track(
#     source=(frame for _, _, frame in loader.as_frame_generator()),
#     stream=True,
# )
# loader.timestamps[i] aligns exactly with YOLO result index i

# ── Cleanup temp directory when done ───────────────────────────────────────
loader.release()
```

---

### 3. `VideoUtils` — Metadata without loading frames

Uses pure FFprobe. `get_video_stats()` automatically falls back to a packet-count pass when
`nb_frames` is missing or zero (common on VFR/MOV/MKV containers).

```python
from custom_ffmpeg import VideoUtils

stats = VideoUtils.get_video_stats("sample_video.mp4")
# Returns: { rotation, width, height, fps, frame_count }

if stats:
    print(f"Resolution : {stats['width']}x{stats['height']}")
    print(f"FPS        : {stats['fps']}")
    print(f"Frame count: {stats['frame_count']}")   # always accurate, even on VFR
    print(f"Rotation   : {stats['rotation']}°")

# Rotation only
rotation = VideoUtils.get_video_rotation("sample_video.mp4")
# Priority: Display Matrix → stream_tags.rotate → w>h portrait fallback
```

---

## API Reference

### `StreamingFrameLoader(video_path, rotation=0)`

| Member | Type | Description |
|---|---|---|
| `fps` / `avg_fps` | `float` | Nominal container FPS (display only) |
| `width`, `height` | `int` | Frame dimensions after rotation |
| `timestamps` | `list[float]` | Per-frame PTS in seconds (ffprobe) |
| `__iter__()` | yields `(int, float, ndarray)` | `(frame_idx, timestamp_sec, BGR array)` |
| `as_numpy_generator()` | yields `ndarray` | Frame arrays only — pass directly to YOLO |
| `get_timestamp(idx)` | `float` | PTS in seconds for frame `idx` |
| `get_dt(idx)` | `float` | True Δt between frame `idx` and `idx-1` |

### `LazyFrameLoader(video_path, rotation=0, output_dir=None)`

| Member | Type | Description |
|---|---|---|
| `fps` / `avg_fps` | `float` | Nominal container FPS (display only) |
| `width`, `height` | `int` | Frame dimensions |
| `timestamps` | `list[float]` | Per-frame PTS in seconds (ffprobe) |
| `__getitem__(idx)` | `str` | Absolute path to frame PNG |
| `__len__()` | `int` | Total frames extracted |
| `as_frame_generator()` | yields `(int, float, ndarray)` | On-demand disk reads (requires `cv2`) |
| `get_timestamp(idx)` | `float` | PTS in seconds for frame `idx` |
| `get_dt(idx)` | `float` | True Δt between frame `idx` and `idx-1` |
| `release()` | — | Deletes the temporary frames directory |

### `VideoUtils`

| Method | Returns | Description |
|---|---|---|
| `get_video_stats(path)` | `dict \| None` | `rotation, width, height, fps, frame_count` |
| `get_video_rotation(path)` | `int` | Rotation angle in degrees (`0/90/180/270`) |

---

## Publishing to PyPI

1. **Install build tools:**
   ```bash
   pip install build twine
   ```

2. **Build the package:**
   ```bash
   python -m build
   ```
   Creates `dist/custom_ffmpeg-x.y.z-py3-none-any.whl` and `.tar.gz`.

3. **Test locally before uploading:**
   ```bash
   # Validate metadata and README render
   twine check dist/*

   # Install in a clean environment
   python -m venv test_env && source test_env/bin/activate
   pip install dist/custom_ffmpeg-*.whl
   python -c "import custom_ffmpeg; print('OK')"
   deactivate && rm -rf test_env
   ```

4. **Upload to PyPI:**
   ```bash
   python -m twine upload dist/*
   # Username: __token__   Password: pypi-... (your API token)
   ```
   *(Use `--repository testpypi` to do a dry run on TestPyPI first.)*


- **Lazy Frame Loader**: Exact 1-to-1 mapping of encoded video frames to extracted images.
- **Video Utilities**: Extract rotation, dimensions, and FPS relying purely on FFmpeg/FFprobe.
- **Zero third-party dependencies**: Does not require `opencv-python` or other heavy image processing libraries.

## Prerequisites

This library requires FFmpeg and FFprobe to be installed on your system and available in your system's PATH. 
You can verify your installation by running:
```bash
ffmpeg -version
ffprobe -version
```

## Installation

You can install this module directly into any project using `pip`. 

### Install Locally
If you have cloned or downloaded this repository, navigate to your new project's environment and install it using the path to the `custom_ffmpeg` directory:

```bash
# Standard installation
pip install /path/to/custom_ffmpeg

# Editable installation (useful if you are actively developing the custom_ffmpeg module)
pip install -e /path/to/custom_ffmpeg
```

### Install via Git
If this module is hosted in a git repository, you can install it directly via Git:
```bash
pip install git+https://github.com/yourusername/your-repo.git#subdirectory=scripts/custom_ffmpeg
```

## Usage

### 1. Extracting Frames (LazyFrameLoader)

The `LazyFrameLoader` will use FFmpeg to extract frames into a temporary folder. Since there are no OpenCV dependencies, accessing an index will return the **absolute file path** to the extracted PNG frame.

```python
from custom_ffmpeg import LazyFrameLoader

# Initialize the loader (this will automatically run ffmpeg to extract frames)
video_path = "sample_video.mp4"
loader = LazyFrameLoader(video_path)

print(f"Total frames extracted: {len(loader)}")
print(f"Video FPS: {loader.fps}")

# Access the first frame
frame_path = loader[0]
print(f"Path to first frame: {frame_path}")

# Calculate true time delta (dt) between frames for accurate kinematics
# This is especially important for VFR (Variable Frame Rate) videos.
dt = loader.get_dt(1)
print(f"Time delta between frame 0 and 1: {dt} seconds")

# Cleanup the temporary extracted frames directory when done
loader.release()
```

### 2. Getting Video Metadata (VideoUtils)

`VideoUtils` uses FFprobe to extract accurate metadata from the video without loading any frames.

```python
from custom_ffmpeg import VideoUtils

video_path = "sample_video.mp4"
stats = VideoUtils.get_video_stats(video_path)

if stats:
    print(f"Resolution: {stats['width']}x{stats['height']}")
    print(f"FPS: {stats['fps']}")
    print(f"Total Frames: {stats['frame_count']}")
    print(f"Rotation: {stats['rotation']} degrees")
```

## Publishing to PyPI

If you want to publish this package to the public Python Package Index (PyPI) or a private repository, follow these steps:

1. **Install Build Tools:**
   Ensure you have `build` and `twine` installed in your environment.
   ```bash
   pip install build twine
   ```

2. **Build the Package:**
   Run the following command from the same directory as `pyproject.toml`. It will create a `dist/` folder containing a `.tar.gz` source archive and a `.whl` built distribution.
   ```bash
   python -m build
   ```

3. **Test the Build Locally (Optional but Recommended):**
   Before uploading, it is a good idea to verify the built package locally.
   
   First, check if the package description will render correctly on PyPI:
   ```bash
   twine check dist/*
   ```
   
   Next, you can test installing the compiled `.whl` file in a fresh virtual environment to ensure it works exactly as the end-user will experience it:
   ```bash
   # Create a test virtual environment
   python -m venv test_env
   source test_env/bin/activate
   
   # Install the built wheel file
   pip install dist/custom_ffmpeg-0.1.0-py3-none-any.whl
   
   # Verify the import works
   python -c "import custom_ffmpeg; print('Success!')"
   
   # Clean up and exit
   deactivate
   rm -rf test_env
   ```

4. **Upload to PyPI:**
   Use twine to upload the generated archives. You will be prompted for your PyPI API token (username is `__token__`).
   ```bash
   python -m twine upload dist/*
   ```
   *(To test the upload safely without publishing to the real PyPI, use TestPyPI: `python -m twine upload --repository testpypi dist/*`)*
