Skip to content

Commit 38399f8

Browse files
authored
webrtccmdline additions and some minor improvements (#1209)
* Small bug fix on setting RTP port and added echo server to webrtccmdline. * Remove stack trace for expected socket exception.
1 parent 09555fb commit 38399f8

13 files changed

+377
-22
lines changed

.dockerignore

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
**/.classpath
2+
**/.dockerignore
3+
**/.env
4+
**/.git
5+
**/.gitignore
6+
**/.project
7+
**/.settings
8+
**/.toolstarget
9+
**/.vs
10+
**/.vscode
11+
**/*.*proj.user
12+
**/*.dbmdl
13+
**/*.jfm
14+
**/azds.yaml
15+
**/bin
16+
**/charts
17+
**/docker-compose*
18+
**/Dockerfile*
19+
**/node_modules
20+
**/npm-debug.log
21+
**/obj
22+
**/secrets.dev.yaml
23+
**/values.dev.yaml
24+
LICENSE
25+
README.md

Dockerfile-webrtccmdline

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# docker build -t webrtccmdline -f Dockerfile-webrtccmdline .
2+
# docker login
3+
# Either:
4+
# docker tag webrtccmdline:latest docker.io/azaclauson/webrtccmdline:latest
5+
# docker push azaclauson/webrtccmdline:latest
6+
# or:
7+
# az acr login --name sipsorcerycr
8+
# docker tag webrtccmdline:latest sipsorcerycr.azurecr.io/webrtccmdline:latest
9+
# docker push sipsorcerycr.azurecr.io/webrtccmdline:latest
10+
# To run a local container:
11+
# docker run -it --rm -p 8081:8081 -p 60042:60042/udp webrtccmdline --ws --stun stun:stun.l.google.com:19302
12+
13+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
14+
WORKDIR /src
15+
COPY . .
16+
WORKDIR /src/examples/webrtccmdline
17+
RUN dotnet publish "webrtccmdline.csproj" -c Release -o /app/publish
18+
19+
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final
20+
WORKDIR /app
21+
EXPOSE 8080-8081
22+
EXPOSE 60042
23+
COPY --from=build /app/publish .
24+
ENTRYPOINT ["dotnet", "webrtccmdline.dll", "--port", "60042", "--ws", "--stun", "stun:stun.l.google.com:19302"]

examples/webrtccmdline/EchoServer.cs

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
//-----------------------------------------------------------------------------
2+
// Filename: Program.cs
3+
//
4+
// Description: Implements a WebRTC Echo Test Server suitable for interoperability
5+
// testing as per specification at:
6+
// https://github.com/sipsorcery/webrtc-echoes/blob/master/doc/EchoTestSpecification.md
7+
//
8+
// Author(s):
9+
// Aaron Clauson ([email protected])
10+
//
11+
// History:
12+
// 19 Feb 2021 Aaron Clauson Created, Dublin, Ireland.
13+
// 14 Apr 2021 Aaron Clauson Added data channel support.
14+
//
15+
// License:
16+
// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file.
17+
//-----------------------------------------------------------------------------
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Linq;
22+
using System.Net;
23+
using System.Text;
24+
using System.Text.Json;
25+
using System.Threading;
26+
using System.Threading.Tasks;
27+
using EmbedIO;
28+
using Microsoft.Extensions.Logging;
29+
using Microsoft.Extensions.Logging.Abstractions;
30+
using Serilog;
31+
using Serilog.Events;
32+
using Serilog.Extensions.Logging;
33+
using SIPSorcery.Net;
34+
using SIPSorceryMedia.Abstractions;
35+
36+
namespace SIPSorcery.Examples
37+
{
38+
public class EchoServerOptions
39+
{
40+
public const string DEFAULT_WEBSERVER_LISTEN_URL = "http://*:8080/";
41+
public const LogEventLevel DEFAULT_VERBOSITY = LogEventLevel.Debug;
42+
public const int TEST_TIMEOUT_SECONDS = 10;
43+
44+
//[Option('l', "listen", Required = false, Default = DEFAULT_WEBSERVER_LISTEN_URL,
45+
// HelpText = "The URL the web server will listen on.")]
46+
//public string ServerUrl { get; set; }
47+
48+
//[Option("timeout", Required = false, Default = TEST_TIMEOUT_SECONDS,
49+
// HelpText = "Timeout in seconds to close the peer connection. Set to 0 for no timeout.")]
50+
//public int TestTimeoutSeconds { get; set; }
51+
52+
//[Option('v', "verbosity", Required = false, Default = DEFAULT_VERBOSITY,
53+
// HelpText = "The log level verbosity (0=Verbose, 1=Debug, 2=Info, 3=Warn...).")]
54+
//public LogEventLevel Verbosity { get; set; }
55+
}
56+
57+
public class EchoServer
58+
{
59+
private static Microsoft.Extensions.Logging.ILogger logger = NullLogger.Instance;
60+
61+
private static List<IPAddress> _icePresets = new List<IPAddress>();
62+
63+
public void Start()
64+
{
65+
// Apply any command line options
66+
//if (args.Length > 0)
67+
//{
68+
// url = args[0];
69+
// for(int i=1; i<args.Length; i++)
70+
// {
71+
// if(IPAddress.TryParse(args[i], out var addr))
72+
// {
73+
// _icePresets.Add(addr);
74+
// Console.WriteLine($"ICE candidate preset address {addr} added.");
75+
// }
76+
// }
77+
//}
78+
79+
string listenUrl = EchoServerOptions.DEFAULT_WEBSERVER_LISTEN_URL;
80+
LogEventLevel verbosity = EchoServerOptions.DEFAULT_VERBOSITY;
81+
int pcTimeout = EchoServerOptions.TEST_TIMEOUT_SECONDS;
82+
83+
//if (args != null)
84+
//{
85+
// Options opts = null;
86+
// var parseResult = Parser.Default.ParseArguments<Options>(args)
87+
// .WithParsed(o => opts = o);
88+
89+
// listenUrl = opts != null && !string.IsNullOrEmpty(opts.ServerUrl) ? opts.ServerUrl : listenUrl;
90+
// verbosity = opts != null ? opts.Verbosity : verbosity;
91+
// pcTimeout = opts != null ? opts.TestTimeoutSeconds : pcTimeout;
92+
//}
93+
94+
logger = AddConsoleLogger(verbosity);
95+
96+
// Start the web server.
97+
using (var server = CreateWebServer(listenUrl, pcTimeout))
98+
{
99+
server.RunAsync();
100+
101+
Console.WriteLine("ctrl-c to exit.");
102+
var mre = new ManualResetEvent(false);
103+
Console.CancelKeyPress += (sender, eventArgs) =>
104+
{
105+
// cancel the cancellation to allow the program to shutdown cleanly
106+
eventArgs.Cancel = true;
107+
mre.Set();
108+
};
109+
110+
mre.WaitOne();
111+
}
112+
}
113+
114+
private static WebServer CreateWebServer(string url, int pcTimeout)
115+
{
116+
var server = new WebServer(o => o
117+
.WithUrlPrefix(url)
118+
.WithMode(HttpListenerMode.EmbedIO))
119+
.WithCors("*", "*", "*")
120+
.WithAction("/offer", HttpVerbs.Post, (ctx) => Offer(ctx, pcTimeout))
121+
.WithAction("/icecandidate", HttpVerbs.Post, (ctx) => Offer(ctx, pcTimeout));
122+
//.WithStaticFolder("/", "../../html", false);
123+
server.StateChanged += (s, e) => Console.WriteLine($"WebServer New State - {e.NewState}");
124+
125+
return server;
126+
}
127+
128+
private async static Task Offer(IHttpContext context, int pcTimeout)
129+
{
130+
var offer = await context.GetRequestDataAsync<RTCSessionDescriptionInit>();
131+
132+
var jsonOptions = new JsonSerializerOptions();
133+
jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
134+
135+
var echoServer = new WebRTCEchoServer(_icePresets);
136+
var pc = await echoServer.GotOffer(offer);
137+
138+
if (pc != null)
139+
{
140+
var answer = new RTCSessionDescriptionInit { type = RTCSdpType.answer, sdp = pc.localDescription.sdp.ToString() };
141+
context.Response.ContentType = "application/json";
142+
using (var responseStm = context.OpenResponseStream(false, false))
143+
{
144+
await JsonSerializer.SerializeAsync(responseStm, answer, jsonOptions);
145+
}
146+
147+
if (pcTimeout != 0)
148+
{
149+
logger.LogDebug($"Setting peer connection close timeout to {pcTimeout} seconds.");
150+
151+
var timeout = new Timer((state) =>
152+
{
153+
if (!pc.IsClosed)
154+
{
155+
logger.LogWarning("Test timed out.");
156+
pc.close();
157+
}
158+
}, null, pcTimeout * 1000, Timeout.Infinite);
159+
pc.OnClosed += timeout.Dispose;
160+
}
161+
}
162+
}
163+
164+
private static Microsoft.Extensions.Logging.ILogger AddConsoleLogger(
165+
LogEventLevel logLevel = LogEventLevel.Debug)
166+
{
167+
var serilogLogger = new LoggerConfiguration()
168+
.Enrich.FromLogContext()
169+
.MinimumLevel.Is(logLevel)
170+
.WriteTo.Console()
171+
.CreateLogger();
172+
var factory = new SerilogLoggerFactory(serilogLogger);
173+
SIPSorcery.LogFactory.Set(factory);
174+
return factory.CreateLogger<Program>();
175+
}
176+
}
177+
178+
public class WebRTCEchoServer
179+
{
180+
private const int VP8_PAYLOAD_ID = 96;
181+
182+
private static Microsoft.Extensions.Logging.ILogger logger = NullLogger.Instance;
183+
184+
private List<IPAddress> _presetIceAddresses;
185+
186+
public WebRTCEchoServer(List<IPAddress> presetAddresses)
187+
{
188+
logger = SIPSorcery.LogFactory.CreateLogger<WebRTCEchoServer>();
189+
_presetIceAddresses = presetAddresses;
190+
}
191+
192+
public async Task<RTCPeerConnection> GotOffer(RTCSessionDescriptionInit offer)
193+
{
194+
logger.LogTrace($"SDP offer received.");
195+
logger.LogTrace(offer.sdp);
196+
197+
var pc = new RTCPeerConnection();
198+
199+
if (_presetIceAddresses != null)
200+
{
201+
foreach (var addr in _presetIceAddresses)
202+
{
203+
var rtpPort = pc.GetRtpChannel().RTPPort;
204+
var publicIPv4Candidate = new RTCIceCandidate(RTCIceProtocol.udp, addr, (ushort)rtpPort, RTCIceCandidateType.host);
205+
pc.addLocalIceCandidate(publicIPv4Candidate);
206+
}
207+
}
208+
209+
SDP offerSDP = SDP.ParseSDPDescription(offer.sdp);
210+
211+
if (offerSDP.Media.Any(x => x.Media == SDPMediaTypesEnum.audio))
212+
{
213+
MediaStreamTrack audioTrack = new MediaStreamTrack(SDPWellKnownMediaFormatsEnum.PCMU);
214+
pc.addTrack(audioTrack);
215+
}
216+
217+
if (offerSDP.Media.Any(x => x.Media == SDPMediaTypesEnum.video))
218+
{
219+
MediaStreamTrack videoTrack = new MediaStreamTrack(new VideoFormat(VideoCodecsEnum.VP8, VP8_PAYLOAD_ID));
220+
pc.addTrack(videoTrack);
221+
}
222+
223+
pc.OnRtpPacketReceived += (IPEndPoint rep, SDPMediaTypesEnum media, RTPPacket rtpPkt) =>
224+
{
225+
pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType);
226+
};
227+
228+
pc.OnTimeout += (mediaType) => logger.LogWarning($"Timeout for {mediaType}.");
229+
pc.oniceconnectionstatechange += (state) => logger.LogInformation($"ICE connection state changed to {state}.");
230+
pc.onsignalingstatechange += () => logger.LogInformation($"Signaling state changed to {pc.signalingState}.");
231+
pc.onconnectionstatechange += (state) =>
232+
{
233+
logger.LogInformation($"Peer connection state changed to {state}.");
234+
if (state == RTCPeerConnectionState.failed)
235+
{
236+
pc.Close("ice failure");
237+
}
238+
};
239+
240+
pc.ondatachannel += (dc) =>
241+
{
242+
logger.LogInformation($"Data channel opened for label {dc.label}, stream ID {dc.id}.");
243+
dc.onmessage += (rdc, proto, data) =>
244+
{
245+
logger.LogInformation($"Data channel got message: {Encoding.UTF8.GetString(data)}");
246+
rdc.send(Encoding.UTF8.GetString(data));
247+
};
248+
};
249+
250+
var setResult = pc.setRemoteDescription(offer);
251+
if (setResult == SetDescriptionResultEnum.OK)
252+
{
253+
var answer = pc.createAnswer();
254+
await pc.setLocalDescription(answer);
255+
256+
logger.LogTrace($"SDP answer created.");
257+
logger.LogTrace(answer.sdp);
258+
259+
return pc;
260+
}
261+
else
262+
{
263+
logger.LogWarning($"Failed to set remote description {setResult}.");
264+
return null;
265+
}
266+
}
267+
}
268+
}

0 commit comments

Comments
 (0)