Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added data/negative-timestamps.ts
Binary file not shown.
20 changes: 19 additions & 1 deletion ffmpeg/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,24 @@ int filtergraph_write(AVFrame *inf, struct input_ctx *ictx, struct output_ctx *o
// So in this case just increment the pts by 1/fps
ts_step = av_rescale_q_rnd(1, av_inv_q(octx->fps), vst->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
}

// Check for negative timestamp steps and use a default frame duration instead
if (ts_step < 0) {
int64_t frame_duration = av_rescale_q_rnd(1, av_inv_q(octx->fps), vst->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
av_log(NULL, AV_LOG_WARNING, "Detected negative timestamp step (%lld) at PTS %lld. Using frame duration (%lld) instead.\n",
(long long)ts_step, (long long)inf->pts, (long long)frame_duration);
ts_step = frame_duration;
}

// Check for abnormal positive steps
int64_t max_reasonable_step = av_rescale_q_rnd(10, av_inv_q(octx->fps), vst->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
if (ts_step > max_reasonable_step) {
int64_t frame_duration = av_rescale_q_rnd(1, av_inv_q(octx->fps), vst->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
av_log(NULL, AV_LOG_WARNING, "Detected abnormal timestamp step (%lld) at PTS %lld. Using frame duration (%lld) instead.\n",
(long long)ts_step, (long long)inf->pts, (long long)frame_duration);
ts_step = frame_duration;
}

filter->custom_pts += ts_step;
filter->prev_frame_pts = inf->pts;
} else {
Expand All @@ -366,7 +384,7 @@ int filtergraph_write(AVFrame *inf, struct input_ctx *ictx, struct output_ctx *o

if (inf) {
// Apply the custom pts, then reset for the next output
int old_pts = inf->pts;
uint64_t old_pts = inf->pts;
inf->pts = filter->custom_pts;
ret = av_buffersrc_write_frame(filter->src_ctx, inf);
inf->pts = old_pts;
Expand Down
80 changes: 80 additions & 0 deletions ffmpeg/timestamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ffmpeg

import (
"os"
"testing"
"time"
)

// Tests fix for VFR inputs causing infinite frame duplication, resulting in huge output files.
func TestTimestampRegression(t *testing.T) {
InitFFmpeg()

inputFile := "../data/negative-timestamps.ts"
if _, err := os.Stat(inputFile); err != nil {
t.Skip("Problematic input file not available")
}

// Test the exact pattern that triggered infinite loops:
// passthrough FPS followed by 30fps conversion
passthroughProfile := P240p30fps16x9
passthroughProfile.Framerate = 0 // passthrough

options := []TranscodeOptions{
{
Oname: "test_passthrough.mp4",
Accel: Software,
Profile: passthroughProfile,
AudioEncoder: ComponentOptions{Name: "drop"},
},
{
Oname: "test_30fps.mp4",
Accel: Software,
Profile: P240p30fps16x9,
AudioEncoder: ComponentOptions{Name: "drop"},
},
}

// Should complete quickly with fix, would hang without it
done := make(chan error, 1)
var result *TranscodeResults

go func() {
var err error
result, err = Transcode3(&TranscodeOptionsIn{
Fname: inputFile,
Accel: Software,
}, options)
done <- err
}()

select {
case err := <-done:
if err != nil {
// Size limit error means protection worked
if err == ErrTranscoderOutputSize {
t.Log("Size limit triggered")
return
}
t.Fatal(err)
}

if result == nil {
t.Fatal("No result")
}

// Verify outputs are reasonably sized (not 20-190GB)
for _, opt := range options {
if stat, err := os.Stat(opt.Oname); err == nil {
size := stat.Size()
if size > 10*1024*1024 { // 10MB is suspicious for this input
t.Errorf("%s too large: %d bytes", opt.Oname, size)
}
os.Remove(opt.Oname)
}
}

case <-time.After(30 * time.Second):
t.Fatal("Hung for 30s - infinite loop detected")
}
}