Skip to content
This repository was archived by the owner on Feb 26, 2026. It is now read-only.

Commit c17b8f1

Browse files
authored
Merge pull request #8 from sipsorcery-org/sip-to-openai
SIP to OpenAI
2 parents 0fdc12b + 7931350 commit c17b8f1

File tree

6 files changed

+494
-1
lines changed

6 files changed

+494
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Several sample applications demonstrating different scenarios are available in t
167167
- **LocalFunctions** – showcases the local function calling feature.
168168
- **GetPaid** – extends local functions to simulate payment requests.
169169
- **GetStartedDI** – illustrates using the library with .NET Dependency Injection.
170+
- **GetStartedSIP** – demonstrates how to create a SIP-to-OpenAI WebRTC gateway that receives SIP (VoIP) calls and bridges to OpenAI.
170171
- **ASP.NET Get Started** – ASP.NET application bridging a browser WebRTC client to OpenAI.
171172
- **ASP.NET Local Function** – ASP.NET application that builds on the Get Started example and adds a local function to tailor OpenAI responses.
172173

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<LangVersion>12.0</LangVersion>
7+
<Nullable>enable</Nullable>
8+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
9+
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
10+
</PropertyGroup>
11+
12+
<PropertyGroup>
13+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
18+
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
19+
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
20+
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\..\src\SIPSorcery.OpenAI.WebRTC.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<Folder Include="Properties\" />
29+
</ItemGroup>
30+
31+
</Project>

