Reader
app.infrastructure.video.readerPyAV-based video reader with GPU acceleration, rotation extraction, and frame seeking.
See Also
VideoMetadata— describes the opened video streamVideoDecodeWorker— reads frames asynchronously
Layer: Infrastructure. Context: Implements concrete processing and I/O behind application-facing contracts.
ZeroStreamsInVideoException
Bases: VideoStreamStateException
Signal the zero streams in video error condition.
| PARAMETER | DESCRIPTION |
|---|---|
msg
|
Error message to include in the exception.
TYPE:
|
path
|
Filesystem path of the video or resource.
TYPE:
|
__init__(msg: str, path: str) -> None
VideoReader
Opens and decodes video files using PyAV (ffmpeg wrapper) with GPU acceleration and rotation support.
| ATTRIBUTE | DESCRIPTION |
|---|---|
_path |
Video file path.
TYPE:
|
_container |
PyAV container handle.
TYPE:
|
_stream |
First video stream extracted from the container.
TYPE:
|
_current_index |
Current sequential frame position.
TYPE:
|
_frame_iter |
Iterator over decoded frames from the current stream.
|
_base_rotation |
Rotation degrees extracted from video metadata.
TYPE:
|
_manual_rotation |
User-applied rotation from the UI.
TYPE:
|
_w |
Final width after total rotation is applied.
TYPE:
|
_h |
Final height after total rotation is applied.
TYPE:
|
Note
Rotation extraction fallback order:
1. stream.metadata.get("rotate").
2. stream.side_data[...].rotation.
3. Android MP4 fallback via _get_mp4_rotation().
Frame seeking modes:
- Sequential path: buffer-friendly for playback or batch processing.
- Hard seek path: fast jump to an arbitrary frame via FFmpeg seek.
- Post-seek anchor: single-use frame timestamp recovery after a seek.
- Initial sequential read: a newly opened reader starts at zero, so the
first read_next_frame() returns actual_index == 0.
Rotation handling:
- Metadata rotation is extracted when the reader opens.
- Additional rotation can be applied through manual_rotation.
- Final dimensions use (base_rot + manual_rot) % 360 and swap width and
height for 90 or 270 degrees.
- Each frame is rotated with np.rot90() and normalized with
ascontiguousarray().
VideoReader is not thread-safe. Use a separate instance per decoding thread.
Example
reader = VideoReader("/path/to/video.mp4")
frame = reader.read_frame_at_index(100)
idx, frame = reader.read_next_frame()
metadata = reader.metadata
reader.close()
| PARAMETER | DESCRIPTION |
|---|---|
path
|
Filesystem path of the video or resource.
TYPE:
|
basename
property
Return filename only (no directory path).
container: InputContainer
deletable
property
writable
Get PyAV InputContainer (video file handle).
| RETURNS | DESCRIPTION |
|---|---|
InputContainer
|
Open PyAV container associated with this reader. |
| RAISES | DESCRIPTION |
|---|---|
VideoStreamStateException
|
If no container has been assigned. |
fps: float
property
Frames per second (playback speed).
| RETURNS | DESCRIPTION |
|---|---|
float
|
Average frame rate of the selected video stream. |
| RAISES | DESCRIPTION |
|---|---|
VideoStreamStateException
|
If the stream has no average frame rate. |
frame_count: int
property
Total frames in video (or 0 if unknown).
| RETURNS | DESCRIPTION |
|---|---|
int
|
Frame count reported by the selected stream, or |
height: int
property
Final height after rotation applied (may be swapped with width).
| RETURNS | DESCRIPTION |
|---|---|
int
|
Decoded frame height after base and manual rotations are applied. |
manual_rotation: int
property
writable
Return the user-applied rotation in degrees, normalized to [0, 360).
| RETURNS | DESCRIPTION |
|---|---|
int
|
Manual rotation offset currently applied to decoded frames. |
metadata: VideoMetadata
property
Return VideoMetadata snapshot (static; immutable after creation).
| RETURNS | DESCRIPTION |
|---|---|
VideoMetadata
|
Snapshot containing the source path, rotated dimensions, frame rate, |
VideoMetadata
|
and stream frame count. |
path: str
property
Return full file path.
| RETURNS | DESCRIPTION |
|---|---|
str
|
Source path supplied when this reader was created. |
path_file_name: str
property
Return filename only (split on /).
| RETURNS | DESCRIPTION |
|---|---|
str
|
Final slash-separated component of the source path. |
stream: VideoStream
deletable
property
writable
Get PyAV VideoStream (first video track).
| RETURNS | DESCRIPTION |
|---|---|
VideoStream
|
Selected PyAV video stream. |
| RAISES | DESCRIPTION |
|---|---|
VideoStreamStateException
|
If no video stream has been assigned. |
width: int
property
Final width after rotation applied (may be swapped with height).
| RETURNS | DESCRIPTION |
|---|---|
int
|
Decoded frame width after base and manual rotations are applied. |
__init__(path: str) -> None
close() -> None
Close and release video file handle.
open() -> None
Open _path via av.open() with GPU acceleration (hwaccel=auto).
read_frame_at_index(frame_index: int) -> RGBFrame | None
Read frame at frame_index (0-based), using fast or hard seek path.
Note
Three-path logic:
- Path 1: sequential buffer iteration when the target is within ±60 frames.
- Path 2: hard seek to PTS, then anchor on the first decoded frame timestamp.
- Fallback: return None if the frame cannot be reached.
A request for index zero decodes and returns the first frame, which
is also the frame used by the first export iteration.
Implementation details:
- _current_index tracks a pure sequential count and ignores variable
frame rate timestamps.
- After a seek, timestamp data is used once to re-anchor
_current_index.
- Subsequent frames switch back to sequential counting.
- Permission errors trigger a close, reopen, and retry path.
| RETURNS | DESCRIPTION |
|---|---|
RGBFrame | None
|
RGBFrame | None: The requested RGB frame, or |
read_next_frame() -> tuple[int, RGBFrame]
Read next frame sequentially, return (frame_index, rgb_frame).
Note
Increments _current_index by 1, uses pure sequential counting, and
returns the current index before incrementing. Since open() resets
the counter to zero, the first call returns frame index zero.
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the stream ends and |