diff --git a/bbb_presentation_video/__init__.py b/bbb_presentation_video/__init__.py index aba5c39..d1394b7 100644 --- a/bbb_presentation_video/__init__.py +++ b/bbb_presentation_video/__init__.py @@ -114,6 +114,12 @@ def __getattr__(self, attr: str) -> Any: help="generate video for a specific pod instead of default pod", default=DEFAULT_PRESENTATION_POD, ) + parser.add_argument( + "--ignore-record-status", + action="store_true", + help="ignore recording start/stop events and generate video for the entire meeting", + default=False, + ) args = parser.parse_args() @@ -159,6 +165,7 @@ def __getattr__(self, attr: str) -> Any: args.start, args.end, args.pod, + args.ignore_record_status, ) renderer.render() diff --git a/bbb_presentation_video/renderer/__init__.py b/bbb_presentation_video/renderer/__init__.py index 5f5e312..b9f6f68 100644 --- a/bbb_presentation_video/renderer/__init__.py +++ b/bbb_presentation_video/renderer/__init__.py @@ -26,6 +26,7 @@ class Codec(Enum): H264 = "h264" VP9 = "vp9" + RAW_VIDEO = "rawvideo" class Encoder: @@ -60,7 +61,16 @@ def join(self) -> None: self.queue.put(None) self.thread.join() - def run(self) -> None: + def output_raw(self) -> None: + with open(self.output, "wb") as f: + while True: + buf = self.queue.get() + if buf is None: + break + f.write(buf) + self.ret_queue.put(buf) + + def output_ffmpeg(self) -> None: if self.codec == Codec.H264: codec_opts = ["-c:v", "libx264", "-qp", "0", "-preset", "ultrafast"] elif self.codec == Codec.VP9: @@ -128,9 +138,16 @@ def run(self) -> None: if ffmpeg.returncode != 0: raise CalledProcessError(returncode=ffmpeg.returncode, cmd=ffmpeg_cmdline) + def run(self) -> None: + if self.codec == Codec.RAW_VIDEO: + self.output_raw() + else: + self.output_ffmpeg() + class Renderer: events: EventsInfo + start_time: Fraction = Fraction(0) length: Fraction input: str output: str @@ -139,6 +156,7 @@ class Renderer: framerate: Fraction codec: Codec pod_id: str + ignore_record_status: bool frame: int framestep: Fraction @@ -154,9 +172,10 @@ def __init__( height: int, framerate: Fraction, codec: Codec, - start_time: Fraction, - end_time: Fraction, + start_time: Fraction | None, + end_time: Fraction | None, pod_id: str, + ignore_record_status: bool, ): self.events = events self.input = input @@ -166,22 +185,28 @@ def __init__( self.framerate = framerate self.codec = codec self.pod_id = pod_id + self.ignore_record_status = ignore_record_status # Current video position state self.frame = 1 self.framestep = 1 / framerate self.pts = Fraction(0) - self.recording = False + if self.ignore_record_status: + print("\tRenderer: ignoring record status events") + self.recording = True + else: + self.recording = False # Only the section of recording within the time range of start_time # through end_time will be included in the final video + if events.length is None: + raise ValueError("Recording length cannot be determined from events.xml") if start_time is not None: self.start_time = start_time - else: - self.start_time = 0 - assert events.length is not None if end_time is not None and end_time < events.length: self.length = end_time + else: + self.length = events.length # Cairo rendering context self.surface = cairo.ImageSurface(cairo.FORMAT_RGB24, self.width, self.height) @@ -193,10 +218,15 @@ def __init__( font_options.set_hint_style(cairo.HINT_STYLE_NONE) self.ctx.set_font_options(font_options) - def update_record(self, event: RecordEvent) -> None: + def update_record(self, event: RecordEvent) -> bool: + if self.ignore_record_status: + return False + if self.recording != event["status"]: self.recording = event["status"] print(f"\tRenderer: recording: {self.recording}") + return True + return False def render(self) -> None: cursor = CursorRenderer( @@ -225,8 +255,7 @@ def render(self) -> None: shapes_changed = False cursor_changed = False recording_changed = False - assert self.events.length is not None - while self.pts < self.events.length: + while self.pts < self.length: event_ts = Fraction(0) while True: if len(self.events.events) == 0: @@ -276,8 +305,8 @@ def render(self) -> None: elif name == "clear": shapes.update_clear(cast(events.ClearEvent, event)) elif name == "record": - self.update_record(cast(events.RecordEvent, event)) - recording_changed = True + if self.update_record(cast(events.RecordEvent, event)): + recording_changed = True elif name == "presenter": cursor.update_presenter(cast(events.PresenterEvent, event)) elif name == "join": @@ -339,12 +368,12 @@ def render(self) -> None: # Output a frame encoder.put(bytearray(self.surface.get_data())) + presentation_changed = False + shapes_changed = False + cursor_changed = False + recording_changed = False + self.frame += 1 self.pts += self.framestep - presentation_changed = False - shapes_changed = False - cursor_changed = False - recording_changed = False - encoder.join()