From 949e8fcff93a0922aff991092bf6f125f5f53be2 Mon Sep 17 00:00:00 2001 From: willhuo Date: Wed, 11 Feb 2026 18:05:52 +0800 Subject: [PATCH 1/2] add mux based on SharpMP4 to save mp4 --- RtspClientExample/Program.cs | 198 ++++++++++++++++++++- RtspClientExample/RtspClientExample.csproj | 1 + 2 files changed, 194 insertions(+), 5 deletions(-) diff --git a/RtspClientExample/Program.cs b/RtspClientExample/Program.cs index d6b4def..277f036 100644 --- a/RtspClientExample/Program.cs +++ b/RtspClientExample/Program.cs @@ -1,7 +1,11 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using SharpMP4; +using SharpMP4.Builders; +using SharpMP4.Tracks; using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; namespace RtspClientExample @@ -10,6 +14,13 @@ public static class Program { private static ILogger logger = null!; + // MP4 录制相关(使用 SharpMP4,将 H264 + AAC 封装为 MP4) + private static IMp4Builder? mp4Builder; + private static object mp4Lock = new(); + private static H264Track? mp4VideoTrack; + private static AACTrack? mp4AudioTrack; + private static Stream? mp4OutputStream; + private const string ProfileMJPEG = "JPEG"; private const string ProfileH264 = "H264"; private const string ProfileH265 = "H265"; @@ -47,16 +58,19 @@ static void Main() // string url = "rtsp://192.168.0.89/media/video2"; - string url = "rtsp://127.0.0.1/screenlive+audiodevice"; + //string url = "rtsp://127.0.0.1/screenlive+audiodevice"; bool usePlayback = false; // string url = "rtsp://192.168.3.72/ProfileG/Recording-1/recording/play.smp"; string username = "admin"; - string password = "admin"; + string password = "asdasdasd."; // Axis Tests //String url = "rtsp://192.168.1.125/onvif-media/media.amp?profile=quality_h264"; //String url = "rtsp://user:password@192.168.1.102/onvif-media/media.amp?profile=quality_h264"; + + //hik Tests + string url = "rtsp://admin:asdasdasd.@192.168.0.210:554/Streaming/Channels/101"; // Bosch Tests //String url = "rtsp://192.168.1.124/rtsp_tunnel?h26x=4&line=1&inst=1"; @@ -96,7 +110,7 @@ static void Main() switch (args.StreamType) { case "H264": - NewH264Stream(args, client); + NewH264StreamToMp4(args, client); break; case "H265": NewH265Stream(args, client); @@ -127,7 +141,7 @@ static void Main() NewAMRAudioStream(client); break; case "AAC": - NewAACAudioStream(arg, client); + NewAACAudioStreamToMp4(arg, client); break; default: logger.LogWarning("Unknow Audio format {streamtype}", arg.StreamType); @@ -180,8 +194,56 @@ static void Main() client.Stop(); Console.WriteLine("Finished"); + + // 结束时收尾 MP4 文件 + lock (mp4Lock) + { + if (mp4Builder != null) + { + mp4Builder.FinalizeMedia(); + mp4Builder = null; + } + + mp4OutputStream?.Dispose(); + mp4OutputStream = null; + } + } + + /// + /// 确保 MP4 录制器和输出流已创建。 + /// + private static void EnsureMp4RecorderCreated() + { + if (mp4Builder != null) return; + + string now = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string filename = "rtsp_capture_" + now + ".mp4"; + + mp4OutputStream = new BufferedStream(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read)); + mp4Builder = new Mp4Builder(new SingleStreamOutput(mp4OutputStream)); } + /// + /// 根据 AAC 的 FrequencyIndex 计算采样率(参照 AAC 规范)。 + /// + private static uint GetAacSampleRateFromIndex(int index) => index switch + { + 0 => 96000, + 1 => 88200, + 2 => 64000, + 3 => 48000, + 4 => 44100, + 5 => 32000, + 6 => 24000, + 7 => 22050, + 8 => 16000, + 9 => 12000, + 10 => 11025, + 11 => 8000, + 12 => 7350, + _ => 44100 + }; + private static void NewAACAudioStream(NewStreamEventArgs arg, RTSPClient client) { string now = DateTime.Now.ToString("yyyyMMdd_HHmmss"); @@ -231,6 +293,46 @@ void ReceiveAudioAAC(RTSPClient client, SimpleDataEventArgs dataArgs) client.SetupAudioPayload(ProfileAAC, ReceiveAudioAAC); } + private static void NewAACAudioStreamToMp4(NewStreamEventArgs arg, RTSPClient client) + { + var config = arg.StreamConfigurationData as AacStreamConfigurationData; + Debug.Assert(config != null, "config is invalid"); + + lock (mp4Lock) + { + EnsureMp4RecorderCreated(); + + if (mp4AudioTrack == null) + { + uint sampleRate = GetAacSampleRateFromIndex(config.FrequencyIndex); + byte channels = (byte)config.ChannelConfiguration; + + // 第三个参数是位宽,这里直接用 16bit + mp4AudioTrack = new AACTrack(channels, sampleRate, 16); + mp4Builder!.AddTrack(mp4AudioTrack); + } + } + + void ReceiveAudioAAC(RTSPClient client, SimpleDataEventArgs dataArgs) + { + foreach (var data in dataArgs.Data) + { + // 对于 MP4,只写 AAC 原始帧数据(不要再加 ADTS 头) + var frame = data.ToArray(); + + lock (mp4Lock) + { + if (mp4Builder != null && mp4AudioTrack != null) + { + mp4Builder.ProcessTrackSample(mp4AudioTrack.TrackID, frame); + } + } + } + } + + client.SetupAudioPayload(ProfileAAC, ReceiveAudioAAC); + } + private static void NewAMRAudioStream(RTSPClient client) { string now = DateTime.Now.ToString("yyyyMMdd_HHmmss"); @@ -390,6 +492,73 @@ void ReceivedVideoData_H264(RTSPClient client, SimpleDataEventArgs dataArgs) client.SetupVideoPayload(ProfileH264, ReceivedVideoData_H264); } + private static void NewH264StreamToMp4(NewStreamEventArgs args, RTSPClient client) + { + if (args.StreamConfigurationData is H264StreamConfigurationData h264StreamConfigurationData) + { + lock (mp4Lock) + { + EnsureMp4RecorderCreated(); + + if (mp4VideoTrack == null) + { + mp4VideoTrack = new H264Track(); + mp4Builder!.AddTrack(mp4VideoTrack); + } + + // 先把 SPS/PPS 等 OutOfBand NAL 喂给 MP4,便于构建解码配置 + foreach (ReadOnlySpan data in h264StreamConfigurationData.OutOfBandNal) + { + if (data.IsEmpty) continue; + + // 去掉 Annex-B 起始码再喂给 SharpMp4 + var sample = StripAnnexBPrefix(data); + mp4Builder!.ProcessTrackSample(mp4VideoTrack.TrackID, sample); + } + } + } + + void ReceivedVideoData_H264(RTSPClient client, SimpleDataEventArgs dataArgs) + { + foreach (var nalUnitMem in dataArgs.Data) + { + var nalUnit = nalUnitMem.Span; + + // 原来的日志保留 + if (nalUnit.Length > 5) + { + int nal_ref_idc = (nalUnit[4] >> 5) & 0x03; + int nal_unit_type = nalUnit[4] & 0x1F; + string description = nal_unit_type switch + { + 1 => "NON IDR NAL", + 5 => "IDR NAL", + 6 => "SEI NAL", + 7 => "SPS NAL", + 8 => "PPS NAL", + 9 => "ACCESS UNIT DELIMITER NAL", + _ => "OTHER NAL", + }; + logger.LogInformation("NAL Ref = {nal_ref_idc} NAL Type = {nal_unit_type} {description}", + nal_ref_idc, nal_unit_type, description); + } + + //var sample = nalUnitMem.ToArray(); + var sample = StripAnnexBPrefix(nalUnitMem.Span); + + lock (mp4Lock) + { + if (mp4Builder != null && mp4VideoTrack != null) + { + mp4Builder.ProcessTrackSample(mp4VideoTrack.TrackID, sample); + } + } + } + } + + client.SetupVideoPayload(ProfileH264, ReceivedVideoData_H264); + } + private static void WriteNalToFileIfNotEmpty(FileStream fs_v, ReadOnlySpan nal) { if (nal.IsEmpty) return; @@ -397,5 +566,24 @@ private static void WriteNalToFileIfNotEmpty(FileStream fs_v, ReadOnlySpan fs_v.Write([0x00, 0x00, 0x00, 0x01]); fs_v.Write(nal); } + + /// + /// 去掉 Annex-B 起始码(00 00 01 / 00 00 00 01),返回裸 NAL 数据。 + /// + private static byte[] StripAnnexBPrefix(ReadOnlySpan nal) + { + int offset = 0; + + if (nal.Length >= 4 && nal[0] == 0x00 && nal[1] == 0x00 && nal[2] == 0x00 && nal[3] == 0x01) + { + offset = 4; + } + else if (nal.Length >= 3 && nal[0] == 0x00 && nal[1] == 0x00 && nal[2] == 0x01) + { + offset = 3; + } + + return nal[offset..].ToArray(); + } } } diff --git a/RtspClientExample/RtspClientExample.csproj b/RtspClientExample/RtspClientExample.csproj index 3496916..6f1d39d 100644 --- a/RtspClientExample/RtspClientExample.csproj +++ b/RtspClientExample/RtspClientExample.csproj @@ -12,5 +12,6 @@ + \ No newline at end of file From 8d67c0a2677cdd175fee85587c4bd0de541c0244 Mon Sep 17 00:00:00 2001 From: willhuo Date: Wed, 11 Feb 2026 18:35:02 +0800 Subject: [PATCH 2/2] down to net8 --- RtspClientExample/Program.cs | 5 +++-- .../PublishProfiles/FolderProfile.pubxml | 16 ++++++++++++++++ RtspClientExample/RtspClientExample.csproj | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 RtspClientExample/Properties/PublishProfiles/FolderProfile.pubxml diff --git a/RtspClientExample/Program.cs b/RtspClientExample/Program.cs index 277f036..060c2df 100644 --- a/RtspClientExample/Program.cs +++ b/RtspClientExample/Program.cs @@ -70,7 +70,7 @@ static void Main() //String url = "rtsp://user:password@192.168.1.102/onvif-media/media.amp?profile=quality_h264"; //hik Tests - string url = "rtsp://admin:asdasdasd.@192.168.0.210:554/Streaming/Channels/101"; + string url = "rtsp://office.hgnet.tech:554/Streaming/Channels/101"; // Bosch Tests //String url = "rtsp://192.168.1.124/rtsp_tunnel?h26x=4&line=1&inst=1"; @@ -179,7 +179,8 @@ static void Main() Console.WriteLine("Press ENTER to exit"); ConsoleKeyInfo key = default; - while (key.Key != ConsoleKey.Enter && !client.StreamingFinished()) + var endTime = DateTime.Now.AddSeconds(30); + while (key.Key != ConsoleKey.Enter && !client.StreamingFinished()&& DateTime.Now < endTime) { while (!Console.KeyAvailable && !client.StreamingFinished()) { diff --git a/RtspClientExample/Properties/PublishProfiles/FolderProfile.pubxml b/RtspClientExample/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..82c918b --- /dev/null +++ b/RtspClientExample/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\linux-x64\ + FileSystem + <_TargetId>Folder + net8.0 + linux-x64 + true + true + false + + \ No newline at end of file diff --git a/RtspClientExample/RtspClientExample.csproj b/RtspClientExample/RtspClientExample.csproj index 6f1d39d..22db10c 100644 --- a/RtspClientExample/RtspClientExample.csproj +++ b/RtspClientExample/RtspClientExample.csproj @@ -1,6 +1,6 @@  - net10.0 + net8.0 Exe false enable @@ -12,6 +12,6 @@ - + \ No newline at end of file