Skip to content

Commit

Permalink
Merge pull request #2114 from bzczb/i869-default-stream
Browse files Browse the repository at this point in the history
Fix ffmpeg_reader not selecting a default stream
  • Loading branch information
OsaAjani authored Feb 6, 2025
2 parents d0513ae + 02f6e92 commit 1d71301
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 38 deletions.
79 changes: 41 additions & 38 deletions moviepy/video/io/ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,9 @@ def _reset_state(self):
# should be ignored
self._inside_output = False

# flag which indicates that a default stream has not been found yet
self._default_stream_found = False
# map from stream type to default stream
# if a default stream is not indicated, pick the first one available
self._default_streams = {}

# current input file, stream and chapter, which will be built at runtime
self._current_input_file = {"streams": []}
Expand Down Expand Up @@ -484,20 +485,14 @@ def parse(self):
"stream_number": stream_number,
"stream_type": stream_type_lower,
"language": language,
"default": not self._default_stream_found
or line.endswith("(default)"),
"default": (stream_type_lower not in self._default_streams)
and line.endswith("(default)"),
}
self._default_stream_found = True

# for default streams, set their numbers globally, so it's
# easy to get without iterating all
if self._current_stream["default"]:
self.result[
f"default_{stream_type_lower}_input_number"
] = input_number
self.result[
f"default_{stream_type_lower}_stream_number"
] = stream_number
self._default_streams[stream_type_lower] = self._current_stream

# exit chapter
if self._current_chapter:
Expand All @@ -516,21 +511,18 @@ def parse(self):
input_number
]

# add new input file to self.result
# add new input file to result
self.result["inputs"].append(self._current_input_file)
self._current_input_file = {"input_number": input_number}

# parse relevant data by stream type
try:
global_data, stream_data = self.parse_data_by_stream_type(
stream_type, line
)
stream_data = self.parse_data_by_stream_type(stream_type, line)
except NotImplementedError as exc:
warnings.warn(
f"{str(exc)}\nffmpeg output:\n\n{self.infos}", UserWarning
)
else:
self.result.update(global_data)
self._current_stream.update(stream_data)
elif line.startswith(" Metadata:"):
# enter group " Metadata:"
Expand Down Expand Up @@ -596,7 +588,7 @@ def parse(self):
self._last_metadata_field_added = field
self._current_chapter["metadata"][field] = value

# last input file, must be included in self.result
# last input file, must be included in the result
if self._current_input_file:
self._current_input_file["streams"].append(self._current_stream)
# include their chapters, if there are any
Expand All @@ -609,6 +601,33 @@ def parse(self):
]
self.result["inputs"].append(self._current_input_file)

# set any missing default automatically
for stream in self._current_input_file["streams"]:
if stream["stream_type"] not in self._default_streams:
self._default_streams[stream["stream_type"]] = stream
stream["default"] = True

# set some global info based on the defaults
for stream_type_lower, stream_data in self._default_streams.items():
self.result[f"default_{stream_type_lower}_input_number"] = stream_data[
"input_number"
]
self.result[f"default_{stream_type_lower}_stream_number"] = stream_data[
"stream_number"
]

if stream_type_lower == "audio":
self.result["audio_found"] = True
self.result["audio_fps"] = stream_data["fps"]
self.result["audio_bitrate"] = stream_data["bitrate"]
elif stream_type_lower == "video":
self.result["video_found"] = True
self.result["video_size"] = stream_data.get("size", None)
self.result["video_bitrate"] = stream_data.get("bitrate", None)
self.result["video_fps"] = stream_data["fps"]
self.result["video_codec_name"] = stream_data.get("codec_name", None)
self.result["video_profile"] = stream_data.get("profile", None)

