import json import math import os import sys import time from enum import Enum import click import cv2 import numpy as np from .commons import config_path_option from .config import Config from .defaults import CONFIG_PATH, FOLDER_PATH class State(Enum): PLAYING = 0 PAUSED = 1 WAIT = 2 END = 3 def __str__(self): if self.value == 0: return "Playing" if self.value == 1: return "Paused" if self.value == 2: return "Wait" if self.value == 3: return "End" return "..." def now(): return round(time.time() * 1000) def fix_time(x): return x if x > 0 else 1 class Presentation: def __init__(self, config, last_frame_next: bool = False): self.last_frame_next = last_frame_next self.slides = config["slides"] self.files = config["files"] self.lastframe = [] self.caps = [None for _ in self.files] self.reset() self.add_last_slide() def add_last_slide(self): last_slide_end = self.slides[-1]["end_animation"] last_animation = len(self.files) self.slides.append( dict( start_animation=last_slide_end, end_animation=last_animation, type="last", number=len(self.slides) + 1, terminated=False, ) ) def reset(self): self.current_animation = 0 self.load_this_cap(0) self.current_slide_i = 0 self.slides[-1]["terminated"] = False def next(self): if self.current_slide["type"] == "last": self.current_slide["terminated"] = True else: self.current_slide_i = min(len(self.slides) - 1, self.current_slide_i + 1) self.rewind_slide() def prev(self): self.current_slide_i = max(0, self.current_slide_i - 1) self.rewind_slide() def rewind_slide(self): self.current_animation = self.current_slide["start_animation"] self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) def load_this_cap(self, cap_number): if self.caps[cap_number] == None: # unload other caps for i in range(len(self.caps)): if self.caps[i] != None: self.caps[i].release() self.caps[i] = None # load this cap self.caps[cap_number] = cv2.VideoCapture(self.files[cap_number]) @property def current_slide(self): return self.slides[self.current_slide_i] @property def current_cap(self): self.load_this_cap(self.current_animation) return self.caps[self.current_animation] @property def fps(self): return self.current_cap.get(cv2.CAP_PROP_FPS) # This function updates the state given the previous state. # It does this by reading the video information and checking if the state is still correct. # It returns the frame to show (lastframe) and the new state. def update_state(self, state): if state == State.PAUSED: if len(self.lastframe) == 0: _, self.lastframe = self.current_cap.read() return self.lastframe, state still_playing, frame = self.current_cap.read() if still_playing: self.lastframe = frame elif state in [state.WAIT, state.PAUSED]: return self.lastframe, state elif self.current_slide["type"] == "last" and self.current_slide["terminated"]: return self.lastframe, State.END if not still_playing: if self.current_slide["end_animation"] == self.current_animation + 1: if self.current_slide["type"] == "slide": # To fix "it always ends one frame before the animation", uncomment this. # But then clears on the next slide will clear the stationary after this slide. if self.last_frame_next: self.load_this_cap(self.next_cap) self.next_cap = self.caps[self.current_animation + 1] self.next_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) _, self.lastframe = self.next_cap.read() state = State.WAIT elif self.current_slide["type"] == "loop": self.current_animation = self.current_slide["start_animation"] state = State.PLAYING self.rewind_slide() elif self.current_slide["type"] == "last": self.current_slide["terminated"] = True elif ( self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation ): state = State.WAIT else: # Play next video! self.current_animation += 1 self.load_this_cap(self.current_animation) # Reset video to position zero if it has been played before self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) return self.lastframe, state class Display: def __init__(self, presentations, config, start_paused=False, fullscreen=False): self.presentations = presentations self.start_paused = start_paused self.config = config self.state = State.PLAYING self.lastframe = None self.current_presentation_i = 0 self.lag = 0 self.last_time = now() if fullscreen: cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN) cv2.setWindowProperty( "Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN ) @property def current_presentation(self): return self.presentations[self.current_presentation_i] def run(self): while True: self.lastframe, self.state = self.current_presentation.update_state( self.state ) if self.state == State.PLAYING or self.state == State.PAUSED: if self.start_paused: self.state = State.PAUSED self.start_paused = False if self.state == State.END: if self.current_presentation_i == len(self.presentations) - 1: self.quit() else: self.current_presentation_i += 1 self.state = State.PLAYING self.handle_key() self.show_video() self.show_info() def show_video(self): self.lag = now() - self.last_time self.last_time = now() cv2.imshow("Video", self.lastframe) def show_info(self): info = np.zeros((130, 420), np.uint8) font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255) grid_x = [30, 230] grid_y = [30, 70, 110] cv2.putText( info, f"Animation: {self.current_presentation.current_animation}", (grid_x[0], grid_y[0]), *font_args, ) cv2.putText(info, f"State: {self.state}", (grid_x[1], grid_y[0]), *font_args) cv2.putText( info, f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}", (grid_x[0], grid_y[1]), *font_args, ) cv2.putText( info, f"Slide Type: {self.current_presentation.current_slide['type']}", (grid_x[1], grid_y[1]), *font_args, ) cv2.putText( info, f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}", ((grid_x[0] + grid_x[1]) // 2, grid_y[2]), *font_args, ) cv2.imshow("Info", info) def handle_key(self): sleep_time = math.ceil(1000 / self.current_presentation.fps) key = cv2.waitKeyEx(fix_time(sleep_time - self.lag)) if self.config.QUIT.match(key): self.quit() elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key): self.state = State.PAUSED elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key): self.state = State.PLAYING elif self.state == State.WAIT and ( self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key) ): self.current_presentation.next() self.state = State.PLAYING elif self.state == State.PLAYING and self.config.CONTINUE.match(key): self.current_presentation.next() elif self.config.BACK.match(key): if self.current_presentation.current_slide_i == 0: self.current_presentation_i = max(0, self.current_presentation_i - 1) self.current_presentation.reset() self.state = State.PLAYING else: self.current_presentation.prev() self.state = State.PLAYING elif self.config.REWIND.match(key): self.current_presentation.rewind_slide() self.state = State.PLAYING def quit(self): cv2.destroyAllWindows() sys.exit() @click.command() @click.argument("scenes", nargs=-1) @config_path_option @click.option( "--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.", ) @click.option("--start-paused", is_flag=True, help="Start paused.") @click.option("--fullscreen", is_flag=True, help="Fullscreen mode.") @click.option( "--last-frame-next", is_flag=True, help="Show the next animation first frame as last frame (hack).", ) @click.help_option("-h", "--help") def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next): """Present the different scenes.""" presentations = list() for scene in scenes: config_file = os.path.join(folder, f"{scene}.json") if not os.path.exists(config_file): raise Exception( f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class" ) config = json.load(open(config_file)) presentations.append(Presentation(config, last_frame_next=last_frame_next)) if os.path.exists(config_path): config = Config.parse_file(config_path) else: config = Config() display = Display( presentations, config=config, start_paused=start_paused, fullscreen=fullscreen ) display.run()