mediaref 0.1.0__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.
- mediaref/__init__.py +27 -0
- mediaref/_internal.py +254 -0
- mediaref/_typing.py +5 -0
- mediaref/cached_av/__init__.py +72 -0
- mediaref/cached_av/container_mixin.py +109 -0
- mediaref/cached_av/input_container_mixin.py +92 -0
- mediaref/core.py +217 -0
- mediaref/loader.py +169 -0
- mediaref/py.typed +2 -0
- mediaref/resource_cache.py +86 -0
- mediaref/video_decoder/__init__.py +61 -0
- mediaref/video_decoder/base.py +187 -0
- mediaref/video_decoder/frame_batch.py +66 -0
- mediaref/video_decoder/pyav_decoder.py +322 -0
- mediaref/video_decoder/torchcodec_decoder.py +75 -0
- mediaref/video_decoder/types.py +79 -0
- mediaref-0.1.0.dist-info/METADATA +118 -0
- mediaref-0.1.0.dist-info/RECORD +20 -0
- mediaref-0.1.0.dist-info/WHEEL +4 -0
- mediaref-0.1.0.dist-info/licenses/LICENSE +22 -0
mediaref/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""MediaRef - Lightweight media reference management for images and videos."""
|
|
2
|
+
|
|
3
|
+
from .core import MediaRef
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__all__ = ["MediaRef"]
|
|
7
|
+
|
|
8
|
+
# Optional loader module (requires extra dependencies)
|
|
9
|
+
try:
|
|
10
|
+
from .loader import cleanup_cache, load_batch # noqa: F401
|
|
11
|
+
from .video_decoder import BaseVideoDecoder, FrameBatch, PyAVVideoDecoder, TorchCodecVideoDecoder # noqa: F401
|
|
12
|
+
from .video_decoder.types import BatchDecodingStrategy, VideoStreamMetadata # noqa: F401
|
|
13
|
+
|
|
14
|
+
__all__.extend(
|
|
15
|
+
[
|
|
16
|
+
"load_batch",
|
|
17
|
+
"cleanup_cache",
|
|
18
|
+
"BatchDecodingStrategy",
|
|
19
|
+
"VideoStreamMetadata",
|
|
20
|
+
"BaseVideoDecoder",
|
|
21
|
+
"FrameBatch",
|
|
22
|
+
"PyAVVideoDecoder",
|
|
23
|
+
"TorchCodecVideoDecoder",
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
except ImportError:
|
|
27
|
+
pass
|
mediaref/_internal.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Internal loading and encoding utilities."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import gc
|
|
5
|
+
import os
|
|
6
|
+
from fractions import Fraction
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal, Optional, Union
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
import av
|
|
12
|
+
import cv2
|
|
13
|
+
import numpy as np
|
|
14
|
+
import PIL.Image
|
|
15
|
+
import PIL.ImageOps
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from . import cached_av
|
|
19
|
+
|
|
20
|
+
# Constants
|
|
21
|
+
REQUEST_TIMEOUT = 60 # HTTP request timeout in seconds
|
|
22
|
+
NANOSECOND = 1_000_000_000 # 1 second in nanoseconds
|
|
23
|
+
|
|
24
|
+
# Garbage collection for PyAV reference cycles
|
|
25
|
+
_CALLED_TIMES = 0
|
|
26
|
+
GC_COLLECTION_INTERVAL = 10
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# Image Loading
|
|
31
|
+
# ============================================================================
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_image_as_bgra(path_or_uri: str) -> np.ndarray:
|
|
35
|
+
"""Load image from any source and return as BGRA numpy array.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
path_or_uri: File path, URL, or data URI
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
BGRA numpy array
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If loading fails
|
|
45
|
+
FileNotFoundError: If local file doesn't exist
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
if path_or_uri.startswith("data:"):
|
|
49
|
+
return _load_from_data_uri(path_or_uri)
|
|
50
|
+
else:
|
|
51
|
+
# Load as PIL image and convert to BGRA
|
|
52
|
+
pil_image = _load_pil_image(path_or_uri)
|
|
53
|
+
return _pil_to_bgra_array(pil_image)
|
|
54
|
+
except FileNotFoundError:
|
|
55
|
+
raise
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise ValueError(f"Failed to load image from {path_or_uri}: {e}") from e
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_pil_image(
|
|
61
|
+
image: Union[str, PIL.Image.Image],
|
|
62
|
+
) -> PIL.Image.Image:
|
|
63
|
+
"""Load image to PIL Image.
|
|
64
|
+
|
|
65
|
+
Adapted from: https://github.com/huggingface/diffusers/blob/main/src/diffusers/utils/loading_utils.py
|
|
66
|
+
"""
|
|
67
|
+
if isinstance(image, str):
|
|
68
|
+
if image.startswith("http://") or image.startswith("https://"):
|
|
69
|
+
image = PIL.Image.open(requests.get(image, stream=True, timeout=REQUEST_TIMEOUT).raw)
|
|
70
|
+
elif os.path.isfile(image):
|
|
71
|
+
image = PIL.Image.open(image)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Incorrect path or URL. URLs must start with `http://` or `https://`, "
|
|
75
|
+
f"and {image} is not a valid path."
|
|
76
|
+
)
|
|
77
|
+
elif isinstance(image, PIL.Image.Image):
|
|
78
|
+
pass
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"Incorrect format used for the image. Should be a URL linking to an image, a local path, or a PIL image."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Handle EXIF orientation
|
|
85
|
+
image = PIL.ImageOps.exif_transpose(image)
|
|
86
|
+
|
|
87
|
+
# Convert to RGB
|
|
88
|
+
image = image.convert("RGB")
|
|
89
|
+
|
|
90
|
+
return image
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _pil_to_bgra_array(pil_image: PIL.Image.Image) -> np.ndarray:
|
|
94
|
+
"""Convert PIL image to BGRA numpy array."""
|
|
95
|
+
# Ensure image is in RGB mode
|
|
96
|
+
if pil_image.mode != "RGB":
|
|
97
|
+
pil_image = pil_image.convert("RGB")
|
|
98
|
+
|
|
99
|
+
# Convert to numpy array and then to BGRA
|
|
100
|
+
rgb_array = np.array(pil_image)
|
|
101
|
+
return cv2.cvtColor(rgb_array, cv2.COLOR_RGB2BGRA)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Video Loading
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def load_video_frame_as_bgra(
|
|
110
|
+
path_or_url: str,
|
|
111
|
+
pts_ns: int,
|
|
112
|
+
*,
|
|
113
|
+
keep_av_open: bool = False,
|
|
114
|
+
) -> np.ndarray:
|
|
115
|
+
"""Load video frame and return as BGRA numpy array.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path_or_url: File path or URL to video
|
|
119
|
+
pts_ns: Presentation timestamp in nanoseconds
|
|
120
|
+
keep_av_open: Keep AV container open in cache
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
BGRA numpy array
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If loading fails
|
|
127
|
+
FileNotFoundError: If local file doesn't exist
|
|
128
|
+
"""
|
|
129
|
+
global _CALLED_TIMES
|
|
130
|
+
_CALLED_TIMES += 1
|
|
131
|
+
if _CALLED_TIMES % GC_COLLECTION_INTERVAL == 0:
|
|
132
|
+
gc.collect()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Validate local file exists
|
|
136
|
+
if not path_or_url.startswith(("http://", "https://")):
|
|
137
|
+
if not Path(path_or_url).exists():
|
|
138
|
+
raise FileNotFoundError(f"Video file not found: {path_or_url}")
|
|
139
|
+
|
|
140
|
+
# Convert nanoseconds to fraction
|
|
141
|
+
pts_fraction = Fraction(pts_ns, NANOSECOND)
|
|
142
|
+
|
|
143
|
+
# Open video and read frame
|
|
144
|
+
container = cached_av.open(path_or_url, "r", keep_av_open=keep_av_open)
|
|
145
|
+
try:
|
|
146
|
+
frame = _read_frame_at_pts(container, pts_fraction)
|
|
147
|
+
rgb_array = frame.to_ndarray(format="rgb24")
|
|
148
|
+
return cv2.cvtColor(rgb_array, cv2.COLOR_RGB2BGRA)
|
|
149
|
+
finally:
|
|
150
|
+
if not keep_av_open:
|
|
151
|
+
container.close()
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
raise
|
|
154
|
+
except Exception as e:
|
|
155
|
+
pts_seconds = pts_ns / NANOSECOND
|
|
156
|
+
raise ValueError(f"Failed to load frame at {pts_seconds:.3f}s from {path_or_url}: {e}") from e
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read_frame_at_pts(
|
|
160
|
+
container: av.container.InputContainer,
|
|
161
|
+
pts: Fraction,
|
|
162
|
+
) -> av.VideoFrame:
|
|
163
|
+
"""Read single frame at or after given timestamp."""
|
|
164
|
+
if not container.streams.video:
|
|
165
|
+
raise ValueError("No video streams found")
|
|
166
|
+
|
|
167
|
+
stream = container.streams.video[0]
|
|
168
|
+
|
|
169
|
+
# Seek to the timestamp
|
|
170
|
+
container.seek(int(pts / stream.time_base), stream=stream)
|
|
171
|
+
|
|
172
|
+
# Decode frames until we find the right one
|
|
173
|
+
for frame in container.decode(stream):
|
|
174
|
+
if frame.time >= float(pts):
|
|
175
|
+
return frame
|
|
176
|
+
|
|
177
|
+
raise ValueError(f"Frame not found at {float(pts):.2f}s")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ============================================================================
|
|
181
|
+
# Data URI Handling
|
|
182
|
+
# ============================================================================
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _load_from_data_uri(data_uri: str) -> np.ndarray:
|
|
186
|
+
"""Load image from data URI."""
|
|
187
|
+
parsed = urlparse(data_uri)
|
|
188
|
+
if parsed.scheme != "data":
|
|
189
|
+
raise ValueError(f"Invalid data URI scheme: {parsed.scheme}")
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Extract base64 data from data URI
|
|
193
|
+
data_part = parsed.path.split(",", 1)[1]
|
|
194
|
+
return _decode_from_base64(data_part)
|
|
195
|
+
except (IndexError, ValueError) as e:
|
|
196
|
+
raise ValueError(f"Invalid data URI format: {e}") from e
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _decode_from_base64(data: str) -> np.ndarray:
|
|
200
|
+
"""Decode base64 string to BGRA numpy array."""
|
|
201
|
+
try:
|
|
202
|
+
image_bytes = base64.b64decode(data)
|
|
203
|
+
nparr = np.frombuffer(image_bytes, np.uint8)
|
|
204
|
+
bgr_array = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
205
|
+
|
|
206
|
+
if bgr_array is None:
|
|
207
|
+
raise ValueError("Failed to decode base64 image data")
|
|
208
|
+
|
|
209
|
+
return cv2.cvtColor(bgr_array, cv2.COLOR_BGR2BGRA)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise ValueError(f"Failed to decode base64 data: {e}") from e
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================================
|
|
215
|
+
# Encoding
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def encode_array_to_base64(
|
|
220
|
+
array: np.ndarray,
|
|
221
|
+
format: Literal["png", "jpeg", "bmp"],
|
|
222
|
+
quality: Optional[int] = None,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Encode BGRA numpy array to base64 string.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
array: BGRA numpy array
|
|
228
|
+
format: Output format ('png', 'jpeg', or 'bmp')
|
|
229
|
+
quality: JPEG quality (1-100), ignored for PNG and BMP
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Base64 encoded string
|
|
233
|
+
"""
|
|
234
|
+
# Convert BGRA to BGR for cv2 encoding
|
|
235
|
+
bgr_array = cv2.cvtColor(array, cv2.COLOR_BGRA2BGR)
|
|
236
|
+
|
|
237
|
+
# Encode based on format
|
|
238
|
+
if format == "png":
|
|
239
|
+
success, encoded = cv2.imencode(".png", bgr_array)
|
|
240
|
+
elif format == "jpeg":
|
|
241
|
+
if quality is None:
|
|
242
|
+
quality = 85
|
|
243
|
+
if not (1 <= quality <= 100):
|
|
244
|
+
raise ValueError("JPEG quality must be between 1 and 100")
|
|
245
|
+
success, encoded = cv2.imencode(".jpg", bgr_array, [cv2.IMWRITE_JPEG_QUALITY, quality])
|
|
246
|
+
elif format == "bmp":
|
|
247
|
+
success, encoded = cv2.imencode(".bmp", bgr_array)
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
250
|
+
|
|
251
|
+
if not success:
|
|
252
|
+
raise ValueError(f"Failed to encode image as {format}")
|
|
253
|
+
|
|
254
|
+
return base64.b64encode(encoded.tobytes()).decode("utf-8")
|
mediaref/_typing.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Literal, overload
|
|
3
|
+
|
|
4
|
+
import av
|
|
5
|
+
import av.container
|
|
6
|
+
|
|
7
|
+
from .._typing import PathLike
|
|
8
|
+
from ..resource_cache import ResourceCache
|
|
9
|
+
from .input_container_mixin import InputContainerMixin
|
|
10
|
+
|
|
11
|
+
DEFAULT_CACHE_SIZE = int(os.environ.get("AV_CACHE_SIZE", 10))
|
|
12
|
+
|
|
13
|
+
# Global container cache for efficient video file access
|
|
14
|
+
_container_cache: ResourceCache["MockedInputContainer"] = ResourceCache(max_size=DEFAULT_CACHE_SIZE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def open(
|
|
19
|
+
file: PathLike, mode: Literal["r"], *, keep_av_open: bool = False, **kwargs
|
|
20
|
+
) -> av.container.InputContainer: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def open(file: PathLike, mode: Literal["w"], **kwargs) -> av.container.OutputContainer: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def open(file: PathLike, mode: Literal["r", "w"], *, keep_av_open: bool = False, **kwargs):
|
|
28
|
+
"""
|
|
29
|
+
Open video container with optional caching for read operations.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
file: Video file path or URL
|
|
33
|
+
mode: Access mode ('r' for read, 'w' for write)
|
|
34
|
+
keep_av_open: Enable caching for read containers
|
|
35
|
+
**kwargs: Additional arguments passed to av.open
|
|
36
|
+
"""
|
|
37
|
+
if mode == "r":
|
|
38
|
+
if not keep_av_open:
|
|
39
|
+
# Direct access without caching
|
|
40
|
+
return av.open(file, "r", **kwargs)
|
|
41
|
+
|
|
42
|
+
# Use cached container when keep_av_open=True
|
|
43
|
+
cache_key = str(file)
|
|
44
|
+
if cache_key not in _container_cache:
|
|
45
|
+
return MockedInputContainer(file, **kwargs)
|
|
46
|
+
return _container_cache[cache_key].obj
|
|
47
|
+
else:
|
|
48
|
+
return av.open(file, mode, **kwargs)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cleanup_cache():
|
|
52
|
+
"""Clear all cached video containers from memory."""
|
|
53
|
+
_container_cache.clear()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MockedInputContainer(InputContainerMixin):
|
|
57
|
+
"""Cached wrapper for PyAV InputContainer with reference counting."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, file: PathLike, **kwargs):
|
|
60
|
+
self._cache_key = str(file)
|
|
61
|
+
self._container: av.container.InputContainer = av.open(file, "r", **kwargs)
|
|
62
|
+
_container_cache.add_entry(self._cache_key, self)
|
|
63
|
+
|
|
64
|
+
def __enter__(self) -> "MockedInputContainer":
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def close(self):
|
|
68
|
+
"""Release container reference and cleanup when no longer needed."""
|
|
69
|
+
_container_cache.release_entry(self._cache_key)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["open", "cleanup_cache"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
from types import TracebackType
|
|
3
|
+
from typing import Any, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
import av.container
|
|
6
|
+
from av.container.streams import StreamContainer
|
|
7
|
+
from av.format import ContainerFormat
|
|
8
|
+
|
|
9
|
+
Real = Union[int, float, Fraction]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContainerMixin:
|
|
13
|
+
"""Mixin that provides Container interface by delegating to _container.
|
|
14
|
+
|
|
15
|
+
Methods and properties copied from PyAV container core interface:
|
|
16
|
+
https://github.com/PyAV-Org/PyAV/blob/main/av/container/core.pyi#L70-L98
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_container: av.container.Container
|
|
20
|
+
|
|
21
|
+
# Properties from Container (copied from PyAV container core interface)
|
|
22
|
+
@property
|
|
23
|
+
def writeable(self) -> bool:
|
|
24
|
+
return self._container.writeable
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return self._container.name
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def metadata_encoding(self) -> str:
|
|
32
|
+
return self._container.metadata_encoding
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def metadata_errors(self) -> str:
|
|
36
|
+
return self._container.metadata_errors
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def file(self) -> Any:
|
|
40
|
+
return self._container.file
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def buffer_size(self) -> int:
|
|
44
|
+
return self._container.buffer_size
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def input_was_opened(self) -> bool:
|
|
48
|
+
return self._container.input_was_opened
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def io_open(self) -> Any:
|
|
52
|
+
return self._container.io_open
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def open_files(self) -> Any:
|
|
56
|
+
return self._container.open_files
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def format(self) -> ContainerFormat:
|
|
60
|
+
return self._container.format
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def options(self) -> dict[str, str]:
|
|
64
|
+
return self._container.options
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def container_options(self) -> dict[str, str]:
|
|
68
|
+
return self._container.container_options
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def stream_options(self) -> list[dict[str, str]]:
|
|
72
|
+
return self._container.stream_options
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def streams(self) -> StreamContainer:
|
|
76
|
+
return self._container.streams
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def metadata(self) -> dict[str, str]:
|
|
80
|
+
return self._container.metadata
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def open_timeout(self) -> Optional[Real]:
|
|
84
|
+
return self._container.open_timeout
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def read_timeout(self) -> Optional[Real]:
|
|
88
|
+
return self._container.read_timeout
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def flags(self) -> int:
|
|
92
|
+
return self._container.flags
|
|
93
|
+
|
|
94
|
+
def __enter__(self) -> "ContainerMixin":
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def __exit__(
|
|
98
|
+
self,
|
|
99
|
+
exc_type: Optional[Type[BaseException]],
|
|
100
|
+
exc_val: Optional[BaseException],
|
|
101
|
+
exc_tb: Optional[TracebackType],
|
|
102
|
+
) -> bool:
|
|
103
|
+
return self._container.__exit__(exc_type, exc_val, exc_tb)
|
|
104
|
+
|
|
105
|
+
def set_timeout(self, timeout: Optional[Real]) -> None:
|
|
106
|
+
return self._container.set_timeout(timeout)
|
|
107
|
+
|
|
108
|
+
def start_timeout(self) -> None:
|
|
109
|
+
return self._container.start_timeout()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any, Iterator, Optional, Union, overload
|
|
2
|
+
|
|
3
|
+
import av.container
|
|
4
|
+
from av.audio.frame import AudioFrame
|
|
5
|
+
from av.audio.stream import AudioStream
|
|
6
|
+
from av.packet import Packet
|
|
7
|
+
from av.stream import Stream
|
|
8
|
+
from av.subtitles.stream import SubtitleStream
|
|
9
|
+
from av.subtitles.subtitle import SubtitleSet
|
|
10
|
+
from av.video.frame import VideoFrame
|
|
11
|
+
from av.video.stream import VideoStream
|
|
12
|
+
|
|
13
|
+
from .container_mixin import ContainerMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InputContainerMixin(ContainerMixin):
|
|
17
|
+
"""Mixin that provides InputContainer interface by delegating to _container.
|
|
18
|
+
|
|
19
|
+
Methods and properties copied from PyAV input container interface:
|
|
20
|
+
https://github.com/PyAV-Org/PyAV/blob/main/av/container/input.pyi#L14-L49
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_container: av.container.InputContainer # type: ignore[override]
|
|
24
|
+
|
|
25
|
+
# InputContainer-specific properties (copied from PyAV input container interface)
|
|
26
|
+
@property
|
|
27
|
+
def start_time(self) -> int:
|
|
28
|
+
return self._container.start_time
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def duration(self) -> Optional[int]:
|
|
32
|
+
return self._container.duration
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def bit_rate(self) -> int:
|
|
36
|
+
return self._container.bit_rate
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def size(self) -> int:
|
|
40
|
+
return self._container.size
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> "InputContainerMixin":
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
return self._container.close()
|
|
47
|
+
|
|
48
|
+
def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]:
|
|
49
|
+
return self._container.demux(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
@overload
|
|
52
|
+
def decode(self, video: int) -> Iterator[VideoFrame]: ...
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def decode(self, audio: int) -> Iterator[AudioFrame]: ...
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def decode(self, subtitles: int) -> Iterator[SubtitleSet]: ...
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ...
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def decode(self, *args: SubtitleStream) -> Iterator[SubtitleSet]: ...
|
|
68
|
+
|
|
69
|
+
def decode(self, *args: Any, **kwargs: Any) -> Iterator[Union[VideoFrame, AudioFrame, SubtitleSet]]:
|
|
70
|
+
return self._container.decode(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
def seek(
|
|
73
|
+
self,
|
|
74
|
+
offset: int,
|
|
75
|
+
*,
|
|
76
|
+
backward: bool = True,
|
|
77
|
+
any_frame: bool = False,
|
|
78
|
+
stream: Optional[Union[Stream, VideoStream, AudioStream]] = None,
|
|
79
|
+
unsupported_frame_offset: bool = False,
|
|
80
|
+
unsupported_byte_offset: bool = False,
|
|
81
|
+
) -> None:
|
|
82
|
+
return self._container.seek(
|
|
83
|
+
offset,
|
|
84
|
+
backward=backward,
|
|
85
|
+
any_frame=any_frame,
|
|
86
|
+
stream=stream,
|
|
87
|
+
unsupported_frame_offset=unsupported_frame_offset,
|
|
88
|
+
unsupported_byte_offset=unsupported_byte_offset,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def flush_buffers(self) -> None:
|
|
92
|
+
return self._container.flush_buffers()
|