A custom FFmpeg and FFprobe wrapper for precise frame extraction and video metadata
Project description
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, 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
# Standard
pip install /path/to/scripts/custom_ffmpeg
# Editable (recommended while developing)
pip install -e /path/to/scripts/custom_ffmpeg
From a Git repository
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.
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.
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).
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
-
Install build tools:
pip install build twine
-
Build the package:
python -m build
Creates
dist/custom_ffmpeg-x.y.z-py3-none-any.whland.tar.gz. -
Test locally before uploading:
# 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
-
Upload to PyPI:
python -m twine upload dist/* # Username: __token__ Password: pypi-... (your API token)
(Use
--repository testpypito 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-pythonor 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:
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:
# 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:
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.
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.
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:
-
Install Build Tools: Ensure you have
buildandtwineinstalled in your environment.pip install build twine
-
Build the Package: Run the following command from the same directory as
pyproject.toml. It will create adist/folder containing a.tar.gzsource archive and a.whlbuilt distribution.python -m build
-
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:
twine check dist/*
Next, you can test installing the compiled
.whlfile in a fresh virtual environment to ensure it works exactly as the end-user will experience it:# 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
-
Upload to PyPI: Use twine to upload the generated archives. You will be prompted for your PyPI API token (username is
__token__).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/*)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file custom_ffmpeg_v-0.1.2.tar.gz.
File metadata
- Download URL: custom_ffmpeg_v-0.1.2.tar.gz
- Upload date:
- Size: 16.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6c8d1c6faabfc0cb4061103f542e75d3e9c011584bd984051c903745ab894f9
|
|
| MD5 |
4de78600f407b2b59d441664f72cfea7
|
|
| BLAKE2b-256 |
3562754e92771f55230c2f61a52ee165c409554b769feea4b148360425dee01f
|
File details
Details for the file custom_ffmpeg_v-0.1.2-py3-none-any.whl.
File metadata
- Download URL: custom_ffmpeg_v-0.1.2-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66c6ff4d254f1f2d233a995751727bdbb333ee557775e4914bb117ee18a1a56d
|
|
| MD5 |
b3073d1cafecade57464445a4e1fcb5d
|
|
| BLAKE2b-256 |
2cc5ca4d4e944740f45c634c7d4b47287d51e50cb83b214ac47bf7ba9ecf77b0
|