deffcode 0.2.4__py3-none-any.whl → 0.2.6__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
@@ -19,6 +19,7 @@ limitations under the License.
19
19
  """
20
20
 
21
21
  # import the necessary packages
22
+ import platform
22
23
  import logging
23
24
  import numpy as np
24
25
  import subprocess as sp
@@ -106,10 +107,16 @@ class FFdecoder:
106
107
  # handles user-defined parameters
107
108
  self.__extra_params = {}
108
109
 
109
- # handle process to be frames written
110
+ # handles process to be frames written
110
111
  self.__process = None
111
112
 
112
- # handle exclusive metadata
113
+ # handles disabling window for ffmpeg subprocess on Windows
114
+ self.__ffmpeg_window_disabler_patch = False
115
+
116
+ # checks machine OS
117
+ self.__machine_OS = platform.system()
118
+
119
+ # handles exclusive metadata
113
120
  self.__ff_pixfmt_metadata = None # metadata
114
121
  self.__raw_frame_num = None # raw-frame number
115
122
  self.__raw_frame_pixfmt = None # raw-frame pixformat
@@ -127,23 +134,26 @@ class FFdecoder:
127
134
  # operation mode variable
128
135
  self.__opmode = None
129
136
 
130
- # handle termination
137
+ # handles termination
131
138
  self.__terminate_stream = False
132
139
 
133
140
  # cleans and reformat user-defined parameters
134
141
  self.__extra_params = {
135
- str(k).strip(): str(v).strip()
136
- if not (v is None) and not isinstance(v, (dict, list, int, float, tuple))
137
- else v
142
+ str(k).strip(): (
143
+ str(v).strip()
144
+ if not (v is None)
145
+ and not isinstance(v, (dict, list, int, float, tuple))
146
+ else v
147
+ )
138
148
  for k, v in ffparams.items()
139
149
  }
140
150
 
141
- # handle custom Sourcer API params
151
+ # handles custom Sourcer API params
142
152
  sourcer_params = self.__extra_params.pop("-custom_sourcer_params", {})
143
153
  # reset improper values
144
154
  sourcer_params = {} if not isinstance(sourcer_params, dict) else sourcer_params
145
155
 
146
- # handle user ffmpeg pre-headers(parameters such as `-re`) parameters (must be a list)
156
+ # handles user ffmpeg pre-headers(parameters such as `-re`) parameters (must be a list)
147
157
  self.__ffmpeg_prefixes = self.__extra_params.pop("-ffprefixes", [])
148
158
  # check if not valid type
149
159
  if not isinstance(self.__ffmpeg_prefixes, list):
@@ -199,6 +209,35 @@ class FFdecoder:
199
209
  # handle valid FFmpeg assets location
200
210
  self.__ffmpeg = self.__sourcer_metadata["ffmpeg_binary_path"]
201
211
 
212
+ # handle YUV pixel formats(such as `yuv420p`, `yuv444p`, `nv12`, `nv21` etc.)
213
+ # patch for compatibility with OpenCV APIs.
214
+ self.__cv_patch = self.__extra_params.pop("-enforce_cv_patch", False)
215
+ if not (isinstance(self.__cv_patch, bool)):
216
+ self.__cv_patch = False
217
+ self.__verbose_logs and logger.critical(
218
+ "Enforcing OpenCV compatibility patch for YUV/NV video frames."
219
+ )
220
+
221
+ # handle disabling window for ffmpeg subprocess on Windows OS
222
+ # this patch prevents ffmpeg creation window from opening when
223
+ # building exe files
224
+ ffmpeg_window_disabler_patch = self.__extra_params.pop(
225
+ "-disable_ffmpeg_window", False
226
+ )
227
+ if ffmpeg_window_disabler_patch and isinstance(
228
+ ffmpeg_window_disabler_patch, bool
229
+ ):
230
+ # check if value is valid
231
+ if self.__machine_OS != "Windows" or self.__verbose_logs:
232
+ logger.warning(
233
+ "Optional `-disable_ffmpeg_window` flag is only available on Windows OS with `verbose=False`. Discarding!"
234
+ )
235
+ else:
236
+ self.__ffmpeg_window_disabler_patch = ffmpeg_window_disabler_patch
237
+ else:
238
+ # handle improper values
239
+ self.__ffmpeg_window_disabler_patch = False
240
+
202
241
  # handle pass-through audio mode works in conjunction with WriteGear [TODO]
203
242
  self.__passthrough_mode = self.__extra_params.pop("-passthrough_audio", False)
204
243
  if not (isinstance(self.__passthrough_mode, bool)):
@@ -279,7 +318,6 @@ class FFdecoder:
279
318
  self.__custom_resolution = None
280
319
 
281
320
  def formulate(self):
282
-
283
321
  """
