Skip to content

Commit 706c6ad

Browse files
authored
Merge pull request #2 from jimm98y/features/ffmpeg-rtp
F RTSP server for ffmpeg RTP streams
2 parents c35c2f7 + 7355ff8 commit 706c6ad

File tree

15 files changed

+521
-220
lines changed

15 files changed

+521
-220
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Simple RTSP client that supports H264, H265 for video and AAC for audio.
77
## SharpRTSPServer
88
Simple RTSP server that supports H264, H265 for video and AAC, PCMU and PCMA for audio.
99

10+
## FFmpeg RTSP Server
11+
Sample RTSP server for ffmpeg RTP streams. Fully configurable in appsettings.json.
12+
1013
## Remarks
1114
Still work in progress, APIs are subject to change.
1215

src/RTSPServerApp/Program.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
TrakBox videoTrackBox = null;
2424
double videoFrameRate = 0;
2525

26+
ITrack rtspVideoTrack = null;
27+
ITrack rtspAudioTrack = null;
28+
2629
// frag_bunny.mp4 audio is not playable in VLC on Windows 11 (works on MacOS)
2730
using (Stream fs = new BufferedStream(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)))
2831
{
@@ -42,14 +45,16 @@
4245
if (h264VisualSample != null)
4346
{
4447
var avcC = (h264VisualSample.Children.First(x => x.Type == AvcConfigurationBox.TYPE) as AvcConfigurationBox).AvcDecoderConfigurationRecord;
45-
server.VideoTrack = new SharpRTSPServer.H264Track(avcC.AvcProfileIndication, 0, avcC.AvcLevelIndication);
48+
rtspVideoTrack = new SharpRTSPServer.H264Track(avcC.AvcProfileIndication, 0, avcC.AvcLevelIndication);
49+
server.AddVideoTrack(rtspVideoTrack);
4650
}
4751
else
4852
{
4953
var h265VisualSample = videoTrackBox.GetMdia().GetMinf().GetStbl().GetStsd().Children.FirstOrDefault(x => x.Type == VisualSampleEntryBox.TYPE6 || x.Type == VisualSampleEntryBox.TYPE7) as VisualSampleEntryBox;
5054
if(h265VisualSample != null)
5155
{
52-
server.VideoTrack = new SharpRTSPServer.H265Track();
56+
rtspVideoTrack = new SharpRTSPServer.H265Track();
57+
server.AddVideoTrack(rtspVideoTrack);
5358
}
5459
else
5560
{
@@ -67,7 +72,8 @@
6772
{
6873
var audioConfigDescriptor = audioSampleEntry.GetAudioSpecificConfigDescriptor();
6974
int audioSamplingRate = audioConfigDescriptor.GetSamplingFrequency();
70-
server.AudioTrack = new SharpRTSPServer.AACTrack(await audioConfigDescriptor.ToBytes(), audioSamplingRate, audioConfigDescriptor.ChannelConfiguration);
75+
rtspAudioTrack = new SharpRTSPServer.AACTrack(await audioConfigDescriptor.ToBytes(), audioSamplingRate, audioConfigDescriptor.ChannelConfiguration);
76+
server.AddAudioTrack(rtspAudioTrack);
7177
}
7278
else
7379
{
@@ -92,18 +98,18 @@
9298
{
9399
if (videoIndex == 0)
94100
{
95-
if (server.VideoTrack is SharpRTSPServer.H264Track h264VideoTrack)
101+
if (rtspVideoTrack is SharpRTSPServer.H264Track h264VideoTrack)
96102
{
97103
h264VideoTrack.SetParameterSets(videoTrack[0][0], videoTrack[0][1]);
98104
}
99-
else if (server.VideoTrack is SharpRTSPServer.H265Track h265VideoTrack)
105+
else if (rtspVideoTrack is SharpRTSPServer.H265Track h265VideoTrack)
100106
{
101107
h265VideoTrack.SetParameterSets(videoTrack[0][0], videoTrack[0][1], videoTrack[0][2]);
102108
}
103109
videoIndex++;
104110
}
105111

106-
server.FeedInRawVideoSamples((uint)(videoIndex * videoSampleDuration), (List<byte[]>)videoTrack[videoIndex++ % videoTrack.Count]);
112+
rtspVideoTrack.FeedInRawSamples((uint)(videoIndex * videoSampleDuration), (List<byte[]>)videoTrack[videoIndex++ % videoTrack.Count]);
107113

108114
if (videoIndex % videoTrack.Count == 0)
109115
{
@@ -116,10 +122,10 @@
116122
{
117123
var audioSampleDuration = SharpMp4.AACTrack.AAC_SAMPLE_SIZE;
118124
var audioTrack = parsedMDAT[audioTrackId];
119-
audioTimer = new Timer(audioSampleDuration * 1000 / (server.AudioTrack as SharpRTSPServer.AACTrack).SamplingRate);
125+
audioTimer = new Timer(audioSampleDuration * 1000 / (rtspAudioTrack as SharpRTSPServer.AACTrack).SamplingRate);
120126
audioTimer.Elapsed += (s, e) =>
121127
{
122-
server.FeedInRawAudioSamples((uint)(audioIndex * audioSampleDuration), new List<byte[]>() { audioTrack[0][audioIndex++ % audioTrack[0].Count] });
128+
rtspAudioTrack.FeedInRawSamples((uint)(audioIndex * audioSampleDuration), new List<byte[]>() { audioTrack[0][audioIndex++ % audioTrack[0].Count] });
123129

124130
if (audioIndex % audioTrack[0].Count == 0)
125131
{

src/RTSPServerFFmpeg/Program.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using Microsoft.Extensions.Configuration;
2+
using SharpRTSPServer;
3+
using System;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Text;
7+
using System.Threading;
8+
9+
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
10+
string hostName = config["HostName"];
11+
ushort port = ushort.Parse(config["Port"]);
12+
string userName = config["UserName"];
13+
string password = config["Password"];
14+
15+
string ffmpegPath = config["FFmpegPath"]; // path to ffmpeg.exe
16+
string ffmpegArgs = config["FFmpegArgs"]; // Arguments that will be passed to the ffmpeg process
17+
string videoUri = config["VideoUri"]; // RTP video URI
18+
string audioUri = config["AudioUri"]; // RTP audio URI
19+
string sdpFile = config["SDPFile"]; // SDP file path (Optional in case ffmpegPath and ffmpegArgs are not specified. You have to launch ffmpeg before starting the server.)
20+
21+
SemaphoreSlim semaphore = new SemaphoreSlim(0);
22+
StringBuilder sdpBuilder = new StringBuilder();
23+
ProcessStartInfo info = new ProcessStartInfo();
24+
Process process = null;
25+
string lastLine = null;
26+
string sdp = null;
27+
28+
if (!string.IsNullOrEmpty(ffmpegPath) && !string.IsNullOrEmpty(ffmpegArgs))
29+
{
30+
// launch ffmpeg, parse the output and start streaming
31+
// ffmpeg.exe -re -stream_loop -1 -i frag_bunny.mp4 -vcodec copy -an -f rtp rtp://127.0.0.1:11111 -vn -acodec copy -f rtp rtp://127.0.0.1:11113
32+
info.FileName = ffmpegPath;
33+
info.Arguments = ffmpegArgs;
34+
info.RedirectStandardOutput = true;
35+
info.UseShellExecute = false;
36+
37+
process = Process.Start(info);
38+
process.OutputDataReceived += Process_OutputDataReceived;
39+
process.BeginOutputReadLine();
40+
41+
// wait until the SDP is read
42+
semaphore.Wait();
43+
44+
sdp = sdpBuilder.ToString();
45+
}
46+
else if(!string.IsNullOrEmpty(sdpFile))
47+
{
48+
// optionally, read SDP from a file
49+
sdp = File.ReadAllText(sdpFile);
50+
}
51+
else
52+
{
53+
throw new Exception("Invalid configuration! Either ffmpegPath and ffmpegArgs, or SDPFile must be specified!");
54+
}
55+
56+
if (string.IsNullOrEmpty(videoUri) && string.IsNullOrEmpty(audioUri))
57+
throw new Exception("Invalid configuration! Either VideoUri, AudioUri or both must be specified!");
58+
59+
using (var server = new RTSPServer(port, userName, password))
60+
{
61+
if (!string.IsNullOrEmpty(videoUri))
62+
server.AddVideoTrack(new ProxyTrack(ProxyTrackType.Video, videoUri));
63+
64+
if (!string.IsNullOrEmpty(audioUri))
65+
server.AddAudioTrack(new ProxyTrack(ProxyTrackType.Audio, audioUri));
66+
67+
server.OverrideSDP(sdp, true);
68+
69+
try
70+
{
71+
server.StartListen();
72+
}
73+
catch (Exception ex)
74+
{
75+
Console.WriteLine(ex.ToString());
76+
}
77+
78+
Console.WriteLine($"RTSP URL is rtsp://{userName}:{password}@{hostName}:{port}");
79+
80+
Console.WriteLine("Press any key to exit");
81+
while (!Console.KeyAvailable)
82+
{
83+
Thread.Sleep(250);
84+
}
85+
86+
if (process != null)
87+
{
88+
process.Kill();
89+
}
90+
}
91+
92+
void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
93+
{
94+
if(lastLine == "" && e.Data == "")
95+
{
96+
semaphore.Release();
97+
return;
98+
}
99+
100+
lastLine = e.Data;
101+
102+
if (!string.IsNullOrEmpty(e.Data) && !e.Data.StartsWith("SDP:"))
103+
{
104+
sdpBuilder.AppendLine(e.Data);
105+
}
106+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>disable</ImplicitUsings>
7+
<Nullable>disable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<Content Include="appsettings.json">
12+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
13+
</Content>
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
18+
<PackageReference Include="SharpMp4" Version="0.0.6" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\SharpRTSPServer\SharpRTSPServer.csproj" />
23+
</ItemGroup>
24+
25+
</Project>

src/RTSPServerFFmpeg/appsettings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"HostName": "127.0.0.1",
3+
"Port": 8554,
4+
"UserName": "admin",
5+
"Password": "password",
6+
"FFmpegPath": "C:\\Users\\lukas\\Downloads\\ffmpeg-6.0-full_build\\bin\\ffmpeg.exe",
7+
"FFmpegArgs": "-re -stream_loop -1 -i C:\\Git\\SharpMediaCoder\\src\\SharpMediaPlayer\\frag_bunny.mp4 -vcodec copy -an -f rtp rtp://127.0.0.1:11111 -vn -acodec copy -f rtp rtp://127.0.0.1:11113",
8+
"VideoUri": "rtp://127.0.0.1:11111",
9+
"AudioUri": "rtp://127.0.0.1:11113",
10+
"SDPFile": null
11+
}

src/SharpRTSPClient.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RTSPServerApp", "RTSPServer
1313
EndProject
1414
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{660C9331-283E-462F-9DB1-1BC2BCB12B2D}"
1515
EndProject
16+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RTSPServerFFmpeg", "RTSPServerFFmpeg\RTSPServerFFmpeg.csproj", "{4D660B91-8244-4C49-8A46-E0DA989F5111}"
17+
EndProject
1618
Global
1719
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1820
Debug|Any CPU = Debug|Any CPU
@@ -35,13 +37,18 @@ Global
3537
{A1E94FC9-5F61-41F3-99AC-3C081E1F965B}.Debug|Any CPU.Build.0 = Debug|Any CPU
3638
{A1E94FC9-5F61-41F3-99AC-3C081E1F965B}.Release|Any CPU.ActiveCfg = Release|Any CPU
3739
{A1E94FC9-5F61-41F3-99AC-3C081E1F965B}.Release|Any CPU.Build.0 = Release|Any CPU
40+
{4D660B91-8244-4C49-8A46-E0DA989F5111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41+
{4D660B91-8244-4C49-8A46-E0DA989F5111}.Debug|Any CPU.Build.0 = Debug|Any CPU
42+
{4D660B91-8244-4C49-8A46-E0DA989F5111}.Release|Any CPU.ActiveCfg = Release|Any CPU
43+
{4D660B91-8244-4C49-8A46-E0DA989F5111}.Release|Any CPU.Build.0 = Release|Any CPU
3844
EndGlobalSection
3945
GlobalSection(SolutionProperties) = preSolution
4046
HideSolutionNode = FALSE
4147
EndGlobalSection
4248
GlobalSection(NestedProjects) = preSolution
4349
{A98D08A3-1617-4953-AB98-11CFB6159529} = {660C9331-283E-462F-9DB1-1BC2BCB12B2D}
4450
{A1E94FC9-5F61-41F3-99AC-3C081E1F965B} = {660C9331-283E-462F-9DB1-1BC2BCB12B2D}
51+
{4D660B91-8244-4C49-8A46-E0DA989F5111} = {660C9331-283E-462F-9DB1-1BC2BCB12B2D}
4552
EndGlobalSection
4653
GlobalSection(ExtensibilityGlobals) = postSolution
4754
SolutionGuid = {BB335B23-D422-4DDC-9B06-833B6E100A0C}

src/SharpRTSPServer/AACTrack.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ namespace SharpRTSPServer
99
/// <summary>
1010
/// AAC track.
1111
/// </summary>
12-
public class AACTrack : ITrack
12+
public class AACTrack : TrackBase
1313
{
1414
/// <summary>
1515
/// AAC Audio Codec name.
1616
/// </summary>
17-
public string Codec => "mpeg4-generic";
17+
public override string Codec => "mpeg4-generic";
1818

1919
/// <summary>
2020
/// Track ID. Used to identify the track in the SDP.
2121
/// </summary>
22-
public int ID { get; set; } = 1;
22+
public override int ID { get; set; } = 1;
2323

2424
/// <summary>
2525
/// Sampling rate.
@@ -39,14 +39,14 @@ public class AACTrack : ITrack
3939
/// <summary>
4040
/// Is the track ready?
4141
/// </summary>
42-
public bool IsReady { get { return ConfigDescriptor != null && ConfigDescriptor.Length > 0; } }
42+
public override bool IsReady { get { return ConfigDescriptor != null && ConfigDescriptor.Length > 0; } }
4343

4444
private int _payloadType = -1;
4545

4646
/// <summary>
4747
/// Payload type. AAC uses a dynamic payload type, which by default we calculate as 96 + track ID.
4848
/// </summary>
49-
public int PayloadType
49+
public override int PayloadType
5050
{
5151
get
5252
{
@@ -101,7 +101,7 @@ public void SetConfigDescriptor(byte[] configDescriptor)
101101
/// </summary>
102102
/// <param name="sdp">SDP <see cref="StringBuilder"/>.</param>
103103
/// <returns><see cref="StringBuilder"/>.</returns>
104-
public StringBuilder BuildSDP(StringBuilder sdp)
104+
public override StringBuilder BuildSDP(StringBuilder sdp)
105105
{
106106
sdp.Append($"m=audio 0 RTP/AVP {PayloadType}\n"); // <---- Payload Type 0 means G711 ULAW, 96+ means dynamic payload type
107107
sdp.Append($"a=control:trackID={ID}\n");
@@ -117,7 +117,7 @@ public StringBuilder BuildSDP(StringBuilder sdp)
117117
/// <param name="samples">An array of AAC fragments. By default single fragment is expected.</param>
118118
/// <param name="rtpTimestamp">RTP timestamp in the timescale of the track.</param>
119119
/// <returns>RTP packets.</returns>
120-
public (List<Memory<byte>>, List<IMemoryOwner<byte>>) CreateRtpPackets(List<byte[]> samples, uint rtpTimestamp)
120+
public override (List<Memory<byte>>, List<IMemoryOwner<byte>>) CreateRtpPackets(List<byte[]> samples, uint rtpTimestamp)
121121
{
122122
List<Memory<byte>> rtpPackets = new List<Memory<byte>>();
123123
List<IMemoryOwner<byte>> memoryOwners = new List<IMemoryOwner<byte>>();

0 commit comments

Comments
 (0)