From f64e899de94d75f56146c959eb122518c490ec4b Mon Sep 17 00:00:00 2001 From: cedricve Date: Thu, 26 Feb 2026 20:37:10 +0100 Subject: [PATCH] Populate/sanitize NALUs and avoid empty MP4 Fill missing SPS/PPS/VPS from camera config before closing recordings and warn when parameter sets are incomplete (for both continuous and motion-detection flows). Sanitize parameter sets (remove Annex-B start codes and drop empty NALUs) before writing AVC/HEVC descriptors. Prevent creation of empty MP4 files by flushing/closing and removing files when no audio/video samples were added, and only add an audio track when audio samples exist. --- machinery/src/capture/main.go | 26 +++++++++++++++++++++++++ machinery/src/video/mp4.go | 36 ++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/machinery/src/capture/main.go b/machinery/src/capture/main.go index fa9a60c..125f3bc 100644 --- a/machinery/src/capture/main.go +++ b/machinery/src/capture/main.go @@ -159,6 +159,19 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat } // Close mp4 + if len(mp4Video.SPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.SPSNALUs) > 0 { + mp4Video.SPSNALUs = configuration.Config.Capture.IPCamera.SPSNALUs + } + if len(mp4Video.PPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.PPSNALUs) > 0 { + mp4Video.PPSNALUs = configuration.Config.Capture.IPCamera.PPSNALUs + } + if len(mp4Video.VPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.VPSNALUs) > 0 { + mp4Video.VPSNALUs = configuration.Config.Capture.IPCamera.VPSNALUs + } + if (videoCodec == "H264" && (len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) || + (videoCodec == "H265" && (len(mp4Video.VPSNALUs) == 0 || len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) { + log.Log.Warning("capture.main.HandleRecordStream(continuous): closing MP4 without full parameter sets, moov may be incomplete") + } mp4Video.Close(&config) log.Log.Info("capture.main.HandleRecordStream(continuous): recording finished: file save: " + name) @@ -580,6 +593,19 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat lastRecordingTime = pkt.CurrentTime // This will close the recording and write the last packet. + if len(mp4Video.SPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.SPSNALUs) > 0 { + mp4Video.SPSNALUs = configuration.Config.Capture.IPCamera.SPSNALUs + } + if len(mp4Video.PPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.PPSNALUs) > 0 { + mp4Video.PPSNALUs = configuration.Config.Capture.IPCamera.PPSNALUs + } + if len(mp4Video.VPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.VPSNALUs) > 0 { + mp4Video.VPSNALUs = configuration.Config.Capture.IPCamera.VPSNALUs + } + if (videoCodec == "H264" && (len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) || + (videoCodec == "H265" && (len(mp4Video.VPSNALUs) == 0 || len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) { + log.Log.Warning("capture.main.HandleRecordStream(motiondetection): closing MP4 without full parameter sets, moov may be incomplete") + } mp4Video.Close(&config) log.Log.Info("capture.main.HandleRecordStream(motiondetection): file save: " + name) diff --git a/machinery/src/video/mp4.go b/machinery/src/video/mp4.go index dab8900..f154295 100644 --- a/machinery/src/video/mp4.go +++ b/machinery/src/video/mp4.go @@ -431,7 +431,12 @@ func (mp4 *MP4) Close(config *models.Config) { mp4.TotalKeyframesReceived, mp4.TotalKeyframesWritten, mp4.SegmentCount, mp4.FragmentKeyframeCount)) if mp4.VideoTotalDuration == 0 && mp4.AudioTotalDuration == 0 { - log.Log.Error("mp4.Close(): no video or audio samples added, cannot create MP4 file") + log.Log.Error("mp4.Close(): no video or audio samples added, removing empty MP4 file") + mp4.Writer.Flush() + _ = mp4.FileWriter.Sync() + _ = mp4.FileWriter.Close() + _ = os.Remove(mp4.FileName) + return } // Add final pending samples before closing @@ -554,7 +559,9 @@ func (mp4 *MP4) Close(config *models.Config) { case "H264", "AVC1": init.AddEmptyTrack(videoTimescale, "video", "und") includePS := true - err := init.Moov.Traks[0].SetAVCDescriptor("avc1", mp4.SPSNALUs, mp4.PPSNALUs, includePS) + spsNALUs := sanitizeParameterSets(mp4.SPSNALUs) + ppsNALUs := sanitizeParameterSets(mp4.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()) } @@ -573,7 +580,10 @@ func (mp4 *MP4) Close(config *models.Config) { case "H265", "HVC1": init.AddEmptyTrack(videoTimescale, "video", "und") includePS := true - err := init.Moov.Traks[0].SetHEVCDescriptor("hvc1", mp4.VPSNALUs, mp4.SPSNALUs, mp4.PPSNALUs, [][]byte{}, includePS) + vpsNALUs := sanitizeParameterSets(mp4.VPSNALUs) + spsNALUs := sanitizeParameterSets(mp4.SPSNALUs) + ppsNALUs := sanitizeParameterSets(mp4.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()) } @@ -589,8 +599,8 @@ func (mp4 *MP4) Close(config *models.Config) { init.Moov.Traks[0].Mdia.Mdhd.ModificationTime = macTime } - // Try adding audio track if available - if mp4.AudioTrackName == "AAC" || mp4.AudioTrackName == "MP4A" { + // Try adding audio track if available and samples were recorded. + if (mp4.AudioTrackName == "AAC" || mp4.AudioTrackName == "MP4A") && mp4.AudioTotalDuration > 0 { // Add an audio track to the moov box init.AddEmptyTrack(audioTimescale, "audio", "und") @@ -828,6 +838,22 @@ func removeAnnexBStartCode(nalu []byte) []byte { return nalu } +// sanitizeParameterSets removes Annex B start codes and drops empty NALUs. +func sanitizeParameterSets(nalus [][]byte) [][]byte { + if len(nalus) == 0 { + return nalus + } + clean := make([][]byte, 0, len(nalus)) + for _, nalu := range nalus { + trimmed := removeAnnexBStartCode(nalu) + if len(trimmed) == 0 { + continue + } + clean = append(clean, trimmed) + } + return clean +} + // splitNALUs splits Annex B data into raw NAL units without start codes. func splitNALUs(data []byte) [][]byte { var nalus [][]byte