284
322
  This method formulates all necessary FFmpeg pipeline arguments and executes it inside the FFmpeg `subprocess` pipe.
285
323
 
@@ -411,13 +449,15 @@ class FFdecoder:
411
449
  else:
412
450
  logger.warning(
413
451
  "{} Switching to default `{}` pixel-format!".format(
414
- "Provided FFmpeg does not supports `{}` pixel-format.".format(
415
- self.__sourcer_metadata["output_frames_pixfmt"]
416
- if "output_frames_pixfmt" in self.__sourcer_metadata
417
- else self.__frame_format
418
- )
419
- if self.__frame_format != "null"
420
- else "No usable pixel-format defined.",
452
+ (
453
+ "Provided FFmpeg does not supports `{}` pixel-format.".format(
454
+ self.__sourcer_metadata["output_frames_pixfmt"]
455
+ if "output_frames_pixfmt" in self.__sourcer_metadata
456
+ else self.__frame_format
457
+ )
458
+ if self.__frame_format != "null"
459
+ else "No usable pixel-format defined."
460
+ ),
421
461
  default_pixfmt,
422
462
  )
423
463
  )
@@ -428,7 +468,9 @@ class FFdecoder:
428
468
  for x in self.__ff_pixfmt_metadata
429
469
  if x[0] == rawframe_pixfmt
430
470
  ][0]
431
- raw_bit_per_component = rawframesbpp // self.__raw_frame_depth
471
+ raw_bit_per_component = (
472
+ rawframesbpp // self.__raw_frame_depth if self.__raw_frame_depth else 0
473
+ )
432
474
  if 4 <= raw_bit_per_component <= 8:
433
475
  self.__raw_frame_dtype = np.dtype("u1")
434
476
  elif 8 < raw_bit_per_component <= 16 and rawframe_pixfmt.endswith(
@@ -456,9 +498,9 @@ class FFdecoder:
456
498
  self.__raw_frame_pixfmt = rawframe_pixfmt
457
499
  # also override as metadata(if available)
458
500
  if "output_frames_pixfmt" in self.__sourcer_metadata:
459
- self.__sourcer_metadata[
460
- "output_frames_pixfmt"
461
- ] = self.__raw_frame_pixfmt
501
+ self.__sourcer_metadata["output_frames_pixfmt"] = (
502
+ self.__raw_frame_pixfmt
503
+ )
462
504
 
463
505
  # handle raw-frame resolution
464
506
  # notify FFmpeg `-s` parameter cannot be assigned directly
@@ -598,7 +640,7 @@ class FFdecoder:
598
640
  self.__raw_frame_num = None
599
641
  # log that number of frames are unknown
600
642
  self.__verbose_logs and logger.info(
601
- "Live/Network Stream detected! Number of frames in given source are not known."
643
+ "Number of frames in given source are unknown. Live/Network/Looping stream detected!"
602
644
  )
603
645
 
604
646
  # log Mode of Operation
@@ -626,11 +668,15 @@ class FFdecoder:
626
668
  self.__process is None
627
669
  ), "Pipeline is not running! You must call `formulate()` method first."
