Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Piskun
03a7ed27e0
Merge 968f73fd67 into ffbecfffb9 2026-07-08 07:15:02 +00:00
bigcat88
968f73fd67
fix(Video): stream the video transcode instead of buffering every frame in RAM
Some checks are pending
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Signed-off-by: bigcat88 <bigcat88@icloud.com>
2026-07-08 10:14:46 +03:00
2 changed files with 4 additions and 60 deletions

View File

@ -80,7 +80,7 @@ def probe_audio_params(container: InputContainer, audio_stream, max_packets: int
try:
frames = packet.decode()
except av.error.FFmpegError:
frames = ()
return 0, 0
if frames:
return frames[0].sample_rate, frames[0].layout.nb_channels
if i >= max_packets:
@ -88,7 +88,7 @@ def probe_audio_params(container: InputContainer, audio_stream, max_packets: int
return 0, 0
def write_output_metadata(container: InputContainer, output, metadata: dict | None):
def write_output_metadata(container: InputContainer, output, metadata: Optional[dict]):
"""Copy the source container's metadata, then overlay the caller's tags."""
for key, value in container.metadata.items():
if metadata is None or key not in metadata:
@ -512,7 +512,7 @@ class VideoFromFile(VideoInput):
path: str | io.BytesIO,
format: VideoContainer,
codec: VideoCodec,
metadata: dict | None,
metadata: Optional[dict],
bit_depth: int,
):
"""Re-encode to H.264/AAC one frame at a time; peak memory does not scale with video length."""
@ -626,8 +626,7 @@ class VideoFromFile(VideoInput):
video_done = True
if last_video_pts is not None:
# the source continues past the window: hold the last kept frame to the window end
end_offset = video_pts_offset if video_pts_offset is not None else start_pts
last_video_end = max(last_video_end, end_pts - end_offset)
last_video_end = max(last_video_end, end_pts - video_pts_offset)
break
# the source's true display duration of this frame; average_rate is not a
# frame duration (sparse/VFR sources), so it is only the fallback

View File

@ -595,61 +595,6 @@ def test_save_to_transcode_irregular_vfr_keeps_span():
assert trimmed["video_seconds"] == pytest.approx(10.0, abs=0.05)
def test_save_to_transcode_trim_survives_missing_leading_pts():
"""A trim should survive pts-less kept frames followed by a real-pts frame past the window."""
nulled_frames = 0
class _PacketProxy:
def __init__(self, packet):
self._packet = packet
def __getattr__(self, name):
return getattr(self._packet, name)
@property
def stream(self):
return self._packet.stream
def decode(self):
nonlocal nulled_frames
frames = self._packet.decode()
for frame in frames:
if nulled_frames < 2:
frame.pts = None
nulled_frames += 1
return frames
class _ContainerProxy:
def __init__(self, real):
self._real = real
def __getattr__(self, name):
return getattr(self._real, name)
def demux(self, *streams):
for packet in self._real.demux(*streams):
yield _PacketProxy(packet)
file_path = create_transcode_source(frames=10, audio_streams=0)
try:
buffer = io.BytesIO()
with av.open(file_path) as container:
# 0.05 s window: both pts-less frames are kept (synthesized pts 0 and 512),
# and the first real-pts frame (1024 ticks) already lies past end_pts (768)
VideoFromFile(file_path, duration=0.05)._save_transcoded(
_ContainerProxy(container), buffer, VideoContainer.MP4, VideoCodec.H264, None, 8
)
assert nulled_frames == 2
buffer.seek(0)
with av.open(buffer) as container:
video_stream = container.streams.video[0]
frames = [f for p in container.demux(video_stream) for f in p.decode()]
assert len(frames) == 2
assert float(video_stream.duration * video_stream.time_base) == pytest.approx(2 / 30, abs=0.01)
finally:
os.unlink(file_path)
def test_save_to_transcode_bakes_rotation():
"""A 90-degree display-matrix rotation swaps the output dimensions (portrait video)"""
file_path = create_transcode_source(width=64, height=32, rotation=True)