diff --git a/machinery/src/video/mp4.go b/machinery/src/video/mp4.go index 7dee969..c913442 100644 --- a/machinery/src/video/mp4.go +++ b/machinery/src/video/mp4.go @@ -32,24 +32,6 @@ const MacEpochOffset uint64 = 2082844800 // resulting in ~3 second fragments (assuming a typical GOP interval). const FragmentDurationMs = 3000 -// MinNormalGOPMs is the maximum spacing between two consecutive IDRs that -// we still consider an anomalous "loop/restart seam". 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. -// -// This must be set well below the smallest plausible *legitimate* GOP -// length. Typical IP cameras use GOP intervals of 1000-2000 ms, and the -// arrival timing of consecutive IDRs can jitter by a few hundred ms due to -// network/RTSP buffering. A threshold close to 1 s (e.g. 950) caused -// false positives on cameras with ~1 s GOPs (warnings like -// "gap=800 ms / 300 ms / 200 ms" while the stream itself was healthy). -// 400 ms is comfortably below any realistic GOP yet still catches the -// virtual-rtsp / ffmpeg loop-seam pattern (seam IDRs typically arrive -// 100-200 ms after the prior IDR). -const MinNormalGOPMs = 400 - type MP4 struct { // FileName is the name of the file FileName string @@ -74,8 +56,6 @@ type MP4 struct { FreeBoxSize int64 FragmentStartRawPTS uint64 // Raw PTS for timing when to flush fragments FragmentStartDTS uint64 // Accumulated VideoTotalDuration at fragment start (matches tfdt) - LastKeyframeRawPTS uint64 // Raw PTS of the most recently seen keyframe (in any fragment) - LastKeyframeGapMs uint64 // Gap (ms) between the previous two consecutive keyframes MoofBoxes int64 // Number of moof boxes in the file MoofBoxSizes []int64 // Sizes of each moof box SegmentDurations []uint64 // Duration of each segment in timescale units @@ -253,24 +233,6 @@ func (mp4 *MP4) flushPendingVideoSample(nextPTS uint64) bool { var duration uint64 if nextPTS > 0 && nextPTS > mp4.VideoFullSample.DecodeTime { duration = nextPTS - mp4.VideoFullSample.DecodeTime - // Guard against forward PTS jumps (e.g. when looping a source MP4 - // through virtual-rtsp the upstream ffmpeg may insert a large offset - // at the loop boundary, or the RTSP stream may stall briefly). - // Without this clamp the sample gets a huge duration which appears - // as a discontinuity in the trun/sidx/mvhd and causes browsers - // (Video.js / MSE) to abort playback with a "media corruption" - // error around the loop boundary. - var maxPlausible uint64 = 1000 // 1 second hard ceiling - if mp4.LastVideoSampleDTS > 0 && mp4.LastVideoSampleDTS*10 < maxPlausible { - maxPlausible = mp4.LastVideoSampleDTS * 10 - } - if duration > maxPlausible { - log.Log.Warning(fmt.Sprintf("mp4.flushPendingVideoSample(): video PTS jumped forward (nextPTS=%d, prevDTS=%d, gap=%d ms) - clamping to %d ms (likely source loop/stall discontinuity)", nextPTS, mp4.VideoFullSample.DecodeTime, duration, maxPlausible)) - duration = mp4.LastVideoSampleDTS - if duration == 0 { - duration = 33 - } - } } else { // No valid nextPTS (Close case) or PTS went backwards (jitter/discontinuity) if nextPTS > 0 { @@ -327,37 +289,6 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p } shouldFlush := !mp4.Start || elapsed >= FragmentDurationMs - // Detect upstream source-loop / restart discontinuity. When an MP4 is - // looped through virtual-rtsp (ffmpeg `-stream_loop -1 -re`) the loop - // seam emits a fresh IDR much sooner than a normal GOP would. PTS keeps - // growing monotonically, so the timing-only `elapsed` check above does - // not catch it and the seam IDR ends up as a mid-fragment sync sample. - // MSE-based players (Video.js / Chromium / Firefox) reject the resulting - // fragment with a "media corruption" error because the inner IDR resets - // frame_num/POC inside what they expect to be a single GOP. Force a - // fragment boundary whenever two consecutive keyframes arrive much - // closer than a normal GOP. - // - // We only flag this as a seam when it is a *sudden* anomaly: the - // previous keyframe gap must have been healthy (>= MinNormalGOPMs). - // This avoids false positives on cameras that legitimately emit - // short-interval IDRs (short GOP, motion-triggered recovery IDRs, - // all-intra streams) where every keyframe would otherwise be flagged - // in a cascade, producing many tiny fragments and log spam. - if trackID == uint32(mp4.VideoTrack) && mp4.Start && - mp4.LastKeyframeRawPTS > 0 && pts > mp4.LastKeyframeRawPTS { - gap := pts - mp4.LastKeyframeRawPTS - if !shouldFlush && gap < MinNormalGOPMs && - (mp4.LastKeyframeGapMs == 0 || mp4.LastKeyframeGapMs >= 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", gap, elapsed)) - shouldFlush = true - } - mp4.LastKeyframeGapMs = gap - } - if trackID == uint32(mp4.VideoTrack) { - mp4.LastKeyframeRawPTS = pts - } - if shouldFlush { // Write the previous segment to the file if mp4.Start { @@ -462,14 +393,6 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p if started { dts = 1 } - // Guard against forward PTS jumps (e.g. virtual-rtsp loop - // boundary or upstream stalls). Without this clamp the - // audio trun would carry an enormous sample duration that - // renders the recording unplayable in browsers. - if mp4.LastAudioSampleDTS > 0 && dts > mp4.LastAudioSampleDTS*10 { - log.Log.Warning(fmt.Sprintf("mp4.AddSampleToTrack(): audio PTS jumped forward (pts=%d, prevDTS=%d, gap=%d) - clamping to last known duration", pts, mp4.AudioFullSample.DecodeTime, dts)) - dts = mp4.LastAudioSampleDTS - } mp4.LastAudioSampleDTS = dts //fmt.Printf("Adding sample to track %d, PTS: %d, Duration: %d, size: %d\n", trackID, pts, dts, len(aac[7:])) mp4.AudioTotalDuration += dts diff --git a/machinery/src/video/mp4_loopseam_test.go b/machinery/src/video/mp4_loopseam_test.go deleted file mode 100644 index 2a69444..0000000 --- a/machinery/src/video/mp4_loopseam_test.go +++ /dev/null @@ -1,102 +0,0 @@ -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 ~150ms after previous (vs normal ~1000ms). - // This matches the realistic virtual-rtsp / ffmpeg `-stream_loop` - // loop boundary, where the new clip's first IDR is emitted shortly - // after the previous clip's final IDR. - pts -= 820 - 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++ - } - } - } -}