628
670
 
629
- # formulated raw frame size
671
+ # formulated raw frame size and apply YUV pixel formats patch(if applicable)
630
672
  raw_frame_size = (
631
- self.__raw_frame_depth
632
- * self.__raw_frame_resolution[0]
633
- * self.__raw_frame_resolution[1]
673
+ (self.__raw_frame_resolution[0] * (self.__raw_frame_resolution[1] * 3 // 2))
674
+ if self.__raw_frame_pixfmt.startswith(("yuv", "nv")) and self.__cv_patch
675
+ else (
676
+ self.__raw_frame_depth
677
+ * self.__raw_frame_resolution[0]
678
+ * self.__raw_frame_resolution[1]
679
+ )
634
680
  )
635
681
  # next dataframe as numpy ndarray
636
682
  nparray = None
@@ -668,6 +714,12 @@ class FFdecoder:
668
714
  self.__raw_frame_depth,
669
715
  )
670
716
  )[:, :, 0]
717
+ elif self.__raw_frame_pixfmt.startswith(("yuv", "nv")) and self.__cv_patch:
718
+ # reconstruct exclusive YUV formats frames for OpenCV APIs
719
+ frame = frame.reshape(
720
+ self.__raw_frame_resolution[1] * 3 // 2,
721
+ self.__raw_frame_resolution[0],
722
+ )
671
723
  else:
672
724
  # reconstruct default frames
