Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 195 additions & 6 deletions RtspClientExample/Program.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -96,7 +110,7 @@ static void Main()
switch (args.StreamType)
{
case "H264":
NewH264Stream(args, client);
NewH264StreamToMp4(args, client);
break;
case "H265":
NewH265Stream(args, client);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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())
{
Expand All @@ -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;
}
}

/// <summary>
/// 确保 MP4 录制器和输出流已创建。
/// </summary>
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));
}

/// <summary>
/// 根据 AAC 的 FrequencyIndex 计算采样率(参照 AAC 规范)。
/// </summary>
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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -390,12 +493,98 @@ 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<byte> 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<byte> nal)
{
if (nal.IsEmpty) return;
// Write Start Code
fs_v.Write([0x00, 0x00, 0x00, 0x01]);
fs_v.Write(nal);
}

/// <summary>
/// 去掉 Annex-B 起始码(00 00 01 / 00 00 00 01),返回裸 NAL 数据。
/// </summary>
private static byte[] StripAnnexBPrefix(ReadOnlySpan<byte> 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();
}
}
}
16 changes: 16 additions & 0 deletions RtspClientExample/Properties/PublishProfiles/FolderProfile.pubxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0\publish\linux-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
</Project>
3 changes: 2 additions & 1 deletion RtspClientExample/RtspClientExample.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Nullable>enable</Nullable>
Expand All @@ -12,5 +12,6 @@
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageReference Include="SharpMP4" Version="0.1.3" />
</ItemGroup>
</Project>