From 3d4e37dfb93a203ead2d1ea5f8d2d2750ccd6973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Mon, 4 May 2026 22:14:36 +0200 Subject: [PATCH] Force fragment flush on close keyframes Add a MinNormalGOPMs constant (950 ms) and use it to detect unusually close consecutive IDRs; when a keyframe arrives sooner than this threshold and the current fragment is still short, force a fragment flush to isolate the seam and avoid mid-fragment sync samples. Replace the previous hardcoded 500 ms check in mp4.go and add TestMP4LoopSeamIsolation to reproduce the loop-seam pattern and assert that seam IDRs are isolated into their own fragments. --- machinery/src/video/mp4.go | 11 ++- machinery/src/video/mp4_loopseam_test.go | 99 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 machinery/src/video/mp4_loopseam_test.go diff --git a/machinery/src/video/mp4.go b/machinery/src/video/mp4.go index bc7643d..938e2e3 100644 --- a/machinery/src/video/mp4.go +++ b/machinery/src/video/mp4.go @@ -32,6 +32,15 @@ const MacEpochOffset uint64 = 2082844800 // resulting in ~3 second fragments (assuming a typical GOP interval). const FragmentDurationMs = 3000 +// MinNormalGOPMs is the minimum spacing we expect between two consecutive +// IDRs of a healthy source (typical encoders produce IDRs every 1000ms or +// more). When two keyframes arrive closer than this, we treat the second one +// as an upstream restart/loop-seam and force a fresh fragment so the seam +// IDR cannot end up as a mid-fragment sync sample. The check only runs when +// the current fragment has not yet reached FragmentDurationMs, so it never +// fires during normal multi-GOP fragments at intended GOP boundaries. +const MinNormalGOPMs = 950 + type MP4 struct { // FileName is the name of the file FileName string @@ -321,7 +330,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p // seam IDR into its own fragment so each fragment stays a clean GOP. if !shouldFlush && trackID == uint32(mp4.VideoTrack) && mp4.Start && mp4.LastKeyframeRawPTS > 0 && pts > mp4.LastKeyframeRawPTS && - pts-mp4.LastKeyframeRawPTS < 500 { + pts-mp4.LastKeyframeRawPTS < MinNormalGOPMs { log.Log.Warning(fmt.Sprintf("mp4.AddSampleToTrack(): forcing fragment flush at unexpectedly close keyframe (gap=%d ms, fragment elapsed=%d ms) - likely upstream loop/restart discontinuity", pts-mp4.LastKeyframeRawPTS, elapsed)) shouldFlush = true } diff --git a/machinery/src/video/mp4_loopseam_test.go b/machinery/src/video/mp4_loopseam_test.go new file mode 100644 index 0000000..6c24812 --- /dev/null +++ b/machinery/src/video/mp4_loopseam_test.go @@ -0,0 +1,99 @@ +package video + +import ( + "fmt" + "os" + "testing" + + mp4ff "github.com/Eyevinn/mp4ff/mp4" + "github.com/kerberos-io/agent/machinery/src/models" +) + +// TestMP4LoopSeamIsolation reproduces the loop-seam pattern from the +// failing virtual-rtsp recordings: ~1s GOPs, but at the source-MP4 +// loop boundary an IDR arrives prematurely (~200-870ms after the +// previous IDR). Without the fix this seam IDR ends up bunched into +// the same fragment as the prior GOP's IDR which trips macOS +// VideoToolbox (kVTVideoDecoderBadDataErr / -12909). The fix forces +// a fragment flush whenever two IDRs arrive closer than MinNormalGOPMs. +func TestMP4LoopSeamIsolation(t *testing.T) { + tmpFile := "/tmp/test_loop_seam.mp4" + defer os.Remove(tmpFile) + + sps := []byte{0x67, 0x42, 0xc0, 0x1e, 0xd9, 0x00, 0xa0, 0x47, 0xfe, 0xc8} + pps := []byte{0x68, 0xce, 0x38, 0x80} + mp4Video := NewMP4(tmpFile, [][]byte{sps}, [][]byte{pps}, nil, 30) + mp4Video.SetWidth(1920) + mp4Video.SetHeight(1080) + v := mp4Video.AddVideoTrack("H264") + + mk := func(k bool) []byte { + nt := byte(0x01) + if k { + nt = 0x65 + } + f := []byte{0, 0, 0, 1, nt} + for i := 0; i < 200; i++ { + f = append(f, byte(i)) + } + return f + } + + frameDur := uint64(33) + pts := uint64(0) + emit := func(n int, gopLen int) { + for f := 0; f < n; f++ { + isKey := (f % gopLen) == 0 + mp4Video.AddSampleToTrack(v, isKey, mk(isKey), pts) + pts += frameDur + } + } + + // 17 seconds of normal content (last "good" IDR at sec 17). + emit(17*30, 30) + // Seam: IDR arrives ~867ms after previous (vs normal 1000ms). + pts -= 100 + emit(13*30, 30) + + mp4Video.Close(&models.Config{Signing: &models.Signing{PrivateKey: ""}}) + + f, _ := os.Open(tmpFile) + defer f.Close() + parsed, err := mp4ff.DecodeFile(f) + if err != nil { + t.Fatalf("decode: %v", err) + } + + fragIdx := 0 + for _, seg := range parsed.Segments { + for _, fr := range seg.Fragments { + for _, traf := range fr.Moof.Trafs { + if traf.Tfhd.TrackID != 1 { + continue + } + tfdt := traf.Tfdt.BaseMediaDecodeTime() + offset := uint64(0) + var keys []uint64 + for _, trun := range traf.Truns { + for _, s := range trun.Samples { + if (s.Flags>>24)&0x03 == 0x02 { + keys = append(keys, offset) + } + offset += uint64(s.Dur) + } + } + fmt.Printf("frag %d tfdt=%d samples_dur=%d keys@%v\n", + fragIdx, tfdt, offset, keys) + for i := 1; i < len(keys); i++ { + gap := keys[i] - keys[i-1] + if gap < MinNormalGOPMs { + t.Errorf("frag %d (tfdt=%d): two IDRs only %d ms apart "+ + "in same fragment (< %d) - seam was not isolated", + fragIdx, tfdt, gap, MinNormalGOPMs) + } + } + fragIdx++ + } + } + } +}