deffcode 0.2.8__tar.gz → 0.2.9__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.8 → deffcode-0.2.9}/PKG-INFO +14 -13
- {deffcode-0.2.8 → deffcode-0.2.9}/README.md +13 -12
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode/ffdecoder.py +124 -26
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode/ffhelper.py +2 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode/sourcer.py +1 -1
- deffcode-0.2.9/deffcode/version.py +1 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode.egg-info/PKG-INFO +14 -13
- {deffcode-0.2.8 → deffcode-0.2.9}/pyproject.toml +1 -1
- {deffcode-0.2.8 → deffcode-0.2.9}/tests/test_ffdecoder.py +210 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/tests/test_ffhelper.py +17 -0
- deffcode-0.2.8/deffcode/version.py +0 -1
- {deffcode-0.2.8 → deffcode-0.2.9}/LICENSE +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode/__init__.py +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode/utils.py +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode.egg-info/SOURCES.txt +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode.egg-info/dependency_links.txt +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode.egg-info/requires.txt +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/deffcode.egg-info/top_level.txt +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/setup.cfg +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/setup.py +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/tests/test_sourcer.py +0 -0
- {deffcode-0.2.8 → deffcode-0.2.9}/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.9
|
|
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
|
|
@@ -108,6 +108,7 @@ Here are some key features that stand out:
|
|
|
108
108
|
- Hands down the easiest [**Index based Camera Device Capturing**][decoding-camera-devices-using-indexes], similar to OpenCV.
|
|
109
109
|
- Easy to code **Real-time [Simple][transcoding-live-simple-filtergraphs] & [Complex][transcoding-live-complex-filtergraphs] Filtergraphs**. _(Yes, You read it correctly "Real-time"!)_
|
|
110
110
|
- Native **[Multi-Input Source Configurations][multi-input-source-configurations]** support for decoding complex topologies.
|
|
111
|
+
- Native **[Multi-Outputs Configurations][multi-outputs-configurations]** support for recording/streaming while decoding, in a single FFmpeg pipeline.
|
|
111
112
|
- Lightning fast dedicated **GPU-Accelerated Video [Decoding][hardware-accelerated-video-decoding] & [Transcoding][hardware-accelerated-video-transcoding]**.
|
|
112
113
|
- Enables precise FFmpeg [**Key-frame Seeking**][extracting-key-frames-as-png-image] with pinpoint accuracy.
|
|
113
114
|
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
|
|
@@ -237,6 +238,9 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
237
238
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
238
239
|
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
239
240
|
- [Decoding crossfade transition between multiple inputs][decoding-crossfade-transition-between-multiple-inputs]
|
|
241
|
+
- [Multi-Outputs Configurations][multi-outputs-configurations]
|
|
242
|
+
- [Recording while decoding][recording-while-decoding]
|
|
243
|
+
- [Encode once, duplicate to file and stream][encode-once-duplicate-to-file-and-stream]
|
|
240
244
|
|
|
241
245
|
</details>
|
|
242
246
|
|
|
@@ -357,24 +361,18 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
357
361
|
|
|
358
362
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
359
363
|
|
|
360
|
-
[](https://doi.org/10.5281/zenodo.23446604)
|
|
361
365
|
|
|
362
366
|
```BibTeX
|
|
363
367
|
@software{deffcode,
|
|
364
368
|
author = {Abhishek Thakur},
|
|
365
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
366
|
-
month =
|
|
369
|
+
title = {abhiTronix/deffcode: v0.2.8},
|
|
370
|
+
month = oct,
|
|
367
371
|
year = 2026,
|
|
368
372
|
publisher = {Zenodo},
|
|
369
|
-
version = {v0.2.
|
|
370
|
-
doi = {10.5281/zenodo.
|
|
371
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
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
|
-
},
|
|
373
|
+
version = {v0.2.8},
|
|
374
|
+
doi = {10.5281/zenodo.23446604},
|
|
375
|
+
url = {https://doi.org/10.5281/zenodo.23446604},
|
|
378
376
|
}
|
|
379
377
|
```
|
|
380
378
|
|
|
@@ -489,6 +487,9 @@ Advanced Recipes
|
|
|
489
487
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
490
488
|
[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
489
|
[decoding-crossfade-transition-between-multiple-inputs]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-crossfade-transition-between-multiple-inputs
|
|
490
|
+
[multi-outputs-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#multi-outputs-configurations
|
|
491
|
+
[recording-while-decoding]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#recording-while-decoding
|
|
492
|
+
[encode-once-duplicate-to-file-and-stream]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#encode-once-duplicate-to-file-and-stream
|
|
492
493
|
|
|
493
494
|
<!--
|
|
494
495
|
CI Apps URLs
|
|
@@ -68,6 +68,7 @@ Here are some key features that stand out:
|
|
|
68
68
|
- Hands down the easiest [**Index based Camera Device Capturing**][decoding-camera-devices-using-indexes], similar to OpenCV.
|
|
69
69
|
- Easy to code **Real-time [Simple][transcoding-live-simple-filtergraphs] & [Complex][transcoding-live-complex-filtergraphs] Filtergraphs**. _(Yes, You read it correctly "Real-time"!)_
|
|
70
70
|
- Native **[Multi-Input Source Configurations][multi-input-source-configurations]** support for decoding complex topologies.
|
|
71
|
+
- Native **[Multi-Outputs Configurations][multi-outputs-configurations]** support for recording/streaming while decoding, in a single FFmpeg pipeline.
|
|
71
72
|
- Lightning fast dedicated **GPU-Accelerated Video [Decoding][hardware-accelerated-video-decoding] & [Transcoding][hardware-accelerated-video-transcoding]**.
|
|
72
73
|
- Enables precise FFmpeg [**Key-frame Seeking**][extracting-key-frames-as-png-image] with pinpoint accuracy.
|
|
73
74
|
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
|
|
@@ -197,6 +198,9 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
197
198
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
198
199
|
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
199
200
|
- [Decoding crossfade transition between multiple inputs][decoding-crossfade-transition-between-multiple-inputs]
|
|
201
|
+
- [Multi-Outputs Configurations][multi-outputs-configurations]
|
|
202
|
+
- [Recording while decoding][recording-while-decoding]
|
|
203
|
+
- [Encode once, duplicate to file and stream][encode-once-duplicate-to-file-and-stream]
|
|
200
204
|
|
|
201
205
|
</details>
|
|
202
206
|
|
|
@@ -317,24 +321,18 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
317
321
|
|
|
318
322
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
319
323
|
|
|
320
|
-
[](https://doi.org/10.5281/zenodo.23446604)
|
|
321
325
|
|
|
322
326
|
```BibTeX
|
|
323
327
|
@software{deffcode,
|
|
324
328
|
author = {Abhishek Thakur},
|
|
325
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
326
|
-
month =
|
|
329
|
+
title = {abhiTronix/deffcode: v0.2.8},
|
|
330
|
+
month = oct,
|
|
327
331
|
year = 2026,
|
|
328
332
|
publisher = {Zenodo},
|
|
329
|
-
version = {v0.2.
|
|
330
|
-
doi = {10.5281/zenodo.
|
|
331
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
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
|
-
},
|
|
333
|
+
version = {v0.2.8},
|
|
334
|
+
doi = {10.5281/zenodo.23446604},
|
|
335
|
+
url = {https://doi.org/10.5281/zenodo.23446604},
|
|
338
336
|
}
|
|
339
337
|
```
|
|
340
338
|
|
|
@@ -449,6 +447,9 @@ Advanced Recipes
|
|
|
449
447
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
450
448
|
[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
449
|
[decoding-crossfade-transition-between-multiple-inputs]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-crossfade-transition-between-multiple-inputs
|
|
450
|
+
[multi-outputs-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#multi-outputs-configurations
|
|
451
|
+
[recording-while-decoding]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#recording-while-decoding
|
|
452
|
+
[encode-once-duplicate-to-file-and-stream]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#encode-once-duplicate-to-file-and-stream
|
|
452
453
|
|
|
453
454
|
<!--
|
|
454
455
|
CI Apps URLs
|
|
@@ -26,9 +26,11 @@ import platform
|
|
|
26
26
|
import queue
|
|
27
27
|
import re
|
|
28
28
|
import subprocess as sp
|
|
29
|
+
import sys
|
|
29
30
|
import threading
|
|
30
|
-
from collections import OrderedDict
|
|
31
|
+
from collections import OrderedDict, deque
|
|
31
32
|
from collections.abc import Generator
|
|
33
|
+
from itertools import pairwise
|
|
32
34
|
from types import TracebackType
|
|
33
35
|
from typing import Any, Self
|
|
34
36
|
|
|
@@ -36,6 +38,7 @@ import numpy as np
|
|
|
36
38
|
|
|
37
39
|
# import utils packages
|
|
38
40
|
from .ffhelper import (
|
|
41
|
+
TERMINATE_TIMEOUT_SUBPROCESS,
|
|
39
42
|
get_supported_pixfmts,
|
|
40
43
|
get_supported_vdecoders,
|
|
41
44
|
)
|
|
@@ -207,6 +210,15 @@ class FFdecoder:
|
|
|
207
210
|
self.__ffmpeg_prefixes = []
|
|
208
211
|
sourcer_params["-ffprefixes"] = self.__ffmpeg_prefixes
|
|
209
212
|
|
|
213
|
+
# handles user-defined additional FFmpeg outputs, appended after the rawvideo pipe output (must be a flat list)
|
|
214
|
+
self.__extra_outputs = self.__extra_params.pop("-extra_outputs", [])
|
|
215
|
+
if not isinstance(self.__extra_outputs, list) or any(
|
|
216
|
+
isinstance(x, (list, tuple, dict)) for x in self.__extra_outputs
|
|
217
|
+
):
|
|
218
|
+
logger.warning("Discarding invalid `-extra_outputs` value! It must be a flat list.")
|
|
219
|
+
self.__extra_outputs = []
|
|
220
|
+
self.__extra_outputs = [str(x) for x in self.__extra_outputs]
|
|
221
|
+
|
|
210
222
|
# pass parameter(if specified) to Sourcer API, specifying where to save the downloaded FFmpeg Static
|
|
211
223
|
# assets on Windows(if specified)
|
|
212
224
|
sourcer_params["-ffmpeg_download_path"] = self.__extra_params.pop(
|
|
@@ -229,6 +241,17 @@ class FFdecoder:
|
|
|
229
241
|
# multi-input output streams are selected via `-map`, so probe with it too
|
|
230
242
|
if self.__is_multi and "-map" in self.__extra_params:
|
|
231
243
|
sourcer_params["-map"] = self.__extra_params["-map"]
|
|
244
|
+
# filtergraph labels consumed only by `-extra_outputs` must also be consumed while
|
|
245
|
+
# probing, otherwise FFmpeg aborts with "unconnected output"; map all into the probe sink.
|
|
246
|
+
if self.__extra_outputs and "-filter_complex" in sourcer_params:
|
|
247
|
+
if "-map" in self.__extra_params:
|
|
248
|
+
sourcer_params["-map"] = self.__extra_params["-map"]
|
|
249
|
+
sourcer_params["-clones"] = [
|
|
250
|
+
arg
|
|
251
|
+
for key, val in pairwise(self.__extra_outputs)
|
|
252
|
+
if key == "-map" and val.startswith("[")
|
|
253
|
+
for arg in (key, val)
|
|
254
|
+
]
|
|
232
255
|
|
|
233
256
|
# define dict to store user-defined parameters
|
|
234
257
|
self.__user_metadata = {}
|
|
@@ -280,6 +303,13 @@ class FFdecoder:
|
|
|
280
303
|
"Enabling asynchronous `showinfo` per-frame metadata extraction."
|
|
281
304
|
)
|
|
282
305
|
|
|
306
|
+
# handle capturing FFmpeg stderr tail for diagnosing pipeline failures
|
|
307
|
+
self.__capture_stderr = self.__extra_params.pop("-capture_stderr", False)
|
|
308
|
+
if not isinstance(self.__capture_stderr, bool):
|
|
309
|
+
self.__capture_stderr = False
|
|
310
|
+
self.__stderr_tail: deque[str] = deque(maxlen=50)
|
|
311
|
+
self.__returncode: int | None = None
|
|
312
|
+
|
|
283
313
|
# handle disabling window for ffmpeg subprocess on Windows OS
|
|
284
314
|
# this patch prevents ffmpeg creation window from opening when
|
|
285
315
|
# building exe files
|
|
@@ -783,16 +813,39 @@ class FFdecoder:
|
|
|
783
813
|
while not self.__terminate_stream: # infinite raw frames
|
|
784
814
|
frame = self.__fetchNextFrame()
|
|
785
815
|
if frame is None:
|
|
786
|
-
self.
|
|
816
|
+
self.__handle_stream_end()
|
|
787
817
|
break
|
|
788
818
|
yield self.__attach_metadata(frame)
|
|
789
819
|
else:
|
|
790
820
|
for _ in range(self.__raw_frame_num): # finite raw frames
|
|
791
821
|
frame = self.__fetchNextFrame()
|
|
792
822
|
if frame is None:
|
|
793
|
-
self.
|
|
823
|
+
self.__handle_stream_end()
|
|
794
824
|
break
|
|
795
825
|
yield self.__attach_metadata(frame)
|
|
826
|
+
# generator exhausted, FFmpeg may still be flushing/exiting cleanly
|
|
827
|
+
self.__terminate_stream = True
|
|
828
|
+
|
|
829
|
+
def __handle_stream_end(self) -> None:
|
|
830
|
+
"""
|
|
831
|
+
Internal: marks stream as ended on a short read and, when `-capture_stderr`
|
|
832
|
+
is enabled and FFmpeg exited with a non-zero code, logs the `stderr_tail`.
|
|
833
|
+
"""
|
|
834
|
+
self.__terminate_stream = True
|
|
835
|
+
if not self.__capture_stderr or self.__process is None:
|
|
836
|
+
return
|
|
837
|
+
try:
|
|
838
|
+
retcode = self.__process.wait(timeout=TERMINATE_TIMEOUT_SUBPROCESS)
|
|
839
|
+
except sp.TimeoutExpired:
|
|
840
|
+
return
|
|
841
|
+
if retcode:
|
|
842
|
+
# let the reader thread drain remaining stderr lines first
|
|
843
|
+
self.__stderr_thread and self.__stderr_thread.join(timeout=TERMINATE_TIMEOUT_SUBPROCESS)
|
|
844
|
+
logger.error(
|
|
845
|
+
"FFmpeg exited with code {}. Last stderr lines:\n{}".format(
|
|
846
|
+
retcode, "\n".join(self.__stderr_tail)
|
|
847
|
+
)
|
|
848
|
+
)
|
|
796
849
|
|
|
797
850
|
def __attach_metadata(self, frame: np.ndarray):
|
|
798
851
|
"""
|
|
@@ -830,6 +883,25 @@ class FFdecoder:
|
|
|
830
883
|
"""
|
|
831
884
|
self.terminate()
|
|
832
885
|
|
|
886
|
+
@property
|
|
887
|
+
def returncode(self) -> int | None:
|
|
888
|
+
"""
|
|
889
|
+
A property object that holds FFmpeg process exit code, set by `terminate()`.
|
|
890
|
+
|
|
891
|
+
**Returns:** FFmpeg exit code, or `None` if pipeline has not been terminated yet.
|
|
892
|
+
"""
|
|
893
|
+
return self.__returncode
|
|
894
|
+
|
|
895
|
+
@property
|
|
896
|
+
def stderr_tail(self) -> list[str]:
|
|
897
|
+
"""
|
|
898
|
+
A property object that holds the last 50 lines of FFmpeg's stderr output.
|
|
899
|
+
Only populated when `-capture_stderr` (or `-extract_metadata`) is enabled.
|
|
900
|
+
|
|
901
|
+
**Returns:** List of stderr lines (oldest first).
|
|
902
|
+
"""
|
|
903
|
+
return list(self.__stderr_tail)
|
|
904
|
+
|
|
833
905
|
@property
|
|
834
906
|
def metadata(self) -> str:
|
|
835
907
|
"""
|
|
@@ -908,8 +980,10 @@ class FFdecoder:
|
|
|
908
980
|
else:
|
|
909
981
|
# otherwise discard and log it
|
|
910
982
|
logger.warning(
|
|
911
|
-
"Manually assigned `{}` metadata property value is of invalid type. Discarding!"
|
|
912
|
-
|
|
983
|
+
"Manually assigned `{}` metadata property value is of invalid type. Discarding!".format(
|
|
984
|
+
key
|
|
985
|
+
)
|
|
986
|
+
)
|
|
913
987
|
# delete invalid key
|
|
914
988
|
del value[key]
|
|
915
989
|
# There is no concept of a tuple in the JSON format.
|
|
@@ -974,11 +1048,12 @@ class FFdecoder:
|
|
|
974
1048
|
|
|
975
1049
|
cmd += output_parameters
|
|
976
1050
|
cmd += ["-f", "rawvideo", "-"]
|
|
977
|
-
|
|
978
|
-
#
|
|
979
|
-
#
|
|
1051
|
+
cmd += self.__extra_outputs
|
|
1052
|
+
# When metadata extraction or stderr capture is enabled we must pipe stderr
|
|
1053
|
+
# regardless of verbose mode so the background reader thread can consume
|
|
1054
|
+
# it. Without PIPE the reader would have nothing to read (verbose
|
|
980
1055
|
# inherits parent stderr; silent discards it).
|
|
981
|
-
if self.__extract_metadata:
|
|
1056
|
+
if self.__extract_metadata or self.__capture_stderr:
|
|
982
1057
|
stderr_target = sp.PIPE
|
|
983
1058
|
elif self.__verbose_logs:
|
|
984
1059
|
stderr_target = None
|
|
@@ -1001,18 +1076,20 @@ class FFdecoder:
|
|
|
1001
1076
|
),
|
|
1002
1077
|
)
|
|
1003
1078
|
|
|
1004
|
-
# spin up the stderr reader thread that
|
|
1005
|
-
#
|
|
1006
|
-
if self.__extract_metadata:
|
|
1007
|
-
self.
|
|
1079
|
+
# spin up the stderr reader thread that records stderr tail, and parses
|
|
1080
|
+
# `showinfo` lines into the metadata queue consumed by generateFrame()
|
|
1081
|
+
if self.__extract_metadata or self.__capture_stderr:
|
|
1082
|
+
if self.__extract_metadata:
|
|
1083
|
+
self.__metadata_queue = queue.Queue()
|
|
1008
1084
|
self.__stderr_stop.clear()
|
|
1009
1085
|
self.__stderr_thread = threading.Thread(target=self.__read_stderr, daemon=True)
|
|
1010
1086
|
self.__stderr_thread.start()
|
|
1011
1087
|
|
|
1012
1088
|
def __read_stderr(self) -> None:
|
|
1013
1089
|
"""
|
|
1014
|
-
Internal: background daemon that
|
|
1015
|
-
|
|
1090
|
+
Internal: background daemon that appends FFmpeg stderr lines to `__stderr_tail`
|
|
1091
|
+
(echoing them when verbose), and parses `showinfo` lines into per-frame metadata
|
|
1092
|
+
dicts on `__metadata_queue` when `-extract_metadata` is enabled.
|
|
1016
1093
|
Exits when FFmpeg closes stderr or when `__stderr_stop` is signalled.
|
|
1017
1094
|
"""
|
|
1018
1095
|
assert self.__process is not None and self.__process.stderr is not None
|
|
@@ -1022,6 +1099,11 @@ class FFdecoder:
|
|
|
1022
1099
|
if self.__stderr_stop.is_set():
|
|
1023
1100
|
break
|
|
1024
1101
|
decoded = line.decode("utf-8", errors="ignore")
|
|
1102
|
+
self.__stderr_tail.append(decoded.rstrip())
|
|
1103
|
+
# echo so verbose console output stays unchanged
|
|
1104
|
+
self.__verbose_logs and sys.stderr.write(decoded)
|
|
1105
|
+
if not self.__extract_metadata:
|
|
1106
|
+
continue
|
|
1025
1107
|
match = _SHOWINFO_REGEX.search(decoded)
|
|
1026
1108
|
if not match:
|
|
1027
1109
|
continue
|
|
@@ -1037,35 +1119,51 @@ class FFdecoder:
|
|
|
1037
1119
|
pass
|
|
1038
1120
|
finally:
|
|
1039
1121
|
# sentinel so consumers unblock on EOF
|
|
1040
|
-
self.__metadata_queue.put(None)
|
|
1122
|
+
self.__metadata_queue is not None and self.__metadata_queue.put(None)
|
|
1041
1123
|
|
|
1042
|
-
def terminate(self) -> None:
|
|
1124
|
+
def terminate(self) -> int | None:
|
|
1043
1125
|
"""
|
|
1044
1126
|
Safely terminates all processes.
|
|
1127
|
+
|
|
1128
|
+
**Returns:** FFmpeg process exit code (also available via `returncode`), or `None` if no process was launched.
|
|
1045
1129
|
"""
|
|
1046
1130
|
|
|
1047
1131
|
# signal we are closing
|
|
1048
1132
|
self.__verbose_logs and logger.debug("Terminating FFdecoder Pipeline...")
|
|
1133
|
+
stream_exhausted = self.__terminate_stream
|
|
1049
1134
|
self.__terminate_stream = True
|
|
1050
1135
|
self.__stderr_stop.set()
|
|
1051
1136
|
# check if no process was initiated at first place
|
|
1052
|
-
if self.__process is None
|
|
1137
|
+
if self.__process is None:
|
|
1053
1138
|
logger.info("Pipeline already terminated.")
|
|
1054
|
-
return
|
|
1139
|
+
return self.__returncode
|
|
1055
1140
|
# Attempt to close pipeline.
|
|
1056
1141
|
# close `stdin` output
|
|
1057
1142
|
self.__process.stdin and self.__process.stdin.close()
|
|
1058
1143
|
# close `stdout` output
|
|
1059
1144
|
self.__process.stdout and self.__process.stdout.close()
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1145
|
+
# let FFmpeg finalize `-extra_outputs` (trailers/moov) on EPIPE, or exit cleanly
|
|
1146
|
+
# after stream end, before forcing termination
|
|
1147
|
+
if self.__extra_outputs or stream_exhausted:
|
|
1148
|
+
try:
|
|
1149
|
+
self.__process.wait(TERMINATE_TIMEOUT_SUBPROCESS)
|
|
1150
|
+
except sp.TimeoutExpired:
|
|
1151
|
+
pass
|
|
1152
|
+
# terminate process if still processing, escalate to kill if unresponsive
|
|
1153
|
+
if self.__process.poll() is None:
|
|
1154
|
+
self.__process.terminate()
|
|
1155
|
+
try:
|
|
1156
|
+
self.__process.wait(TERMINATE_TIMEOUT_SUBPROCESS)
|
|
1157
|
+
except sp.TimeoutExpired:
|
|
1158
|
+
self.__process.kill()
|
|
1159
|
+
self.__process.wait()
|
|
1160
|
+
# join the stderr reader thread (it hits EOF once FFmpeg exits) so it does not outlive
|
|
1161
|
+
# the pipeline, then close `stderr`; closing earlier blocks on the reader's buffer lock
|
|
1067
1162
|
if self.__stderr_thread is not None and self.__stderr_thread.is_alive():
|
|
1068
1163
|
self.__stderr_thread.join(timeout=2.0)
|
|
1069
1164
|
self.__stderr_thread = None
|
|
1165
|
+
self.__process.stderr and self.__process.stderr.close()
|
|
1166
|
+
self.__returncode = self.__process.returncode
|
|
1070
1167
|
self.__process = None
|
|
1071
1168
|
logger.info("Pipeline terminated successfully.")
|
|
1169
|
+
return self.__returncode
|
|
@@ -677,6 +677,8 @@ def check_sp_output(*args: Any, **kwargs: Any) -> bytes:
|
|
|
677
677
|
# handle additional params
|
|
678
678
|
retrieve_stderr = kwargs.pop("force_retrieve_stderr", False)
|
|
679
679
|
timeout = kwargs.pop("timeout", None)
|
|
680
|
+
# never inherit parent's stdin, avoids blocking on unexpected input reads
|
|
681
|
+
kwargs.setdefault("stdin", sp.DEVNULL)
|
|
680
682
|
|
|
681
683
|
# execute command in subprocess
|
|
682
684
|
process = sp.Popen(
|
|
@@ -321,7 +321,7 @@ class Sourcer:
|
|
|
321
321
|
_params = self.__sourcer_params.copy()
|
|
322
322
|
_params["-ffprefixes"] = _prefixes
|
|
323
323
|
# output routing references all inputs; meaningless per-source
|
|
324
|
-
for _key in ("-vf", "-filter_complex", "-map"):
|
|
324
|
+
for _key in ("-vf", "-filter_complex", "-map", "-clones"):
|
|
325
325
|
_params.pop(_key, None)
|
|
326
326
|
# Resolve to an absolute path: on Unix `self.__ffmpeg` may be
|
|
327
327
|
# the bare command "ffmpeg" found via PATH, which the nested
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__: str = "0.2.9"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deffcode
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
|
@@ -108,6 +108,7 @@ Here are some key features that stand out:
|
|
|
108
108
|
- Hands down the easiest [**Index based Camera Device Capturing**][decoding-camera-devices-using-indexes], similar to OpenCV.
|
|
109
109
|
- Easy to code **Real-time [Simple][transcoding-live-simple-filtergraphs] & [Complex][transcoding-live-complex-filtergraphs] Filtergraphs**. _(Yes, You read it correctly "Real-time"!)_
|
|
110
110
|
- Native **[Multi-Input Source Configurations][multi-input-source-configurations]** support for decoding complex topologies.
|
|
111
|
+
- Native **[Multi-Outputs Configurations][multi-outputs-configurations]** support for recording/streaming while decoding, in a single FFmpeg pipeline.
|
|
111
112
|
- Lightning fast dedicated **GPU-Accelerated Video [Decoding][hardware-accelerated-video-decoding] & [Transcoding][hardware-accelerated-video-transcoding]**.
|
|
112
113
|
- Enables precise FFmpeg [**Key-frame Seeking**][extracting-key-frames-as-png-image] with pinpoint accuracy.
|
|
113
114
|
- Effortless [**Metadata Extraction**][extracting-video-metadata] from all streams available in the source.
|
|
@@ -237,6 +238,9 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
237
238
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
238
239
|
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
239
240
|
- [Decoding crossfade transition between multiple inputs][decoding-crossfade-transition-between-multiple-inputs]
|
|
241
|
+
- [Multi-Outputs Configurations][multi-outputs-configurations]
|
|
242
|
+
- [Recording while decoding][recording-while-decoding]
|
|
243
|
+
- [Encode once, duplicate to file and stream][encode-once-duplicate-to-file-and-stream]
|
|
240
244
|
|
|
241
245
|
</details>
|
|
242
246
|
|
|
@@ -357,24 +361,18 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
357
361
|
|
|
358
362
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
359
363
|
|
|
360
|
-
[](https://doi.org/10.5281/zenodo.23446604)
|
|
361
365
|
|
|
362
366
|
```BibTeX
|
|
363
367
|
@software{deffcode,
|
|
364
368
|
author = {Abhishek Thakur},
|
|
365
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
366
|
-
month =
|
|
369
|
+
title = {abhiTronix/deffcode: v0.2.8},
|
|
370
|
+
month = oct,
|
|
367
371
|
year = 2026,
|
|
368
372
|
publisher = {Zenodo},
|
|
369
|
-
version = {v0.2.
|
|
370
|
-
doi = {10.5281/zenodo.
|
|
371
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
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
|
-
},
|
|
373
|
+
version = {v0.2.8},
|
|
374
|
+
doi = {10.5281/zenodo.23446604},
|
|
375
|
+
url = {https://doi.org/10.5281/zenodo.23446604},
|
|
378
376
|
}
|
|
379
377
|
```
|
|
380
378
|
|
|
@@ -489,6 +487,9 @@ Advanced Recipes
|
|
|
489
487
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
490
488
|
[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
489
|
[decoding-crossfade-transition-between-multiple-inputs]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#decoding-crossfade-transition-between-multiple-inputs
|
|
490
|
+
[multi-outputs-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#multi-outputs-configurations
|
|
491
|
+
[recording-while-decoding]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#recording-while-decoding
|
|
492
|
+
[encode-once-duplicate-to-file-and-stream]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_output/#encode-once-duplicate-to-file-and-stream
|
|
492
493
|
|
|
493
494
|
<!--
|
|
494
495
|
CI Apps URLs
|
|
@@ -450,6 +450,79 @@ def test_extract_luma_invalid_type() -> None:
|
|
|
450
450
|
decoder is not None and decoder.terminate()
|
|
451
451
|
|
|
452
452
|
|
|
453
|
+
@pytest.mark.parametrize("verbose", [True, False])
|
|
454
|
+
def test_capture_stderr_on_failure(verbose: bool) -> None:
|
|
455
|
+
"""
|
|
456
|
+
With `-capture_stderr`, an FFmpeg pipeline that dies (invalid filter) must end
|
|
457
|
+
the generator gracefully, expose its non-zero exit code via `terminate()`/`returncode`,
|
|
458
|
+
and keep its error output in `stderr_tail`.
|
|
459
|
+
"""
|
|
460
|
+
decoder = FFdecoder(
|
|
461
|
+
return_testvideo_path(fmt="vo"),
|
|
462
|
+
custom_ffmpeg=return_static_ffmpeg(),
|
|
463
|
+
verbose=verbose,
|
|
464
|
+
**{"-capture_stderr": True, "-vf": "nonexistentfilter"},
|
|
465
|
+
).formulate()
|
|
466
|
+
assert decoder.returncode is None, "Test failed - returncode set before termination"
|
|
467
|
+
assert list(decoder.generateFrame()) == [], "Test failed - expected no frames"
|
|
468
|
+
retcode = decoder.terminate()
|
|
469
|
+
assert retcode and retcode == decoder.returncode, f"Test failed - got exit code {retcode}"
|
|
470
|
+
assert any("nonexistentfilter" in line for line in decoder.stderr_tail), (
|
|
471
|
+
f"Test failed - error missing from stderr_tail: {decoder.stderr_tail}"
|
|
472
|
+
)
|
|
473
|
+
# repeated terminate must be idempotent
|
|
474
|
+
assert decoder.terminate() == retcode, "Test failed - returncode lost on re-terminate"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@pytest.mark.parametrize("capture", [True, "yes"])
|
|
478
|
+
def test_capture_stderr_success(capture: Any) -> None:
|
|
479
|
+
"""
|
|
480
|
+
A cleanly finished pipeline must report exit code 0. Non-bool
|
|
481
|
+
`-capture_stderr` values are discarded, leaving `stderr_tail` empty.
|
|
482
|
+
"""
|
|
483
|
+
decoder = FFdecoder(
|
|
484
|
+
return_testvideo_path(fmt="vo"),
|
|
485
|
+
custom_ffmpeg=return_static_ffmpeg(),
|
|
486
|
+
**{"-capture_stderr": capture, "-vframes": 5},
|
|
487
|
+
).formulate()
|
|
488
|
+
assert len(list(decoder.generateFrame())) == 5, "Test failed - expected 5 frames"
|
|
489
|
+
assert decoder.terminate() == 0, "Test failed - expected exit code 0"
|
|
490
|
+
assert bool(decoder.stderr_tail) == (capture is True), (
|
|
491
|
+
f"Test failed - unexpected stderr_tail: {decoder.stderr_tail}"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_returncode_on_stream_exhausted() -> None:
|
|
496
|
+
"""
|
|
497
|
+
Decoding a source to its end and terminating right away must report exit
|
|
498
|
+
code 0, not a SIGTERM-induced one while FFmpeg is still exiting.
|
|
499
|
+
"""
|
|
500
|
+
decoder = FFdecoder(
|
|
501
|
+
return_testvideo_path(fmt="vo"), custom_ffmpeg=return_static_ffmpeg()
|
|
502
|
+
).formulate()
|
|
503
|
+
# discard frames to consume fast, racing FFmpeg exit
|
|
504
|
+
assert sum(1 for _ in decoder.generateFrame()) > 0, "Test failed - expected frames"
|
|
505
|
+
assert decoder.terminate() == 0, "Test failed - expected exit code 0"
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def test_metadata_invalid_type_discarded() -> None:
|
|
509
|
+
"""
|
|
510
|
+
Assigning an invalid-typed value to a source metadata property must be
|
|
511
|
+
discarded with a warning, not raise `AttributeError`.
|
|
512
|
+
"""
|
|
513
|
+
decoder = FFdecoder(
|
|
514
|
+
return_testvideo_path(fmt="vo"), custom_ffmpeg=return_static_ffmpeg()
|
|
515
|
+
).formulate()
|
|
516
|
+
try:
|
|
517
|
+
original = json.loads(decoder.metadata)["source_video_pixfmt"]
|
|
518
|
+
decoder.metadata = {"source_video_pixfmt": 1234}
|
|
519
|
+
assert json.loads(decoder.metadata)["source_video_pixfmt"] == original, (
|
|
520
|
+
"Test failed - invalid-typed value was not discarded"
|
|
521
|
+
)
|
|
522
|
+
finally:
|
|
523
|
+
decoder.terminate()
|
|
524
|
+
|
|
525
|
+
|
|
453
526
|
@pytest.mark.parametrize(
|
|
454
527
|
"custom_params, checks",
|
|
455
528
|
[
|
|
@@ -884,3 +957,140 @@ def test_multi_input_composites(
|
|
|
884
957
|
frames = [f for f in decoder.generateFrame() if f is not None]
|
|
885
958
|
assert frames and frames[0].shape == (*shape, 3)
|
|
886
959
|
assert abs(len(frames) - n_frames) <= 1
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _side_path(name: str) -> str:
|
|
963
|
+
return os.path.join(tempfile.gettempdir(), "temp_write", name)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
test_data = [
|
|
967
|
+
(
|
|
968
|
+
{"-extra_outputs": ["-y", "-map", "0", "-c", "copy", _side_path("side_copy.mkv")]},
|
|
969
|
+
[_side_path("side_copy.mkv")],
|
|
970
|
+
None,
|
|
971
|
+
),
|
|
972
|
+
(
|
|
973
|
+
{
|
|
974
|
+
"-filter_complex": "[0:v]split=2[main][rec];[main]scale=320:-2[main]",
|
|
975
|
+
"-map": "[main]",
|
|
976
|
+
"-extra_outputs": [
|
|
977
|
+
"-y",
|
|
978
|
+
"-map",
|
|
979
|
+
"[rec]",
|
|
980
|
+
"-c:v",
|
|
981
|
+
"libx264",
|
|
982
|
+
_side_path("side_split.mp4"),
|
|
983
|
+
],
|
|
984
|
+
},
|
|
985
|
+
[_side_path("side_split.mp4")],
|
|
986
|
+
320,
|
|
987
|
+
),
|
|
988
|
+
(
|
|
989
|
+
{
|
|
990
|
+
"-extra_outputs": [
|
|
991
|
+
"-y",
|
|
992
|
+
"-map",
|
|
993
|
+
"0:v",
|
|
994
|
+
"-c:v",
|
|
995
|
+
"libx264",
|
|
996
|
+
"-flags",
|
|
997
|
+
"+global_header",
|
|
998
|
+
"-f",
|
|
999
|
+
"tee",
|
|
1000
|
+
# tee treats `\` as escape char, so use forward slashes for Windows paths
|
|
1001
|
+
"{}|[f=mpegts]{}".format(
|
|
1002
|
+
_side_path("side_tee.mkv"), _side_path("side_tee.ts")
|
|
1003
|
+
).replace("\\", "/"),
|
|
1004
|
+
]
|
|
1005
|
+
},
|
|
1006
|
+
[_side_path("side_tee.mkv"), _side_path("side_tee.ts")],
|
|
1007
|
+
None,
|
|
1008
|
+
),
|
|
1009
|
+
]
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@pytest.mark.parametrize("ffparams, side_files, width", test_data)
|
|
1013
|
+
def test_extra_outputs(ffparams: dict[str, Any], side_files: list[str], width: int | None) -> None:
|
|
1014
|
+
"""
|
|
1015
|
+
Testing `-extra_outputs` writes side outputs from the same pipeline while frames
|
|
1016
|
+
still reach Python intact (incl. `-filter_complex` labels consumed only by side outputs)
|
|
1017
|
+
"""
|
|
1018
|
+
decoder = None
|
|
1019
|
+
source = return_testvideo_path(fmt="av")
|
|
1020
|
+
actual_count, actual_shape = actual_frame_count_n_frame_size(source)
|
|
1021
|
+
os.makedirs(_side_path(""), exist_ok=True)
|
|
1022
|
+
try:
|
|
1023
|
+
decoder = FFdecoder(
|
|
1024
|
+
source, frame_format="bgr24", custom_ffmpeg=return_static_ffmpeg(), **ffparams
|
|
1025
|
+
).formulate()
|
|
1026
|
+
frames = [f for f in decoder.generateFrame() if f is not None]
|
|
1027
|
+
assert decoder.terminate() == 0, "Test failed - FFmpeg exited with an error"
|
|
1028
|
+
assert len(frames) == actual_count, f"Test failed - got {len(frames)} frames"
|
|
1029
|
+
# width proves output metadata came from the probe, not the source fallback
|
|
1030
|
+
assert frames[0].shape[1] == (width or actual_shape[1]), (
|
|
1031
|
+
f"Test failed - unexpected frame shape {frames[0].shape}"
|
|
1032
|
+
)
|
|
1033
|
+
for side in side_files:
|
|
1034
|
+
# count via FFmpeg, since OpenCV on macOS (AVFoundation) can't read `.mkv`; use
|
|
1035
|
+
# default FFmpeg, as the pinned 2022 static build dies silently probing `.ts` on CI
|
|
1036
|
+
side_decoder = FFdecoder(side).formulate()
|
|
1037
|
+
side_count = sum(f is not None for f in side_decoder.generateFrame())
|
|
1038
|
+
side_decoder.terminate()
|
|
1039
|
+
assert side_count > 0, f"Test failed - empty {side}"
|
|
1040
|
+
except Exception as e:
|
|
1041
|
+
pytest.fail(str(e))
|
|
1042
|
+
finally:
|
|
1043
|
+
decoder is not None and decoder.terminate()
|
|
1044
|
+
for side in side_files:
|
|
1045
|
+
remove_file_safe(side)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def test_extra_outputs_early_terminate() -> None:
|
|
1049
|
+
"""
|
|
1050
|
+
Terminating mid-stream must still leave a valid (finalized) side `.mp4`
|
|
1051
|
+
"""
|
|
1052
|
+
decoder = None
|
|
1053
|
+
side = _side_path("side_early.mp4")
|
|
1054
|
+
os.makedirs(_side_path(""), exist_ok=True)
|
|
1055
|
+
try:
|
|
1056
|
+
decoder = FFdecoder(
|
|
1057
|
+
return_testvideo_path(fmt="av"),
|
|
1058
|
+
frame_format="bgr24",
|
|
1059
|
+
custom_ffmpeg=return_static_ffmpeg(),
|
|
1060
|
+
**{"-extra_outputs": ["-y", "-map", "0:v", "-c:v", "libx264", side]},
|
|
1061
|
+
).formulate()
|
|
1062
|
+
for num, frame in enumerate(decoder.generateFrame()):
|
|
1063
|
+
if frame is None or num == 9:
|
|
1064
|
+
break
|
|
1065
|
+
decoder.terminate()
|
|
1066
|
+
assert actual_frame_count_n_frame_size(side)[0] >= 10, (
|
|
1067
|
+
"Test failed - side output not finalized"
|
|
1068
|
+
)
|
|
1069
|
+
except Exception as e:
|
|
1070
|
+
pytest.fail(str(e))
|
|
1071
|
+
finally:
|
|
1072
|
+
decoder is not None and decoder.terminate()
|
|
1073
|
+
remove_file_safe(side)
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
@pytest.mark.parametrize("value", ["foo", [["-map", "0"]], {"a": 1}])
|
|
1077
|
+
def test_extra_outputs_invalid(value: Any) -> None:
|
|
1078
|
+
"""
|
|
1079
|
+
Invalid `-extra_outputs` values must be discarded, leaving normal decoding untouched
|
|
1080
|
+
"""
|
|
1081
|
+
decoder = None
|
|
1082
|
+
source = return_testvideo_path(fmt="vo")
|
|
1083
|
+
actual_count, actual_shape = actual_frame_count_n_frame_size(source)
|
|
1084
|
+
try:
|
|
1085
|
+
decoder = FFdecoder(
|
|
1086
|
+
source,
|
|
1087
|
+
frame_format="bgr24",
|
|
1088
|
+
custom_ffmpeg=return_static_ffmpeg(),
|
|
1089
|
+
**{"-extra_outputs": value},
|
|
1090
|
+
).formulate()
|
|
1091
|
+
frames = [f for f in decoder.generateFrame() if f is not None]
|
|
1092
|
+
assert len(frames) == actual_count and frames[0].shape == actual_shape
|
|
1093
|
+
except Exception as e:
|
|
1094
|
+
pytest.fail(str(e))
|
|
1095
|
+
finally:
|
|
1096
|
+
decoder is not None and decoder.terminate()
|
|
@@ -159,6 +159,23 @@ def test_check_sp_output_happy_path() -> None:
|
|
|
159
159
|
assert b"ffmpeg version" in output, "Expected ffmpeg version banner in stdout"
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def test_check_sp_output_stdin_devnull(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Testing check_sp_output defaults `stdin` to DEVNULL, while honoring caller override.
|
|
165
|
+
"""
|
|
166
|
+
seen = []
|
|
167
|
+
real_popen = ffhelper.sp.Popen
|
|
168
|
+
|
|
169
|
+
def spy(*args, **kwargs):
|
|
170
|
+
seen.append(kwargs.get("stdin"))
|
|
171
|
+
return real_popen(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
monkeypatch.setattr(ffhelper.sp, "Popen", spy)
|
|
174
|
+
check_sp_output([return_static_ffmpeg(), "-version"], timeout=5.0)
|
|
175
|
+
check_sp_output([return_static_ffmpeg(), "-version"], stdin=None, timeout=5.0)
|
|
176
|
+
assert seen == [ffhelper.sp.DEVNULL, None], f"Unexpected stdin values: {seen}"
|
|
177
|
+
|
|
178
|
+
|
|
162
179
|
def test_check_sp_output_force_retrieve_stderr_bypass() -> None:
|
|
163
180
|
"""
|
|
164
181
|
Testing that `force_retrieve_stderr=True` suppresses CalledProcessError on
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__: str = "0.2.8"
|
|
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
|
|
File without changes
|