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>
This commit is contained in:
Jérome Eertmans
2022-09-21 16:10:37 +02:00
committed by GitHub
parent 068484b828
commit 382084f9ef

View File

@ -11,6 +11,7 @@ import click
import cv2 import cv2
import numpy as np import numpy as np
from pydantic import ValidationError from pydantic import ValidationError
from tqdm import tqdm
from .commons import config_path_option from .commons import config_path_option
from .config import Config, PresentationConfig, SlideConfig, SlideType from .config import Config, PresentationConfig, SlideConfig, SlideType
@ -63,6 +64,7 @@ class Presentation:
self.current_slide_index = 0 self.current_slide_index = 0
self.current_animation = self.current_slide.start_animation self.current_animation = self.current_slide.start_animation
self.current_file = None
self.loaded_animation_cap = -1 self.loaded_animation_cap = -1
self.cap = None # cap = cv2.VideoCapture self.cap = None # cap = cv2.VideoCapture
@ -112,6 +114,8 @@ class Presentation:
file = "{}_reversed{}".format(*os.path.splitext(file)) file = "{}_reversed{}".format(*os.path.splitext(file))
self.reversed_animation = animation self.reversed_animation = animation
self.current_file = file
self.cap = cv2.VideoCapture(file) self.cap = cv2.VideoCapture(file)
self.loaded_animation_cap = animation self.loaded_animation_cap = animation
@ -204,6 +208,11 @@ class Presentation:
else: else:
return self.next_animation == self.current_slide.end_animation return self.next_animation == self.current_slide.end_animation
@property
def current_frame_number(self) -> int:
"""Returns current frame number."""
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
def update_state(self, state) -> Tuple[np.ndarray, State]: def update_state(self, state) -> Tuple[np.ndarray, State]:
""" """
Updates the current state given the previous one. Updates the current state given the previous one.
@ -262,6 +271,7 @@ class Display:
skip_all=False, skip_all=False,
resolution=(1980, 1080), resolution=(1980, 1080),
interpolation_flag=cv2.INTER_LINEAR, interpolation_flag=cv2.INTER_LINEAR,
record_to=None,
): ):
self.presentations = presentations self.presentations = presentations
self.start_paused = start_paused self.start_paused = start_paused
@ -270,6 +280,8 @@ class Display:
self.fullscreen = fullscreen self.fullscreen = fullscreen
self.resolution = resolution self.resolution = resolution
self.interpolation_flag = interpolation_flag self.interpolation_flag = interpolation_flag
self.record_to = record_to
self.recordings = []
self.window_flags = ( self.window_flags = (
cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_NORMAL cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO | cv2.WINDOW_NORMAL
) )
@ -300,7 +312,7 @@ class Display:
@property @property
def current_presentation(self) -> Presentation: def current_presentation(self) -> Presentation:
"""Returns the current presentation""" """Returns the current presentation."""
return self.presentations[self.current_presentation_index] return self.presentations[self.current_presentation_index]
def run(self): def run(self):
@ -331,6 +343,12 @@ class Display:
self.lag = now() - self.last_time self.lag = now() - self.last_time
self.last_time = now() self.last_time = now()
if not self.record_to is None:
pres = self.current_presentation
self.recordings.append(
(pres.current_file, pres.current_frame_number, pres.fps)
)
frame = self.lastframe frame = self.lastframe
# If Window was manually closed (impossible in fullscreen), # If Window was manually closed (impossible in fullscreen),
@ -425,6 +443,35 @@ class Display:
def quit(self): def quit(self):
"""Destroys all windows created by presentations and exits gracefully.""" """Destroys all windows created by presentations and exits gracefully."""
cv2.destroyAllWindows() cv2.destroyAllWindows()
if not self.record_to is None and len(self.recordings) > 0:
file, frame_number, fps = self.recordings[0]
cap = cv2.VideoCapture(file)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
_, frame = cap.read()
w, h = frame.shape[:2]
fourcc = cv2.VideoWriter_fourcc(*"XVID")
out = cv2.VideoWriter(self.record_to, fourcc, fps, (h, w))
out.write(frame)
for _file, frame_number, _ in tqdm(
self.recordings[1:], desc="Creating recording file", leave=False
):
if file != _file:
cap.release()
file = _file
cap = cv2.VideoCapture(_file)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
_, frame = cap.read()
out.write(frame)
cap.release()
out.release()
self.exit = True self.exit = True
@ -493,6 +540,12 @@ def _list_scenes(folder) -> List[str]:
help="Set the interpolation flag to be used when resizing image. See OpenCV cv::InterpolationFlags.", help="Set the interpolation flag to be used when resizing image. See OpenCV cv::InterpolationFlags.",
show_default=True, show_default=True,
) )
@click.option(
"--record-to",
type=click.Path(dir_okay=False),
default=None,
help="If set, the presentation will be recorded into a AVI video file with given name.",
)
@click.help_option("-h", "--help") @click.help_option("-h", "--help")
def present( def present(
scenes, scenes,
@ -503,6 +556,7 @@ def present(
skip_all, skip_all,
resolution, resolution,
interpolation_flag, interpolation_flag,
record_to,
): ):
"""Present the different scenes.""" """Present the different scenes."""
@ -562,6 +616,13 @@ def present(
else: else:
config = Config() config = Config()
if not record_to is None:
_, ext = os.path.splitext(record_to)
if ext.lower() != ".avi":
raise click.UsageError(
f"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
)
display = Display( display = Display(
presentations, presentations,
config=config, config=config,
@ -570,5 +631,6 @@ def present(
skip_all=skip_all, skip_all=skip_all,
resolution=resolution, resolution=resolution,
interpolation_flag=INTERPOLATION_FLAGS[interpolation_flag], interpolation_flag=INTERPOLATION_FLAGS[interpolation_flag],
record_to=record_to,
) )
display.run() display.run()