deffcode 0.2.7__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.7 → deffcode-0.2.9}/PKG-INFO +26 -14
- {deffcode-0.2.7 → deffcode-0.2.9}/README.md +23 -11
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode/ffdecoder.py +133 -30
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode/ffhelper.py +68 -16
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode/sourcer.py +56 -31
- deffcode-0.2.9/deffcode/version.py +1 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode.egg-info/PKG-INFO +26 -14
- {deffcode-0.2.7 → deffcode-0.2.9}/pyproject.toml +8 -3
- {deffcode-0.2.7 → deffcode-0.2.9}/tests/test_ffdecoder.py +247 -1
- {deffcode-0.2.7 → deffcode-0.2.9}/tests/test_ffhelper.py +80 -2
- {deffcode-0.2.7 → deffcode-0.2.9}/tests/test_sourcer.py +25 -0
- deffcode-0.2.7/deffcode/version.py +0 -1
- {deffcode-0.2.7 → deffcode-0.2.9}/LICENSE +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode/__init__.py +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode/utils.py +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode.egg-info/SOURCES.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode.egg-info/dependency_links.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode.egg-info/requires.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/deffcode.egg-info/top_level.txt +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/setup.cfg +0 -0
- {deffcode-0.2.7 → deffcode-0.2.9}/setup.py +0 -0
- {deffcode-0.2.7 → 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
|
|
@@ -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
|
|
@@ -108,12 +108,13 @@ 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.
|
|
114
115
|
- Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
|
|
115
116
|
- Out-of-the-box support for all prominent Computer Vision libraries.
|
|
116
|
-
- Cross-platform, runs on Python 3.
|
|
117
|
+
- Cross-platform, runs on Python 3.11+, and easy to install.
|
|
117
118
|
|
|
118
119
|
<!--
|
|
119
120
|
- [x] Lossless Transcoding support with [WriteGear](https://abhitronix.github.io/deffcode/latest/gears/writegear/introduction/). #TODO
|
|
@@ -235,6 +236,11 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
235
236
|
- [CUDA-NVENC-accelerated Video Transcoding with WriteGear API][cuda-nvenc-accelerated-video-transcoding-with-writegear-api]
|
|
236
237
|
- [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
238
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
239
|
+
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
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]
|
|
238
244
|
|
|
239
245
|
</details>
|
|
240
246
|
|
|
@@ -271,9 +277,8 @@ decoder = FFdecoder("https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1
|
|
|
271
277
|
|
|
272
278
|
# grab RGB24(default) 3D frames from decoder
|
|
273
279
|
for frame in decoder.generateFrame():
|
|
274
|
-
|
|
275
280
|
# lets print its shape
|
|
276
|
-
print(frame.shape)
|
|
281
|
+
print(frame.shape) # (1080, 1920, 3)
|
|
277
282
|
|
|
278
283
|
# terminate the decoder
|
|
279
284
|
decoder.terminate()
|
|
@@ -288,7 +293,9 @@ The primary function of [**Sourcer API**][sourcer-api] is to **gather metadata i
|
|
|
288
293
|
from deffcode import Sourcer
|
|
289
294
|
|
|
290
295
|
# initialize and formulate the decoder using suitable source
|
|
291
|
-
sourcer = Sourcer(
|
|
296
|
+
sourcer = Sourcer(
|
|
297
|
+
"https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1MB.mp4"
|
|
298
|
+
).probe_stream()
|
|
292
299
|
|
|
293
300
|
# print metadata as `json.dump`
|
|
294
301
|
print(sourcer.retrieve_metadata(pretty_json=True))
|
|
@@ -354,18 +361,18 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
354
361
|
|
|
355
362
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
356
363
|
|
|
357
|
-
[](https://doi.org/10.5281/zenodo.23446604)
|
|
358
365
|
|
|
359
366
|
```BibTeX
|
|
360
367
|
@software{deffcode,
|
|
361
368
|
author = {Abhishek Thakur},
|
|
362
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
363
|
-
month =
|
|
364
|
-
year =
|
|
369
|
+
title = {abhiTronix/deffcode: v0.2.8},
|
|
370
|
+
month = oct,
|
|
371
|
+
year = 2026,
|
|
365
372
|
publisher = {Zenodo},
|
|
366
|
-
version = {v0.2.
|
|
367
|
-
doi = {10.5281/zenodo.
|
|
368
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
373
|
+
version = {v0.2.8},
|
|
374
|
+
doi = {10.5281/zenodo.23446604},
|
|
375
|
+
url = {https://doi.org/10.5281/zenodo.23446604},
|
|
369
376
|
}
|
|
370
377
|
```
|
|
371
378
|
|
|
@@ -478,6 +485,11 @@ Advanced Recipes
|
|
|
478
485
|
[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
486
|
[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
487
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
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
|
|
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
|
|
481
493
|
|
|
482
494
|
<!--
|
|
483
495
|
CI Apps URLs
|
|
@@ -68,12 +68,13 @@ 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.
|
|
74
75
|
- Maintains the standard easy to learn [**OpenCV-Python**](https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html) coding syntax.
|
|
75
76
|
- Out-of-the-box support for all prominent Computer Vision libraries.
|
|
76
|
-
- Cross-platform, runs on Python 3.
|
|
77
|
+
- Cross-platform, runs on Python 3.11+, and easy to install.
|
|
77
78
|
|
|
78
79
|
<!--
|
|
79
80
|
- [x] Lossless Transcoding support with [WriteGear](https://abhitronix.github.io/deffcode/latest/gears/writegear/introduction/). #TODO
|
|
@@ -195,6 +196,11 @@ Once you have DeFFcode installed, checkout our Well-Documented **[Recipes 🍱][
|
|
|
195
196
|
- [CUDA-NVENC-accelerated Video Transcoding with WriteGear API][cuda-nvenc-accelerated-video-transcoding-with-writegear-api]
|
|
196
197
|
- [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
198
|
- [Multi-Input Source Configurations][multi-input-source-configurations]
|
|
199
|
+
- [Decoding multiple inputs as 2x2 video grid _(mosaic)_][decoding-multiple-inputs-as-2x2-video-grid-mosaic]
|
|
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]
|
|
198
204
|
|
|
199
205
|
</details>
|
|
200
206
|
|
|
@@ -231,9 +237,8 @@ decoder = FFdecoder("https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1
|
|
|
231
237
|
|
|
232
238
|
# grab RGB24(default) 3D frames from decoder
|
|
233
239
|
for frame in decoder.generateFrame():
|
|
234
|
-
|
|
235
240
|
# lets print its shape
|
|
236
|
-
print(frame.shape)
|
|
241
|
+
print(frame.shape) # (1080, 1920, 3)
|
|
237
242
|
|
|
238
243
|
# terminate the decoder
|
|
239
244
|
decoder.terminate()
|
|
@@ -248,7 +253,9 @@ The primary function of [**Sourcer API**][sourcer-api] is to **gather metadata i
|
|
|
248
253
|
from deffcode import Sourcer
|
|
249
254
|
|
|
250
255
|
# initialize and formulate the decoder using suitable source
|
|
251
|
-
sourcer = Sourcer(
|
|
256
|
+
sourcer = Sourcer(
|
|
257
|
+
"https://abhitronix.github.io/html/Big_Buck_Bunny_1080_10s_1MB.mp4"
|
|
258
|
+
).probe_stream()
|
|
252
259
|
|
|
253
260
|
# print metadata as `json.dump`
|
|
254
261
|
print(sourcer.retrieve_metadata(pretty_json=True))
|
|
@@ -314,18 +321,18 @@ It is something I am doing with my own free time. But so much more needs to be d
|
|
|
314
321
|
|
|
315
322
|
Here is a Bibtex entry you can use to cite this project in a publication:
|
|
316
323
|
|
|
317
|
-
[](https://doi.org/10.5281/zenodo.23446604)
|
|
318
325
|
|
|
319
326
|
```BibTeX
|
|
320
327
|
@software{deffcode,
|
|
321
328
|
author = {Abhishek Thakur},
|
|
322
|
-
title = {abhiTronix/deffcode: v0.2.
|
|
323
|
-
month =
|
|
324
|
-
year =
|
|
329
|
+
title = {abhiTronix/deffcode: v0.2.8},
|
|
330
|
+
month = oct,
|
|
331
|
+
year = 2026,
|
|
325
332
|
publisher = {Zenodo},
|
|
326
|
-
version = {v0.2.
|
|
327
|
-
doi = {10.5281/zenodo.
|
|
328
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
333
|
+
version = {v0.2.8},
|
|
334
|
+
doi = {10.5281/zenodo.23446604},
|
|
335
|
+
url = {https://doi.org/10.5281/zenodo.23446604},
|
|
329
336
|
}
|
|
330
337
|
```
|
|
331
338
|
|
|
@@ -438,6 +445,11 @@ Advanced Recipes
|
|
|
438
445
|
[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
446
|
[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
447
|
[multi-input-source-configurations]: https://abhitronix.github.io/deffcode/latest/recipes/advanced/multi_input/#multi-input-source-configurations
|
|
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
|
|
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
|
|
441
453
|
|
|
442
454
|
<!--
|
|
443
455
|
CI Apps URLs
|
|
@@ -26,16 +26,19 @@ 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
|
-
from typing import Any
|
|
35
|
+
from typing import Any, Self
|
|
34
36
|
|
|
35
37
|
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(
|
|
@@ -226,6 +238,20 @@ class FFdecoder:
|
|
|
226
238
|
if {"-vf", "-filter_complex"}.intersection(self.__extra_params.keys()):
|
|
227
239
|
key = "-vf" if "-vf" in self.__extra_params else "-filter_complex"
|
|
228
240
|
sourcer_params[key] = self.__extra_params[key]
|
|
241
|
+
# multi-input output streams are selected via `-map`, so probe with it too
|
|
242
|
+
if self.__is_multi and "-map" in self.__extra_params:
|
|
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
|
+
]
|
|
229
255
|
|
|
230
256
|
# define dict to store user-defined parameters
|
|
231
257
|
self.__user_metadata = {}
|
|
@@ -277,6 +303,13 @@ class FFdecoder:
|
|
|
277
303
|
"Enabling asynchronous `showinfo` per-frame metadata extraction."
|
|
278
304
|
)
|
|
279
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
|
+
|
|
280
313
|
# handle disabling window for ffmpeg subprocess on Windows OS
|
|
281
314
|
# this patch prevents ffmpeg creation window from opening when
|
|
282
315
|
# building exe files
|
|
@@ -364,7 +397,7 @@ class FFdecoder:
|
|
|
364
397
|
# reset improper values
|
|
365
398
|
self.__custom_resolution = None
|
|
366
399
|
|
|
367
|
-
def formulate(self) ->
|
|
400
|
+
def formulate(self) -> Self:
|
|
368
401
|
"""
|
|
369
402
|
This method formulates all necessary FFmpeg pipeline arguments and executes it inside the FFmpeg `subprocess` pipe.
|
|
370
403
|
|
|
@@ -662,7 +695,9 @@ class FFdecoder:
|
|
|
662
695
|
if "-frames:v" in input_params:
|
|
663
696
|
self.__raw_frame_num = input_params["-frames:v"]
|
|
664
697
|
elif (
|
|
665
|
-
|
|
698
|
+
# composite output length is unrelated to the primary source's length
|
|
699
|
+
not self.__is_multi
|
|
700
|
+
and self.__sourcer_metadata["approx_video_nframes"] is not None
|
|
666
701
|
and self.__sourcer_metadata["approx_video_nframes"] > 0
|
|
667
702
|
):
|
|
668
703
|
self.__raw_frame_num = self.__sourcer_metadata["approx_video_nframes"]
|
|
@@ -778,16 +813,39 @@ class FFdecoder:
|
|
|
778
813
|
while not self.__terminate_stream: # infinite raw frames
|
|
779
814
|
frame = self.__fetchNextFrame()
|
|
780
815
|
if frame is None:
|
|
781
|
-
self.
|
|
816
|
+
self.__handle_stream_end()
|
|
782
817
|
break
|
|
783
818
|
yield self.__attach_metadata(frame)
|
|
784
819
|
else:
|
|
785
820
|
for _ in range(self.__raw_frame_num): # finite raw frames
|
|
786
821
|
frame = self.__fetchNextFrame()
|
|
787
822
|
if frame is None:
|
|
788
|
-
self.
|
|
823
|
+
self.__handle_stream_end()
|
|
789
824
|
break
|
|
790
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
|
+
)
|
|
791
849
|
|
|
792
850
|
def __attach_metadata(self, frame: np.ndarray):
|
|
793
851
|
"""
|
|
@@ -825,6 +883,25 @@ class FFdecoder:
|
|
|
825
883
|
"""
|
|
826
884
|
self.terminate()
|
|
827
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
|
+
|
|
828
905
|
@property
|
|
829
906
|
def metadata(self) -> str:
|
|
830
907
|
"""
|
|
@@ -903,8 +980,10 @@ class FFdecoder:
|
|
|
903
980
|
else:
|
|
904
981
|
# otherwise discard and log it
|
|
905
982
|
logger.warning(
|
|
906
|
-
"Manually assigned `{}` metadata property value is of invalid type. Discarding!"
|
|
907
|
-
|
|
983
|
+
"Manually assigned `{}` metadata property value is of invalid type. Discarding!".format(
|
|
984
|
+
key
|
|
985
|
+
)
|
|
986
|
+
)
|
|
908
987
|
# delete invalid key
|
|
909
988
|
del value[key]
|
|
910
989
|
# There is no concept of a tuple in the JSON format.
|
|
@@ -956,7 +1035,7 @@ class FFdecoder:
|
|
|
956
1035
|
)
|
|
957
1036
|
if _src_meta.get("source_demuxer"):
|
|
958
1037
|
cmd += ["-f", _src_meta["source_demuxer"]]
|
|
959
|
-
cmd += ["-i", _src]
|
|
1038
|
+
cmd += ["-i", _src_meta.get("source", _src)]
|
|
960
1039
|
else:
|
|
961
1040
|
cmd += self.__ffmpeg_prefixes
|
|
962
1041
|
cmd += input_parameters
|
|
@@ -969,11 +1048,12 @@ class FFdecoder:
|
|
|
969
1048
|
|
|
970
1049
|
cmd += output_parameters
|
|
971
1050
|
cmd += ["-f", "rawvideo", "-"]
|
|
972
|
-
|
|
973
|
-
#
|
|
974
|
-
#
|
|
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
|
|
975
1055
|
# inherits parent stderr; silent discards it).
|
|
976
|
-
if self.__extract_metadata:
|
|
1056
|
+
if self.__extract_metadata or self.__capture_stderr:
|
|
977
1057
|
stderr_target = sp.PIPE
|
|
978
1058
|
elif self.__verbose_logs:
|
|
979
1059
|
stderr_target = None
|
|
@@ -996,18 +1076,20 @@ class FFdecoder:
|
|
|
996
1076
|
),
|
|
997
1077
|
)
|
|
998
1078
|
|
|
999
|
-
# spin up the stderr reader thread that
|
|
1000
|
-
#
|
|
1001
|
-
if self.__extract_metadata:
|
|
1002
|
-
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()
|
|
1003
1084
|
self.__stderr_stop.clear()
|
|
1004
1085
|
self.__stderr_thread = threading.Thread(target=self.__read_stderr, daemon=True)
|
|
1005
1086
|
self.__stderr_thread.start()
|
|
1006
1087
|
|
|
1007
1088
|
def __read_stderr(self) -> None:
|
|
1008
1089
|
"""
|
|
1009
|
-
Internal: background daemon that
|
|
1010
|
-
|
|
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.
|
|
1011
1093
|
Exits when FFmpeg closes stderr or when `__stderr_stop` is signalled.
|
|
1012
1094
|
"""
|
|
1013
1095
|
assert self.__process is not None and self.__process.stderr is not None
|
|
@@ -1017,6 +1099,11 @@ class FFdecoder:
|
|
|
1017
1099
|
if self.__stderr_stop.is_set():
|
|
1018
1100
|
break
|
|
1019
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
|
|
1020
1107
|
match = _SHOWINFO_REGEX.search(decoded)
|
|
1021
1108
|
if not match:
|
|
1022
1109
|
continue
|
|
@@ -1032,35 +1119,51 @@ class FFdecoder:
|
|
|
1032
1119
|
pass
|
|
1033
1120
|
finally:
|
|
1034
1121
|
# sentinel so consumers unblock on EOF
|
|
1035
|
-
self.__metadata_queue.put(None)
|
|
1122
|
+
self.__metadata_queue is not None and self.__metadata_queue.put(None)
|
|
1036
1123
|
|
|
1037
|
-
def terminate(self) -> None:
|
|
1124
|
+
def terminate(self) -> int | None:
|
|
1038
1125
|
"""
|
|
1039
1126
|
Safely terminates all processes.
|
|
1127
|
+
|
|
1128
|
+
**Returns:** FFmpeg process exit code (also available via `returncode`), or `None` if no process was launched.
|
|
1040
1129
|
"""
|
|
1041
1130
|
|
|
1042
1131
|
# signal we are closing
|
|
1043
1132
|
self.__verbose_logs and logger.debug("Terminating FFdecoder Pipeline...")
|
|
1133
|
+
stream_exhausted = self.__terminate_stream
|
|
1044
1134
|
self.__terminate_stream = True
|
|
1045
1135
|
self.__stderr_stop.set()
|
|
1046
1136
|
# check if no process was initiated at first place
|
|
1047
|
-
if self.__process is None
|
|
1137
|
+
if self.__process is None:
|
|
1048
1138
|
logger.info("Pipeline already terminated.")
|
|
1049
|
-
return
|
|
1139
|
+
return self.__returncode
|
|
1050
1140
|
# Attempt to close pipeline.
|
|
1051
1141
|
# close `stdin` output
|
|
1052
1142
|
self.__process.stdin and self.__process.stdin.close()
|
|
1053
1143
|
# close `stdout` output
|
|
1054
1144
|
self.__process.stdout and self.__process.stdout.close()
|
|
1055
|
-
#
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
|
1062
1162
|
if self.__stderr_thread is not None and self.__stderr_thread.is_alive():
|
|
1063
1163
|
self.__stderr_thread.join(timeout=2.0)
|
|
1064
1164
|
self.__stderr_thread = None
|
|
1165
|
+
self.__process.stderr and self.__process.stderr.close()
|
|
1166
|
+
self.__returncode = self.__process.returncode
|
|
1065
1167
|
self.__process = None
|
|
1066
1168
|
logger.info("Pipeline terminated successfully.")
|
|
1169
|
+
return self.__returncode
|
|
@@ -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,85 @@ 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
|
+
# never inherit parent's stdin, avoids blocking on unexpected input reads
|
|
681
|
+
kwargs.setdefault("stdin", sp.DEVNULL)
|
|
682
|
+
|
|
658
683
|
# execute command in subprocess
|
|
659
684
|
process = sp.Popen(
|
|
660
685
|
*args,
|
|
661
686
|
stdout=sp.PIPE,
|
|
662
|
-
stderr=sp.
|
|
687
|
+
stderr=sp.PIPE if retrieve_stderr else sp.DEVNULL,
|
|
663
688
|
**kwargs,
|
|
664
689
|
)
|
|
665
|
-
|
|
666
|
-
|
|
690
|
+
|
|
691
|
+
# communicate and poll process with two-step timeout handling
|
|
692
|
+
timeout_occurred = False
|
|
693
|
+
try:
|
|
694
|
+
output, stderr = process.communicate(timeout=timeout)
|
|
695
|
+
except sp.TimeoutExpired:
|
|
696
|
+
timeout_occurred = True
|
|
697
|
+
logger.warning(
|
|
698
|
+
f"[Pipeline-Warning] :: Process exceeded timeout of {timeout}s. "
|
|
699
|
+
"Attempting graceful termination..."
|
|
700
|
+
)
|
|
701
|
+
# Step 1: polite SIGTERM, give process a chance to clean up
|
|
702
|
+
process.terminate()
|
|
703
|
+
try:
|
|
704
|
+
output, stderr = process.communicate(timeout=TERMINATE_TIMEOUT_SUBPROCESS)
|
|
705
|
+
except sp.TimeoutExpired:
|
|
706
|
+
# Step 2: process ignored SIGTERM, force kill
|
|
707
|
+
logger.error("[Pipeline-Error] :: Process unresponsive to SIGTERM. Hard killing...")
|
|
708
|
+
process.kill()
|
|
709
|
+
output, stderr = process.communicate()
|
|
710
|
+
|
|
667
711
|
retcode = process.poll()
|
|
712
|
+
|
|
668
713
|
# handle return code
|
|
669
|
-
if
|
|
670
|
-
|
|
714
|
+
# Bypass CalledProcessError if caller wants stderr (some FFmpeg commands
|
|
715
|
+
# exit non-zero by design) or if we killed the process via our timeout.
|
|
716
|
+
if retcode and not retrieve_stderr and not timeout_occurred:
|
|
717
|
+
logger.error(
|
|
718
|
+
"[Pipeline-Error] :: {}".format(output.decode("utf-8") if output else "No output")
|
|
719
|
+
)
|
|
671
720
|
cmd = kwargs.get("args")
|
|
672
721
|
if cmd is None:
|
|
673
722
|
cmd = args[0]
|
|
674
723
|
error = sp.CalledProcessError(retcode, cmd)
|
|
675
724
|
error.output = output
|
|
676
725
|
raise error
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
726
|
+
|
|
727
|
+
# warn if process emitted nothing on either stream
|
|
728
|
+
if not (bool(output) or bool(stderr)):
|
|
729
|
+
logger.error(
|
|
730
|
+
"[Pipeline-Error] :: Pipeline failed to extract any data from command: {}!".format(
|
|
731
|
+
args[0] if args else []
|
|
732
|
+
)
|
|
681
733
|
)
|
|
682
|
-
|
|
683
|
-
# return
|
|
734
|
+
|
|
735
|
+
# return stderr when explicitly requested (and present), else stdout
|
|
684
736
|
return stderr if retrieve_stderr and stderr else output
|