diff --git a/backend/app/services/video_renderer.py b/backend/app/services/video_renderer.py index e91f912..90daac1 100644 --- a/backend/app/services/video_renderer.py +++ b/backend/app/services/video_renderer.py @@ -307,7 +307,8 @@ class VideoRendererService: # Get detailed video AND audio properties for uniform encoding video_props = await self._get_video_properties(source_video_path) - logger.info(f"Source Properties: {video_props}") + source_duration = await self._get_video_duration(source_video_path) + logger.info(f"Source Properties: {video_props}, Duration: {source_duration:.2f}s") segment_files = [] current_time = 0.0 @@ -320,6 +321,18 @@ class VideoRendererService: cue_index = placement["ad_cue_index"] ad_duration = placement["ad_duration"] resume_from = placement.get("resume_from", pause_point) # Fallback for backwards compat + + # Validate pause_point is within video bounds + if pause_point >= source_duration: + logger.warning( + f"Cue {cue_index}: pause_point {pause_point:.2f}s exceeds video duration " + f"{source_duration:.2f}s, clamping to {source_duration - 0.1:.2f}s" + ) + pause_point = max(0, source_duration - 0.1) # Clamp to 100ms before end + + if resume_from >= source_duration: + resume_from = pause_point + overlap = max(0, pause_point - resume_from) # Get the AD audio for this cue @@ -401,7 +414,6 @@ class VideoRendererService: current_time = pause_point # 4. Add final segment from last pause point to end (re-encoded for uniformity) - source_duration = await self._get_video_duration(source_video_path) logger.info( f"Final segment check: current_time={current_time:.2f}s, " f"source_duration={source_duration:.2f}s, " @@ -550,6 +562,8 @@ class VideoRendererService: async def _extract_frame(self, video_path: str, time_point: float, output_path: str): """Extract a single frame as PNG using ffmpeg.""" + logger.debug(f"Extracting frame at {time_point:.2f}s from {video_path}") + cmd = [ self.ffmpeg_path, "-y", @@ -561,6 +575,14 @@ class VideoRendererService: ] await self._run_ffmpeg(cmd) + # Verify frame was actually created (FFmpeg can "succeed" but produce nothing + # if time_point is beyond video duration) + if not os.path.exists(output_path): + raise FileNotFoundError( + f"Frame extraction failed: no frame created at {time_point:.2f}s " + f"(time may be beyond video duration)" + ) + async def _create_freeze_segment( self, frame_path: str,