diff --git a/media_shrinker.py b/media_shrinker.py index d72a8d5..d0c333a 100644 --- a/media_shrinker.py +++ b/media_shrinker.py @@ -1507,9 +1507,11 @@ def _parse_probe_payload( raise MediaShrinkerError(f"{source_path} has no audio stream") format_section = payload.get("format", {}) - duration = _first_float( - audio_stream.get("duration"), format_section.get("duration") - ) + # Prefer the stream duration, but a stream-level "0"/"0.000000" (reported by + # some containers) is unusable and must fall back to the format duration. + duration = _first_float(audio_stream.get("duration")) + if duration <= 0: + duration = _first_float(format_section.get("duration")) if duration <= 0: raise MediaShrinkerError(f"{source_path} has no usable duration") diff --git a/tests/test_media_shrinker.py b/tests/test_media_shrinker.py index 0ab2397..7c3dc2c 100644 --- a/tests/test_media_shrinker.py +++ b/tests/test_media_shrinker.py @@ -238,6 +238,28 @@ def test_parse_probe_payload_uses_known_source_size_without_stat(self) -> None: self.assertEqual(probe.size_bytes, 123) + def test_parse_probe_payload_falls_back_to_format_duration_when_stream_is_zero( + self, + ) -> None: + # Real ffprobe output: some containers report a stream-level + # "duration": "0.000000" while the true duration lives in the format + # section. The stream's unusable zero must not shadow the format value. + payload = { + "streams": [ + { + "codec_type": "audio", + "codec_name": "aac", + "duration": "0.000000", + "bit_rate": "128000", + } + ], + "format": {"duration": "123.5", "size": "456"}, + } + + probe = media_shrinker._parse_probe_payload(payload, Path("stream.mp4")) + + self.assertEqual(probe.duration_seconds, 123.5) + class PlanningTests(unittest.TestCase): def test_pcm_wav_uses_lossless_flac_first_and_preserves_container_metadata(