examples/GetStartedSIP/Program.cs

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
//-----------------------------------------------------------------------------
2+
// Filename: Program.cs
3+
//
4+
// Description: An example showing how to use SIPSorcery with OpenAI's WebRTC endpoint.
5+
// This demo shows the concept of how you could bridge SIP calls to OpenAI, though
6+
// a complete implementation would require additional SIP handling logic.
7+
//
8+
// Usage:
9+
// set OPENAI_API_KEY=your_openai_key
10+
// dotnet run
11+
//
12+
// Author(s):
13+
// Aaron Clauson (aaron@sipsorcery.com)
14+
//
15+
// History:
16+
// 09 Aug 2025 Aaron Clauson Created, Dublin, Ireland.
17+
//
18+
// License:
19+
// BSD 3-Clause "New" or "Revised" License and the additional
20+
// BDS BY-NC-SA restriction, see included LICENSE.md file.
21+
//-----------------------------------------------------------------------------
22+
23+
using Microsoft.Extensions.Logging;
24+
using Serilog;
25+
using Serilog.Extensions.Logging;
26+
using SIPSorcery.Net;
27+
using SIPSorcery.OpenAIWebRTC;
28+
using SIPSorcery.OpenAIWebRTC.Models;
29+
using SIPSorcery.SIP;
30+
using SIPSorcery.SIP.App;
31+
using SIPSorceryMedia.Abstractions;
32+
using System;
33+
using System.Collections.Concurrent;
34+
using System.Net;
35+
using System.Threading.Tasks;
36+
37+
namespace demo;
38+
39+
record SIPToOpenAiCall(SIPUserAgent ua, RTPSession voip, WebRTCEndPoint? webrtc);
40+
41+
class Program
42+
{
43+
private static int SIP_LISTEN_PORT = 5060;
44+
45+
/// <summary>
46+
/// Keeps track of the current active calls. It includes both received and placed calls.
47+
/// </summary>
48+
private static ConcurrentDictionary<string, SIPToOpenAiCall> _calls = new ConcurrentDictionary<string, SIPToOpenAiCall>();
49+
50+
static async Task Main()
51+
{
52+
Log.Logger = new LoggerConfiguration()
53+
.MinimumLevel.Debug()
54+
//.MinimumLevel.Verbose()
55+
.Enrich.FromLogContext()
56+
.WriteTo.Console()
57+
.CreateLogger();
58+
59+
var loggerFactory = new SerilogLoggerFactory(Log.Logger);
60+
SIPSorcery.LogFactory.Set(loggerFactory);
61+
62+
Log.Logger.Information("SIP-to-WebRTC OpenAI Demo Program");
63+
64+
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
65+
66+
if (string.IsNullOrWhiteSpace(openAiKey))
67+
{
68+
Log.Logger.Error("Please provide your OpenAI key as an environment variable. For example: set OPENAI_API_KEY=<your openai api key>");
69+
return;
70+
}
71+
72+
var logger = loggerFactory.CreateLogger<Program>();
73+
74+
SIPSorcery.LogFactory.Set(loggerFactory);
75+
var sipTransport = new SIPTransport();
76+
sipTransport.EnableTraceLogs();
77+
sipTransport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, SIP_LISTEN_PORT)));
78+
sipTransport.SIPTransportRequestReceived += (lep, rep, req) => OnRequest(lep, rep, req, sipTransport, openAiKey);
79+
80+
Console.WriteLine("Wait for ctrl-c to indicate user exit.");
81+
82+
var exitTcs = new TaskCompletionSource<object?>();
83+
Console.CancelKeyPress += (s, e) =>
84+
{
85+
e.Cancel = true;
86+
exitTcs.TrySetResult(null);
87+
};
88+
89+
await exitTcs.Task;
90+
}
91+
92+
/// <summary>
93+
/// Because this is a server user agent the SIP transport must start listening for client user agents.
94+
/// </summary>
95+
private static async Task OnRequest(SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPRequest sipRequest, SIPTransport sipTransport, string openAiKey)
96+
{
97+
try
98+
{
99+
if (sipRequest.Header.From != null &&
100+
sipRequest.Header.From.FromTag != null &&
101+
sipRequest.Header.To != null &&
102+
sipRequest.Header.To.ToTag != null)
103+
{
104+
// This is an in-dialog request that will be handled directly by a user agent instance.
105+
}
106+
else if (sipRequest.Method == SIPMethodsEnum.INVITE)
107+
{
108+
Log.Information($"Incoming call request: {localSIPEndPoint}<-{remoteEndPoint} {sipRequest.URI}.");
109+
110+
SIPUserAgent ua = new SIPUserAgent(sipTransport, null);
111+
ua.OnCallHungup += OnHangup;
112+
ua.ServerCallCancelled += (uas, cancelReq) => Log.Debug("Incoming call cancelled by remote party.");
113+
ua.OnDtmfTone += (key, duration) => OnDtmfTone(ua, key, duration);
114+
ua.OnRtpEvent += (evt, hdr) => Log.Debug($"rtp event {evt.EventID}, duration {evt.Duration}, end of event {evt.EndOfEvent}, timestamp {hdr.Timestamp}, marker {hdr.MarkerBit}.");
115+
//ua.OnTransactionTraceMessage += (tx, msg) => Log.LogDebug($"uas tx {tx.TransactionId}: {msg}");
116+
ua.ServerCallRingTimeout += (uas) =>
117+
{
118+
Log.Warning($"Incoming call timed out in {uas.ClientTransaction.TransactionState} state waiting for client ACK, terminating.");
119+
ua.Hangup();
120+
};
121+
122+
//bool wasMangled = false;
123+
//sipRequest.Body = SIPPacketMangler.MangleSDP(sipRequest.Body, remoteEndPoint.Address.ToString(), out wasMangled);
124+
//Log.LogDebug("INVITE was mangled=" + wasMangled + " remote=" + remoteEndPoint.Address.ToString() + ".");
125+
//sipRequest.Header.ContentLength = sipRequest.Body.Length;
126+
127+
var uas = ua.AcceptCall(sipRequest);
128+
var rtpSession = CreateRtpSession(ua);
129+
130+
// Insert a brief delay to allow testing of the "Ringing" progress response.
131+
// Without the delay the call gets answered before it can be sent.
132+
//await Task.Delay(500);
133+
134+
//if (!string.IsNullOrWhiteSpace(_publicIPAddress))
135+
//{
136+
// await ua.Answer(uas, rtpSession, IPAddress.Parse(_publicIPAddress));
137+
//}
138+
//else
139+
//{
140+
await ua.Answer(uas, rtpSession);
141+
//}
142+
143+
if (ua.IsCallActive)
144+
{
145+
await rtpSession.Start();
146+
_calls.TryAdd(ua.Dialogue.CallId, new SIPToOpenAiCall(ua, rtpSession, null));
147+
148+
Log.Information($"Call answered, call ID {ua.Dialogue.CallId}.");
149+
150+
// Create a WebRTC session to OpenAI.
151+
await CreateOpenAIWebRTCSession(new SerilogLoggerFactory(Log.Logger), openAiKey, ua.Dialogue.CallId, rtpSession);
152+
}
153+
}
154+
else if (sipRequest.Method == SIPMethodsEnum.BYE)
155+
{
156+
SIPResponse byeResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.CallLegTransactionDoesNotExist, null);
157+
await sipTransport.SendResponseAsync(byeResponse);
158+
}
159+
else if (sipRequest.Method == SIPMethodsEnum.SUBSCRIBE)
160+
{
161+
SIPResponse notAllowededResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.MethodNotAllowed, null);
162+
await sipTransport.SendResponseAsync(notAllowededResponse);
163+
}
164+
else if (sipRequest.Method == SIPMethodsEnum.OPTIONS || sipRequest.Method == SIPMethodsEnum.REGISTER)
165+
{
166+
SIPResponse optionsResponse = SIPResponse.GetResponse(sipRequest, SIPResponseStatusCodesEnum.Ok, null);
167+
await sipTransport.SendResponseAsync(optionsResponse);
168+
}
169+
}
170+
catch (Exception reqExcp)
171+
{
172+
Log.Warning($"Exception handling {sipRequest.Method}. {reqExcp.Message}");
173+
}
174+
}
175+
176+
/// <summary>
177+
/// Example of how to create a basic RTP session object and hook up the event handlers.
178+
/// </summary>
179+
/// <param name="ua">The user agent the RTP session is being created for.</param>
180+
/// <returns>A new RTP session object.</returns>
181+
private static RTPSession CreateRtpSession(SIPUserAgent ua)
182+
{
183+
var rtpSession = new RTPSession(false, false, false);
184+
rtpSession.addTrack(new MediaStreamTrack(AudioCommonlyUsedFormats.OpusWebRTC));
185+
rtpSession.AcceptRtpFromAny = true;
186+
187+
// Wire up the event handler for RTP packets received from the remote party.
188+
//rtpSession.OnRtpPacketReceived += (ep, type, rtp) => OnRtpPacketReceived(ua, ep, type, rtp);
189+
rtpSession.OnTimeout += (mediaType) =>
190+
{
191+
if (ua?.Dialogue != null)
192+
{
193+
Log.Warning($"RTP timeout on call with {ua.Dialogue.RemoteTarget}, hanging up.");
194+
}
195+
else
196+
{
197+
Log.Warning($"RTP timeout on incomplete call, closing RTP session.");
198+
}
199+
200+
ua?.Hangup();
201+
};
202+
203+
return rtpSession;
204+
}
205+
206+
private static async Task CreateOpenAIWebRTCSession(ILoggerFactory loggerFactory, string openAiKey, string sipCallID, RTPSession rtpSession)
207+
{
208+
var logger = loggerFactory.CreateLogger<WebRTCEndPoint>();
209+
var webrtcEndPoint = new WebRTCEndPoint(openAiKey, logger);
210+
211+
if (_calls.TryGetValue(sipCallID, out var existing))
212+
{
213+
var updated = existing with { webrtc = webrtcEndPoint };
214+
_calls.TryUpdate(sipCallID, updated, existing);
215+
}
216+
217+
var negotiateConnectResult = await webrtcEndPoint.StartConnect();
218+
219+
if (negotiateConnectResult.IsLeft)
220+
{
221+
Log.Logger.Error($"Failed to negotiation connection to OpenAI Realtime WebRTC endpoint: {negotiateConnectResult.LeftAsEnumerable().First()}");
222+
return;
223+
}
224+
225+
webrtcEndPoint.OnPeerConnectionConnected += () =>
226+
{
227+
Log.Logger.Information("WebRTC peer connection established.");
228+
229+
webrtcEndPoint.ConnectRTPSession(rtpSession, AudioCommonlyUsedFormats.OpusWebRTC);
230+
231+
var voice = RealtimeVoicesEnum.shimmer;
232+
233+
// Optionally send a session update message to adjust the session parameters.
234+
var sessionUpdateResult = webrtcEndPoint.DataChannelMessenger.SendSessionUpdate(
235+
voice,
236+
"Keep it short.",
237+
transcriptionModel: TranscriptionModelEnum.Whisper1);
238+
239+
if (sessionUpdateResult.IsLeft)
240+
{
241+
Log.Logger.Error($"Failed to send session update message: {sessionUpdateResult.LeftAsEnumerable().First()}");
242+
}
243+
244+
// Trigger the conversation by sending a response create message.
245+
var result = webrtcEndPoint.DataChannelMessenger.SendResponseCreate(voice, "Say Hi!");
246+
if (result.IsLeft)
247+
{
248+
Log.Logger.Error($"Failed to send response create message: {result.LeftAsEnumerable().First()}");
249+
}
250+
};
251+
252+
webrtcEndPoint.OnDataChannelMessage += (dc, message) =>
253+
{
254+
var log = message switch
255+
{
256+
RealtimeServerEventSessionUpdated sessionUpdated => $"Session updated: {sessionUpdated.ToJson()}",
257+
//RealtimeServerEventConversationItemInputAudioTranscriptionDelta inputDelta => $"ME ⌛: {inputDelta.Delta?.Trim()}",
258+
RealtimeServerEventConversationItemInputAudioTranscriptionCompleted inputTranscript => $"ME ✅: {inputTranscript.Transcript?.Trim()}",
259+
//RealtimeServerEventResponseAudioTranscriptDelta responseDelta => $"AI ⌛: {responseDelta.Delta?.Trim()}",
260+
RealtimeServerEventResponseAudioTranscriptDone responseTranscript => $"AI ✅: {responseTranscript.Transcript?.Trim()}",
261+
//_ => $"Received {message.Type} -> {message.GetType().Name}"
262+
_ => string.Empty
263+
};
264+
265+
if (log != string.Empty)
266+
{
267+
Log.Information(log);
268+
}
269+
};
270+
}
271+
272+
/// <summary>
273+
/// Event handler for receiving RTP packets.
274+
/// </summary>
275+
/// <param name="ua">The SIP user agent associated with the RTP session.</param>
276+
/// <param name="type">The media type of the RTP packet (audio or video).</param>
277+
/// <param name="rtpPacket">The RTP packet received from the remote party.</param>
278+
private static void OnRtpPacketReceived(SIPUserAgent ua, IPEndPoint remoteEp, SDPMediaTypesEnum type, RTPPacket rtpPacket)
279+
{
280+
// The raw audio data is available in rtpPacket.Payload.
281+
Log.Verbose($"OnRtpPacketReceived from {remoteEp}.");
282+
}
283+
284+
/// <summary>
285+
/// Event handler for receiving a DTMF tone.
286+
/// </summary>
287+
/// <param name="ua">The user agent that received the DTMF tone.</param>
288+
/// <param name="key">The DTMF tone.</param>
289+
/// <param name="duration">The duration in milliseconds of the tone.</param>
290+
private static void OnDtmfTone(SIPUserAgent ua, byte key, int duration)
291+
{
292+
string callID = ua.Dialogue.CallId;
293+
Log.Information($"Call {callID} received DTMF tone {key}, duration {duration}ms.");
294+
}
295+
296+
/// <summary>
297+
/// Remove call from the active calls list.
298+
/// </summary>
299+
/// <param name="dialogue">The dialogue that was hungup.</param>
300+
private static void OnHangup(SIPDialogue dialogue)
301+
{
302+
if (dialogue != null)
303+
{
304+
string callID = dialogue.CallId;
305+
if (_calls.ContainsKey(callID))
306+
{
307+
if (_calls.TryRemove(callID, out var call))
308+
{
309+
Log.Information($"Call {callID} removed.");
310+
311+
// This app only uses each SIP user agent once so here the agent is
312+
// explicitly closed to prevent is responding to any new SIP requests.
313+
call.ua.Close();
314+
call.webrtc?.Close();
315+
}
316+
}
317+
}
318+
}
319+
}

0 commit comments

Comments
 (0)