673
725
  frame = frame.reshape(
@@ -783,9 +835,11 @@ class FFdecoder:
783
835
  self.__verbose_logs and logger.info(
784
836
  "Updating `{}`{} metadata property to `{}`.".format(
785
837
  key,
786
- " and its counterpart"
787
- if key in counterpart_prop.values()
788
- else "",
838
+ (
839
+ " and its counterpart"
840
+ if key in counterpart_prop.values()
841
+ else ""
842
+ ),
789
843
  value[key],
790
844
  )
791
845
  )
@@ -817,7 +871,6 @@ class FFdecoder:
817
871
  raise ValueError("Invalid datatype metadata assigned. Aborting!")
818
872
 
819
873
  def __launch_FFdecoderline(self, input_params, output_params):
820
-
821
874
  """
822
875
  This Internal method executes FFmpeg pipeline arguments inside a `subprocess` pipe in a new process.
823
876
 
@@ -856,7 +909,13 @@ class FFdecoder:
856
909
  else:
857
910
  # In silent mode
858
911
  self.__process = sp.Popen(
859
- cmd, stdin=sp.DEVNULL, stdout=sp.PIPE, stderr=sp.DEVNULL
912
+ cmd,
913
+ stdin=sp.DEVNULL,
914
+ stdout=sp.PIPE,
915
+ stderr=sp.DEVNULL,
916
+ creationflags=( # this prevents ffmpeg creation window from opening when building exe files on Windows
917
+ sp.DETACHED_PROCESS if self.__ffmpeg_window_disabler_patch else 0
918
+ ),
860
919
  )
861
920
 
862
921
  def terminate(self):
@@ -877,9 +936,7 @@ class FFdecoder:
877
936
  # close `stdout` output
878
937
  self.__process.stdout and self.__process.stdout.close()
879
938
  # terminate/kill process if still processing
880
- if self.__process.poll() is None:
881
- # demuxers prefer kill
882
- self.__process.kill()
939
+ self.__process.poll() is None and self.__process.terminate()
883
940
  # wait if not exiting
884
941
  self.__process.wait()
885
942
  self.__process = None
deffcode/ffhelper.py CHANGED
@@ -27,8 +27,7 @@ import subprocess as sp
27
27
 
28
28
  from tqdm import tqdm
29
29
  from pathlib import Path
30
- from requests.adapters import HTTPAdapter
31
- from requests.packages.urllib3.util.retry import Retry
30
+ from requests.adapters import HTTPAdapter, Retry
32
31
 
33
32
  # import utils packages
34
33
  from .utils import logger_handler, delete_file_safe
@@ -48,7 +47,7 @@ class TimeoutHTTPAdapter(HTTPAdapter):
48
47
  A custom Transport Adapter with default timeouts
49
48
  """
50
49
 
51
- def __init__(self, *args, **kwargs):
50
+ def __init__(self, *args, **kwargs):
52
51
  self.timeout = DEFAULT_TIMEOUT
53
52
  if "timeout" in kwargs:
54
53
  self.timeout = kwargs["timeout"]
@@ -215,7 +214,11 @@ def download_ffmpeg_binaries(path, os_windows=False, os_bit=""):
215
214
  http.mount("https://", adapter)
216
215
  response = http.get(file_url, stream=True)
217
216
  response.raise_for_status()
218
- total_length = response.headers.get("content-length")
217
+ total_length = (
218
+ response.headers.get("content-length")
219
+ if "content-length" in response.headers
220
+ else len(response.content)
221
+ )
219
222
  assert not (
220
223
  total_length is None
221
224
  ), "[Helper:ERROR] :: Failed to retrieve files, check your Internet connectivity!"
@@ -339,14 +342,15 @@ def get_supported_demuxers(path):
339
342
 
340
343
  **Returns:** List of supported demuxers.
341
344
  """
345
+ # extract and clean FFmpeg output
342
346
  demuxers = check_sp_output([path, "-hide_banner", "-demuxers"])
343
347
  splitted = [x.decode("utf-8").strip() for x in demuxers.split(b"\n")]
344
- supported_demuxers = splitted[splitted.index("--") + 1 : len(splitted) - 1]
345
- # compile regex
346
- finder = re.compile(r"\s\s[a-z0-9_,-]+\s+")
347
- # find all outputs
348
- outputs = finder.findall("\n".join(supported_demuxers))
349
- # return output findings
348
+ split_index = [idx for idx, s in enumerate(splitted) if "--" in s][0]
349
+ supported_demuxers = splitted[split_index + 1 : len(splitted) - 1]
350
+ # search all demuxers
351
+ outputs = [re.search(r"\s[a-z0-9_,-]{2,}\s", d) for d in supported_demuxers]
352
+ outputs = [o.group(0) for o in outputs if o]
353
+ # return demuxers output
350
354
  return [o.strip() if not ("," in o) else o.split(",")[-1].strip() for o in outputs]
351
355
 
352
356
 
@@ -515,10 +519,12 @@ def extract_device_n_demuxer(path, machine_OS=None, verbose=False):
515
519
  logger.info(
516
520
  "[{}]: {}".format(
517
521
  idx,
518
- dev
519
- if machine_OS != "Linux"
520
- else "{} at path `{}`".format(
521
- next(iter(dev.values()))[0], next(iter(dev.keys()))
522
+ (
523
+ dev
524
+ if machine_OS != "Linux"
525
+ else "{} at path `{}`".format(
526
+ next(iter(dev.values()))[0], next(iter(dev.keys()))
527
+ )
522
528
  ),
523
529
  )
524
530
  )
@@ -629,8 +635,11 @@ def is_valid_url(path, url=None, verbose=False):
629
635
  protocols = check_sp_output([path, "-hide_banner", "-protocols"])
630
636
  splitted = [x.decode("utf-8").strip() for x in protocols.split(b"\n")]
631
637
  supported_protocols = splitted[splitted.index("Output:") + 1 : len(splitted) - 1]
632
- # rtsp is a demuxer somehow
633
- supported_protocols += ["rtsp"] if "rtsp" in get_supported_demuxers(path) else []
638
+ # RTSP is a demuxer somehow
639
+ # support both RTSP and RTSPS(over SSL)
640
+ supported_protocols += (
641
+ ["rtsp", "rtsps"] if "rtsp" in get_supported_demuxers(path) else []
642
+ )
634
643
  # Test and return result whether scheme is supported
635
644
  if extracted_scheme_url and extracted_scheme_url in supported_protocols:
636
645
  verbose and logger.debug(
@@ -674,7 +683,7 @@ def check_sp_output(*args, **kwargs):
674
683
  retcode = process.poll()
675
684
  # handle return code
676
685
  if retcode and not (retrieve_stderr):
677
- logger.error("[Pipline-Error] :: {}".format(output.decode("utf-8")))
686
+ logger.error("[Pipeline-Error] :: {}".format(output.decode("utf-8")))
678
687
  cmd = kwargs.get("args")
679
688
  if cmd is None:
680
689
  cmd = args[0]
@@ -683,7 +692,7 @@ def check_sp_output(*args, **kwargs):
683
692
  raise error
684
693
  # raise error if no output
685
694
  bool(output) or bool(stderr) or logger.error(
686
- "[Pipline-Error] :: Pipline failed to exact any data from command: {}!".format(
695
+ "[Pipeline-Error] :: Pipeline failed to exact any data from command: {}!".format(
687
696
  args[0] if args else []
688
697
  )
689
698
  )
deffcode/sourcer.py CHANGED
@@ -87,7 +87,7 @@ class Sourcer:
87
87
  verbose (bool): enables/disables verbose.
88
88
  sourcer_params (dict): provides the flexibility to control supported internal and FFmpeg parameters.
89
89
  """
90
- # checks if machine in-use is running windows os or not
90
+ # checks machine OS
91
91
  self.__machine_OS = platform.system()
92
92
 
93
93
  # define internal parameters
@@ -100,9 +100,11 @@ class Sourcer:
100
100
 
101
101
  # sanitize sourcer_params
102
102
  self.__sourcer_params = {
103
- str(k).strip(): str(v).strip()
104
- if not isinstance(v, (dict, list, int, float, tuple))
105
- else v
103
+ str(k).strip(): (
104
+ str(v).strip()
105
+ if not isinstance(v, (dict, list, int, float, tuple))
106
+ else v
107
+ )
106
108
  for k, v in sourcer_params.items()
107
109
  }
108
110
 
@@ -199,6 +201,7 @@ class Sourcer:
199
201
 
200
202
  # various source stream params
201
203
  self.__default_video_resolution = "" # handles stream resolution
204
+ self.__default_video_orientation = "" # handles stream's video orientation
202
205
  self.__default_video_framerate = "" # handles stream framerate
203
206
  self.__default_video_bitrate = "" # handles stream's video bitrate
204
207
  self.__default_video_pixfmt = "" # handles stream's video pixfmt
@@ -218,6 +221,7 @@ class Sourcer:
218
221
  self.__output_frames_resolution = "" # handles output stream resolution
219
222
  self.__output_framerate = "" # handles output stream framerate
220
223
  self.__output_frames_pixfmt = "" # handles output frame pixel format
224
+ self.__output_orientation = "" # handles output frame orientation
221
225
 
222
226
  # check whether metadata probed or not?
223
227
  self.__metadata_probed = False
@@ -251,6 +255,7 @@ class Sourcer:
251
255
  if video_rfparams:
252
256
  self.__default_video_resolution = video_rfparams["resolution"]
253
257
  self.__default_video_framerate = video_rfparams["framerate"]
258
+ self.__default_video_orientation = video_rfparams["orientation"]
254
259
 
255
260
  # parse output parameters through filters (if available)
256
261
  if not (self.__metadata_output is None):
@@ -261,6 +266,7 @@ class Sourcer:
261
266
  if out_video_rfparams:
262
267
  self.__output_frames_resolution = out_video_rfparams["resolution"]
263
268
  self.__output_framerate = out_video_rfparams["framerate"]
269
+ self.__output_orientation = out_video_rfparams["orientation"]
264
270
  # parse output pixel-format
265
271
  self.__output_frames_pixfmt = self.__extract_video_pixfmt(
266
272
  default_stream=default_stream_indexes[0], extract_output=True
@@ -357,11 +363,18 @@ class Sourcer:
357
363
  "source_video_resolution": self.__default_video_resolution,
358
364
  "source_video_pixfmt": self.__default_video_pixfmt,
359
365
  "source_video_framerate": self.__default_video_framerate,
366
+ "source_video_orientation": self.__default_video_orientation,
360
367
  "source_video_decoder": self.__default_video_decoder,
361
368
  "source_duration_sec": self.__default_source_duration,
362
369
  "approx_video_nframes": (
363
370
  int(self.__approx_video_nframes)
364
371
  if self.__approx_video_nframes
372
+ and not any(
373
+ "loop" in x for x in self.__ffmpeg_prefixes
374
+ ) # check if any loops in prefix
375
+ and not any(
376
+ "loop" in x for x in dict2Args(self.__sourcer_params)
377
+ ) # check if any loops in filters
365
378
  else None
366
379
  ),
367
380
  "source_video_bitrate": self.__default_video_bitrate,
@@ -379,6 +392,7 @@ class Sourcer:
379
392
  "output_frames_resolution": self.__output_frames_resolution,
380
393
  "output_frames_pixfmt": self.__output_frames_pixfmt,
381
394
  "output_framerate": self.__output_framerate,
395
+ "output_orientation": self.__output_orientation,
382
396
  }
383
397
  )
