Skip to content

Commit 4fc2454

Browse files
committed
feat(lib): smarter files reversing
Implement a smarter generation of reversed files by splitting the video into smaller segments. Closes #434
1 parent b080739 commit 4fc2454

File tree

2 files changed

+75
-4
lines changed

2 files changed

+75
-4
lines changed

manim_slides/slide/base.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,11 @@ def _save_slides(self, use_cache: bool = True) -> None:
475475

476476
for pre_slide_config in tqdm(
477477
self._slides,
478-
desc=f"Concatenating animation files to '{scene_files_folder}' and generating reversed animations",
478+
desc=f"Concatenating animations to '{scene_files_folder}' and generating reversed animations",
479479
leave=self._leave_progress_bar,
480480
ascii=True if platform.system() == "Windows" else None,
481481
disable=not self._show_progress_bar,
482+
unit=" slides",
482483
):
483484
slide_files = files[pre_slide_config.slides_slice]
484485

@@ -492,7 +493,13 @@ def _save_slides(self, use_cache: bool = True) -> None:
492493

493494
# We only reverse video if it was not present
494495
if not use_cache or not rev_file.exists():
495-
reverse_video_file(dst_file, rev_file)
496+
reverse_video_file(
497+
dst_file,
498+
rev_file,
499+
leave=self._leave_progress_bar,
500+
ascii=True if platform.system() == "Windows" else None,
501+
disable=not self._show_progress_bar,
502+
)
496503

497504
slides.append(
498505
SlideConfig.from_pre_slide_config_and_files(

manim_slides/utils.py

+66-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
import os
33
import tempfile
44
from collections.abc import Iterator
5+
from multiprocessing import Pool
56
from pathlib import Path
7+
from typing import Any, Optional
68

79
import av
10+
from tqdm import tqdm
811

912
from .logger import logger
1013

@@ -89,8 +92,9 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
8992
c.link_to(n)
9093

9194

92-
def reverse_video_file(src: Path, dest: Path) -> None:
95+
def reverse_video_file_in_one_chunk(src_and_dest: tuple[Path, Path]) -> None:
9396
"""Reverses a video file, writing the result to `dest`."""
97+
src, dest = src_and_dest
9498
with (
9599
av.open(str(src)) as input_container,
96100
av.open(str(dest), mode="w") as output_container,
@@ -120,8 +124,68 @@ def reverse_video_file(src: Path, dest: Path) -> None:
120124

121125
for _ in range(frames_count):
122126
frame = graph.pull()
123-
frame.pict_type = 5 # Otherwise we get a warning saying it is changed
127+
frame.pict_type = "NONE" # Otherwise we get a warning saying it is changed
124128
output_container.mux(output_stream.encode(frame))
125129

126130
for packet in output_stream.encode():
127131
output_container.mux(packet)
132+
133+
134+
def reverse_video_file(
135+
src: Path,
136+
dest: Path,
137+
max_segment_duration: float = 1,
138+
processes: Optional[int] = None,
139+
**tqdm_kwargs: Any,
140+
) -> None:
141+
"""Reverses a video file, writing the result to `dest`."""
142+
with av.open(str(src)) as input_container: # Fast path if file is short enough
143+
input_stream = input_container.streams.video[0]
144+
if input_stream.duration:
145+
if (
146+
float(input_stream.duration * input_stream.time_base)
147+
<= max_segment_duration
148+
):
149+
return reverse_video_file_in_one_chunk((src, dest))
150+
else:
151+
logger.debug(
152+
f"Could not determine duration of {src}, falling back to segmentation."
153+
)
154+
155+
with tempfile.TemporaryDirectory() as tmpdirname:
156+
tmpdir = Path(tmpdirname)
157+
with av.open(
158+
str(tmpdir / "%04d.mp4"),
159+
"w",
160+
format="segment",
161+
options={"segment_time": str(max_segment_duration)},
162+
) as output_container:
163+
output_stream = output_container.add_stream(
164+
template=input_stream,
165+
)
166+
167+
for packet in input_container.demux(input_stream):
168+
if packet.dts is None:
169+
continue
170+
171+
packet.stream = output_stream
172+
output_container.mux(packet)
173+
174+
src_files = list(tmpdir.iterdir())
175+
rev_files = [
176+
src_file.with_stem("rev_" + src_file.stem) for src_file in src_files
177+
]
178+
179+
with Pool(processes, maxtasksperchild=1) as pool:
180+
for _ in tqdm(
181+
pool.imap_unordered(
182+
reverse_video_file_in_one_chunk, zip(src_files, rev_files)
183+
),
184+
desc="Reversing large file by cutting it in segments",
185+
total=len(src_files),
186+
unit=" files",
187+
**tqdm_kwargs,
188+
):
189+
pass # We just consume the iterator
190+
191+
concatenate_video_files(rev_files[::-1], dest)

0 commit comments

Comments
 (0)