|
2 | 2 | import os
|
3 | 3 | import tempfile
|
4 | 4 | from collections.abc import Iterator
|
| 5 | +from multiprocessing import Pool |
5 | 6 | from pathlib import Path
|
| 7 | +from typing import Any, Optional |
6 | 8 |
|
7 | 9 | import av
|
| 10 | +from tqdm import tqdm |
8 | 11 |
|
9 | 12 | from .logger import logger
|
10 | 13 |
|
@@ -89,8 +92,9 @@ def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
|
89 | 92 | c.link_to(n)
|
90 | 93 |
|
91 | 94 |
|
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: |
93 | 96 | """Reverses a video file, writing the result to `dest`."""
|
| 97 | + src, dest = src_and_dest |
94 | 98 | with (
|
95 | 99 | av.open(str(src)) as input_container,
|
96 | 100 | av.open(str(dest), mode="w") as output_container,
|
@@ -120,8 +124,68 @@ def reverse_video_file(src: Path, dest: Path) -> None:
|
120 | 124 |
|
121 | 125 | for _ in range(frames_count):
|
122 | 126 | 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 |
124 | 128 | output_container.mux(output_stream.encode(frame))
|
125 | 129 |
|
126 | 130 | for packet in output_stream.encode():
|
127 | 131 | 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