Repository navigation
Fill stream pauses with black video to keep chat/chapters in sync - #6
Merged
Merged
Conversation
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
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
PacketScanner: Scans video packet timestamps to detect gaps where ads were removedplanPadding(): Plans where to cut the capture and insert filler, matching detected gaps to recorded pauses or estimating positionsfillerUnitFrames(): Calculates filler duration that aligns with both video frames and AAC audio frames to prevent driftbuildConcatList(): Generates FFmpeg concat demuxer scripts for reassembling the padded videoStream Pause Handling
pausesToIntervals(): Converts wall-clock pause times to intervals relative to stream startstreamTimeToVideoTime(): Maps stream timestamps to captured video timestamps by removing pause durationssyncChatDumpToVideo(): Adjusts chat comment offsets to match the captured video timelineVideo Conversion Integration
remuxFileWithPadding()function that:VOD Management
BaseVOD.ts:
stream_pauses_paddedflag to track if pauses were filled during conversionpauseIntervalsgetter to compute pause intervals from stream pausesneeds_video_syncto determine if chat/chapters need time adjustmentsyncChatdumpToVideo()to create a synced chat dump filegetChatdumpForVideo()to return the appropriate chat dump (synced or original)BaseVODChapter.ts: Added
video_offsetandvideo_durationfields for chapter times in the captured videoAutomation & UI
video.pad_stream_pausessetting to enable/disable the featureImplementation Details
The padding algorithm works by:
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
PacketScanner,planPadding(), and pause interval utilitieshttps://claude.ai/code/session_01XuPs3nkTaDwZGoCD8oKKEV