384
398
  else:
@@ -391,6 +405,7 @@ class Sourcer:
391
405
  "output_frames_resolution": self.__default_video_resolution,
392
406
  "output_frames_pixfmt": self.__default_video_pixfmt,
393
407
  "output_framerate": self.__default_video_framerate,
408
+ "output_orientation": self.__default_video_orientation,
394
409
  }
395
410
  )
396
411
  # log it
@@ -483,14 +498,18 @@ class Sourcer:
483
498
  self.__source_demuxer = source_demuxer
484
499
  self.__verbose_logs and logger.debug(
485
500
  "Successfully configured device `{}` at index `{}` with demuxer `{}`.".format(
486
- self.__extracted_devices_list[index]
487
- if self.__machine_OS != "Linux"
488
- else next(
489
- iter(self.__extracted_devices_list[index].values())
490
- )[0],
491
- index
492
- if index >= 0
493
- else len(self.__extracted_devices_list) + index,
501
+ (
502
+ self.__extracted_devices_list[index]
503
+ if self.__machine_OS != "Linux"
504
+ else next(
505
+ iter(self.__extracted_devices_list[index].values())
506
+ )[0]
507
+ ),
508
+ (
509
+ index
510
+ if index >= 0
511
+ else len(self.__extracted_devices_list) + index
512
+ ),
494
513
  self.__source_demuxer,
495
514
  )
496
515
  )
