Skip to content

Fill stream pauses with black video to keep chat/chapters in sync - #6

Merged
second2falcon merged 2 commits into
masterfrom
claude/intelligent-bardeen-nch87n
Oct 4, 2026
Merged

second2falcon merged 2 commits into
masterfrom
claude/intelligent-bardeen-nch87n

Conversation

@second2falcon

Copy link
Copy Markdown
Owner

Summary

Add support for filling stream pauses (ad breaks) with black, silent video during VOD conversion. This keeps the captured video length matching the stream duration, so chat messages and chapter timestamps remain synchronized without manual adjustment.

Key Changes

Core Padding Logic

  • PadVideo.ts (new): Implements the core padding algorithm
    • PacketScanner: Scans video packet timestamps to detect gaps where ads were removed
    • planPadding(): Plans where to cut the capture and insert filler, matching detected gaps to recorded pauses or estimating positions
    • fillerUnitFrames(): Calculates filler duration that aligns with both video frames and AAC audio frames to prevent drift
    • buildConcatList(): Generates FFmpeg concat demuxer scripts for reassembling the padded video

Stream Pause Handling

  • StreamPauses.ts (new): Utilities for working with stream pauses
    • pausesToIntervals(): Converts wall-clock pause times to intervals relative to stream start
    • streamTimeToVideoTime(): Maps stream timestamps to captured video timestamps by removing pause durations
    • syncChatDumpToVideo(): Adjusts chat comment offsets to match the captured video timeline

Video Conversion Integration

  • Video.ts: Added remuxFileWithPadding() function that:
    • Scans video packets to detect timestamp gaps
    • Plans padding based on recorded pauses
    • Encodes a single filler unit (black/silent) and repeats it for each pause
    • Uses stream copy for the actual capture, minimizing re-encoding
    • Falls back to normal remux if padding isn't possible

VOD Management

  • BaseVOD.ts:

    • Added stream_pauses_padded flag to track if pauses were filled during conversion
    • Added pauseIntervals getter to compute pause intervals from stream pauses
    • Added needs_video_sync to determine if chat/chapters need time adjustment
    • Added syncChatdumpToVideo() to create a synced chat dump file
    • Added getChatdumpForVideo() to return the appropriate chat dump (synced or original)
  • BaseVODChapter.ts: Added video_offset and video_duration fields for chapter times in the captured video

Automation & UI

  • BaseAutomator.ts: Integrated padding into the conversion workflow with fallback to normal remux
  • Controllers/Vod.ts: Updated to use synced chat dumps when cutting clips from captured video
  • Vue components: Updated to display and use video-adjusted chapter offsets
  • Config: Added video.pad_stream_pauses setting to enable/disable the feature

Implementation Details

The padding algorithm works by:

  1. Scanning all video packets to find timestamp gaps (where streamlink dropped ads)
  2. Matching detected gaps to recorded pause times, or estimating pause positions from keyframes
  3. Cutting the capture at each pause boundary and inserting black filler
  4. Using FFmpeg's concat demuxer to reassemble with stream copy (no re-encoding of capture)
  5. Only encoding a small filler unit that repeats, minimizing overhead

Chat and chapter synchronization is handled by computing how much stream time was removed before each timestamp, then subtracting that offset to map back to video time.

Testing

  • Comprehensive unit tests for PacketScanner, planPadding(), and pause interval utilities
  • Tests cover gap detection, pause matching, estimation, and edge cases

https://claude.ai/code/session_01XuPs3nkTaDwZGoCD8oKKEV

claude added 2 commits October 4, 2026 20:34
Chapter "Open in player" links passed the chapter start as offset=,
which twitch-vod-chat treats as the chat offset, so chat was shifted by
the chapter start time instead of the video seeking there. Pass it as
t= (seek target) instead, and bump the twitch-vod-chat submodule to the
version that supports t=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XuPs3nkTaDwZGoCD8oKKEV
Streamlink filters out Twitch ad segments and the remux to mp4 collapses the
resulting gaps, so the captured video is shorter than the stream. Chat and
chapters are timed in stream time, so they drifted further after every ad
break (12+ minutes on a 4.5h stream).

Sync existing captures to the video using the recorded stream pauses:
- Map stream time to video time by removing the pauses before each point
  (StreamPauses helper). Nothing changes for captures without pauses.
- Serve a synced copy of the chat dump (<basename>.synced.chatdump) at
  /api/v0/vod/:uuid/synced.chatdump, generated on demand. The original
  chat dump is never modified.
- Use it in the player links, chat render for the captured video and clip
  chat cuts.
- Add video_offset/video_duration to chapters and use them for everything
  that refers to the captured video (chapter table, player and editor
  links, embedded chapters, LosslessCut EDL, VTT, split by chapters). The
  stream time offset is kept for Twitch links and chapter editing.

Optionally fill the pauses with black video when converting
(video.pad_stream_pauses, off by default):
- Cut the capture at each pause, at the exact timestamp gap if one matches
  or at the nearest keyframe of the estimated position, and insert a
  black, silent filler of the pause length using the concat demuxer.
- The capture is only stream copied; one short filler unit is encoded
  and repeated.
- Padded vods are flagged (stream_pauses_padded) so no sync is applied.
- Falls back to a normal conversion on any failure.

Also fix Helper.path_ffprobe returning the ffmpeg binary on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XuPs3nkTaDwZGoCD8oKKEV
@second2falcon
second2falcon merged commit 824c1f5 into master Oct 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants