Skip to content

Commit 382084f

Browse files
feat(cli): record presentation (#25)
* feat(cli): record presentation As proposed in #21, it is now possible to record a presentation output to a video file, with option `--record-to="some_file.avi"`. Closes #21 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 068484b commit 382084f

File tree

1 file changed

+63
-1
lines changed

1 file changed

+63
-1
lines changed

manim_slides/present.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import cv2
1212
import numpy as np
1313
from pydantic import ValidationError
14+
from tqdm import tqdm
1415

1516
from .commons import config_path_option
1617
from .config import Config, PresentationConfig, SlideConfig, SlideType
@@ -63,6 +64,7 @@ def __init__(self, config: PresentationConfig):
6364

6465
self.current_slide_index = 0
6566
self.current_animation = self.current_slide.start_animation
67+
self.current_file = None
6668

6769
self.loaded_animation_cap = -1
6870
self.cap = None # cap = cv2.VideoCapture
@@ -112,6 +114,8 @@ def load_animation_cap(self, animation: int):
112114
file = "{}_reversed{}".format(*os.path.splitext(file))
113115
self.reversed_animation = animation
114116

117+
self.current_file = file
118+
115119
self.cap = cv2.VideoCapture(file)
116120
self.loaded_animation_cap = animation
117121

@@ -204,6 +208,11 @@ def is_last_animation(self) -> int:
204208
else:
205209
return self.next_animation == self.current_slide.end_animation
206210

211+
@property
212+
def current_frame_number(self) -> int:
213+
"""Returns current frame number."""
214+
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
215+
207216
def update_state(self, state) -> Tuple[np.ndarray, State]:
208217
"""
209218
Updates the current state given the previous one.
@@ -262,6 +271,7 @@ def __init__(
262271
skip_all=False,
263272
resolution=(1980, 1080),
264273
interpolation_flag=cv2.INTER_LINEAR,
274+
record_to=None,
265275
):
266276
self.presentations = presentations
267277
self.start_paused = start_paused
@@ -270,6 +280,8 @@ def __init__(
270280
self.fullscreen = fullscreen
271281
self.resolution = resolution
272282
self.interpolation_flag = interpolation_flag
283+
self.record_to = record_to
284+
self.recordings = []
273285
self.window_flags = (
274286
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_NORMAL
275287
)
@@ -300,7 +312,7 @@ def __init__(
300312

301313
@property
302314
def current_presentation(self) -> Presentation:
303-
"""Returns the current presentation"""
315+
"""Returns the current presentation."""
304316
return self.presentations[self.current_presentation_index]
305317

306318
def run(self):
@@ -331,6 +343,12 @@ def show_video(self):
331343
self.lag = now() - self.last_time
332344
self.last_time = now()
333345

346+
if not self.record_to is None:
347+
pres = self.current_presentation
348+
self.recordings.append(
349+
(pres.current_file, pres.current_frame_number, pres.fps)
350+
)
351+
334352
frame = self.lastframe
335353

336354
# If Window was manually closed (impossible in fullscreen),
@@ -425,6 +443,35 @@ def handle_key(self):
425443
def quit(self):
426444
"""Destroys all windows created by presentations and exits gracefully."""
427445
cv2.destroyAllWindows()
446+
447+
if not self.record_to is None and len(self.recordings) > 0:
448+
file, frame_number, fps = self.recordings[0]
449+
450+
cap = cv2.VideoCapture(file)
451+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
452+
_, frame = cap.read()
453+
454+
w, h = frame.shape[:2]
455+
fourcc = cv2.VideoWriter_fourcc(*"XVID")
456+
out = cv2.VideoWriter(self.record_to, fourcc, fps, (h, w))
457+
458+
out.write(frame)
459+
460+
for _file, frame_number, _ in tqdm(
461+
self.recordings[1:], desc="Creating recording file", leave=False
462+
):
463+
if file != _file:
464+
cap.release()
465+
file = _file
466+
cap = cv2.VideoCapture(_file)
467+
468+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
469+
_, frame = cap.read()
470+
out.write(frame)
471+
472+
cap.release()
473+
out.release()
474+
428475
self.exit = True
429476

430477

@@ -493,6 +540,12 @@ def _list_scenes(folder) -> List[str]:
493540
help="Set the interpolation flag to be used when resizing image. See OpenCV cv::InterpolationFlags.",
494541
show_default=True,
495542
)
543+
@click.option(
544+
"--record-to",
545+
type=click.Path(dir_okay=False),
546+
default=None,
547+
help="If set, the presentation will be recorded into a AVI video file with given name.",
548+
)
496549
@click.help_option("-h", "--help")
497550
def present(
498551
scenes,
@@ -503,6 +556,7 @@ def present(
503556
skip_all,
504557
resolution,
505558
interpolation_flag,
559+
record_to,
506560
):
507561
"""Present the different scenes."""
508562

@@ -562,6 +616,13 @@ def value_proc(value: str):
562616
else:
563617
config = Config()
564618

619+
if not record_to is None:
620+
_, ext = os.path.splitext(record_to)
621+
if ext.lower() != ".avi":
622+
raise click.UsageError(
623+
f"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
624+
)
625+
565626
display = Display(
566627
presentations,
567628
config=config,
@@ -570,5 +631,6 @@ def value_proc(value: str):
570631
skip_all=skip_all,
571632
resolution=resolution,
572633
interpolation_flag=INTERPOLATION_FLAGS[interpolation_flag],
634+
record_to=record_to,
573635
)
574636
display.run()

0 commit comments

Comments
 (0)