From 185a1402570c417d98abdbf5a8fb84f31122ab12 Mon Sep 17 00:00:00 2001 From: Marco van Dijk Date: Wed, 15 Oct 2025 16:31:06 +0200 Subject: [PATCH 1/3] Correct NOPT timestamps --- ffmpeg/decoder.h | 6 ++++++ ffmpeg/filter.c | 23 ++++++++++++++++++++++- ffmpeg/transcoder.c | 19 ++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/ffmpeg/decoder.h b/ffmpeg/decoder.h index 2c0d106689..0ad8587f7a 100755 --- a/ffmpeg/decoder.h +++ b/ffmpeg/decoder.h @@ -55,6 +55,12 @@ struct input_ctx { // In HW transcoding, demuxer is opened once and used, // so it is necessary to check whether the input pixel format does not change in the middle. enum AVPixelFormat last_format; + + // per-segment tracking (reset for each segment) + int64_t segment_first_pts; // best-effort pts of first decoded video frame + int64_t segment_last_pts; // best-effort pts of most recent decoded video frame + int64_t segment_accum_duration; // sum of decoded frame durations (input timebase) + int segment_pts_samples; // number of decoded frames contributing timestamps }; // Exported methods diff --git a/ffmpeg/filter.c b/ffmpeg/filter.c index bb0cf93f4a..7960fe6083 100644 --- a/ffmpeg/filter.c +++ b/ffmpeg/filter.c @@ -334,7 +334,28 @@ int filtergraph_write(AVFrame *inf, struct input_ctx *ictx, struct output_ctx *o // Timestamp handling code AVStream *vst = ictx->ic->streams[ictx->vi]; if (inf) { // Non-Flush Frame - inf->opaque = (void *) inf->pts; // Store original PTS for calc later + if (is_video) { + int64_t pts = inf->pts; + if (pts == AV_NOPTS_VALUE) { + av_log(NULL, AV_LOG_WARNING, "Filter frame pts is AV_NOPTS_VALUE. Falling back to best_effort_timestamp\n"); + pts = inf->best_effort_timestamp; + } + if (pts == AV_NOPTS_VALUE && ictx->segment_pts_samples > 0) { + av_log(NULL, AV_LOG_WARNING, "Filter frame pts is still AV_NOPTS_VALUE. Falling back to segment_last_pts + step\n"); + int64_t step = inf->duration; + if (!step && vst->r_frame_rate.den){ + step = av_rescale_q(1, av_inv_q(vst->r_frame_rate), vst->time_base); + } + if (step){ + pts = ictx->segment_last_pts + step; + } + } + inf->pts = pts; + } + inf->opaque = (void *) inf->pts; + if (inf->pts == AV_NOPTS_VALUE) { + av_log(NULL, AV_LOG_ERROR, "Filter frame pts remains AV_NOPTS_VALUE\n"); + } if (is_video && octx->fps.den) { // Custom PTS set when FPS filter is used int64_t ts_step = inf->pts - filter->prev_frame_pts; diff --git a/ffmpeg/transcoder.c b/ffmpeg/transcoder.c index 8942f093e6..a2d946baee 100755 --- a/ffmpeg/transcoder.c +++ b/ffmpeg/transcoder.c @@ -326,6 +326,11 @@ int transcode(struct transcode_thread *h, int nb_outputs = h->nb_outputs; int outputs_ready = 0, hit_eof = 0; + ictx->segment_first_pts = AV_NOPTS_VALUE; + ictx->segment_last_pts = AV_NOPTS_VALUE; + ictx->segment_pts_samples = 0; + ictx->segment_accum_duration = 0; + ipkt = av_packet_alloc(); if (!ipkt) LPMS_ERR(transcode_cleanup, "Unable to allocated packet"); dframe = av_frame_alloc(); @@ -426,7 +431,19 @@ int transcode(struct transcode_thread *h, decoded_results->frames += dframe->width && dframe->height; decoded_results->pixels += dframe->width * dframe->height; has_frame = has_frame && dframe->width && dframe->height; - if (has_frame) last_frame = ictx->last_frame_v; + if (has_frame) { + last_frame = ictx->last_frame_v; + int64_t pts = dframe->pts; + if (pts == AV_NOPTS_VALUE) pts = dframe->best_effort_timestamp; + if (pts == AV_NOPTS_VALUE && ictx->segment_pts_samples > 0 && dframe->duration) + pts = ictx->segment_last_pts + dframe->duration; + if (ictx->segment_first_pts == AV_NOPTS_VALUE && pts != AV_NOPTS_VALUE) + ictx->segment_first_pts = pts; + if (pts != AV_NOPTS_VALUE) ictx->segment_last_pts = pts; + ictx->segment_pts_samples++; + if (dframe->duration) + ictx->segment_accum_duration += dframe->duration; + } } else if (AVMEDIA_TYPE_AUDIO == ist->codecpar->codec_type) { has_frame = has_frame && dframe->nb_samples; if (has_frame) last_frame = ictx->last_frame_a; From 388556da1000dc47baaffe16fe6703c0a2a26000 Mon Sep 17 00:00:00 2001 From: Marco van Dijk Date: Thu, 4 Dec 2025 15:55:03 +0100 Subject: [PATCH 2/3] Fix test to match new output on null PTS --- ffmpeg/ffmpeg_test.go | 56 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/ffmpeg/ffmpeg_test.go b/ffmpeg/ffmpeg_test.go index f39d1f56fb..699a0239c5 100644 --- a/ffmpeg/ffmpeg_test.go +++ b/ffmpeg/ffmpeg_test.go @@ -2504,8 +2504,8 @@ func TestTranscoder_LargeOutputs(t *testing.T) { close(closeCh) assert.Nil(err) assert.Equal(120, res.Decoded.Frames) - assert.Equal(116, res.Encoded[0].Frames) // ffmpeg probably drops missing timestamp frames - assert.Equal(56, res.Encoded[1].Frames) + assert.Equal(120, res.Encoded[0].Frames) + assert.Equal(60, res.Encoded[1].Frames) cmd := ` # check input properties to ensure they still have the weird timestamps ffprobe -of csv -hide_banner -show_entries frame=pts_time,pkt_dts_time,media_type,pict_type $1/../data/missing-dts.ts 2>&1 | grep video > input.out @@ -2642,25 +2642,26 @@ func TestTranscoder_LargeOutputs(t *testing.T) { cat <<- 'EOF2' > expected-output.out frame,video,25994.033333,25994.033333,I, frame,video,25994.066667,25994.066667,P - frame,video,25994.100000,25994.100000,B - frame,video,25994.133333,25994.133333,P + frame,video,25994.100000,25994.100000,P + frame,video,25994.133333,25994.133333,B frame,video,25994.166667,25994.166667,B frame,video,25994.200000,25994.200000,B - frame,video,25994.233333,25994.233333,B - frame,video,25994.266667,25994.266667,P + frame,video,25994.233333,25994.233333,P + frame,video,25994.266667,25994.266667,B frame,video,25994.300000,25994.300000,B - frame,video,25994.333333,25994.333333,P - frame,video,25994.366667,25994.366667,B + frame,video,25994.333333,25994.333333,B + frame,video,25994.366667,25994.366667,P frame,video,25994.400000,25994.400000,B frame,video,25994.433333,25994.433333,B - frame,video,25994.466667,25994.466667,P - frame,video,25994.500000,25994.500000,B + frame,video,25994.466667,25994.466667,B + frame,video,25994.500000,25994.500000,P frame,video,25994.533333,25994.533333,B frame,video,25994.566667,25994.566667,B - frame,video,25994.600000,25994.600000,P - frame,video,25994.666667,25994.666667,P, - frame,video,25994.700000,25994.700000,B, - frame,video,25994.733333,25994.733333,B, + frame,video,25994.600000,25994.600000,B + frame,video,25994.633333,25994.633333,P, + frame,video,25994.666667,25994.666667,B, + frame,video,25994.700000,25994.700000,P, + frame,video,25994.733333,25994.733333,P, frame,video,25994.766667,25994.766667,B, frame,video,25994.800000,25994.800000,P, frame,video,25994.833333,25994.833333,B, @@ -2669,32 +2670,35 @@ func TestTranscoder_LargeOutputs(t *testing.T) { frame,video,25994.933333,25994.933333,P, frame,video,25994.966667,25994.966667,B, frame,video,25995.000000,25995.000000,P, - frame,video,25995.033333,25995.033333,B, - frame,video,25995.066667,25995.066667,B, + frame,video,25995.033333,25995.033333,P, + frame,video,25995.066667,25995.066667,P, + frame,video,25995.100000,25995.100000,P, frame,video,25995.133333,25995.133333,B, frame,video,25995.166667,25995.166667,P, frame,video,25995.200000,25995.200000,B, + frame,video,25995.233333,25995.233333,B, frame,video,25995.266667,25995.266667,B, - frame,video,25995.300000,25995.300000,B, - frame,video,25995.333333,25995.333333,P, + frame,video,25995.300000,25995.300000,P, + frame,video,25995.333333,25995.333333,B, frame,video,25995.366667,25995.366667,B, frame,video,25995.400000,25995.400000,B, - frame,video,25995.433333,25995.433333,B, - frame,video,25995.466667,25995.466667,P, + frame,video,25995.433333,25995.433333,P, + frame,video,25995.466667,25995.466667,B, frame,video,25995.500000,25995.500000,B, frame,video,25995.533333,25995.533333,B, - frame,video,25995.566667,25995.566667,B, - frame,video,25995.600000,25995.600000,P, + frame,video,25995.566667,25995.566667,P, + frame,video,25995.600000,25995.600000,B, frame,video,25995.633333,25995.633333,B, frame,video,25995.666667,25995.666667,B, + frame,video,25995.700000,25995.700000,P, frame,video,25995.733333,25995.733333,B, - frame,video,25995.766667,25995.766667,P, + frame,video,25995.766667,25995.766667,B, frame,video,25995.800000,25995.800000,B, - frame,video,25995.833333,25995.833333,B, + frame,video,25995.833333,25995.833333,P, frame,video,25995.866667,25995.866667,B, - frame,video,25995.900000,25995.900000,P, + frame,video,25995.900000,25995.900000,B, frame,video,25995.933333,25995.933333,B, - frame,video,25995.966667,N/A,B, + frame,video,25995.966667,N/A,P, frame,video,25996.000000,N/A,P, EOF2 diff -u expected-output.out output.out From 02b95e397b05ebbdf41ecb821a676bfddcd3a97d Mon Sep 17 00:00:00 2001 From: Marco van Dijk Date: Thu, 4 Dec 2025 16:12:49 +0100 Subject: [PATCH 3/3] Remove some more segment-tracking leftover from the `guard_output_size` branch --- ffmpeg/decoder.h | 3 --- ffmpeg/filter.c | 2 +- ffmpeg/transcoder.c | 13 ++++--------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/ffmpeg/decoder.h b/ffmpeg/decoder.h index 0ad8587f7a..70e8fbb51c 100755 --- a/ffmpeg/decoder.h +++ b/ffmpeg/decoder.h @@ -57,10 +57,7 @@ struct input_ctx { enum AVPixelFormat last_format; // per-segment tracking (reset for each segment) - int64_t segment_first_pts; // best-effort pts of first decoded video frame int64_t segment_last_pts; // best-effort pts of most recent decoded video frame - int64_t segment_accum_duration; // sum of decoded frame durations (input timebase) - int segment_pts_samples; // number of decoded frames contributing timestamps }; // Exported methods diff --git a/ffmpeg/filter.c b/ffmpeg/filter.c index 7960fe6083..d9452c6db0 100644 --- a/ffmpeg/filter.c +++ b/ffmpeg/filter.c @@ -340,7 +340,7 @@ int filtergraph_write(AVFrame *inf, struct input_ctx *ictx, struct output_ctx *o av_log(NULL, AV_LOG_WARNING, "Filter frame pts is AV_NOPTS_VALUE. Falling back to best_effort_timestamp\n"); pts = inf->best_effort_timestamp; } - if (pts == AV_NOPTS_VALUE && ictx->segment_pts_samples > 0) { + if (pts == AV_NOPTS_VALUE && ictx->segment_last_pts != AV_NOPTS_VALUE) { av_log(NULL, AV_LOG_WARNING, "Filter frame pts is still AV_NOPTS_VALUE. Falling back to segment_last_pts + step\n"); int64_t step = inf->duration; if (!step && vst->r_frame_rate.den){ diff --git a/ffmpeg/transcoder.c b/ffmpeg/transcoder.c index a2d946baee..7d0231863a 100755 --- a/ffmpeg/transcoder.c +++ b/ffmpeg/transcoder.c @@ -326,10 +326,7 @@ int transcode(struct transcode_thread *h, int nb_outputs = h->nb_outputs; int outputs_ready = 0, hit_eof = 0; - ictx->segment_first_pts = AV_NOPTS_VALUE; ictx->segment_last_pts = AV_NOPTS_VALUE; - ictx->segment_pts_samples = 0; - ictx->segment_accum_duration = 0; ipkt = av_packet_alloc(); if (!ipkt) LPMS_ERR(transcode_cleanup, "Unable to allocated packet"); @@ -434,15 +431,13 @@ int transcode(struct transcode_thread *h, if (has_frame) { last_frame = ictx->last_frame_v; int64_t pts = dframe->pts; + // Try best effort timestamp if pts is not available if (pts == AV_NOPTS_VALUE) pts = dframe->best_effort_timestamp; - if (pts == AV_NOPTS_VALUE && ictx->segment_pts_samples > 0 && dframe->duration) + // If best effort timestamp is not available, try to use segment last pts + duration + if (pts == AV_NOPTS_VALUE && ictx->segment_last_pts != AV_NOPTS_VALUE && dframe->duration) pts = ictx->segment_last_pts + dframe->duration; - if (ictx->segment_first_pts == AV_NOPTS_VALUE && pts != AV_NOPTS_VALUE) - ictx->segment_first_pts = pts; + // Only update segment last pts if pts is valid if (pts != AV_NOPTS_VALUE) ictx->segment_last_pts = pts; - ictx->segment_pts_samples++; - if (dframe->duration) - ictx->segment_accum_duration += dframe->duration; } } else if (AVMEDIA_TYPE_AUDIO == ist->codecpar->codec_type) { has_frame = has_frame && dframe->nb_samples;