From cb7a251456bc123a88fd88672b013a32118b872e Mon Sep 17 00:00:00 2001 From: Fred Petitpont Date: Thu, 23 Sep 2021 11:06:47 +0200 Subject: [PATCH 01/10] Adding default start timecode --- ffprobe/ffprobe.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index f1eb3dc..3ee3f52 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -42,6 +42,7 @@ def __init__(self, path_to_video): self.audio = [] self.subtitle = [] self.attachment = [] + self.timecode = None for line in iter(p.stdout.readline, b''): line = line.decode('UTF-8', 'ignore') @@ -92,6 +93,11 @@ def __init__(self, path_to_video): elif stream: data_lines.append(line) + if 'timecode' in line: + if match := re.search('(\d\d:\d\d:\d\d:\d\d)', line, re.IGNORECASE): + self.timecode = match.group(1) + + p.stdout.close() p.stderr.close() @@ -108,7 +114,7 @@ def __init__(self, path_to_video): raise IOError('No such media file or stream is not responding: ' + self.path_to_video) def __repr__(self): - return "".format(**vars(self)) + return "".format(**vars(self)) class FFStream: From 0c9353e51f6c62b556faa7dd67eb19ff3eeb22c1 Mon Sep 17 00:00:00 2001 From: Thibault Chassagnette Date: Wed, 12 Jan 2022 10:24:37 +0100 Subject: [PATCH 02/10] Removing last line of description to avoid -> https://github.com/pypa/setuptools/issues/1390 --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 8b496ab..7a0374b 100644 --- a/setup.py +++ b/setup.py @@ -9,9 +9,7 @@ setup( name='ffprobe-python', version='1.0.3', - description=""" - A wrapper around ffprobe command to extract metadata from media files. - """, + description="A wrapper around ffprobe command to extract metadata from media files.", long_description=long_description, long_description_content_type="text/markdown", author='Simon Hargreaves', From 6c0e098ae172a04e0e258db6a4605634b3aeff85 Mon Sep 17 00:00:00 2001 From: Fred Petitpont Date: Wed, 16 Mar 2022 05:42:23 -0400 Subject: [PATCH 03/10] adding audio channel layout method --- ffprobe/ffprobe.py | 6 ++++++ tests/ffprobe_test.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index 3ee3f52..e8fd6bb 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -223,6 +223,12 @@ def frames(self): return frame_count + def audio_channel_layout(self): + """ + Display a string of the audio channel, ie: mono, stereo, quad + """ + return self.__dict__.get('channel_layout', None) + def duration_seconds(self): """ Returns the runtime duration of the video stream as a floating point number of seconds. diff --git a/tests/ffprobe_test.py b/tests/ffprobe_test.py index 9078f76..2959376 100644 --- a/tests/ffprobe_test.py +++ b/tests/ffprobe_test.py @@ -32,6 +32,8 @@ def test_video (): print('\t\tDuration:', stream.duration_seconds()) print('\t\tFrames:', stream.frames()) print('\t\tIs video:', stream.is_video()) + if stream.is_audio(): + print('\t\tAudio channel layout:', stream.audio_channel_layout()) except FFProbeError as e: print(e) except Exception as e: @@ -52,6 +54,8 @@ def test_stream (): print('\t\tDuration:', stream.duration_seconds()) print('\t\tFrames:', stream.frames()) print('\t\tIs video:', stream.is_video()) + if stream.is_audio(): + print('\t\tAudio channel layout:', stream.audio_channel_layout()) except FFProbeError as e: print(e) except Exception as e: From 147c0c3fbcaefb83aa20340e77f1229b19648299 Mon Sep 17 00:00:00 2001 From: Thibault Chassagnette Date: Tue, 20 Jun 2023 15:54:29 +0200 Subject: [PATCH 04/10] Provide a framerate with 2 decimals instead of an integer --- ffprobe/ffprobe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index e8fd6bb..a68ae14 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -130,7 +130,7 @@ def __init__(self, data_lines): self.__dict__['framerate'] = round( functools.reduce( operator.truediv, map(int, self.__dict__.get('avg_frame_rate', '').split('/')) - ) + ), 2 ) except ValueError: From 264b9b191b8e0b0924503fc8427849e8041adc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Ferrachat?= Date: Thu, 21 Sep 2023 11:36:43 +0200 Subject: [PATCH 05/10] Using r_frame_rate if avg_frame_rate not set --- ffprobe/ffprobe.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index a68ae14..87a9ca4 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -8,6 +8,7 @@ import platform import re import subprocess +from typing import Optional from ffprobe.exceptions import FFProbeError @@ -126,17 +127,11 @@ def __init__(self, data_lines): for line in data_lines: self.__dict__.update({key: value for key, value, *_ in [line.strip().split('=')]}) - try: - self.__dict__['framerate'] = round( - functools.reduce( - operator.truediv, map(int, self.__dict__.get('avg_frame_rate', '').split('/')) - ), 2 - ) + frame_rate = self._frame_rate_from('avg_frame_rate') + if frame_rate is None or frame_rate == 0: + frame_rate = self._frame_rate_from('r_frame_rate') - except ValueError: - self.__dict__['framerate'] = None - except ZeroDivisionError: - self.__dict__['framerate'] = 0 + self.__dict__['framerate'] = frame_rate def __repr__(self): if self.is_video(): @@ -154,6 +149,17 @@ def __repr__(self): return template.format(**self.__dict__) + def _frame_rate_from(self, metadata: str) -> Optional[float]: + try: + return round( + functools.reduce(operator.truediv, map(int, self.__dict__.get(metadata, '').split('/'))), + 2 + ) + except ValueError: + return None + except ZeroDivisionError: + return 0 + def is_audio(self): """ Is this stream labelled as an audio stream? From 4c421e9f99b86f6acb5a80b02274bef10e6c2614 Mon Sep 17 00:00:00 2001 From: Thibault Chassagnette Date: Thu, 5 Oct 2023 14:18:16 +0200 Subject: [PATCH 06/10] Allow reading and indexing metadata from Side Data (#2) --- ffprobe/ffprobe.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index 87a9ca4..6145e2f 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -58,11 +58,7 @@ def __init__(self, path_to_video): # noinspection PyUnboundLocalVariable self.streams.append(FFStream(data_lines)) elif stream: - if '[SIDE_DATA]' in line: - ignoreLine = True - elif '[/SIDE_DATA]' in line: - ignoreLine = False - elif ignoreLine == False: + if '=' in line and ignoreLine == False: data_lines.append(line) self.metadata = {} From d4e4a82a25163f7a15a61de14c68d4038c9cac10 Mon Sep 17 00:00:00 2001 From: Thibault Chassagnette Date: Thu, 21 Nov 2024 11:13:22 +0100 Subject: [PATCH 07/10] Avoid 0/0 framerate from avg_framerate --- ffprobe/ffprobe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index 6145e2f..1b00c26 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -124,7 +124,7 @@ def __init__(self, data_lines): self.__dict__.update({key: value for key, value, *_ in [line.strip().split('=')]}) frame_rate = self._frame_rate_from('avg_frame_rate') - if frame_rate is None or frame_rate == 0: + if frame_rate is None or frame_rate == 0 or frame_rate == "0/0": frame_rate = self._frame_rate_from('r_frame_rate') self.__dict__['framerate'] = frame_rate From 74c6da332fe6294181da960e59b9fad370466c7a Mon Sep 17 00:00:00 2001 From: jeanmichel_nwsb Date: Thu, 26 Jun 2025 11:07:49 +0200 Subject: [PATCH 08/10] add alternate duration --- .gitignore | 2 ++ ffprobe/ffprobe.py | 41 +++++++++++++++++++++++++++++------------ tests/ffprobe_test.py | 2 ++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index b1efb15..8ef98ae 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ pip-selfcheck.json # Other MANIFEST +*.env +.vscode/* \ No newline at end of file diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index 1b00c26..d3403d7 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -30,14 +30,13 @@ def __init__(self, path_to_video): if os.path.isfile(self.path_to_video) or self.path_to_video.startswith('http'): if platform.system() == 'Windows': - cmd = ["ffprobe", "-show_streams", self.path_to_video] + cmd = ["ffprobe", "-show_streams", "-show_format", self.path_to_video] else: cmd = ["ffprobe -show_streams " + pipes.quote(self.path_to_video)] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) - stream = False - ignoreLine = False + aggregate_lines = False self.streams = [] self.video = [] self.audio = [] @@ -49,16 +48,20 @@ def __init__(self, path_to_video): line = line.decode('UTF-8', 'ignore') if '[STREAM]' in line: - stream = True - ignoreLine = False + aggregate_lines = True data_lines = [] - elif '[/STREAM]' in line and stream: + elif '[/STREAM]' in line and aggregate_lines: stream = False - ignoreLine = False # noinspection PyUnboundLocalVariable self.streams.append(FFStream(data_lines)) - elif stream: - if '=' in line and ignoreLine == False: + elif '[FORMAT]' in line: + aggregate_lines = True + data_lines = [] + elif '[/FORMAT]' in line and aggregate_lines: + aggregate_lines = False + self.format = FFFormat(data_lines) + elif aggregate_lines: + if '=' in line: data_lines.append(line) self.metadata = {} @@ -82,12 +85,18 @@ def __init__(self, path_to_video): self.metadata[m.groups()[0]] = m.groups()[1].strip() if '[STREAM]' in line: - stream = True + aggregate_lines = True data_lines = [] elif '[/STREAM]' in line and stream: - stream = False + aggregate_lines = False self.streams.append(FFStream(data_lines)) - elif stream: + elif '[FORMAT]' in line: + aggregate_lines = True + data_lines = [] + elif '[/FORMAT]' in line and aggregate_lines: + aggregate_lines = False + self.format = FFFormat(data_lines) + elif aggregate_lines: data_lines.append(line) if 'timecode' in line: @@ -114,6 +123,14 @@ def __repr__(self): return "".format(**vars(self)) +class FFFormat: + """ + An object representation of the overall container format of a multimedia file. + """ + def __init__(self, data_lines): + for line in data_lines: + self.__dict__.update({key: value for key, value, *_ in [line.strip().split('=')]}) + class FFStream: """ An object representation of an individual stream in a multimedia file. diff --git a/tests/ffprobe_test.py b/tests/ffprobe_test.py index 2959376..ecf5414 100644 --- a/tests/ffprobe_test.py +++ b/tests/ffprobe_test.py @@ -38,6 +38,8 @@ def test_video (): print(e) except Exception as e: print(e) + print(f"\tFormat:\n\t\tDuration: {media.format.duration}") + def test_stream (): for test_stream in test_streams: From 2f3c9a9c13c8cfcbe99e82140b52230d0b066051 Mon Sep 17 00:00:00 2001 From: jeanmichel_nwsb Date: Thu, 26 Jun 2025 11:11:49 +0200 Subject: [PATCH 09/10] little rewrite --- ffprobe/ffprobe.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index d3403d7..453a488 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -60,8 +60,7 @@ def __init__(self, path_to_video): elif '[/FORMAT]' in line and aggregate_lines: aggregate_lines = False self.format = FFFormat(data_lines) - elif aggregate_lines: - if '=' in line: + elif aggregate_lines and '=' in line: data_lines.append(line) self.metadata = {} @@ -96,7 +95,7 @@ def __init__(self, path_to_video): elif '[/FORMAT]' in line and aggregate_lines: aggregate_lines = False self.format = FFFormat(data_lines) - elif aggregate_lines: + elif aggregate_lines and '=' in line: data_lines.append(line) if 'timecode' in line: From f290cba4ab0948e14a5c900c96675324e23872b2 Mon Sep 17 00:00:00 2001 From: jeanmichel_nwsb Date: Wed, 17 Sep 2025 19:32:07 +0200 Subject: [PATCH 10/10] add function to test interlaved-progressive --- ffprobe/ffprobe.py | 10 ++++++++++ tests/ffprobe_test.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ffprobe/ffprobe.py b/ffprobe/ffprobe.py index 453a488..bd86de2 100644 --- a/ffprobe/ffprobe.py +++ b/ffprobe/ffprobe.py @@ -294,3 +294,13 @@ def bit_rate(self): return int(self.__dict__.get('bit_rate', '')) except ValueError: raise FFProbeError('None integer bit_rate') + + def is_progressive(self): + if self.is_video() and self.__dict__.get('field_order', "") == "progressive": + return True + return False + + def is_interlaced(self): + if self.is_video() and self.__dict__.get('field_order', "") in ["tt", "bb", "tb", "bt"]: + return True + return False diff --git a/tests/ffprobe_test.py b/tests/ffprobe_test.py index ecf5414..78d0e8d 100644 --- a/tests/ffprobe_test.py +++ b/tests/ffprobe_test.py @@ -27,8 +27,11 @@ def test_video (): try: if stream.is_video(): frame_rate = stream.frames() / stream.duration_seconds() - print('\t\tFrame Rate:', frame_rate) - print('\t\tFrame Size:', stream.frame_size()) + print('\t\tFrame Rate :', frame_rate) + print('\t\tFrame Size :', stream.frame_size()) + print('\t\tField order:', stream.field_order) + print('\t\tProgressive:', stream.is_progressive()) + print('\t\tInterlaced :', stream.is_interlaced()) print('\t\tDuration:', stream.duration_seconds()) print('\t\tFrames:', stream.frames()) print('\t\tIs video:', stream.is_video()) @@ -53,6 +56,9 @@ def test_stream (): frame_rate = stream.frames() / stream.duration_seconds() print('\t\tFrame Rate:', frame_rate) print('\t\tFrame Size:', stream.frame_size()) + print('\t\tField order:', stream.field_order) + print('\t\tProgressive:', stream.is_progressive()) + print('\t\tInterlaced :', stream.is_interlaced()) print('\t\tDuration:', stream.duration_seconds()) print('\t\tFrames:', stream.frames()) print('\t\tIs video:', stream.is_video())