PR #25622 opened by StormBytePP URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25622 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25622.patch
## Problem Sparse `mov_text` subtitles can lose their intended timestamps when the gap between cues exceeds the signed 32-bit sample-duration limit. With a microsecond subtitle time base, a gap of about 36 minutes is enough to trigger the issue. ## Reproducer Save this as `sparse.srt`: ```srt 1 00:00:01,000 --> 00:00:02,000 First cue 2 01:53:17,916 --> 01:53:26,425 Second cue ``` Convert it to MP4 and back: ```sh ffmpeg -i sparse.srt -c:s mov_text sparse.mp4 ffmpeg -i sparse.mp4 -map 0:s:0 -c:s srt roundtrip.srt ``` Before this change, the second cue can start at approximately `2.000001` seconds instead of its intended start time of `6797.916` seconds. ## Changes - Split long empty intervals by inserting additional empty subtitle samples, keeping each sample duration below the existing limit. - Apply the same handling to leading empty intervals. - Rescale packet DTS values to each subtitle track's time base before comparing them with the track duration. - Skip packets without a DTS during subtitle-end insertion. These changes preserve cue timestamps, durations, text, and the original timescale. ## Validation Verified signed-duration boundary cases, gaps requiring multiple empty samples, mixed track time bases, and fragmented MP4 output. >From 2786173500781d745b47df6982d13a938cc4b00c Mon Sep 17 00:00:00 2001 From: "David C. Manuelda" <[email protected]> Date: Fri, 30 Oct 2026 15:19:32 +0100 Subject: [PATCH] avformat/movenc: preserve long gaps between timed-text subtitles check_pkt() rejects DTS deltas greater than or equal to INT_MAX and replaces the incoming DTS with the previous sample's DTS plus one, discarding its PTS. With a 1/1000000 subtitle time base, an empty interval of 2147.483647 seconds is sufficient to trigger this recovery path. Sparse mov_text subtitles can therefore appear thousands of seconds before their intended presentation time, even when the incoming packets have valid, monotonically increasing timestamps. The empty sample inserted at the end of a cue does not prevent this: its duration is determined by the next sample, which may be hours later. Relaxing check_pkt() alone is not sufficient because the sample-duration helpers also require signed 32-bit durations. Split long empty intervals by inserting additional two-byte empty mov_text samples at steps of INT_MAX - 1 track ticks. This keeps every generated sample duration below the existing validation limit while preserving cue timestamps, durations, text and the original timescale. Apply the same handling to leading empty intervals. Rescale the triggering packet's DTS into each subtitle track's time base before comparing it with that track's duration. Packets from other tracks may use different time bases and must not be compared as raw integers. Ignore packets without a DTS for this subtitle-end insertion pass. A minimal reproducer uses an SRT file containing: 1 00:00:01,000 --> 00:00:02,000 First cue 2 01:53:17,916 --> 01:53:26,425 Second cue Encode with: ffmpeg -i sparse.srt -c:s mov_text sparse.mp4 ffmpeg -i sparse.mp4 -map 0:s:0 -c:s srt roundtrip.srt Before this change the second cue starts at approximately 2.000001 seconds instead of 6797.916 seconds. After this change both cues retain their intended start times and durations. Verified with signed-duration boundary cases, gaps requiring multiple empty samples, mixed track time bases and fragmented MP4 output. --- libavformat/movenc.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/libavformat/movenc.c b/libavformat/movenc.c index debe1acb7e0..f5d951efda3 100644 --- a/libavformat/movenc.c +++ b/libavformat/movenc.c @@ -7824,15 +7824,31 @@ static int mov_write_packet(AVFormatContext *s, AVPacket *pkt) * * 3) See mov_write_trailer for how the final end sample is * handled. + * + * 4) Split long empty intervals before check_pkt's signed duration + * limit. At a microsecond timescale, sparse subtitles can exceed + * this limit after only about 36 minutes without a new cue. */ for (i = 0; i < mov->nb_tracks; i++) { MOVTrack *trk = &mov->tracks[i]; + int64_t dts, end; int ret; - if (trk->par->codec_id == AV_CODEC_ID_MOV_TEXT && - trk->track_duration < pkt->dts && - (trk->entry == 0 || !trk->last_sample_is_subtitle_end)) { - ret = mov_write_subtitle_end_packet(s, i, trk->track_duration); + if (trk->par->codec_id != AV_CODEC_ID_MOV_TEXT || + pkt->dts == AV_NOPTS_VALUE) + continue; + + dts = av_rescale_q(pkt->dts, s->streams[pkt->stream_index]->time_base, + (AVRational){1, trk->timescale}); + while (trk->track_duration < dts) { + if (trk->entry == 0 || !trk->last_sample_is_subtitle_end) + end = trk->track_duration; + else if (dts - trk->track_duration >= INT_MAX) + end = trk->track_duration + INT_MAX - 1; + else + break; + + ret = mov_write_subtitle_end_packet(s, i, end); if (ret < 0) return ret; trk->last_sample_is_subtitle_end = 1; } -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
