Merge pull request #272 from kerberos-io/revert/loopback

Revert/loopback
This commit is contained in:
Cédric Verstraeten
2026-05-18 17:08:05 +02:00
committed by GitHub
2 changed files with 0 additions and 179 deletions

View File

@@ -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

View File

@@ -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++
}
}
}
}