# some video duration utilities
if self.result["video_found"] and self.check_duration:
self.result["video_duration"] = self.result["duration"]
Expand Down Expand Up @@ -646,7 +665,7 @@ def parse_data_by_stream_type(self, stream_type, line):
return {
"Audio": self.parse_audio_stream_data,
"Video": self.parse_video_stream_data,
"Data": lambda _line: ({}, {}),
"Data": lambda _line: {},
}[stream_type](line)
except KeyError:
raise NotImplementedError(
Expand All @@ -656,7 +675,7 @@ def parse_data_by_stream_type(self, stream_type, line):

def parse_audio_stream_data(self, line):
"""Parses data from "Stream ... Audio" line."""
global_data, stream_data = ({"audio_found": True}, {})
stream_data = {}
try:
stream_data["fps"] = int(re.search(r" (\d+) Hz", line).group(1))
except (AttributeError, ValueError):
Expand All @@ -667,14 +686,11 @@ def parse_audio_stream_data(self, line):
stream_data["bitrate"] = (
int(match_audio_bitrate.group(1)) if match_audio_bitrate else None
)
if self._current_stream["default"]:
global_data["audio_fps"] = stream_data["fps"]
global_data["audio_bitrate"] = stream_data["bitrate"]
return (global_data, stream_data)
return stream_data

def parse_video_stream_data(self, line):
"""Parses data from "Stream ... Video" line."""
global_data, stream_data = ({"video_found": True}, {})
stream_data = {}

try:
match_video_size = re.search(r" (\d+)x(\d+)[,\s]", line)
Expand Down Expand Up @@ -736,20 +752,7 @@ def parse_video_stream_data(self, line):
stream_data["codec_name"] = codec_name
stream_data["profile"] = profile

if self._current_stream["default"] or "video_codec_name" not in self.result:
global_data["video_codec_name"] = stream_data.get("codec_name", None)

if self._current_stream["default"] or "video_profile" not in self.result:
global_data["video_profile"] = stream_data.get("profile", None)

if self._current_stream["default"] or "video_size" not in self.result:
global_data["video_size"] = stream_data.get("size", None)
if self._current_stream["default"] or "video_bitrate" not in self.result:
global_data["video_bitrate"] = stream_data.get("bitrate", None)
if self._current_stream["default"] or "video_fps" not in self.result:
global_data["video_fps"] = stream_data["fps"]

return (global_data, stream_data)
return stream_data

def parse_fps(self, line):
"""Parses number of FPS from a line of the ``ffmpeg -i`` command output."""
Expand Down
33 changes: 33 additions & 0 deletions tests/test_ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from moviepy.audio.AudioClip import AudioClip
from moviepy.config import FFMPEG_BINARY
from moviepy.tools import ffmpeg_escape_filename
from moviepy.video.compositing.CompositeVideoClip import clips_array
from moviepy.video.io.ffmpeg_reader import (
FFMPEG_VideoReader,
Expand Down Expand Up @@ -56,6 +57,38 @@ def test_ffmpeg_parse_infos_video_nframes():
assert d["video_n_frames"] == 5


def test_ffmpeg_parse_infos_no_default_stream(util):
"""WMV files don't have "default" streams marked in ffmpeg output.
Make sure that ffmpeg_parse_infos can handle this case.
"""
mp4_filepath = os.path.abspath("media/smpte-2997.mp4")
wmv_filepath = os.path.join(
util.TMP_DIR, "ffmpeg_parse_infos_no_default_stream-smpte-2997.wmv"
)

cmd = [
FFMPEG_BINARY,
"-y",
"-i",
ffmpeg_escape_filename(mp4_filepath),
ffmpeg_escape_filename(wmv_filepath),
]
with open(os.devnull, "w") as stderr:
subprocess.check_call(cmd, stderr=stderr)

d = ffmpeg_parse_infos(wmv_filepath)

for key in (
"default_video_stream_number",
"default_video_input_number",
"default_audio_stream_number",
"default_audio_input_number",
"video_fps",
"audio_fps",
):
assert key in d


@pytest.mark.parametrize(
("decode_file", "expected_duration"),
(
Expand Down

0 comments on commit 1d71301

Please sign in to comment.