1111import cv2
1212import numpy as np
1313from pydantic import ValidationError
14+ from tqdm import tqdm
1415
1516from .commons import config_path_option
1617from .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" )
497550def 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