diff --git a/cht/config.py b/cht/config.py index 53f9b57..c4b7e09 100644 --- a/cht/config.py +++ b/cht/config.py @@ -16,6 +16,7 @@ RELAY_PORT = 4445 # UDP loopback relay for live display # Frame extraction — scene-only, no interval fallback SCENE_THRESHOLD = 0.10 # 0-1, lower = more sensitive; 0.1 catches slide/window changes +SCENE_FLUSH_FRAMES = 2 # extra frames after scene change to flush encoder/muxer buffer (0 to disable) # Segment recording SEGMENT_DURATION = 60 # seconds per .ts segment diff --git a/cht/stream/ffmpeg.py b/cht/stream/ffmpeg.py index 7ce4183..091a2bb 100644 --- a/cht/stream/ffmpeg.py +++ b/cht/stream/ffmpeg.py @@ -64,7 +64,7 @@ def receive_record_and_relay(stream_url, output_path, relay_url): def receive_record_relay_and_detect(stream_url, output_path, relay_url, - scene_threshold=0.10): + scene_threshold=0.10, flush_frames=2): """Single process: receive TCP → record fMP4 + relay UDP + scene detect. One ffmpeg process, three output branches from the same TCP input: @@ -99,14 +99,16 @@ def receive_record_relay_and_detect(stream_url, output_path, relay_url, # Scene detection: CUDA decode (GPU) → select filter (CPU, lightweight) # → showinfo → MJPEG piped to stdout # - # Flush trick: select the scene-change frame AND the next 2 frames. - # The pipeline has 2 levels of buffering (encoder + muxer), so we - # need 2 flush frames to push the real scene-change frame out. - # mod(selected_n,3) prevents chaining: after 2 flushes, selected_n - # hits a multiple of 3 and the chain stops. scene_expr = f"gt(scene,{scene_threshold})" - flush_expr = "eq(n,prev_selected_n+1)*mod(selected_n,3)" - select_expr = f"{scene_expr}+{flush_expr}" + if flush_frames > 0: + # Flush trick: select extra frames after each scene change to push + # the real frame through the encoder+muxer buffer pipeline. + # mod(selected_n, 1+flush_frames) prevents chaining. + mod_val = 1 + flush_frames + flush_expr = f"eq(n,prev_selected_n+1)*mod(selected_n,{mod_val})" + select_expr = f"{scene_expr}+{flush_expr}" + else: + select_expr = scene_expr scene_stream = stream.filter("select", select_expr).filter("showinfo") scene_out = ffmpeg.output( scene_stream, "pipe:1", diff --git a/cht/stream/manager.py b/cht/stream/manager.py index a7d9eea..07e1a63 100644 --- a/cht/stream/manager.py +++ b/cht/stream/manager.py @@ -19,6 +19,7 @@ from cht.config import ( STREAM_PORT, RELAY_PORT, SCENE_THRESHOLD, + SCENE_FLUSH_FRAMES, SESSIONS_DIR, AUDIO_EXTRACT_INTERVAL, AUDIO_SAFETY_MARGIN, @@ -197,6 +198,7 @@ class StreamManager: node = ff.receive_record_relay_and_detect( self.stream_url, self.recording_path, self.relay_url, scene_threshold=self.scene_threshold, + flush_frames=SCENE_FLUSH_FRAMES, ) proc = ff.run_async(node, pipe_stdout=True, pipe_stderr=True) self._procs["recorder"] = proc