diff --git a/RtspClientExample/Program.cs b/RtspClientExample/Program.cs
index d6b4defe..060c2dfa 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://office.hgnet.tech: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);
@@ -165,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())
{
@@ -180,8 +195,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 +294,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 +493,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 +567,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/Properties/PublishProfiles/FolderProfile.pubxml b/RtspClientExample/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 00000000..82c918b8
--- /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 3496916c..22db10c1 100644
--- a/RtspClientExample/RtspClientExample.csproj
+++ b/RtspClientExample/RtspClientExample.csproj
@@ -1,6 +1,6 @@
- net10.0
+ net8.0
Exe
false
enable
@@ -12,5 +12,6 @@
+
\ No newline at end of file