deffcode 0.2.7__tar.gz → 0.2.8__tar.gz
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.
- {deffcode-0.2.7 → deffcode-0.2.8}/PKG-INFO +25 -14
- {deffcode-0.2.7 → deffcode-0.2.8}/README.md +22 -11
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode/ffdecoder.py +9 -4
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode/ffhelper.py +66 -16
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode/sourcer.py +56 -31
- deffcode-0.2.8/deffcode/version.py +1 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode.egg-info/PKG-INFO +25 -14
- {deffcode-0.2.7 → deffcode-0.2.8}/pyproject.toml +8 -3
- {deffcode-0.2.7 → deffcode-0.2.8}/tests/test_ffdecoder.py +37 -1
- {deffcode-0.2.7 → deffcode-0.2.8}/tests/test_ffhelper.py +63 -2
- {deffcode-0.2.7 → deffcode-0.2.8}/tests/test_sourcer.py +25 -0
- deffcode-0.2.7/deffcode/version.py +0 -1
- {deffcode-0.2.7 → deffcode-0.2.8}/LICENSE +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode/__init__.py +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode/utils.py +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode.egg-info/SOURCES.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode.egg-info/dependency_links.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode.egg-info/requires.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/deffcode.egg-info/top_level.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/setup.cfg +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/setup.py +0 -0
- {deffcode-0.2.7 → deffcode-0.2.8}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deffcode
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A cross-platform High-performance & Flexible Real-time Video Frames Decoder in Python.
|
|
5
5
|
Author-email: Abhishek Thakur <abhi.una12@gmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -22,11 +22,11 @@ Classifier: Intended Audience :: Developers
|
|
|
22
22
|
Classifier: Intended Audience :: Science/Research
|
|
23
23
|
Classifier: Intended Audience :: Education
|
|
24
24
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
27
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
28
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
29
|
-
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
Requires-Dist: cython
|
|
@@ -113,7 +113,7 @@ Here are some key features that stand out:
|
|
|
113
113
|
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
|
|
114
114
|
- Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
|
|
115
115
|
- Out-of-the-box support for all prominent Computer Vision libraries.
|
|
116
|
-
- Cross-platform, runs on Python 3.
|
|
116
|
+
- Cross-platform, runs on Python 3.11+, and easy to install.
|
|
117
117
|
|
|
118
118
|
<!--
|
|
119
119
|
- [x] Lossless Transcoding support with [WriteGear](https://abhitronix.github.io/deffcode/latest/gears/writegear/introduction/). #TODO
|
|
@@ -235,6 +235,8 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
235
235
|
- [CUDA-NVENC-accelerated Video Transcoding with WriteGear API][cuda-nvenc-accelerated-video-transcoding-with-writegear-api]
|
|
236
236
|
- [CUDA-NVENC-accelerated End-to-end Lossless Video Transcoding with WriteGear API][cuda-nvenc-accelerated-end-to-end-lossless-video-transcoding-with-writegear-api]
|
|
237
237
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
238
|
+
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
239
|
+
- [Decoding crossfade transition between multiple inputs][decoding-crossfade-transition-between-multiple-inputs]
|
|
238
240
|
|
|
239
241
|
</details>
|
|
240
242
|
|
|
@@ -271,9 +273,8 @@ decoder = FFdecoder("https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1
|
|
|
271
273
|
|
|
272
274
|
# grab RGB24(default) 3D frames from decoder
|
|
273
275
|
for frame in decoder.generateFrame():
|
|
274
|
-
|
|
275
276
|
# lets print its shape
|
|
276
|
-
print(frame.shape)
|
|
277
|
+
print(frame.shape) # (1080, 1920, 3)
|
|
277
278
|
|
|
278
279
|
# terminate the decoder
|
|
279
280
|
decoder.terminate()
|
|
@@ -288,7 +289,9 @@ The primary function of [**Sourcer API**][sourcer-api] is to **gather metadata i
|
|
|
288
289
|
from deffcode import Sourcer
|
|
289
290
|
|
|
290
291
|
# initialize and formulate the decoder using suitable source
|
|
291
|
-
sourcer = Sourcer(
|
|
292
|
+
sourcer = Sourcer(
|
|
293
|
+
"https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1MB.mp4"
|
|
294
|
+
).probe_stream()
|
|
292
295
|
|
|
293
296
|
# print metadata as `json.dump`
|
|
294
297
|
print(sourcer.retrieve_metadata(pretty_json=True))
|
|
@@ -354,18 +357,24 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
354
357
|
|
|
355
358
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
356
359
|
|
|
357
|
-
[](https://doi.org/10.5281/zenodo.19658542)
|
|
358
361
|
|
|
359
362
|
```BibTeX
|
|
360
363
|
@software{deffcode,
|
|
361
364
|
author = {Abhishek Thakur},
|
|
362
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
363
|
-
month =
|
|
364
|
-
year =
|
|
365
|
+
title = {abhiTronix/deffcode: v0.2.7},
|
|
366
|
+
month = apr,
|
|
367
|
+
year = 2026,
|
|
365
368
|
publisher = {Zenodo},
|
|
366
|
-
version = {v0.2.
|
|
367
|
-
doi = {10.5281/zenodo.
|
|
368
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
369
|
+
version = {v0.2.7},
|
|
370
|
+
doi = {10.5281/zenodo.19658542},
|
|
371
|
+
url = {https://doi.org/10.5281/zenodo.19658542},
|
|
372
|
+
swhid = {swh:1:dir:85e8dfd087aed870deb4043c532faaa9b156d1e1
|
|
373
|
+
;origin=https://doi.org/10.5281/zenodo.6976947;vis
|
|
374
|
+
it=swh:1:snp:af1b478890801c02d56b95f563b86761980bf
|
|
375
|
+
82c;anchor=swh:1:rel:85f9af4452f8030317ec2012acc10
|
|
376
|
+
29e8a945e2d;path=abhiTronix-deffcode-5162649
|
|
377
|
+
},
|
|
369
378
|
}
|
|
370
379
|
```
|
|
371
380
|
|
|
@@ -478,6 +487,8 @@ Advanced Recipes
|
|
|
478
487
|
[smart-keyframe-only-decoding-for-heavy-ai-inference]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/extract-frame-metadata/#smart-keyframe-only-decoding-for-heavy-ai-inference
|
|
479
488
|
[variable-frame-rate-vfr-synchronization-via-pts_time]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/extract-frame-metadata/#variable-frame-rate-vfr-synchronization-via-pts_time
|
|
480
489
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
490
|
+
[decoding-multiple-inputs-as-2x2-video-grid-mosaic]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-multiple-inputs-as-2x2-video-grid-mosaic
|
|
491
|
+
[decoding-crossfade-transition-between-multiple-inputs]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-crossfade-transition-between-multiple-inputs
|
|
481
492
|
|
|
482
493
|
<!--
|
|
483
494
|
CI Apps URLs
|
|
@@ -73,7 +73,7 @@ Here are some key features that stand out:
|
|
|
73
73
|
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
|
|
74
74
|
- Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
|
|
75
75
|
- Out-of-the-box support for all prominent Computer Vision libraries.
|
|
76
|
-
- Cross-platform, runs on Python 3.
|
|
76
|
+
- Cross-platform, runs on Python 3.11+, and easy to install.
|
|
77
77
|
|
|
78
78
|
<!--
|
|
79
79
|
- [x] Lossless Transcoding support with [WriteGear](https://abhitronix.github.io/deffcode/latest/gears/writegear/introduction/). #TODO
|
|
@@ -195,6 +195,8 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
195
195
|
- [CUDA-NVENC-accelerated Video Transcoding with WriteGear API][cuda-nvenc-accelerated-video-transcoding-with-writegear-api]
|
|
196
196
|
- [CUDA-NVENC-accelerated End-to-end Lossless Video Transcoding with WriteGear API][cuda-nvenc-accelerated-end-to-end-lossless-video-transcoding-with-writegear-api]
|
|
197
197
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
198
|
+
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
199
|
+
- [Decoding crossfade transition between multiple inputs][decoding-crossfade-transition-between-multiple-inputs]
|
|
198
200
|
|
|
199
201
|
</details>
|
|
200
202
|
|
|
@@ -231,9 +233,8 @@ decoder = FFdecoder("https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1
|
|
|
231
233
|
|
|
232
234
|
# grab RGB24(default) 3D frames from decoder
|
|
233
235
|
for frame in decoder.generateFrame():
|
|
234
|
-
|
|
235
236
|
# lets print its shape
|
|
236
|
-
print(frame.shape)
|
|
237
|
+
print(frame.shape) # (1080, 1920, 3)
|
|
237
238
|
|
|
238
239
|
# terminate the decoder
|
|
239
240
|
decoder.terminate()
|
|
@@ -248,7 +249,9 @@ The primary function of [**Sourcer API**][sourcer-api] is to **gather metadata i
|
|
|
248
249
|
from deffcode import Sourcer
|
|
249
250
|
|
|
250
251
|
# initialize and formulate the decoder using suitable source
|
|
251
|
-
sourcer = Sourcer(
|
|
252
|
+
sourcer = Sourcer(
|
|
253
|
+
"https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1MB.mp4"
|
|
254
|
+
).probe_stream()
|
|
252
255
|
|
|
253
256
|
# print metadata as `json.dump`
|
|
254
257
|
print(sourcer.retrieve_metadata(pretty_json=True))
|
|
@@ -314,18 +317,24 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
314
317
|
|
|
315
318
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
316
319
|
|
|
317
|
-
[](https://doi.org/10.5281/zenodo.19658542)
|
|
318
321
|
|
|
319
322
|
```BibTeX
|
|
320
323
|
@software{deffcode,
|
|
321
324
|
author = {Abhishek Thakur},
|
|
322
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
323
|
-
month =
|
|
324
|
-
year =
|
|
325
|
+
title = {abhiTronix/deffcode: v0.2.7},
|
|
326
|
+
month = apr,
|
|
327
|
+
year = 2026,
|
|
325
328
|
publisher = {Zenodo},
|
|
326
|
-
version = {v0.2.
|
|
327
|
-
doi = {10.5281/zenodo.
|
|
328
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
329
|
+
version = {v0.2.7},
|
|
330
|
+
doi = {10.5281/zenodo.19658542},
|
|
331
|
+
url = {https://doi.org/10.5281/zenodo.19658542},
|
|
332
|
+
swhid = {swh:1:dir:85e8dfd087aed870deb4043c532faaa9b156d1e1
|
|
333
|
+
;origin=https://doi.org/10.5281/zenodo.6976947;vis
|
|
334
|
+
it=swh:1:snp:af1b478890801c02d56b95f563b86761980bf
|
|
335
|
+
82c;anchor=swh:1:rel:85f9af4452f8030317ec2012acc10
|
|
336
|
+
29e8a945e2d;path=abhiTronix-deffcode-5162649
|
|
337
|
+
},
|
|
329
338
|
}
|
|
330
339
|
```
|
|
331
340
|
|
|
@@ -438,6 +447,8 @@ Advanced Recipes
|
|
|
438
447
|
[smart-keyframe-only-decoding-for-heavy-ai-inference]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/extract-frame-metadata/#smart-keyframe-only-decoding-for-heavy-ai-inference
|
|
439
448
|
[variable-frame-rate-vfr-synchronization-via-pts_time]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/extract-frame-metadata/#variable-frame-rate-vfr-synchronization-via-pts_time
|
|
440
449
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
450
|
+
[decoding-multiple-inputs-as-2x2-video-grid-mosaic]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-multiple-inputs-as-2x2-video-grid-mosaic
|
|
451
|
+
[decoding-crossfade-transition-between-multiple-inputs]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-crossfade-transition-between-multiple-inputs
|
|
441
452
|
|
|
442
453
|
<!--
|
|
443
454
|
CI Apps URLs
|
|
@@ -30,7 +30,7 @@ import threading
|
|
|
30
30
|
from collections import OrderedDict
|
|
31
31
|
from collections.abc import Generator
|
|
32
32
|
from types import TracebackType
|
|
33
|
-
from typing import Any
|
|
33
|
+
from typing import Any, Self
|
|
34
34
|
|
|
35
35
|
import numpy as np
|
|
36
36
|
|
|
@@ -226,6 +226,9 @@ class FFdecoder:
|
|
|
226
226
|
if {"-vf", "-filter_complex"}.intersection(self.__extra_params.keys()):
|
|
227
227
|
key = "-vf" if "-vf" in self.__extra_params else "-filter_complex"
|
|
228
228
|
sourcer_params[key] = self.__extra_params[key]
|
|
229
|
+
# multi-input output streams are selected via `-map`, so probe with it too
|
|
230
|
+
if self.__is_multi and "-map" in self.__extra_params:
|
|
231
|
+
sourcer_params["-map"] = self.__extra_params["-map"]
|
|
229
232
|
|
|
230
233
|
# define dict to store user-defined parameters
|
|
231
234
|
self.__user_metadata = {}
|
|
@@ -364,7 +367,7 @@ class FFdecoder:
|
|
|
364
367
|
# reset improper values
|
|
365
368
|
self.__custom_resolution = None
|
|
366
369
|
|
|
367
|
-
def formulate(self) ->
|
|
370
|
+
def formulate(self) -> Self:
|
|
368
371
|
"""
|
|
369
372
|
This method formulates all necessary FFmpeg pipeline arguments and executes it inside the FFmpeg `subprocess` pipe.
|
|
370
373
|
|
|
@@ -662,7 +665,9 @@ class FFdecoder:
|
|
|
662
665
|
if "-frames:v" in input_params:
|
|
663
666
|
self.__raw_frame_num = input_params["-frames:v"]
|
|
664
667
|
elif (
|
|
665
|
-
|
|
668
|
+
# composite output length is unrelated to the primary source's length
|
|
669
|
+
not self.__is_multi
|
|
670
|
+
and self.__sourcer_metadata["approx_video_nframes"] is not None
|
|
666
671
|
and self.__sourcer_metadata["approx_video_nframes"] > 0
|
|
667
672
|
):
|
|
668
673
|
self.__raw_frame_num = self.__sourcer_metadata["approx_video_nframes"]
|
|
@@ -956,7 +961,7 @@ class FFdecoder:
|
|
|
956
961
|
)
|
|
957
962
|
if _src_meta.get("source_demuxer"):
|
|
958
963
|
cmd += ["-f", _src_meta["source_demuxer"]]
|
|
959
|
-
cmd += ["-i", _src]
|
|
964
|
+
cmd += ["-i", _src_meta.get("source", _src)]
|
|
960
965
|
else:
|
|
961
966
|
cmd += self.__ffmpeg_prefixes
|
|
962
967
|
cmd += input_parameters
|
|
@@ -42,8 +42,14 @@ logger.propagate = False
|
|
|
42
42
|
logger.addHandler(logger_handler())
|
|
43
43
|
logger.setLevel(logging.DEBUG)
|
|
44
44
|
|
|
45
|
+
# set default timeout for subprocesses
|
|
46
|
+
MAX_TIMEOUT_SUBPROCESS: float = float(os.getenv("MAX_TIMEOUT_SUBPROCESS", 10.0))
|
|
47
|
+
# set grace period (seconds) between SIGTERM and SIGKILL when terminating
|
|
48
|
+
# a timed-out subprocess. Allows FFmpeg to flush buffers and release hardware
|
|
49
|
+
# resources before being force-killed.
|
|
50
|
+
TERMINATE_TIMEOUT_SUBPROCESS: float = float(os.getenv("TERMINATE_TIMEOUT_SUBPROCESS", 2.0))
|
|
45
51
|
# set default timer for download requests
|
|
46
|
-
|
|
52
|
+
DEFAULT_TIMEOUT_REQUESTS: float = float(os.getenv("DEFAULT_TIMEOUT_REQUESTS", 3.0))
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
class TimeoutHTTPAdapter(HTTPAdapter):
|
|
@@ -52,7 +58,7 @@ class TimeoutHTTPAdapter(HTTPAdapter):
|
|
|
52
58
|
"""
|
|
53
59
|
|
|
54
60
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
55
|
-
self.timeout: float =
|
|
61
|
+
self.timeout: float = DEFAULT_TIMEOUT_REQUESTS
|
|
56
62
|
if "timeout" in kwargs:
|
|
57
63
|
self.timeout = kwargs["timeout"]
|
|
58
64
|
del kwargs["timeout"]
|
|
@@ -210,7 +216,9 @@ def download_ffmpeg_binaries(path: str, os_windows: bool = False, os_bit: str =
|
|
|
210
216
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
211
217
|
)
|
|
212
218
|
# Mount it for https usage
|
|
213
|
-
adapter = TimeoutHTTPAdapter(
|
|
219
|
+
adapter = TimeoutHTTPAdapter(
|
|
220
|
+
timeout=MAX_TIMEOUT_SUBPROCESS, max_retries=retries
|
|
221
|
+
)
|
|
214
222
|
http.mount("https://", adapter)
|
|
215
223
|
response = http.get(file_url, stream=True)
|
|
216
224
|
response.raise_for_status()
|
|
@@ -410,6 +418,7 @@ def extract_device_n_demuxer(
|
|
|
410
418
|
metadata = check_sp_output(
|
|
411
419
|
[path, *default_ffcommand.split(" ")],
|
|
412
420
|
force_retrieve_stderr=True,
|
|
421
|
+
timeout=MAX_TIMEOUT_SUBPROCESS,
|
|
413
422
|
)
|
|
414
423
|
# clean and split metadata
|
|
415
424
|
splitted = [x.decode("utf-8").strip() for x in metadata.split(b"\n")]
|
|
@@ -469,6 +478,7 @@ def extract_device_n_demuxer(
|
|
|
469
478
|
# search in path properties
|
|
470
479
|
metadata_path = check_sp_output(
|
|
471
480
|
["v4l2-ctl", "--device={}".format(path), "--all"],
|
|
481
|
+
timeout=MAX_TIMEOUT_SUBPROCESS,
|
|
472
482
|
)
|
|
473
483
|
# decode path metadata
|
|
474
484
|
decoded_path = metadata_path.decode("utf-8").strip()
|
|
@@ -488,6 +498,7 @@ def extract_device_n_demuxer(
|
|
|
488
498
|
metadata = check_sp_output(
|
|
489
499
|
[path, *default_ffcommand.split(" ")],
|
|
490
500
|
force_retrieve_stderr=True,
|
|
501
|
+
timeout=MAX_TIMEOUT_SUBPROCESS,
|
|
491
502
|
)
|
|
492
503
|
# clean and split metadata
|
|
493
504
|
splitted = [x.decode("utf-8").strip() for x in metadata.split(b"\n")]
|
|
@@ -641,44 +652,83 @@ def check_sp_output(*args: Any, **kwargs: Any) -> bytes:
|
|
|
641
652
|
"""
|
|
642
653
|
## check_sp_output
|
|
643
654
|
|
|
644
|
-
|
|
655
|
+
Executes a subprocess command and returns its `stdout` (or `stderr` when
|
|
656
|
+
requested). On timeout, performs a two-step graceful shutdown — sends
|
|
657
|
+
`SIGTERM` first to allow FFmpeg to flush buffers and release hardware
|
|
658
|
+
resources (decoders, capture devices), then escalates to `SIGKILL` if the
|
|
659
|
+
process fails to exit within a short grace period.
|
|
645
660
|
|
|
646
661
|
Parameters:
|
|
647
662
|
args (based on input): Non Keyword Arguments
|
|
648
663
|
kwargs (based on input): Keyword Arguments
|
|
664
|
+
force_retrieve_stderr (bool): If True, returns stderr. Also
|
|
665
|
+
suppresses `CalledProcessError` on non-zero exit, since some
|
|
666
|
+
FFmpeg diagnostic commands (e.g. `-list_devices`) emit useful
|
|
667
|
+
output on stderr while exiting non-zero by design.
|
|
668
|
+
timeout (float): Seconds to wait before terminating the process.
|
|
649
669
|
|
|
650
|
-
**Returns:** A
|
|
670
|
+
**Returns:** A bytes value.
|
|
651
671
|
"""
|
|
652
672
|
# workaround for python bug: https://bugs.python.org/issue37380
|
|
653
673
|
if platform.system() == "Windows":
|
|
654
674
|
# see comment https://bugs.python.org/msg370334
|
|
655
675
|
sp._cleanup = lambda: None
|
|
676
|
+
|
|
656
677
|
# handle additional params
|
|
657
678
|
retrieve_stderr = kwargs.pop("force_retrieve_stderr", False)
|
|
679
|
+
timeout = kwargs.pop("timeout", None)
|
|
680
|
+
|
|
658
681
|
# execute command in subprocess
|
|
659
682
|
process = sp.Popen(
|
|
660
683
|
*args,
|
|
661
684
|
stdout=sp.PIPE,
|
|
662
|
-
stderr=sp.
|
|
685
|
+
stderr=sp.PIPE if retrieve_stderr else sp.DEVNULL,
|
|
663
686
|
**kwargs,
|
|
664
687
|
)
|
|
665
|
-
|
|
666
|
-
|
|
688
|
+
|
|
689
|
+
# communicate and poll process with two-step timeout handling
|
|
690
|
+
timeout_occurred = False
|
|
691
|
+
try:
|
|
692
|
+
output, stderr = process.communicate(timeout=timeout)
|
|
693
|
+
except sp.TimeoutExpired:
|
|
694
|
+
timeout_occurred = True
|
|
695
|
+
logger.warning(
|
|
696
|
+
f"[Pipeline-Warning] :: Process exceeded timeout of {timeout}s. "
|
|
697
|
+
"Attempting graceful termination..."
|
|
698
|
+
)
|
|
699
|
+
# Step 1: polite SIGTERM, give process a chance to clean up
|
|
700
|
+
process.terminate()
|
|
701
|
+
try:
|
|
702
|
+
output, stderr = process.communicate(timeout=TERMINATE_TIMEOUT_SUBPROCESS)
|
|
703
|
+
except sp.TimeoutExpired:
|
|
704
|
+
# Step 2: process ignored SIGTERM, force kill
|
|
705
|
+
logger.error("[Pipeline-Error] :: Process unresponsive to SIGTERM. Hard killing...")
|
|
706
|
+
process.kill()
|
|
707
|
+
output, stderr = process.communicate()
|
|
708
|
+
|
|
667
709
|
retcode = process.poll()
|
|
710
|
+
|
|
668
711
|
# handle return code
|
|
669
|
-
if
|
|
670
|
-
|
|
712
|
+
# Bypass CalledProcessError if caller wants stderr (some FFmpeg commands
|
|
713
|
+
# exit non-zero by design) or if we killed the process via our timeout.
|
|
714
|
+
if retcode and not retrieve_stderr and not timeout_occurred:
|
|
715
|
+
logger.error(
|
|
716
|
+
"[Pipeline-Error] :: {}".format(output.decode("utf-8") if output else "No output")
|
|
717
|
+
)
|
|
671
718
|
cmd = kwargs.get("args")
|
|
672
719
|
if cmd is None:
|
|
673
720
|
cmd = args[0]
|
|
674
721
|
error = sp.CalledProcessError(retcode, cmd)
|
|
675
722
|
error.output = output
|
|
676
723
|
raise error
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
724
|
+
|
|
725
|
+
# warn if process emitted nothing on either stream
|
|
726
|
+
if not (bool(output) or bool(stderr)):
|
|
727
|
+
logger.error(
|
|
728
|
+
"[Pipeline-Error] :: Pipeline failed to extract any data from command: {}!".format(
|
|
729
|
+
args[0] if args else []
|
|
730
|
+
)
|
|
681
731
|
)
|
|
682
|
-
|
|
683
|
-
# return
|
|
732
|
+
|
|
733
|
+
# return stderr when explicitly requested (and present), else stdout
|
|
684
734
|
return stderr if retrieve_stderr and stderr else output
|
|
@@ -28,11 +28,12 @@ import os
|
|
|
28
28
|
import platform
|
|
29
29
|
import re
|
|
30
30
|
import shutil
|
|
31
|
-
from typing import Any
|
|
31
|
+
from typing import Any, Self
|
|
32
32
|
|
|
33
33
|
import numpy as np
|
|
34
34
|
|
|
35
35
|
from .ffhelper import (
|
|
36
|
+
MAX_TIMEOUT_SUBPROCESS,
|
|
36
37
|
check_sp_output,
|
|
37
38
|
extract_device_n_demuxer,
|
|
38
39
|
get_supported_demuxers,
|
|
@@ -294,7 +295,7 @@ class Sourcer:
|
|
|
294
295
|
# check whether metadata probed or not?
|
|
295
296
|
self.__metadata_probed = False
|
|
296
297
|
|
|
297
|
-
def probe_stream(self, default_stream_indexes: list[int] | tuple[int, int] = (0, 0)) ->
|
|
298
|
+
def probe_stream(self, default_stream_indexes: list[int] | tuple[int, int] = (0, 0)) -> Self:
|
|
298
299
|
"""
|
|
299
300
|
This method Parses/Probes FFmpeg `subprocess` pipe's Standard Output for given input source and Populates the information in private class variables.
|
|
300
301
|
|
|
@@ -308,6 +309,40 @@ class Sourcer:
|
|
|
308
309
|
and len(default_stream_indexes) == 2
|
|
309
310
|
and all(isinstance(x, int) for x in default_stream_indexes)
|
|
310
311
|
), "Invalid default_stream_indexes value!"
|
|
312
|
+
# probe extra inputs first via independent single-source Sourcers, so
|
|
313
|
+
# their resolved source/demuxer (e.g. camera index -> device) can be
|
|
314
|
+
# reused by the combined filtergraph probe below.
|
|
315
|
+
extra_metadata = []
|
|
316
|
+
if self.__is_multi:
|
|
317
|
+
for idx in range(1, len(self.__source_list)):
|
|
318
|
+
_src = self.__source_list[idx]
|
|
319
|
+
_demux = self.__source_demuxer_list[idx]
|
|
320
|
+
_prefixes = self.__ffmpeg_prefixes_list[idx]
|
|
321
|
+
_params = self.__sourcer_params.copy()
|
|
322
|
+
_params["-ffprefixes"] = _prefixes
|
|
323
|
+
# output routing references all inputs; meaningless per-source
|
|
324
|
+
for _key in ("-vf", "-filter_complex", "-map"):
|
|
325
|
+
_params.pop(_key, None)
|
|
326
|
+
# Resolve to an absolute path: on Unix `self.__ffmpeg` may be
|
|
327
|
+
# the bare command "ffmpeg" found via PATH, which the nested
|
|
328
|
+
# `get_valid_ffmpeg_path()` would reject as "not a file".
|
|
329
|
+
_custom_ffmpeg = (
|
|
330
|
+
self.__ffmpeg
|
|
331
|
+
if self.__ffmpeg and os.path.isfile(self.__ffmpeg)
|
|
332
|
+
else (shutil.which(self.__ffmpeg) or "")
|
|
333
|
+
)
|
|
334
|
+
_s = Sourcer(
|
|
335
|
+
_src,
|
|
336
|
+
source_demuxer=_demux,
|
|
337
|
+
custom_ffmpeg=_custom_ffmpeg,
|
|
338
|
+
verbose=self.__verbose_logs,
|
|
339
|
+
**_params,
|
|
340
|
+
)
|
|
341
|
+
_s.probe_stream(default_stream_indexes)
|
|
342
|
+
extra_metadata.append(_s.retrieve_metadata(force_retrieve_missing=True))
|
|
343
|
+
self.__resolved_extra_inputs = [
|
|
344
|
+
(m[0]["source"], m[0].get("source_demuxer")) for m in extra_metadata
|
|
345
|
+
]
|
|
311
346
|
# validate source and extract metadata
|
|
312
347
|
self.__ffsp_output = self.__validate_source(
|
|
313
348
|
self.__source,
|
|
@@ -394,34 +429,7 @@ class Sourcer:
|
|
|
394
429
|
# the guard inside retrieve_metadata (checks for non-empty
|
|
395
430
|
# __multi_source_metadata) prevents `sources: []` self-pollution.
|
|
396
431
|
self.__multi_source_metadata.append(self.retrieve_metadata(force_retrieve_missing=True))
|
|
397
|
-
|
|
398
|
-
_src = self.__source_list[idx]
|
|
399
|
-
_demux = self.__source_demuxer_list[idx]
|
|
400
|
-
_prefixes = self.__ffmpeg_prefixes_list[idx]
|
|
401
|
-
_params = self.__sourcer_params.copy()
|
|
402
|
-
_params["-ffprefixes"] = _prefixes
|
|
403
|
-
# spawn an independent single-source Sourcer per extra input;
|
|
404
|
-
# this reuses the resolved ffmpeg path and isolates per-source
|
|
405
|
-
# parsing state (which probe_stream otherwise clobbers).
|
|
406
|
-
# Resolve to an absolute path: on Unix `self.__ffmpeg` may be
|
|
407
|
-
# the bare command "ffmpeg" found via PATH, which the nested
|
|
408
|
-
# `get_valid_ffmpeg_path()` would reject as "not a file".
|
|
409
|
-
_custom_ffmpeg = (
|
|
410
|
-
self.__ffmpeg
|
|
411
|
-
if self.__ffmpeg and os.path.isfile(self.__ffmpeg)
|
|
412
|
-
else (shutil.which(self.__ffmpeg) or "")
|
|
413
|
-
)
|
|
414
|
-
_s = Sourcer(
|
|
415
|
-
_src,
|
|
416
|
-
source_demuxer=_demux,
|
|
417
|
-
custom_ffmpeg=_custom_ffmpeg,
|
|
418
|
-
verbose=self.__verbose_logs,
|
|
419
|
-
**_params,
|
|
420
|
-
)
|
|
421
|
-
_s.probe_stream(default_stream_indexes)
|
|
422
|
-
self.__multi_source_metadata.append(
|
|
423
|
-
_s.retrieve_metadata(force_retrieve_missing=True)
|
|
424
|
-
)
|
|
432
|
+
self.__multi_source_metadata.extend(extra_metadata)
|
|
425
433
|
|
|
426
434
|
# return reference to the instance object.
|
|
427
435
|
return self
|
|
@@ -671,6 +679,20 @@ class Sourcer:
|
|
|
671
679
|
+ self.__ffmpeg_prefixes
|
|
672
680
|
+ (["-f", source_demuxer] if source_demuxer else [])
|
|
673
681
|
+ ["-i", source]
|
|
682
|
+
# multi-input filtergraphs reference every input (e.g. `[1:v]`),
|
|
683
|
+
# so the probe must ingest all of them to resolve output metadata.
|
|
684
|
+
+ [
|
|
685
|
+
arg
|
|
686
|
+
for (src, demux), prefixes in zip(
|
|
687
|
+
self.__resolved_extra_inputs,
|
|
688
|
+
self.__ffmpeg_prefixes_list[1:],
|
|
689
|
+
strict=True,
|
|
690
|
+
)
|
|
691
|
+
for arg in ["-t", "0.0001"]
|
|
692
|
+
+ prefixes
|
|
693
|
+
+ (["-f", demux] if demux else [])
|
|
694
|
+
+ ["-i", src]
|
|
695
|
+
]
|
|
674
696
|
+ dict2Args(self.__sourcer_params)
|
|
675
697
|
+ ["-f", "null", "-"]
|
|
676
698
|
)
|
|
@@ -687,13 +709,16 @@ class Sourcer:
|
|
|
687
709
|
check_sp_output(
|
|
688
710
|
meta_cmd,
|
|
689
711
|
force_retrieve_stderr=True,
|
|
712
|
+
timeout=MAX_TIMEOUT_SUBPROCESS,
|
|
690
713
|
)
|
|
691
714
|
.decode("utf-8")
|
|
692
715
|
.strip()
|
|
693
716
|
)
|
|
694
717
|
# separate input and output metadata (if available)
|
|
695
718
|
if "Output #" in metadata:
|
|
696
|
-
|
|
719
|
+
metadata, self.__metadata_output = metadata.split("Output #")
|
|
720
|
+
# primary `source_*` metadata must only describe input 0
|
|
721
|
+
metadata = re.split(r"^Input #1\b", metadata, maxsplit=1, flags=re.M)[0]
|
|
697
722
|
# return metadata based on params
|
|
698
723
|
return metadata
|
|
699
724
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__: str = "0.2.8"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deffcode
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: A cross-platform High-performance & Flexible Real-time Video Frames Decoder in Python.
|
|
5
5
|
Author-email: Abhishek Thakur <abhi.una12@gmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -22,11 +22,11 @@ Classifier: Intended Audience :: Developers
|
|
|
22
22
|
Classifier: Intended Audience :: Science/Research
|
|
23
23
|
Classifier: Intended Audience :: Education
|
|
24
24
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
27
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
28
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
29
|
-
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
Requires-Dist: cython
|
|
@@ -113,7 +113,7 @@ Here are some key features that stand out:
|
|
|
113
113
|
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
|
|
114
114
|
- Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
|
|
115
115
|
- Out-of-the-box support for all prominent Computer Vision libraries.
|
|
116
|
-
- Cross-platform, runs on Python 3.
|
|
116
|
+
- Cross-platform, runs on Python 3.11+, and easy to install.
|
|
117
117
|
|
|
118
118
|
<!--
|
|
119
119
|
- [x] Lossless Transcoding support with [WriteGear](https://abhitronix.github.io/deffcode/latest/gears/writegear/introduction/). #TODO
|
|
@@ -235,6 +235,8 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
235
235
|
- [CUDA-NVENC-accelerated Video Transcoding with WriteGear API][cuda-nvenc-accelerated-video-transcoding-with-writegear-api]
|
|
236
236
|
- [CUDA-NVENC-accelerated End-to-end Lossless Video Transcoding with WriteGear API][cuda-nvenc-accelerated-end-to-end-lossless-video-transcoding-with-writegear-api]
|
|
237
237
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
238
|
+
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
239
|
+
- [Decoding crossfade transition between multiple inputs][decoding-crossfade-transition-between-multiple-inputs]
|
|
238
240
|
|
|
239
241
|
</details>
|
|
240
242
|
|
|
@@ -271,9 +273,8 @@ decoder = FFdecoder("https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1
|
|
|
271
273
|
|
|
272
274
|
# grab RGB24(default) 3D frames from decoder
|
|
273
275
|
for frame in decoder.generateFrame():
|
|
274
|
-
|
|
275
276
|
# lets print its shape
|
|
276
|
-
print(frame.shape)
|
|
277
|
+
print(frame.shape) # (1080, 1920, 3)
|
|
277
278
|
|
|
278
279
|
# terminate the decoder
|
|
279
280
|
decoder.terminate()
|
|
@@ -288,7 +289,9 @@ The primary function of [**Sourcer API**][sourcer-api] is to **gather metadata i
|
|
|
288
289
|
from deffcode import Sourcer
|
|
289
290
|
|
|
290
291
|
# initialize and formulate the decoder using suitable source
|
|
291
|
-
sourcer = Sourcer(
|
|
292
|
+
sourcer = Sourcer(
|
|
293
|
+
"https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1MB.mp4"
|
|
294
|
+
).probe_stream()
|
|
292
295
|
|
|
293
296
|
# print metadata as `json.dump`
|
|
294
297
|
print(sourcer.retrieve_metadata(pretty_json=True))
|
|
@@ -354,18 +357,24 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
354
357
|
|
|
355
358
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
356
359
|
|
|
357
|
-
[](https://doi.org/10.5281/zenodo.19658542)
|
|
358
361
|
|
|
359
362
|
```BibTeX
|
|
360
363
|
@software{deffcode,
|
|
361
364
|
author = {Abhishek Thakur},
|
|
362
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
363
|
-
month =
|
|
364
|
-
year =
|
|
365
|
+
title = {abhiTronix/deffcode: v0.2.7},
|
|
366
|
+
month = apr,
|
|
367
|
+
year = 2026,
|
|
365
368
|
publisher = {Zenodo},
|
|
366
|
-
version = {v0.2.
|
|
367
|
-
doi = {10.5281/zenodo.
|
|
368
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
369
|
+
version = {v0.2.7},
|
|
370
|
+
doi = {10.5281/zenodo.19658542},
|
|
371
|
+
url = {https://doi.org/10.5281/zenodo.19658542},
|
|
372
|
+
swhid = {swh:1:dir:85e8dfd087aed870deb4043c532faaa9b156d1e1
|
|
373
|
+
;origin=https://doi.org/10.5281/zenodo.6976947;vis
|
|
374
|
+
it=swh:1:snp:af1b478890801c02d56b95f563b86761980bf
|
|
375
|
+
82c;anchor=swh:1:rel:85f9af4452f8030317ec2012acc10
|
|
376
|
+
29e8a945e2d;path=abhiTronix-deffcode-5162649
|
|
377
|
+
},
|
|
369
378
|
}
|
|
370
379
|
```
|
|
371
380
|
|
|
@@ -478,6 +487,8 @@ Advanced Recipes
|
|
|
478
487
|
[smart-keyframe-only-decoding-for-heavy-ai-inference]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/extract-frame-metadata/#smart-keyframe-only-decoding-for-heavy-ai-inference
|
|
479
488
|
[variable-frame-rate-vfr-synchronization-via-pts_time]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/extract-frame-metadata/#variable-frame-rate-vfr-synchronization-via-pts_time
|
|
480
489
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
490
|
+
[decoding-multiple-inputs-as-2x2-video-grid-mosaic]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-multiple-inputs-as-2x2-video-grid-mosaic
|
|
491
|
+
[decoding-crossfade-transition-between-multiple-inputs]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-crossfade-transition-between-multiple-inputs
|
|
481
492
|
|
|
482
493
|
<!--
|
|
483
494
|
CI Apps URLs
|
|
@@ -7,7 +7,7 @@ name = "deffcode"
|
|
|
7
7
|
description = "A cross-platform High-performance & Flexible Real-time Video Frames Decoder in Python."
|
|
8
8
|
authors = [{name = "Abhishek Thakur", email = "abhi.una12@gmail.com"}]
|
|
9
9
|
license = {text = "Apache License 2.0"}
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
keywords = [
|
|
12
12
|
"FFmpeg",
|
|
13
13
|
"Decoder",
|
|
@@ -30,10 +30,10 @@ classifiers = [
|
|
|
30
30
|
"Intended Audience :: Science/Research",
|
|
31
31
|
"Intended Audience :: Education",
|
|
32
32
|
"License :: OSI Approved :: Apache Software License",
|
|
33
|
-
"Programming Language :: Python :: 3.10",
|
|
34
33
|
"Programming Language :: Python :: 3.11",
|
|
35
34
|
"Programming Language :: Python :: 3.12",
|
|
36
35
|
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
37
|
]
|
|
38
38
|
dependencies = [
|
|
39
39
|
"cython",
|
|
@@ -58,9 +58,14 @@ packages = ["deffcode"]
|
|
|
58
58
|
[tool.setuptools.dynamic]
|
|
59
59
|
version = {attr = "deffcode.version.__version__"}
|
|
60
60
|
|
|
61
|
+
[tool.poetry]
|
|
62
|
+
# Poetry can't read setuptools' dynamic version,
|
|
63
|
+
# keep in sync with deffcode/version.py
|
|
64
|
+
version = "0.2.8"
|
|
65
|
+
|
|
61
66
|
[tool.ruff]
|
|
62
67
|
line-length = 100
|
|
63
|
-
target-version = "
|
|
68
|
+
target-version = "py311"
|
|
64
69
|
extend-exclude = [
|
|
65
70
|
"build",
|
|
66
71
|
"dist",
|
|
@@ -667,7 +667,7 @@ def test_FFdecoder_params(source: str | list[str], ffparams: dict[str, Any], res
|
|
|
667
667
|
# prepare OpenCV parameters
|
|
668
668
|
FOURCC = cv2.VideoWriter_fourcc("M", "J", "P", "G")
|
|
669
669
|
FRAMERATE = metadata_dict["source_video_framerate"]
|
|
670
|
-
FRAMESIZE = tuple(metadata_dict["
|
|
670
|
+
FRAMESIZE = tuple(metadata_dict["output_frames_resolution"])
|
|
671
671
|
|
|
672
672
|
# Define writer with parameters and suitable output filename for e.g. `output_foo.avi`
|
|
673
673
|
writer = cv2.VideoWriter(f_name, FOURCC, FRAMERATE, FRAMESIZE)
|
|
@@ -848,3 +848,39 @@ def test_discard_n_filter_params(frame_format: str, ffparams: dict[str, Any], re
|
|
|
848
848
|
finally:
|
|
849
849
|
# terminate
|
|
850
850
|
decoder is not None and decoder.terminate()
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
_grid = "[0:v][1:v][2:v][3:v]xstack=inputs=4:layout=0_0|w0_0|0_h0|w0_h0[out]"
|
|
854
|
+
test_data = [
|
|
855
|
+
(["320x240"] * 2, {"-filter_complex": "hstack=inputs=2"}, (240, 640), 50),
|
|
856
|
+
(
|
|
857
|
+
["320x240", "160x120"],
|
|
858
|
+
{"-filter_complex": "[1:v]scale=80:-1[s];[0:v][s]overlay=W-w-10:H-h-10"},
|
|
859
|
+
(240, 320),
|
|
860
|
+
50,
|
|
861
|
+
),
|
|
862
|
+
(["320x240"] * 4, {"-filter_complex": _grid, "-map": "[out]"}, (480, 640), 50),
|
|
863
|
+
(
|
|
864
|
+
["320x240"] * 2,
|
|
865
|
+
{"-filter_complex": "[0:v][1:v]xfade=duration=1:offset=1[v]", "-map": "[v]"},
|
|
866
|
+
(240, 320),
|
|
867
|
+
75,
|
|
868
|
+
),
|
|
869
|
+
(["320x240", "160x120"], {"-map": "1:v"}, (120, 160), 50),
|
|
870
|
+
]
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
@pytest.mark.parametrize("sizes, ffparams, shape, n_frames", test_data)
|
|
874
|
+
def test_multi_input_composites(
|
|
875
|
+
sizes: list[str], ffparams: dict[str, Any], shape: tuple[int, int], n_frames: int
|
|
876
|
+
) -> None:
|
|
877
|
+
"""
|
|
878
|
+
Testing multi-input filtergraph recipes yield frames of the composite size and length
|
|
879
|
+
"""
|
|
880
|
+
source = ["testsrc=size={}:rate=25:duration=2".format(s) for s in sizes]
|
|
881
|
+
with FFdecoder(
|
|
882
|
+
source, source_demuxer=["lavfi"] * len(sizes), frame_format="bgr24", **ffparams
|
|
883
|
+
) as decoder:
|
|
884
|
+
frames = [f for f in decoder.generateFrame() if f is not None]
|
|
885
|
+
assert frames and frames[0].shape == (*shape, 3)
|
|
886
|
+
assert abs(len(frames) - n_frames) <= 1
|
|
@@ -145,9 +145,70 @@ def test_get_valid_ffmpeg_path(paths: str, ffmpeg_download_paths: str, results:
|
|
|
145
145
|
@pytest.mark.xfail(raises=Exception)
|
|
146
146
|
def test_check_sp_output() -> None:
|
|
147
147
|
"""
|
|
148
|
-
Testing check_sp_output method
|
|
148
|
+
Testing check_sp_output method raises CalledProcessError on invalid flags.
|
|
149
149
|
"""
|
|
150
|
-
check_sp_output(["ffmpeg", "-Vv"])
|
|
150
|
+
check_sp_output(["ffmpeg", "-Vv"], timeout=2.0)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_check_sp_output_happy_path() -> None:
|
|
154
|
+
"""
|
|
155
|
+
Testing check_sp_output returns stdout bytes for a successful command.
|
|
156
|
+
"""
|
|
157
|
+
output = check_sp_output([return_static_ffmpeg(), "-version"], timeout=5.0)
|
|
158
|
+
assert isinstance(output, bytes) and len(output) > 0, "Expected non-empty stdout"
|
|
159
|
+
assert b"ffmpeg version" in output, "Expected ffmpeg version banner in stdout"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_check_sp_output_force_retrieve_stderr_bypass() -> None:
|
|
163
|
+
"""
|
|
164
|
+
Testing that `force_retrieve_stderr=True` suppresses CalledProcessError on
|
|
165
|
+
non-zero exit and returns stderr bytes. Mirrors how `extract_device_n_demuxer`
|
|
166
|
+
consumes `ffmpeg -list_devices` output (which exits non-zero by design).
|
|
167
|
+
"""
|
|
168
|
+
# `-list_devices true -i dummy` exits non-zero on every platform
|
|
169
|
+
cmd = [
|
|
170
|
+
return_static_ffmpeg(),
|
|
171
|
+
"-hide_banner",
|
|
172
|
+
"-list_devices",
|
|
173
|
+
"-i",
|
|
174
|
+
"dummy",
|
|
175
|
+
]
|
|
176
|
+
stderr = check_sp_output(cmd, force_retrieve_stderr=True, timeout=5.0)
|
|
177
|
+
assert isinstance(stderr, bytes), "Expected bytes return on stderr bypass"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_check_sp_output_graceful_timeout() -> None:
|
|
181
|
+
"""
|
|
182
|
+
Testing two-step timeout handling: a long-running FFmpeg process must be
|
|
183
|
+
terminated within roughly `timeout + grace_period` seconds and must NOT
|
|
184
|
+
raise CalledProcessError (timeout kills are bypassed by design).
|
|
185
|
+
"""
|
|
186
|
+
import time
|
|
187
|
+
|
|
188
|
+
# null source → null sink loop runs forever; ensures we hit the timeout
|
|
189
|
+
cmd = [
|
|
190
|
+
return_static_ffmpeg(),
|
|
191
|
+
"-hide_banner",
|
|
192
|
+
"-loglevel",
|
|
193
|
+
"error",
|
|
194
|
+
"-f",
|
|
195
|
+
"lavfi",
|
|
196
|
+
"-i",
|
|
197
|
+
"nullsrc",
|
|
198
|
+
"-t",
|
|
199
|
+
"60",
|
|
200
|
+
"-f",
|
|
201
|
+
"null",
|
|
202
|
+
"-",
|
|
203
|
+
]
|
|
204
|
+
timeout = 1.0
|
|
205
|
+
start = time.monotonic()
|
|
206
|
+
output = check_sp_output(cmd, timeout=timeout)
|
|
207
|
+
elapsed = time.monotonic() - start
|
|
208
|
+
# must return bytes (possibly empty), no exception
|
|
209
|
+
assert isinstance(output, bytes), "Expected bytes return on graceful timeout"
|
|
210
|
+
# must complete within timeout + 2s grace + reasonable overhead
|
|
211
|
+
assert elapsed < timeout + 5.0, f"Process took {elapsed:.2f}s — graceful shutdown likely stuck"
|
|
151
212
|
|
|
152
213
|
|
|
153
214
|
@pytest.mark.parametrize(
|
|
@@ -196,3 +196,28 @@ def test_probe_stream_n_retrieve_metadata(
|
|
|
196
196
|
pytest.xfail("Test Still Passed!")
|
|
197
197
|
else:
|
|
198
198
|
pytest.fail(str(e))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_multi_input_primary_metadata() -> None:
|
|
202
|
+
"""
|
|
203
|
+
Primary `source_*` metadata must describe input 0 only, even with a
|
|
204
|
+
nonzero default video stream index and a combined filtergraph probe.
|
|
205
|
+
"""
|
|
206
|
+
metadata = (
|
|
207
|
+
Sourcer(
|
|
208
|
+
[
|
|
209
|
+
"testsrc=size=320x240:rate=25:duration=2",
|
|
210
|
+
"testsrc=size=160x120:rate=10:duration=2",
|
|
211
|
+
],
|
|
212
|
+
source_demuxer="lavfi",
|
|
213
|
+
**{"-filter_complex": "[0:v][1:v]overlay"},
|
|
214
|
+
)
|
|
215
|
+
.probe_stream(default_stream_indexes=(1, 0))
|
|
216
|
+
.retrieve_metadata()
|
|
217
|
+
)
|
|
218
|
+
assert metadata["source_video_resolution"] == [320, 240]
|
|
219
|
+
assert metadata["source_video_framerate"] == 25.0
|
|
220
|
+
assert [s["source_video_resolution"] for s in metadata["sources"]] == [
|
|
221
|
+
[320, 240],
|
|
222
|
+
[160, 120],
|
|
223
|
+
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__: str = "0.2.7"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|