deffcode 0.2.8__py3-none-any.whl → 0.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
deffcode/ffdecoder.py CHANGED
@@ -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.__terminate_stream = True
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.__terminate_stream = True
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
- ).format(key)
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
- # When metadata extraction is enabled we must capture stderr regardless
978
- # of verbose mode so the background reader thread can parse showinfo
979
- # lines. Without PIPE the reader would have nothing to read (verbose
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 parses `showinfo` lines and
1005
- # feeds per-frame metadata dicts into the queue consumed by generateFrame()
1006
- if self.__extract_metadata:
1007
- self.__metadata_queue = queue.Queue()
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 parses FFmpeg `showinfo` lines off
1015
- stderr and pushes per-frame metadata dicts onto `__metadata_queue`.
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 or self.__process.poll() is not 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
- # close `stderr` so the background reader thread's blocking readline() unblocks
1061
- self.__process.stderr and self.__process.stderr.close()
1062
- # terminate/kill process if still processing
1063
- self.__process.poll() is None and self.__process.terminate()
1064
- # wait if not exiting
1065
- self.__process.wait()
1066
- # join the stderr reader thread so it does not outlive the pipeline
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
deffcode/ffhelper.py CHANGED
@@ -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(
deffcode/sourcer.py CHANGED
@@ -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
deffcode/version.py CHANGED
@@ -1 +1 @@
1
- __version__: str = "0.2.8"
1
+ __version__: str = "0.2.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deffcode
3
- Version: 0.2.8
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
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19658542.svg)](https://doi.org/10.5281/zenodo.19658542)
364
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23446604.svg)](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.7},
366
- month = apr,
369
+ title = {abhiTronix/deffcode: v0.2.8},
370
+ month = oct,
367
371
  year = 2026,
368
372
  publisher = {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
- },
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
@@ -0,0 +1,11 @@
1
+ deffcode/__init__.py,sha256=BQO_1Yv0E9PZCPtJ8-1z3X0VLP6VgBbdV5RahJU_RIk,286
2
+ deffcode/ffdecoder.py,sha256=4ywrmP076ZGdrZdcIrFP336HXEAyWT2NKmRiJOo7-5Q,55259
3
+ deffcode/ffhelper.py,sha256=Kms1CiVsyB3CgQvLdScGSuL5ml0eRqxm6xrTd-0g6fE,29032
4
+ deffcode/sourcer.py,sha256=xys3OH4AO0RgdaIj-bYc76rP0F3ZZJSocxGuM0OG2N0,43628
5
+ deffcode/utils.py,sha256=bIR1Hu_icLs3Xe7njHR9YE09MYUbvUfhOUMSqSHh730,4729
6
+ deffcode/version.py,sha256=cHThOwcEXKpIPW-ZC1HETHv-MyCCWj4sJISXFDdM8u4,27
7
+ deffcode-0.2.9.dist-info/licenses/LICENSE,sha256=pF-0xwtgiRjzW5P97fyEOCIqe7LV5Sd5cjIC4fBDwVQ,12259
8
+ deffcode-0.2.9.dist-info/METADATA,sha256=WMCK5mRr3eEfDt5wuvh7iG4gOfJlfKNeVttRqf73dIM,35196
9
+ deffcode-0.2.9.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
10
+ deffcode-0.2.9.dist-info/top_level.txt,sha256=1C-kyZgIKFviemaQMmhP0VKR3wkzEZcrf1o5oUckvHI,9
11
+ deffcode-0.2.9.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (84.0.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- deffcode/__init__.py,sha256=BQO_1Yv0E9PZCPtJ8-1z3X0VLP6VgBbdV5RahJU_RIk,286
2
- deffcode/ffdecoder.py,sha256=0N1x1GstU7_RpqXe3RF-Uw4ZMGjBsRiDXlcFaycecxA,50640
3
- deffcode/ffhelper.py,sha256=6DwWsCf22DQH2bXtkBvsh01APc26TFsDfWD5N00snJg,28911
4
- deffcode/sourcer.py,sha256=k_Sau4vYLc8uGTJ5kX0Jorx0HC006bnA5Eplu_Ney7A,43617
5
- deffcode/utils.py,sha256=bIR1Hu_icLs3Xe7njHR9YE09MYUbvUfhOUMSqSHh730,4729
6
- deffcode/version.py,sha256=HUqwShoZiCc_WHZJmLh-SoGXaMatKowDyny2qENFLOU,27
7
- deffcode-0.2.8.dist-info/licenses/LICENSE,sha256=pF-0xwtgiRjzW5P97fyEOCIqe7LV5Sd5cjIC4fBDwVQ,12259
8
- deffcode-0.2.8.dist-info/METADATA,sha256=mZcgTQV75a8E4ytkFBgYiD5bpgRTY0jz6BqKd4wjhUw,34772
9
- deffcode-0.2.8.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
- deffcode-0.2.8.dist-info/top_level.txt,sha256=1C-kyZgIKFviemaQMmhP0VKR3wkzEZcrf1o5oUckvHI,9
11
- deffcode-0.2.8.dist-info/RECORD,,