@@ -590,9 +609,11 @@ class Sourcer:
590
609
  ]
591
610
  if video_bitrate_text:
592
611
  selected_stream = video_bitrate_text[
593
- default_stream
594
- if default_stream > 0 and default_stream < len(video_bitrate_text)
595
- else 0
612
+ (
613
+ default_stream
614
+ if default_stream > 0 and default_stream < len(video_bitrate_text)
615
+ else 0
616
+ )
596
617
  ]
597
618
  filtered_bitrate = re.findall(
598
619
  r",\s[0-9]+\s\w\w[\/]s", selected_stream.strip()
@@ -624,9 +645,11 @@ class Sourcer:
624
645
  ]
625
646
  if meta_text:
626
647
  selected_stream = meta_text[
627
- default_stream
628
- if default_stream > 0 and default_stream < len(meta_text)
629
- else 0
648
+ (
649
+ default_stream
650
+ if default_stream > 0 and default_stream < len(meta_text)
651
+ else 0
652
+ )
630
653
  ]
631
654
  filtered_pixfmt = re.findall(
632
655
  r"Video:\s[a-z0-9_-]*", selected_stream.strip()
@@ -660,9 +683,11 @@ class Sourcer:
660
683
  )
661
684
  if meta_text:
662
685
  selected_stream = meta_text[
663
- default_stream
664
- if default_stream > 0 and default_stream < len(meta_text)
665
- else 0
686
+ (
687
+ default_stream
688
+ if default_stream > 0 and default_stream < len(meta_text)
689
+ else 0
690
+ )
666
691
  ]
