diff --git a/src/OpenIPC.Viewer.Video/Recording/FfmpegRecordingSession.cs b/src/OpenIPC.Viewer.Video/Recording/FfmpegRecordingSession.cs index c2e8b7e..7db3897 100644 --- a/src/OpenIPC.Viewer.Video/Recording/FfmpegRecordingSession.cs +++ b/src/OpenIPC.Viewer.Video/Recording/FfmpegRecordingSession.cs @@ -94,7 +94,15 @@ public void Start() // no row reaches the Recordings tab while REC keeps "running" (#91). psi.ArgumentList.Add(FfmpegCliOptions.RtspTimeoutFlag(_ffmpegPath)); psi.ArgumentList.Add(RtspTimeoutMicros); psi.ArgumentList.Add("-i"); psi.ArgumentList.Add(BuildRtspUri(_options.RtspUri, _options.Credentials).ToString()); - psi.ArgumentList.Add("-c"); psi.ArgumentList.Add("copy"); + // Video is copied untouched. Audio is re-encoded to AAC: OpenIPC cameras + // commonly send G.711 (pcm_alaw/pcm_mulaw), which mp4 has no tag for, and + // a plain "-c copy" failed the header write so nothing was recorded. AAC at + // 8-16 kHz costs next to no CPU. "0:a?" keeps silent cameras working, and + // mapping only video+audio drops ONVIF metadata/data tracks mp4 rejects too. + psi.ArgumentList.Add("-map"); psi.ArgumentList.Add("0:v"); + psi.ArgumentList.Add("-map"); psi.ArgumentList.Add("0:a?"); + psi.ArgumentList.Add("-c:v"); psi.ArgumentList.Add("copy"); + psi.ArgumentList.Add("-c:a"); psi.ArgumentList.Add("aac"); psi.ArgumentList.Add("-f"); psi.ArgumentList.Add("segment"); psi.ArgumentList.Add("-segment_time"); psi.ArgumentList.Add(((int)_options.SegmentDuration.TotalSeconds).ToString(System.Globalization.CultureInfo.InvariantCulture)); psi.ArgumentList.Add("-reset_timestamps"); psi.ArgumentList.Add("1"); diff --git a/src/OpenIPC.Viewer.Video/Recording/LibavformatRecordingSession.cs b/src/OpenIPC.Viewer.Video/Recording/LibavformatRecordingSession.cs index dc8eb29..58e2ffe 100644 --- a/src/OpenIPC.Viewer.Video/Recording/LibavformatRecordingSession.cs +++ b/src/OpenIPC.Viewer.Video/Recording/LibavformatRecordingSession.cs @@ -163,6 +163,7 @@ private unsafe void Run() outputCtx = outRaw; streamMap = new int[(int)inputCtx->nb_streams]; + var hasVideo = false; for (var i = 0; i < (int)inputCtx->nb_streams; i++) { streamMap[i] = -1; @@ -171,6 +172,18 @@ private unsafe void Run() if (kind != AVMediaType.AVMEDIA_TYPE_VIDEO && kind != AVMediaType.AVMEDIA_TYPE_AUDIO) continue; + // OpenIPC cameras commonly send G.711 (pcm_alaw/pcm_mulaw) audio, which + // mp4 has no tag for: copying it made avformat_write_header fail with + // EINVAL and no recording at all. Drop what the container can't hold; + // the video is what a recording is for. + var codecId = inStream->codecpar->codec_id; + if (ffmpeg.avformat_query_codec(outputCtx->oformat, codecId, ffmpeg.FF_COMPLIANCE_NORMAL) != 1) + { + _logger.LogWarning("Recording skips stream {Index}: mp4 cannot hold {Codec}", + i, ffmpeg.avcodec_get_name(codecId)); + continue; + } + var outStream = ffmpeg.avformat_new_stream(outputCtx, null); if (outStream == null) throw new InvalidOperationException("avformat_new_stream returned null"); @@ -183,8 +196,14 @@ private unsafe void Run() outStream->codecpar->codec_tag = 0; streamMap[i] = (int)outStream->index; + hasVideo |= kind == AVMediaType.AVMEDIA_TYPE_VIDEO; } + // Audio is optional, video is not: an audio-only file isn't a recording. + // Fail before avio_open so no empty/useless file is left on disk. + if (!hasVideo) + throw new InvalidOperationException("The camera stream has no video mp4 can hold"); + if ((outputCtx->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0) { ret = ffmpeg.avio_open(&outputCtx->pb, _outputPath, ffmpeg.AVIO_FLAG_WRITE);