Skip to content

Commit 1e651ac

Browse files
authored
Added some SIP demos tuned for k8s. Added some extra classes to help ith NAT & ICE servers (#1418)
* Added some SIP demos tuned for k8s. Added some extra classes to help with NAT & ICE servers. * Put back namespace to help with PR comparison. * Refactored RtpIceChannel to use new IceServerResolver class. * Demo checks. * Fixed typo.
1 parent cf021ac commit 1e651ac

File tree

19 files changed

+1073
-233
lines changed

19 files changed

+1073
-233
lines changed

examples/SIPExamples/SIPCallServer/readme-docker.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

examples/SIPExamples/SIPCallServer/Dockerfile renamed to examples/SIPExamples/SIPCloudCallServer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ EXPOSE 5060/udp
2424
EXPOSE 5061/tcp
2525

2626
# Run the server
27-
ENTRYPOINT ["dotnet", "SIPCallServer.dll"]
27+
ENTRYPOINT ["dotnet", "SIPCloudCallServer.dll"]
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
//-----------------------------------------------------------------------------
2+
// Filename: Program.cs
3+
//
4+
// Description: An example SIP server program to accept and initiate calls.
5+
// This example is derived from teh SIPCallServer example and is tuned to use
6+
// in a Kuberenetes cluster.
7+
//
8+
// Author(s):
9+
// Aaron Clauson (aaron@sipsorcery.com)
10+
//
11+
// History:
12+
// 25 May 2025 Aaron Clauson Created, Dublin, Ireland.
13+
//
14+
// License:
15+
// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file.
16+
//-----------------------------------------------------------------------------
17+
18+
using System;
19+
using System.Collections.Concurrent;
20+
using System.Collections.Generic;
21+
using System.Net;
22+
using System.Security.Cryptography.X509Certificates;
23+
using System.Threading;
24+
using System.Threading.Tasks;
25+
using Microsoft.Extensions.Logging;
26+
using Microsoft.Extensions.Logging.Abstractions;
27+
using Serilog;
28+
using Serilog.Extensions.Logging;
29+
using SIPSorcery.Media;
30+
using SIPSorcery.Net;
31+
using SIPSorcery.SIP;
32+
using SIPSorcery.SIP.App;
33+
using SIPSorceryMedia.Abstractions;
34+
35+
namespace SIPSorcery
36+
{
37+
class Program
38+
{
39+
private const string SIP_CONTACT_HOST_ENV_VAR = "SIP_CONTACT_HOST";
40+
private const string RTP_PORT_ENV_VAR = "RTP_PORT";
41+
private const string STUN_URL_ENV_VAR = "STUN_URL";
42+
43+
private static int SIP_LISTEN_PORT = 5060;
44+
private static int SIPS_LISTEN_PORT = 5061;
45+
private static string SIPS_CERTIFICATE_PATH = "localhost.pfx";
46+
47+
private static Microsoft.Extensions.Logging.ILogger Log = NullLogger.Instance;
48+
49+
private static SIPTransport _sipTransport;
50+
51+
/// <summary>
52+
/// Keeps track of the current active calls. It includes both received and placed calls.
53+
/// </summary>
54+
private static ConcurrentDictionary<string, SIPUserAgent> _calls = new ConcurrentDictionary<string, SIPUserAgent>();
55+
56+
private static int _rtpPort = 0;
57+
58+
private static IPAddress _publicIpAddress = null;
59+
60+
static void Main()
61+
{
62+
Console.WriteLine("SIPSorcery SIP Call Server example.");
63+
64+
Log = AddConsoleLogger();
65+
66+
// Set up a default SIP transport.
67+
_sipTransport = new SIPTransport();
68+
69+
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(SIP_CONTACT_HOST_ENV_VAR)))
70+
{
71+
_sipTransport.ContactHost = Environment.GetEnvironmentVariable(SIP_CONTACT_HOST_ENV_VAR);
72+
}
73+
74+
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(RTP_PORT_ENV_VAR)))
75+
{
76+
int.TryParse(Environment.GetEnvironmentVariable(RTP_PORT_ENV_VAR), out _rtpPort);
77+
}
78+
79+
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(STUN_URL_ENV_VAR)))
80+
{
81+
_publicIpAddress = STUNClient.GetPublicIPAddress(Environment.GetEnvironmentVariable(STUN_URL_ENV_VAR));
82+
}
83+
84+
_sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT)));
85+
_sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.IPv6Any, SIP_LISTEN_PORT)));
86+
_sipTransport.AddSIPChannel(new SIPTCPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT)));
87+
var localhostCertificate = new X509Certificate2(SIPS_CERTIFICATE_PATH);
88+
_sipTransport.AddSIPChannel(new SIPTLSChannel(localhostCertificate, new IPEndPoint(IPAddress.Any, SIPS_LISTEN_PORT)));
89+
// If it's desired to listen on a single IP address use the equivalent of:
90+
//_sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Parse("192.168.11.50"), SIP_LISTEN_PORT)));
91+
_sipTransport.EnableTraceLogs();
92+
93+
_sipTransport.SIPTransportRequestReceived += OnRequest;
94+
95+
// Ctrl-c will gracefully exit the call at any point.
96+
ManualResetEvent exitMre = new ManualResetEvent(false);
97+
Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e)
98+
{
99+
e.Cancel = true;
100+
exitMre.Set();
101+
};
102+
103+
// Wait for a signal saying the call failed, was cancelled with ctrl-c or completed.
104+
exitMre.WaitOne();
105+
106+
Log.LogInformation("Exiting...");
107+
108+
if (_sipTransport != null)
109+
{
110+
Log.LogInformation("Shutting down SIP transport...");
111+
_sipTransport.Shutdown();
112+
}
113+
}
114+
115+
/// <summary>
116+
/// Example of how to create a basic RTP session object and hook up the event handlers.
117+
/// </summary>
118+
/// <param name="ua">The user agent the RTP session is being created for.</param>
119+
/// <param name="dst">THe destination specified on an incoming call. Can be used to
120+
/// set the audio source.</param>
121+
/// <returns>A new RTP session object.</returns>
122+
private static VoIPMediaSession CreateRtpSession(SIPUserAgent ua, string dst, int bindPort)
123+
{
124+
List<AudioCodecsEnum> codecs = new List<AudioCodecsEnum> { AudioCodecsEnum.PCMU, AudioCodecsEnum.PCMA, AudioCodecsEnum.G722 };
125+
126+
var audioSource = AudioSourcesEnum.SineWave;
127+
if (string.IsNullOrEmpty(dst) || !Enum.TryParse(dst, out audioSource))
128+
{
129+
audioSource = AudioSourcesEnum.Music;
130+
}
131+
132+
Log.LogInformation($"RTP audio session source set to {audioSource}.");
133+
134+
AudioExtrasSource audioExtrasSource = new AudioExtrasSource(new AudioEncoder(), new AudioSourceOptions { AudioSource = audioSource });
135+
audioExtrasSource.RestrictFormats(formats => codecs.Contains(formats.Codec));
136+
var rtpAudioSession = new VoIPMediaSession(new MediaEndPoints { AudioSource = audioExtrasSource }, bindPort: bindPort);
137+
rtpAudioSession.AcceptRtpFromAny = true;
138+
139+
// Wire up the event handler for RTP packets received from the remote party.
140+
rtpAudioSession.OnRtpPacketReceived += (ep, type, rtp) => OnRtpPacketReceived(ua, ep, type, rtp);
141+
142+
rtpAudioSession.OnTimeout += (mediaType) =>
143+
{
144+
if (ua?.Dialogue != null)
145+
{
146+
Log.LogWarning($"RTP timeout on call with {ua.Dialogue.RemoteTarget}, hanging up.");
147+
}
148+
else
149+
{
150+
Log.LogWarning($"RTP timeout on incomplete call, closing RTP session.");
151+
}
152+
153+
ua.Hangup();
154+
};
155+
156+
return rtpAudioSession;
157+
}
158+
159+
/// <summary>
160+
/// Event handler for receiving RTP packets.
161+
/// </summary>
162+
/// <param name="ua">The SIP user agent associated with the RTP session.</param>
163+
/// <param name="type">The media type of the RTP packet (audio or video).</param>
164+
/// <param name="rtpPacket">The RTP packet received from the remote party.</param>
165+
private static void OnRtpPacketReceived(SIPUserAgent ua, IPEndPoint remoteEp, SDPMediaTypesEnum type, RTPPacket rtpPacket)
166+
{
167+
// The raw audio data is available in rtpPacket.Payload.
168+
//Log.LogTrace($"OnRtpPacketReceived from {remoteEp}.");
169+
}
170+
171+
/// <summary>
172+
/// Event handler for receiving a DTMF tone.
173+
/// </summary>
174+
/// <param name="ua">The user agent that received the DTMF tone.</param>
175+
/// <param name="key">The DTMF tone.</param>
176+
/// <param name="duration">The duration in milliseconds of the tone.</param>
177+
private static void OnDtmfTone(SIPUserAgent ua, byte key, int duration)
178+
{
179+
string callID = ua.Dialogue.CallId;
180+
Log.LogInformation($"Call {callID} received DTMF tone {key}, duration {duration}ms.");
181+
}
182+
183+
/// <summary>
184+
/// Because this is a server user agent the SIP transport must start listening for client user agents.
185+
/// </summary>
186+
private static async Task OnRequest(SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest)
187+
{
188+
try
189+
{
190+
if (sipRequest.Header.From != null &&
191+
sipRequest.Header.From.FromTag != null &&
192+
sipRequest.Header.To != null &&
193+
sipRequest.Header.To.ToTag != null)
194+
{
195+
// This is an in-dialog request that will be handled directly by a user agent instance.
196+
}
197+
else if (sipRequest.Method == SIPMethodsEnum.INVITE)
198+
{
199+
Log.LogInformation($"Incoming call request: {localSIPEndPoint}<-{remoteEndPoint} {sipRequest.URI}.");
200+
201+
SIPUserAgent ua = new SIPUserAgent(_sipTransport, null);
202+
ua.OnCallHungup += OnHangup;
203+
ua.ServerCallCancelled += (uas, cancelReq) => Log.LogDebug("Incoming call cancelled by remote party.");
204+
ua.OnDtmfTone += (key, duration) => OnDtmfTone(ua, key, duration);
205+
ua.OnRtpEvent += (evt, hdr) => Log.LogDebug($"rtp event {evt.EventID}, duration {evt.Duration}, end of event {evt.EndOfEvent}, timestamp {hdr.Timestamp}, marker {hdr.MarkerBit}.");
206+
//ua.OnTransactionTraceMessage += (tx, msg) => Log.LogDebug($"uas tx {tx.TransactionId}: {msg}");
207+
ua.ServerCallRingTimeout += (uas) =>
208+
{
209+
Log.LogWarning($"Incoming call timed out in {uas.ClientTransaction.TransactionState} state waiting for client ACK, terminating.");
210+
ua.Hangup();
211+
};
212+
var uas = ua.AcceptCall(sipRequest);
213+
var rtpSession = CreateRtpSession(ua, sipRequest.URI.User, _rtpPort);
214+
215+
await ua.Answer(uas, rtpSession, _publicIpAddress);
216+
217+
if (ua.IsCallActive)
218+
{
219+
await rtpSession.Start();
220+
_calls.TryAdd(ua.Dialogue.CallId, ua);
221+
}
222+
}
223+
else if (sipRequest.Method == SIPMethodsEnum.BYE)
224+
{
225+
SIPResponse byeResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.CallLegTransactionDoesNotExist, null);
226+
await _sipTransport.SendResponseAsync(byeResponse);
227+
}
228+
else if (sipRequest.Method == SIPMethodsEnum.SUBSCRIBE)
229+
{
230+
SIPResponse notAllowededResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null);
231+
await _sipTransport.SendResponseAsync(notAllowededResponse);
232+
}
233+
else if (sipRequest.Method == SIPMethodsEnum.OPTIONS || sipRequest.Method == SIPMethodsEnum.REGISTER)
234+
{
235+
SIPResponse optionsResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null);
236+
await _sipTransport.SendResponseAsync(optionsResponse);
237+
}
238+
}
239+
catch (Exception reqExcp)
240+
{
241+
Log.LogWarning($"Exception handling {sipRequest.Method}. {reqExcp.Message}");
242+
}
243+
}
244+
245+
/// <summary>
246+
/// Remove call from the active calls list.
247+
/// </summary>
248+
/// <param name="dialogue">The dialogue that was hungup.</param>
249+
private static void OnHangup(SIPDialogue dialogue)
250+
{
251+
if (dialogue != null)
252+
{
253+
string callID = dialogue.CallId;
254+
if (_calls.ContainsKey(callID))
255+
{
256+
if (_calls.TryRemove(callID, out var ua))
257+
{
258+
// This app only uses each SIP user agent once so here the agent is
259+
// explicitly closed to prevent is responding to any new SIP requests.
260+
ua.Close();
261+
}
262+
}
263+
}
264+
}
265+
266+
/// <summary>
267+
/// Adds a console logger. Can be omitted if internal SIPSorcery debug and warning messages are not required.
268+
/// </summary>
269+
private static Microsoft.Extensions.Logging.ILogger AddConsoleLogger()
270+
{
271+
var serilogLogger = new LoggerConfiguration()
272+
.Enrich.FromLogContext()
273+
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
274+
.WriteTo.Console()
275+
.CreateLogger();
276+
var factory = new SerilogLoggerFactory(serilogLogger);
277+
SIPSorcery.LogFactory.Set(factory);
278+
return factory.CreateLogger<Program>();
279+
}
280+
}
281+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Remove="local-nuget\**" />
10+
<EmbeddedResource Remove="local-nuget\**" />
11+
<None Remove="local-nuget\**" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<None Remove="Dockerfile" />
16+
<None Remove="readme-docker.txt" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
21+
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
22+
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
23+
<PackageReference Include="SIPSorcery" Version="8.0.19-pre" />
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<None Update="localhost.pfx">
28+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
29+
</None>
30+
</ItemGroup>
31+
32+
</Project>
3.95 KB
Binary file not shown.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
C:\dev\sipsorcery\src> dotnet pack SIPSorcery.csproj --configuration Debug --output c:\dev\local-nuget
2+
3+
docker build -t sipsorcery/sipcloudcallserver:0.1 .
4+
docker push sipsorcery/sipcloudcallserver:0.1

0 commit comments

Comments
 (0)