Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[@lapotist](https://github.com/lapotist) [#664](https://github.com/jeertmans/manim-slides/pull/664)
- Fixed presenter looping when reversing multiple slides.
[@wuerfelfreak](https://github.com/wuerfelfreak) [#665](https://github.com/jeertmans/manim-slides/pull/665)
- Fixed intermittent deadlocks when reversing large videos by creating the
worker pool with the `spawn` start method instead of `fork`.
[@Agi-Asi](https://github.com/Agi-Asi) [#672](https://github.com/jeertmans/manim-slides/pull/672)

(v5.6.0)=
## [v5.6.0](https://github.com/jeertmans/manim-slides/compare/v5.5.4...v5.6.0)
Expand Down
16 changes: 14 additions & 2 deletions manim_slides/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import hashlib
import multiprocessing as mp
import os
import shutil
import tempfile
from collections.abc import Iterator
from itertools import pairwise
from multiprocessing import Pool
from pathlib import Path
from typing import Any, TypeVar, cast

Expand Down Expand Up @@ -220,7 +220,19 @@ def reverse_video_file(
src_file.with_stem("rev_" + src_file.stem) for src_file in src_files
]

with Pool(num_processes, maxtasksperchild=1) as pool:
# Worker processes are created with the 'spawn' start method:
# the default 'fork' on Linux duplicates this process while the
# parent holds PyAV/FFmpeg (and possibly tqdm) locks from the
# still-open input container above — a forked child can then
# inherit a lock in a locked state with no owner to release it,
# and the pool deadlocks on its queue semaphore. Observed as
# renders hanging in 'Reversing large file by cutting it in
# segments' (#562) and as a multiprocessing semaphore deadlock
# (#569). 'spawn' starts clean interpreters, which cannot
# inherit held locks, and matches the default on macOS/Windows.
with mp.get_context("spawn").Pool(
num_processes, maxtasksperchild=1
) as pool:
for _ in tqdm(
pool.imap_unordered(
reverse_video_file_in_one_chunk,
Expand Down
20 changes: 19 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pathlib import Path

from manim_slides.utils import merge_basenames
import av

from manim_slides.utils import merge_basenames, reverse_video_file


def test_merge_basenames(paths: list[Path]) -> None:
Expand All @@ -20,3 +22,19 @@ def test_merge_basenames_same_with_different_parent_directories(
p4 = d2 / "d/e/f/two.txt"

assert merge_basenames([p1, p2]).name == merge_basenames([p3, p4]).name


def test_reverse_video_file_segmented(video_file: Path, tmp_path: Path) -> None:
# Regression test for #562 / #569: the segmented path runs a
# multiprocessing pool while the parent still holds an open PyAV input
# container. With the previous fork-based pool this could deadlock
# (forked children inherit held FFmpeg/tqdm locks); with the spawn-based
# pool it must complete and produce a decodable, non-empty video.
dest = tmp_path / "reversed.mp4"

reverse_video_file(video_file, dest, max_segment_duration=0.5)

assert dest.exists()
with av.open(str(dest)) as container:
frames = sum(1 for _ in container.decode(video=0))
assert frames > 0
Loading