diff --git a/machinery/src/capture/gortsplib.go b/machinery/src/capture/gortsplib.go index 9d6653c..9108f22 100644 --- a/machinery/src/capture/gortsplib.go +++ b/machinery/src/capture/gortsplib.go @@ -953,12 +953,31 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets pkt.Data = pkt.Data[4:] if pkt.IsKeyFrame { - annexbNALUStartCode := func() []byte { return []byte{0x00, 0x00, 0x00, 0x01} } - pkt.Data = append(annexbNALUStartCode(), pkt.Data...) - pkt.Data = append(g.VideoH264Forma.PPS, pkt.Data...) - pkt.Data = append(annexbNALUStartCode(), pkt.Data...) - pkt.Data = append(g.VideoH264Forma.SPS, pkt.Data...) - pkt.Data = append(annexbNALUStartCode(), pkt.Data...) + // Prepend SPS and PPS in front of every keyframe so the access unit + // is self-contained. Downstream decoders (and the MP4 writer's in-band + // parameter-set recovery) rely on this; a recording whose first frame + // lacks SPS/PPS produces an MP4 with an empty avcC, which makes FFmpeg + // report "non-existing PPS 0 referenced". + // + // Build the payload in a freshly allocated buffer. The previous code + // did append(g.VideoH264Forma.PPS, pkt.Data...): because the SPS/PPS + // slices are sub-slices of the RTP reassembly buffer (spare capacity), + // that append wrote into - and corrupted - the shared parameter-set + // backing arrays, occasionally poisoning the SPS/PPS stored for the + // recording. + startCode := []byte{0x00, 0x00, 0x00, 0x01} + out := make([]byte, 0, len(g.VideoH264Forma.SPS)+len(g.VideoH264Forma.PPS)+len(pkt.Data)+12) + if len(g.VideoH264Forma.SPS) > 0 { + out = append(out, startCode...) + out = append(out, g.VideoH264Forma.SPS...) + } + if len(g.VideoH264Forma.PPS) > 0 { + out = append(out, startCode...) + out = append(out, g.VideoH264Forma.PPS...) + } + out = append(out, startCode...) + out = append(out, pkt.Data...) + pkt.Data = out } writeStart := time.Now() diff --git a/machinery/src/video/mp4.go b/machinery/src/video/mp4.go index c63281d..e1b3629 100644 --- a/machinery/src/video/mp4.go +++ b/machinery/src/video/mp4.go @@ -571,6 +571,13 @@ func (mp4 *MP4) Close(config *models.Config) { includePS := true spsNALUs, ppsNALUs := normalizeH264ParameterSets(mp4.SPSNALUs, mp4.PPSNALUs) log.Log.Debug("mp4.Close(): AVC parameter sets: SPS=" + formatNaluDebug(spsNALUs) + ", PPS=" + formatNaluDebug(ppsNALUs)) + if len(spsNALUs) == 0 || len(ppsNALUs) == 0 { + // An avcC without both SPS and PPS is invalid: downstream FFmpeg-based + // pipelines decoding this file will report "non-existing PPS 0 referenced" + // and fail to extract any frame. Surface it loudly so the capture-side + // parameter-set handling can be diagnosed. + log.Log.Error(fmt.Sprintf("mp4.Close(): incomplete H264 parameter sets (SPS=%d, PPS=%d) - the avcC will be invalid and downstream decoders will report 'non-existing PPS 0 referenced'", len(spsNALUs), len(ppsNALUs))) + } err := init.Moov.Traks[0].SetAVCDescriptor("avc1", spsNALUs, ppsNALUs, includePS) if err != nil { log.Log.Error("mp4.Close(): error setting AVC descriptor: " + err.Error()) @@ -597,6 +604,11 @@ func (mp4 *MP4) Close(config *models.Config) { includePS := true vpsNALUs, spsNALUs, ppsNALUs := normalizeH265ParameterSets(mp4.VPSNALUs, mp4.SPSNALUs, mp4.PPSNALUs) log.Log.Debug("mp4.Close(): HEVC parameter sets: VPS=" + formatNaluDebug(vpsNALUs) + ", SPS=" + formatNaluDebug(spsNALUs) + ", PPS=" + formatNaluDebug(ppsNALUs)) + if len(vpsNALUs) == 0 || len(spsNALUs) == 0 || len(ppsNALUs) == 0 { + // An hvcC missing VPS/SPS/PPS is invalid and downstream FFmpeg-based + // pipelines will fail to decode the recording. Surface it loudly. + log.Log.Error(fmt.Sprintf("mp4.Close(): incomplete H265 parameter sets (VPS=%d, SPS=%d, PPS=%d) - the hvcC will be invalid and downstream decoders will fail to process the recording", len(vpsNALUs), len(spsNALUs), len(ppsNALUs))) + } err := init.Moov.Traks[0].SetHEVCDescriptor("hvc1", vpsNALUs, spsNALUs, ppsNALUs, [][]byte{}, includePS) if err != nil { log.Log.Error("mp4.Close(): error setting HEVC descriptor: " + err.Error())