667
692
  filtered_pixfmt = re.findall(
668
693
  r",\s[a-z][a-z0-9_-]*", selected_stream.strip()
@@ -689,9 +714,11 @@ class Sourcer:
689
714
  result = {}
690
715
  if meta_text:
691
716
  selected_stream = meta_text[
692
- default_stream
693
- if default_stream > 0 and default_stream < len(meta_text)
694
- else 0
717
+ (
718
+ default_stream
719
+ if default_stream > 0 and default_stream < len(meta_text)
720
+ else 0
721
+ )
695
722
  ]
696
723
  # filter data
697
724
  filtered_audio_bitrate = re.findall(
@@ -719,7 +746,7 @@ class Sourcer:
719
746
 
720
747
  def __extract_resolution_framerate(self, default_stream=0, extract_output=False):
721
748
  """
722
- This Internal method parses default video-stream resolution and framerate from metadata.
749
+ This Internal method parses default video-stream resolution, orientation, and framerate from metadata.
723
750
 
724
751
  Parameters:
725
752
  default_stream (int): selects specific audio-stream in case of multiple ones.
@@ -742,12 +769,30 @@ class Sourcer:
742
769
  if all(x in line for x in identifiers)
743
770
  ]
744
771
  )
772
+ # extract video orientation metadata if available
773
+ identifiers_orientation = ["displaymatrix:", "rotation"]
774
+ meta_text_orientation = (
775
+ [
776
+ line.strip()
777
+ for line in self.__ffsp_output.split("\n")
778
+ if all(x in line for x in identifiers_orientation)
779
+ ]
780
+ if not extract_output
781
+ else [
782
+ line.strip()
783
+ for line in self.__metadata_output.split("\n")
784
+ if all(x in line for x in identifiers_orientation)
785
+ ]
786
+ )
787
+ # use metadata if available
745
788
  result = {}
746
789
  if meta_text:
747
790
  selected_stream = meta_text[
748
- default_stream
749
- if default_stream > 0 and default_stream < len(meta_text)
750
- else 0
791
+ (
792
+ default_stream
793
+ if default_stream > 0 and default_stream < len(meta_text)
794
+ else 0
795
+ )
751
796
  ]
752
797
 
753
798
  # filter data
@@ -775,7 +820,23 @@ class Sourcer:
775
820
  if filtered_resolution:
776
821
  result["resolution"] = [int(x) for x in filtered_resolution[0]]
777
822
 
778
- return result if result and (len(result) == 2) else {}
823
+ # extract video orientation metadata
824
+ if meta_text_orientation:
825
+ selected_stream = meta_text_orientation[
826
+ (
827
+ default_stream
828
+ if default_stream > 0 and default_stream < len(meta_text)
829
+ else 0
830
+ )
831
+ ]
832
+ filtered_orientation = re.findall(
833
+ r"[-]?\d+\.\d+", selected_stream.strip()
834
+ )
835
+ result["orientation"] = float(filtered_orientation[0])
836
+ else:
837
+ result["orientation"] = 0.0
838
+
839
+ return result if result and (len(result) == 3) else {}
779
840
 
780
841
  def __extract_duration(self, inseconds=True):
781
842
  """
deffcode/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.4"
1
+ __version__ = "0.2.6"