simple-video-utils 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple_video_utils/__init__.py +1 -0
- simple_video_utils/frames.py +95 -0
- simple_video_utils/metadata.py +60 -0
- simple_video_utils-0.0.1.dist-info/METADATA +93 -0
- simple_video_utils-0.0.1.dist-info/RECORD +8 -0
- simple_video_utils-0.0.1.dist-info/WHEEL +5 -0
- simple_video_utils-0.0.1.dist-info/licenses/LICENSE +21 -0
- simple_video_utils-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Simple video utilities for frame extraction and metadata."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from collections.abc import Generator
|
|
3
|
+
from typing import BinaryIO
|
|
4
|
+
|
|
5
|
+
import av
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from simple_video_utils.metadata import VideoMetadata, _open_container, video_metadata_from_bytes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _generate_frames(
|
|
12
|
+
container: av.container.InputContainer,
|
|
13
|
+
start_frame: int = 0,
|
|
14
|
+
end_frame: int | None = None,
|
|
15
|
+
) -> Generator[np.ndarray, None, None]:
|
|
16
|
+
"""
|
|
17
|
+
Generate RGB frames from a container.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
container: Open PyAV container.
|
|
21
|
+
start_frame: First frame index to yield (0-based).
|
|
22
|
+
end_frame: Last frame index to yield (inclusive), or None for all.
|
|
23
|
+
|
|
24
|
+
Yields:
|
|
25
|
+
RGB numpy arrays (H, W, 3).
|
|
26
|
+
"""
|
|
27
|
+
frame_index = 0
|
|
28
|
+
for frame in container.decode(video=0):
|
|
29
|
+
if frame_index < start_frame:
|
|
30
|
+
frame_index += 1
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if end_frame is not None and frame_index > end_frame:
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
yield frame.to_ndarray(format='rgb24')
|
|
37
|
+
frame_index += 1
|
|
38
|
+
|
|
39
|
+
def read_frames_exact(
|
|
40
|
+
src: str,
|
|
41
|
+
start_frame: int,
|
|
42
|
+
end_frame: int | None = None,
|
|
43
|
+
) -> Generator[np.ndarray, None, None]:
|
|
44
|
+
"""
|
|
45
|
+
Return frames [start_frame, end_frame] inclusive as RGB np.ndarrays.
|
|
46
|
+
If end_frame is None, reads from start_frame to the end of the video.
|
|
47
|
+
Uses PyAV for efficient frame extraction.
|
|
48
|
+
"""
|
|
49
|
+
if end_frame is not None:
|
|
50
|
+
assert end_frame >= start_frame >= 0, "invalid frame range"
|
|
51
|
+
else:
|
|
52
|
+
assert start_frame >= 0, "start_frame must be non-negative"
|
|
53
|
+
|
|
54
|
+
with _open_container(src) as container:
|
|
55
|
+
stream = container.streams.video[0]
|
|
56
|
+
|
|
57
|
+
# Seek to approximate start position if not starting from beginning
|
|
58
|
+
if start_frame > 0:
|
|
59
|
+
fps = float(stream.average_rate) if stream.average_rate else 30.0
|
|
60
|
+
seek_time_sec = max(0, (start_frame - 30) / fps)
|
|
61
|
+
# Convert seconds to stream time_base units
|
|
62
|
+
seek_timestamp = int(seek_time_sec / float(stream.time_base))
|
|
63
|
+
container.seek(seek_timestamp, stream=stream)
|
|
64
|
+
|
|
65
|
+
yield from _generate_frames(container, start_frame, end_frame)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def read_frames_from_stream(
|
|
69
|
+
stream: BinaryIO,
|
|
70
|
+
skip_frames: int = 0,
|
|
71
|
+
) -> tuple[VideoMetadata, Generator[np.ndarray, None, None]]:
|
|
72
|
+
"""
|
|
73
|
+
Read frames from a video stream (file-like object).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
stream: A file-like object containing video data (e.g., uploaded file).
|
|
77
|
+
skip_frames: Number of initial frames to skip (for resume support).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A tuple of (VideoMetadata, frame_generator).
|
|
81
|
+
The generator yields np.ndarray frames in RGB format (H, W, 3).
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
PyAV handles format detection and seeking automatically.
|
|
85
|
+
Works with MP4, WebM, and other formats.
|
|
86
|
+
"""
|
|
87
|
+
video_data = stream.read()
|
|
88
|
+
meta = video_metadata_from_bytes(video_data)
|
|
89
|
+
|
|
90
|
+
def frame_generator() -> Generator[np.ndarray, None, None]:
|
|
91
|
+
"""Generator that yields frames from the video data."""
|
|
92
|
+
with _open_container(io.BytesIO(video_data)) as container:
|
|
93
|
+
yield from _generate_frames(container, start_frame=skip_frames)
|
|
94
|
+
|
|
95
|
+
return meta, frame_generator()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
import av
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VideoMetadata(NamedTuple):
|
|
10
|
+
width: int
|
|
11
|
+
height: int
|
|
12
|
+
fps: float
|
|
13
|
+
nb_frames: int | None
|
|
14
|
+
time_base: str | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def _open_container(source: str | io.BytesIO):
|
|
19
|
+
"""Context manager for safely opening and closing PyAV containers."""
|
|
20
|
+
container = None
|
|
21
|
+
try:
|
|
22
|
+
container = av.open(source)
|
|
23
|
+
yield container
|
|
24
|
+
except Exception as e:
|
|
25
|
+
msg = "Failed to open video"
|
|
26
|
+
raise RuntimeError(msg) from e
|
|
27
|
+
finally:
|
|
28
|
+
if container:
|
|
29
|
+
container.close()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_metadata_from_container(container: av.container.InputContainer) -> VideoMetadata:
|
|
33
|
+
"""Extract metadata from an open PyAV container."""
|
|
34
|
+
stream = container.streams.video[0]
|
|
35
|
+
fps = float(stream.average_rate) if stream.average_rate else 0.0
|
|
36
|
+
nb_frames = stream.frames if stream.frames > 0 else None
|
|
37
|
+
time_base = str(stream.time_base) if stream.time_base else None
|
|
38
|
+
|
|
39
|
+
return VideoMetadata(
|
|
40
|
+
width=stream.width,
|
|
41
|
+
height=stream.height,
|
|
42
|
+
fps=fps,
|
|
43
|
+
nb_frames=nb_frames,
|
|
44
|
+
time_base=time_base,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def video_metadata_from_bytes(data: bytes) -> VideoMetadata:
|
|
51
|
+
"""Return key video stream metadata from video bytes."""
|
|
52
|
+
with _open_container(io.BytesIO(data)) as container:
|
|
53
|
+
return _get_metadata_from_container(container)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@lru_cache(maxsize=8)
|
|
57
|
+
def video_metadata(url_or_path: str) -> VideoMetadata:
|
|
58
|
+
"""Return key video stream metadata."""
|
|
59
|
+
with _open_container(url_or_path) as container:
|
|
60
|
+
return _get_metadata_from_container(container)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-video-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Shared utilities for processing videos for sign language.
|
|
5
|
+
Author-email: Amit Moryossef <amit@sign.mt>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: av
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: ruff; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Simple Video Utils
|
|
19
|
+
|
|
20
|
+
Lightweight utilities for extracting frames and metadata from videos. Built for sign language processing workflows.
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
[](./LICENSE)
|
|
24
|
+
|
|
25
|
+
## Goal
|
|
26
|
+
|
|
27
|
+
Provide simple, efficient tools for video processing in sign language research and applications.
|
|
28
|
+
Uses PyAV for fast frame extraction with support for multiple formats (MP4, WebM) and remote URLs.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install simple-video-utils
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Extract Video Metadata
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from simple_video_utils.metadata import video_metadata
|
|
42
|
+
|
|
43
|
+
meta = video_metadata("video.mp4")
|
|
44
|
+
print(f"{meta.width}x{meta.height} @ {meta.fps} fps")
|
|
45
|
+
# Output: VideoMetadata(width=1920, height=1080, fps=30.0, nb_frames=450, time_base='1/15360')
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Read Frames from File
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from simple_video_utils.frames import read_frames_exact
|
|
52
|
+
|
|
53
|
+
# Read specific frame range (inclusive)
|
|
54
|
+
frames = list(read_frames_exact("video.mp4", start_frame=0, end_frame=10))
|
|
55
|
+
# Returns 11 frames as numpy arrays (H, W, 3) in RGB format
|
|
56
|
+
|
|
57
|
+
# Read from frame to end of video
|
|
58
|
+
frames = list(read_frames_exact("video.mp4", start_frame=5, end_frame=None))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Read Frames from Stream
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from simple_video_utils.frames import read_frames_from_stream
|
|
65
|
+
|
|
66
|
+
# Useful for uploaded files or in-memory video data
|
|
67
|
+
with open("video.mp4", "rb") as f:
|
|
68
|
+
meta, frames_gen = read_frames_from_stream(f)
|
|
69
|
+
for frame in frames_gen:
|
|
70
|
+
# Process each frame (numpy array)
|
|
71
|
+
pass
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Remote Videos
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from simple_video_utils.metadata import video_metadata
|
|
78
|
+
from simple_video_utils.frames import read_frames_exact
|
|
79
|
+
|
|
80
|
+
# Works with remote URLs
|
|
81
|
+
url = "https://example.com/video.mp4"
|
|
82
|
+
meta = video_metadata(url)
|
|
83
|
+
frames = list(read_frames_exact(url, 0, 5))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
pytest tests/
|
|
91
|
+
ruff check .
|
|
92
|
+
```
|
|
93
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
simple_video_utils/__init__.py,sha256=LRnmuFyziKNpM_Mf2x4UarAiZsOKGunmMDf98WTm1SY,64
|
|
2
|
+
simple_video_utils/frames.py,sha256=VkX-OHGrOc_oJDI7j8xIarVu2jHWdSYEouFgk4PfsJE,3115
|
|
3
|
+
simple_video_utils/metadata.py,sha256=1JALf1AligxFwfArgGjgoFFtrrQTMKsnzY2yH3oeQVk,1672
|
|
4
|
+
simple_video_utils-0.0.1.dist-info/licenses/LICENSE,sha256=-85p81jfHERhmQnds40xOFt4q3cTlX0GFbSUuJU1jxI,1061
|
|
5
|
+
simple_video_utils-0.0.1.dist-info/METADATA,sha256=IIfZpWWdej-F_xb9jKf7TTQadTWlLv6Xahlg_HeOYI8,2378
|
|
6
|
+
simple_video_utils-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
simple_video_utils-0.0.1.dist-info/top_level.txt,sha256=9aYY6Qrg3bqG-QZsunQV8-_qXabGWK26Ub7JC8Eyyys,19
|
|
8
|
+
simple_video_utils-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sign
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simple_video_utils
|