public async Task <string> HandleUpdatedRemoteOffer(string sdp) { Logger.Debug("PeerChannel", "HandleUpdatedRemoteOffer"); var sdpInit = new RTCSessionDescriptionInit() { Type = RTCSdpType.Offer, Sdp = sdp, }; Logger.Debug("PeerChannel", "SetRemoteOffer - SetRemoteDescription"); await Conn.SetRemoteDescription(new RTCSessionDescription(sdpInit)); var opts = new RTCAnswerOptions(); Logger.Debug("PeerChannel", "SetRemoteOffer - CreateAnswer"); var answer = await Conn.CreateAnswer(opts); Logger.Debug("PeerChannel", answer.Sdp); await Conn.SetLocalDescription(answer); Logger.Debug("PeerChannel", "local description set"); return(answer.Sdp); }
public async Task <IActionResult> Put(string from, string to, [FromBody] RTCSessionDescriptionInit sdp) { if (string.IsNullOrEmpty(to) || string.IsNullOrEmpty(from) || sdp == null || sdp.sdp == null) { _logger.LogWarning($"WebRTC signal controller PUT sdp request had invalid parameters."); return(BadRequest()); } if (sdp.type == RTCSdpType.offer) { await ExpireExisting(from, to); } WebRTCSignal sdpSignal = new WebRTCSignal { ID = Guid.NewGuid().ToString(), To = to, From = from, SignalType = WebRTCSignalTypesEnum.sdp.ToString(), Signal = sdp.toJSON(), Inserted = DateTime.UtcNow.ToString("o") }; _context.WebRTCSignals.Add(sdpSignal); await _context.SaveChangesAsync(); return(Ok()); }
private void AddActionsToSignalingWebSocket() { signaling.OnMessage += (sender, message) => { logger.LogDebug($"Received message: {message.Data}"); if (message.Data == "{\"data\":{\"getRemoteMedia\":true}}") { Negotiate(); return; } if (message.Data == "{\"data\":{\"candidate\":null}}") { return; } string correct_message = ConvertString(message.Data); logger.LogDebug($"After nesting:{correct_message}"); if (RTCIceCandidateInit.TryParse(correct_message, out var IceCandidate)) { logger.LogDebug($"Got remote candidate: {correct_message}"); pc.addIceCandidate(IceCandidate); } else if (RTCSessionDescriptionInit.TryParse(correct_message, out var SDP)) { logger.LogDebug($"Setting SDP: {correct_message}"); var result = pc.setRemoteDescription(SDP); if (result != SetDescriptionResultEnum.OK) { logger.LogWarning($"Failed to set remote description, {result}."); pc.Close("failed to set remote description"); } } }; }
public void JsonRoundtripUnitTest() { RTCPeerConnection pcSrc = new RTCPeerConnection(null); var videoTrackSrc = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List <SDPAudioVideoMediaFormat> { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }); pcSrc.addTrack(videoTrackSrc); var offer = pcSrc.createOffer(new RTCOfferOptions()); Assert.NotNull(offer.toJSON()); logger.LogDebug($"offer: {offer.toJSON()}"); var parseResult = RTCSessionDescriptionInit.TryParse(offer.toJSON(), out var init); Assert.True(parseResult); Assert.Equal(RTCSdpType.offer, init.type); Assert.NotNull(init.sdp); SDP sdp = SDP.ParseSDPDescription(init.sdp); Assert.Equal(0, sdp.Version); }
public Task SetRemoteDescription(RTCSessionDescriptionInit sessionDescription) { var tcs = new TaskCompletionSource <object>(); ((Webrtc.PeerConnection)NativeObject).SetRemoteDescription( new SdpObserverProxy(tcs), sessionDescription.ToNative()); return(tcs.Task); }
internal static WebRTCSessionDescription FromSessionDescription(RTCSessionDescriptionInit sessionDescription) { return(new WebRTCSessionDescription() { InternalType = sessionDescription.type, Sdp = sessionDescription.sdp }); }
/// <summary> /// Helper function. Serializes Session Description. /// </summary> /// <param name="description"></param> /// <returns></returns> private string SerializeSessionDescription(RTCSessionDescription description) { var init = new RTCSessionDescriptionInit() { Sdp = description.Sdp, Type = description.SdpType }; return(JsonConvert.SerializeObject(init)); }
public void ParseJavascriptJsonTest() { string json = "{\"type\":\"answer\",\"sdp\":\"v=0\r\no=- 3619871509827895381 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=msid-semantic: WMS\r\nm=video 9 UDP/TLS/RTP/SAVP 100\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:XqpN\r\na=ice-pwd:VOXKGB0AIf10Kqtv+zcQKgLF\r\na=ice-options:trickle\r\na=fingerprint:sha-256 D4:6E:F7:FD:B3:4F:4D:3D:3A:B3:92:2C:CC:F6:4E:46:88:B7:01:E9:B7:E1:77:03:8E:BB:AA:DC:26:1B:9D:2E\r\na=setup:active\r\na=mid:0\r\na=recvonly\r\na=rtcp-mux\r\na=rtpmap:100 VP8/90000\r\n\"}"; var parseResult = RTCSessionDescriptionInit.TryParse(json, out var init); Assert.True(parseResult); Assert.Equal(RTCSdpType.answer, init.type); Assert.NotNull(init.sdp); }
public async Task <ActionResult> Answer(string id, [FromBody] RTCSessionDescriptionInit answer) { _logger.LogDebug($"Echo controller posting answer to {_echoTestRestUrl}/answer/{id}."); //_logger.LogDebug($"Answer={answer}"); HttpClient client = new HttpClient(); var postResp = await client.PostAsync($"{_echoTestRestUrl}/answer/{id}", new StringContent(answer.toJSON(), Encoding.UTF8, REST_CONTENT_TYPE)); _logger.LogDebug($"Echo controller post answer response {postResp.StatusCode}:{postResp.ReasonPhrase}."); return(postResp.IsSuccessStatusCode ? Ok() : BadRequest(postResp.ReasonPhrase)); }
public async Task <string> HandleInitialRemoteOffer(string sdp) { Logger.Debug("PeerChannel", "HandleInitialRemoteOffer"); if (Conn == null) { Logger.Debug("PeerChannel", "should initialize beforehand"); return(string.Empty); } var sdpInit = new RTCSessionDescriptionInit() { Type = RTCSdpType.Offer, Sdp = sdp, }; Logger.Debug("PeerChannel", "SetRemoteOffer - SetRemoteDescription"); await Conn.SetRemoteDescription(new RTCSessionDescription(sdpInit)); if (mediaOption.VideoUpstreamEnabled) { Logger.Debug("PeerChannel", "video upstream setting"); if (screen != null) { await screen.StartCaptureAsync(); } SetupVideoTrack(); } if (mediaOption.AudioUpstreamEnabled) { Logger.Debug("PeerChannel", "audio upstream setting"); SetupAudioTrack(); } var opts = new RTCAnswerOptions(); Logger.Debug("PeerChannel", "SetRemoteOffer - CreateAnswer"); var answer = await Conn.CreateAnswer(opts); Logger.Debug("PeerChannel", answer.Sdp); await Conn.SetLocalDescription(answer); Logger.Debug("PeerChannel", "local description set"); return(answer.Sdp); }
public async Task SetRemoteDescription(string id, RTCSessionDescriptionInit description) { if (!_peerConnections.TryGetValue(id, out var pc)) { throw new ApplicationException("No peer connection is available for the specified id."); } else { _logger.LogDebug("Answer SDP: " + description.sdp); await pc.setRemoteDescription(description); } }
public Task SetRemoteDescription(RTCSessionDescriptionInit sessionDescription) { var tcs = new TaskCompletionSource <object>(); ((Webrtc.RTCPeerConnection)NativeObject).SetRemoteDescription( sessionDescription.ToNative(), (nsError) => { if (nsError != null) { tcs.SetException(new Exception($"{nsError.LocalizedDescription}")); } tcs.SetResult(null); }); return(tcs.Task); }
public IActionResult SetAnswer(string id, [FromBody] RTCSessionDescriptionInit answer) { _logger.LogDebug($"SetAnswer {id} {answer?.type} {answer?.sdp}."); if (string.IsNullOrWhiteSpace(id)) { return(BadRequest("The id cannot be empty in SetAnswer.")); } else if (string.IsNullOrWhiteSpace(answer?.sdp)) { return(BadRequest("The SDP answer cannot be empty in SetAnswer.")); } _webRTCServer.SetRemoteDescription(id, answer); return(Ok()); }
public async Task <ICallInfo> PlaceCallAsync(CallConfiguration config) { Debug.Assert(_peerId == -1); if (PeerConnection != null) { Debug.WriteLine("[Error] We only support connection to one peer at a time."); return(null); } if (CreatePeerConnection()) { string selectedAudioCodecName = (string)_localSettings.Values["SelectedAudioCodecName"]; string selectedVideoCodecName = (string)_localSettings.Values["SelectedVideoCodecName"]; _peerId = PeerId; var offerOptions = new RTCOfferOptions(); offerOptions.OfferToReceiveAudio = true; offerOptions.OfferToReceiveVideo = true; IRTCSessionDescription offer = await PeerConnection.CreateOffer(offerOptions); // Alter sdp to force usage of selected codecs string modifiedSdp = offer.Sdp; SdpUtils.SelectCodec(ref modifiedSdp, selectedAudioCodecName, "audio"); SdpUtils.SelectCodec(ref modifiedSdp, selectedVideoCodecName, "video"); RTCSessionDescriptionInit sdpInit = new RTCSessionDescriptionInit(); sdpInit.Sdp = modifiedSdp; sdpInit.Type = offer.SdpType; var modifiedOffer = new RTCSessionDescription(sdpInit); await PeerConnection.SetLocalDescription(modifiedOffer); Debug.WriteLine($"Sending offer: {modifiedOffer.Sdp}"); string jsonString = SdpToJsonString(modifiedOffer); CallInfo callInfo = new CallInfo(); callInfo.SetCall(new Call()); callInfo.SetSdp(modifiedSdp); callInfo.SetJsonString(jsonString); OnSendMessageToRemotePeer.Invoke(this, jsonString); return(callInfo); } return(null); }
private void DoProcessMessage(Message message) { switch (message.type) { case "Offer": // var offer = JsonUtility.FromJson<RTCSessionDescriptionInit>(message.args); if (RTCSessionDescriptionInit.TryParse(message.args, out RTCSessionDescriptionInit offer)) { Debug.Log($"Got remote SDP, type {offer.type}"); var result = rtcPeerConnection.setRemoteDescription(offer); if (result != SetDescriptionResultEnum.OK) { Debug.Log($"Failed to set remote description, {result}."); rtcPeerConnection.Close("Failed to set remote description"); } else { if (rtcPeerConnection.signalingState == RTCSignalingState.have_remote_offer) { var answerSdp = rtcPeerConnection.createAnswer(); rtcPeerConnection.setLocalDescription(answerSdp); Debug.Log($"Sending SDP answer"); Send("Offer", answerSdp.toJSON()); } } } break; case "IceCandidate": if (RTCIceCandidateInit.TryParse(message.args, out RTCIceCandidateInit candidate)) { Debug.Log($"Got remote Ice Candidate, uri {candidate.candidate}"); rtcPeerConnection.addIceCandidate(candidate); } break; } }
public async Task <ActionResult <string> > Offer([FromBody] RTCSessionDescriptionInit offer) { _logger.LogDebug($"Echo controller posting offer to {_echoTestRestUrl}/offer."); _logger.LogDebug($"Offer={offer}"); HttpClient client = new HttpClient(); var postResp = await client.PostAsync($"{_echoTestRestUrl}/offer", new StringContent(offer.toJSON(), Encoding.UTF8, REST_CONTENT_TYPE)); _logger.LogDebug($"Echo controller post offer response {postResp.StatusCode}:{postResp.ReasonPhrase}."); if (postResp.IsSuccessStatusCode) { var answer = await postResp.Content.ReadAsStringAsync(); _logger.LogDebug($"Answer: {answer}."); return(answer); } else { return(BadRequest(postResp.ReasonPhrase)); } }
private async static Task Offer(IHttpContext context, int pcTimeout) { var offer = await context.GetRequestDataAsync <RTCSessionDescriptionInit>(); var jsonOptions = new JsonSerializerOptions(); jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); var echoServer = new WebRTCEchoServer(_icePresets); var pc = await echoServer.GotOffer(offer); if (pc != null) { var answer = new RTCSessionDescriptionInit { type = RTCSdpType.answer, sdp = pc.localDescription.sdp.ToString() }; context.Response.ContentType = "application/json"; using (var responseStm = context.OpenResponseStream(false, false)) { await JsonSerializer.SerializeAsync(responseStm, answer, jsonOptions); } if (pcTimeout != 0) { logger.LogDebug($"Setting peer connection close timeout to {pcTimeout} seconds."); var timeout = new Timer((state) => { if (!pc.IsClosed) { logger.LogWarning("Test timed out."); pc.close(); } }, null, pcTimeout * 1000, Timeout.Infinite); pc.OnClosed += timeout.Dispose; } } }
/// <summary> /// This application spits out a lot of log messages. In an attempt to make command entry slightly more usable /// this method attempts to always write the current command input as the bottom line on the console output. /// </summary> private static async Task ProcessInput(CancellationTokenSource cts) { // Local function to write the current command in the process of being entered. Action <int, string> writeCommandPrompt = (lastPromptRow, cmd) => { // The cursor is already at the current row. if (Console.CursorTop == lastPromptRow) { // The command was corrected. Need to re-write the whole line. Console.SetCursorPosition(0, Console.CursorTop); Console.Write(new string(' ', Console.WindowWidth)); Console.SetCursorPosition(0, Console.CursorTop); Console.Write($"{COMMAND_PROMPT}{cmd}"); } else { // The cursor row has changed since the last input. Rewrite the prompt and command // on the current line. Console.Write($"{COMMAND_PROMPT}{cmd}"); } }; string command = null; int lastInputRow = Console.CursorTop; while (!cts.IsCancellationRequested) { var inKey = Console.ReadKey(true); if (inKey.Key == ConsoleKey.Enter) { if (command == null) { Console.WriteLine(); Console.Write(COMMAND_PROMPT); } else { // Attempt to execute the current command. switch (command.ToLower()) { case "c": // Close active peer connection. if (_peerConnection != null) { Console.WriteLine(); Console.WriteLine("Closing peer connection"); _peerConnection.Close("user initiated"); } break; case var x when x.StartsWith("cdc"): // Attempt to create a new data channel. if (_peerConnection != null) { (_, var label) = x.Split(" ", 2, StringSplitOptions.None); if (!string.IsNullOrWhiteSpace(label)) { Console.WriteLine(); Console.WriteLine($"Creating data channel for label {label}."); var dc = _peerConnection.createDataChannel(label, null); dc.onmessage += (msg) => logger.LogDebug($" data channel message received on {label}: {msg}"); } else { Console.WriteLine(); Console.WriteLine($"Send message command was in the wrong format. Needs to be: cdc <label>"); } } break; case var x when x.StartsWith("ldc"): // List data channels. if (_peerConnection != null) { if (_peerConnection.DataChannels.Count > 0) { Console.WriteLine(); foreach (var dc in _peerConnection.DataChannels) { Console.WriteLine($" data channel: label {dc.label}, stream ID {dc.id}, is open {dc.IsOpened}."); } } else { Console.WriteLine(); Console.WriteLine(" no data channels available."); } } break; case var x when x.StartsWith("sdc"): // Send data channel message. if (_peerConnection != null) { (_, var label, var msg) = x.Split(" ", 3, StringSplitOptions.None); if (!string.IsNullOrWhiteSpace(label) && !string.IsNullOrWhiteSpace(msg)) { Console.WriteLine(); Console.WriteLine($"Sending message on channel {label}: {msg}"); var dc = _peerConnection.DataChannels.FirstOrDefault(x => x.label == label && x.IsOpened); if (dc != null) { dc.send(msg); } else { Console.WriteLine($"No data channel was found for label {label}."); } } else { Console.WriteLine(); Console.WriteLine($"Send data channel message command was in the wrong format. Needs to be: sdc <label> <message>"); } } break; case "q": // Quit. Console.WriteLine(); Console.WriteLine("Quitting..."); cts.Cancel(); break; case "isalive": // Check responsiveness. Console.WriteLine(); Console.WriteLine("yep"); Console.Write(COMMAND_PROMPT); break; case var x when x.StartsWith("node"): (_, var sdpType, var myUser, string theirUser) = x.Split(" ", 4, StringSplitOptions.None); if (sdpType == "so") { _peerConnection = Createpc(null, _stunServer); var offerSdp = _peerConnection.createOffer(null); await _peerConnection.setLocalDescription(offerSdp); Console.WriteLine($"Our Offer:\n{offerSdp.sdp}"); var offerJson = JsonConvert.SerializeObject(offerSdp, new Newtonsoft.Json.Converters.StringEnumConverter()); var content = new StringContent(offerJson, Encoding.UTF8, "application/json"); var res = await _nodeDssclient.PostAsync($"{_nodeDssUri}data/{theirUser}", content); Console.WriteLine($"node-dss POST result {res.StatusCode}."); } else if (sdpType == "go") { var res = await _nodeDssclient.GetAsync($"{_nodeDssUri}data/{myUser}"); Console.WriteLine($"node-dss GET result {res.StatusCode}."); if (res.StatusCode == HttpStatusCode.OK) { var content = await res.Content.ReadAsStringAsync(); RTCSessionDescriptionInit offerInit = JsonConvert.DeserializeObject <RTCSessionDescriptionInit>(content); Console.WriteLine($"Remote offer:\n{offerInit.sdp}"); _peerConnection = Createpc(null, _stunServer); var setRes = _peerConnection.setRemoteDescription(offerInit); if (setRes != SetDescriptionResultEnum.OK) { // No point continuing. Something will need to change and then try again. _peerConnection.Close("failed to set remote sdp offer"); } else { var answer = _peerConnection.createAnswer(null); await _peerConnection.setLocalDescription(answer); Console.WriteLine($"Our answer:\n{answer.sdp}"); var answerJson = JsonConvert.SerializeObject(answer, new Newtonsoft.Json.Converters.StringEnumConverter()); var answerContent = new StringContent(answerJson, Encoding.UTF8, "application/json"); var postRes = await _nodeDssclient.PostAsync($"{_nodeDssUri}data/{theirUser}", answerContent); Console.WriteLine($"node-dss POST result {res.StatusCode}."); } } } else if (sdpType == "ga") { var res = await _nodeDssclient.GetAsync($"{_nodeDssUri}data/{myUser}"); Console.WriteLine($"node-dss GET result {res.StatusCode}."); if (res.StatusCode == HttpStatusCode.OK) { var content = await res.Content.ReadAsStringAsync(); RTCSessionDescriptionInit answerInit = JsonConvert.DeserializeObject <RTCSessionDescriptionInit>(content); Console.WriteLine($"Remote answer:\n{answerInit.sdp}"); var setRes = _peerConnection.setRemoteDescription(answerInit); if (setRes != SetDescriptionResultEnum.OK) { // No point continuing. Something will need to change and then try again. _peerConnection.Close("failed to set remote sdp answer"); } } } break; default: // Command not recognised. Console.WriteLine(); Console.WriteLine($"Unknown command: {command}"); Console.Write(COMMAND_PROMPT); break; } command = null; } } else if (inKey.Key == ConsoleKey.UpArrow) { // Convenience mechanism to get the current input prompt without // needing to change the command being entered. writeCommandPrompt(lastInputRow, command); } else if (inKey.Key == ConsoleKey.Escape) { // Escape key clears the current command. command = null; writeCommandPrompt(lastInputRow, command); } else if (inKey.Key == ConsoleKey.Backspace) { // Backspace removes the last character. command = (command?.Length > 0) ? command.Substring(0, command.Length - 1) : null; writeCommandPrompt(lastInputRow, command); } else if (!Char.IsControl(inKey.KeyChar)) { // Non-control character, append to current command. command += inKey.KeyChar; if (Console.CursorTop == lastInputRow) { Console.Write(inKey.KeyChar); } else { writeCommandPrompt(lastInputRow, command); } } lastInputRow = Console.CursorTop; } }
private async void OnConnectionStateChanged(RTCPeerConnectionState state) { if (state == RTCPeerConnectionState.failed) { if (!_connectionCompleted) { if (!_rolesReversed) { Logger.LogInformation("First connection attempt failed. Reversing roles..."); _rolesReversed = true; _canSendIceCandidates = false; DisposeConnection(); InitConnection(); if (Role == WebRTCAdapterRole.Accept) { try { Logger.LogInformation("Creating offer..."); RTCSessionDescriptionInit offer = _connection.createOffer(new RTCOfferOptions()); Logger.LogInformation("Setting local description..."); await _connection.setLocalDescription(offer); Logger.LogInformation("Sending offer request..."); var response = await _signalingTransporter.SendRequestAsync <WebRTCOfferRequest, WebRTCOfferResponse>(new WebRTCOfferRequest() { ChannelName = ChannelName, Offer = WebRTCSessionDescription.FromSessionDescription(offer) }, new ResonanceRequestConfig() { Timeout = TimeSpan.FromSeconds(10) }); if (response.Answer.InternalType == RTCSdpType.answer) { var result = _connection.setRemoteDescription(response.Answer.ToSessionDescription()); if (result != SetDescriptionResultEnum.OK) { throw new Exception("Error setting the remote description."); } } else { Logger.LogError($"Invalid answer type received '{response.Answer.InternalType}'."); } FlushIceCandidates(); } catch (Exception ex) { FailConnection(ex); } } } else { FailConnection(new Exception("Second connection attempt failed.")); } } else { OnFailed(new ResonanceWebRTCChannelClosedException("The WebRTC connection has failed.")); } } }
public void MessageFromPeerTaskRun(int peerId, string content) { PeerId = peerId; Task.Run(async() => { Debug.Assert(_peerId == PeerId || _peerId == -1); Debug.Assert(content.Length > 0); if (_peerId != PeerId && _peerId != -1) { Debug.WriteLine("Received a message from unknown peer " + "while already in a conversation with a different peer."); return; } if (!JsonObject.TryParse(content, out JsonObject jMessage)) { Debug.WriteLine($"Received unknown message: {content}"); return; } string type = jMessage.ContainsKey(NegotiationAtributes.Type) ? jMessage.GetNamedString(NegotiationAtributes.Type) : null; if (PeerConnection == null) { if (!string.IsNullOrEmpty(type)) { // Create the peer connection only when call is // about to get initiated. Otherwise ignore the // message from peers which could be result // of old (but not yet fully closed) connections. if (type == "offer" || type == "answer" || type == "json") { Debug.Assert(_peerId == -1); _peerId = PeerId; if (!CreatePeerConnection()) { Debug.WriteLine("Failed to initialize our PeerConnection instance"); OnSignedOut.Invoke(this, null); return; } else if (_peerId != PeerId) { Debug.WriteLine("Received a message from unknown peer while already " + "in a conversation with a different peer."); return; } } } else { Debug.WriteLine("[Warn] Received an untyped message after closing peer connection."); return; } } if (PeerConnection != null && !string.IsNullOrEmpty(type)) { if (type == "offer-loopback") { // Loopback not supported Debug.Assert(false); } string sdp = null; sdp = jMessage.ContainsKey(NegotiationAtributes.Sdp) ? jMessage.GetNamedString(NegotiationAtributes.Sdp) : null; if (string.IsNullOrEmpty(sdp)) { Debug.WriteLine("[Error] Can't parse received session description message."); return; } Debug.WriteLine($"Received session description:\n{content}"); RTCSdpType messageType = RTCSdpType.Offer; switch (type) { case "offer": messageType = RTCSdpType.Offer; break; case "answer": messageType = RTCSdpType.Answer; break; case "pranswer": messageType = RTCSdpType.Pranswer; break; default: Debug.Assert(false, type); break; } var sdpInit = new RTCSessionDescriptionInit(); sdpInit.Sdp = sdp; sdpInit.Type = messageType; var description = new RTCSessionDescription(sdpInit); await PeerConnection.SetRemoteDescription(description); if (messageType == RTCSdpType.Offer) { var answerOptions = new RTCAnswerOptions(); IRTCSessionDescription answer = await PeerConnection.CreateAnswer(answerOptions); await PeerConnection.SetLocalDescription(answer); string jsonString = SdpToJsonString(answer); // Send answer OnSendMessageToRemotePeer.Invoke(this, jsonString); } } else { RTCIceCandidate candidate = null; string sdpMid = jMessage.ContainsKey(NegotiationAtributes.SdpMid) ? jMessage.GetNamedString(NegotiationAtributes.SdpMid) : null; double sdpMLineIndex = jMessage.ContainsKey(NegotiationAtributes.SdpMLineIndex) ? jMessage.GetNamedNumber(NegotiationAtributes.SdpMLineIndex) : -1; string sdpCandidate = jMessage.ContainsKey(NegotiationAtributes.Candidate) ? jMessage.GetNamedString(NegotiationAtributes.Candidate) : null; if (string.IsNullOrEmpty(sdpMid) || sdpMLineIndex == -1 || string.IsNullOrEmpty(sdpCandidate)) { Debug.WriteLine($"[Error] Can't parse received message.\n{content}"); return; } var candidateInit = new RTCIceCandidateInit(); candidateInit.Candidate = sdpCandidate; candidateInit.SdpMid = sdpMid; candidateInit.SdpMLineIndex = (ushort)sdpMLineIndex; candidate = new RTCIceCandidate(candidateInit); await PeerConnection.AddIceCandidate(candidate); Debug.WriteLine($"Receiving ice candidate:\n{content}"); } }).Wait(); }
public async Task <RTCSessionDescriptionInit> GotOffer(RTCSessionDescriptionInit offer) { _logger.LogDebug($"SDP offer received."); _logger.LogTrace($"Offer SDP:\n{offer.sdp}"); var pc = new RTCPeerConnection(); if (_publicIPv4 != null) { var rtpPort = pc.GetRtpChannel().RTPPort; var publicIPv4Candidate = new RTCIceCandidate(RTCIceProtocol.udp, _publicIPv4, (ushort)rtpPort, RTCIceCandidateType.host); pc.addLocalIceCandidate(publicIPv4Candidate); } if (_publicIPv6 != null) { var rtpPort = pc.GetRtpChannel().RTPPort; var publicIPv6Candidate = new RTCIceCandidate(RTCIceProtocol.udp, _publicIPv6, (ushort)rtpPort, RTCIceCandidateType.host); pc.addLocalIceCandidate(publicIPv6Candidate); } MediaStreamTrack audioTrack = new MediaStreamTrack(SDPMediaTypesEnum.audio, false, new List <SDPAudioVideoMediaFormat> { new SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum.PCMU) }, MediaStreamStatusEnum.SendRecv); pc.addTrack(audioTrack); MediaStreamTrack videoTrack = new MediaStreamTrack(new VideoFormat(VideoCodecsEnum.VP8, VP8_PAYLOAD_ID), MediaStreamStatusEnum.SendRecv); pc.addTrack(videoTrack); pc.OnRtpPacketReceived += (IPEndPoint rep, SDPMediaTypesEnum media, RTPPacket rtpPkt) => { pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); //_logger.LogDebug($"RTP {media} pkt received, SSRC {rtpPkt.Header.SyncSource}, SeqNum {rtpPkt.Header.SequenceNumber}."); }; //peerConnection.OnReceiveReport += RtpSession_OnReceiveReport; //peerConnection.OnSendReport += RtpSession_OnSendReport; pc.OnTimeout += (mediaType) => _logger.LogWarning($"Timeout for {mediaType}."); pc.onconnectionstatechange += (state) => { _logger.LogDebug($"Peer connection state changed to {state}."); if (state == RTCPeerConnectionState.failed) { pc.Close("ice failure"); } }; var setResult = pc.setRemoteDescription(offer); if (setResult == SetDescriptionResultEnum.OK) { var offerSdp = pc.createOffer(null); await pc.setLocalDescription(offerSdp); var answer = pc.createAnswer(null); await pc.setLocalDescription(answer); _logger.LogTrace($"Answer SDP:\n{answer.sdp}"); return(answer); } else { return(null); } }
public Task SetRemoteDescription(RTCSessionDescriptionInit sessionDescription) => JsRuntime.CallJsMethodVoidAsync(NativeObject, "setRemoteDescription", sessionDescription) .AsTask();
private async static Task OfferAsync(IHttpContext context) { var offer = await context.GetRequestDataAsync <RTCSessionDescriptionInit>(); logger.LogDebug($"SDP Offer={offer.sdp}"); var jsonOptions = new JsonSerializerOptions(); jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); CancellationTokenSource cts = new CancellationTokenSource(); var ws = new ClientWebSocket(); await ws.ConnectAsync(new Uri(KURENTO_JSONRPC_URL), cts.Token); logger.LogDebug($"Successfully connected web socket client to {KURENTO_JSONRPC_URL}."); try { using (var jsonRpc = new JsonRpc(new WebSocketMessageHandler(ws))) { jsonRpc.AddLocalRpcMethod("ping", new Action(() => { logger.LogDebug($"Ping received"); } )); jsonRpc.AddLocalRpcMethod("onEvent", new Action <KurentoEvent>((evt) => { logger.LogDebug($"Event received type={evt.type}, source={evt.data.source}"); } )); jsonRpc.StartListening(); // Check the server is there. var pingResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.ping.ToString(), new { interval = KEEP_ALIVE_INTERVAL_MS }, cts.Token); logger.LogDebug($"Ping result={pingResult.value}."); // Create a media pipeline. var createPipelineResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.create.ToString(), new { type = "MediaPipeline" }, cts.Token); logger.LogDebug($"Create media pipeline result={createPipelineResult.value}, sessionID={createPipelineResult.sessionId}."); var sessionID = createPipelineResult.sessionId; var mediaPipeline = createPipelineResult.value; // Create a WebRTC end point. var createEndPointResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.create.ToString(), new { type = "WebRtcEndpoint", constructorParams = new { mediaPipeline = mediaPipeline }, sessionId = sessionID }, cts.Token); logger.LogDebug($"Create WebRTC endpoint result={createEndPointResult.value}."); var webRTCEndPointID = createEndPointResult.value; // Connect the WebRTC end point to itself to create a loopback connection (no result for this operation). await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.invoke.ToString(), new { @object = webRTCEndPointID, operation = "connect", operationParams = new { sink = webRTCEndPointID }, sessionId = sessionID }, cts.Token); // Subscribe for events from the WebRTC end point. var subscribeResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.subscribe.ToString(), new { @object = webRTCEndPointID, type = "IceCandidateFound", sessionId = sessionID }, cts.Token); logger.LogDebug($"Subscribe to WebRTC endpoint subscription ID={subscribeResult.value}."); var subscriptionID = subscribeResult.value; subscribeResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.subscribe.ToString(), new { @object = webRTCEndPointID, type = "OnIceCandidate", sessionId = sessionID }, cts.Token); logger.LogDebug($"Subscribe to WebRTC endpoint subscription ID={subscribeResult.value}."); // Send SDP offer. var processOfferResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.invoke.ToString(), new { @object = webRTCEndPointID, operation = "processOffer", operationParams = new { offer = offer.sdp }, sessionId = sessionID }, cts.Token); logger.LogDebug($"SDP answer={processOfferResult.value}."); RTCSessionDescriptionInit answerInit = new RTCSessionDescriptionInit { type = RTCSdpType.answer, sdp = processOfferResult.value }; context.Response.ContentType = "application/json"; using (var responseStm = context.OpenResponseStream(false, false)) { await JsonSerializer.SerializeAsync(responseStm, answerInit, jsonOptions); } // Tell Kurento to start ICE. var gatherCandidatesResult = await jsonRpc.InvokeWithParameterObjectAsync <KurentoResult>( KurentoMethodsEnum.invoke.ToString(), new { @object = webRTCEndPointID, operation = "gatherCandidates", sessionId = sessionID }, cts.Token); logger.LogDebug($"Gather candidates result={gatherCandidatesResult.value}."); } } catch (RemoteInvocationException invokeExcp) { logger.LogError($"JSON RPC invoke exception, error code={invokeExcp.ErrorCode}, msg={invokeExcp.Message}."); } }
public static Webrtc.RTCSessionDescription ToNative(this RTCSessionDescriptionInit description) => new Webrtc.RTCSessionDescription(description.Type.ToNative(), description.Sdp);
static async Task Main() { Console.WriteLine("WebRTC Echo Test Client"); logger = AddConsoleLogger(); CancellationTokenSource cts = new CancellationTokenSource(); #region Set up a simple Windows Form with two picture boxes. _form = new Form(); _form.AutoSize = true; _form.BackgroundImageLayout = ImageLayout.Center; _sourceVideoPicBox = new PictureBox { Size = new Size(VIDEO_FRAME_WIDTH, VIDEO_FRAME_HEIGHT), Location = new Point(0, 0), Visible = true }; _echoVideoPicBox = new PictureBox { Size = new Size(VIDEO_FRAME_WIDTH, VIDEO_FRAME_HEIGHT), Location = new Point(0, VIDEO_FRAME_HEIGHT), Visible = true }; _form.Controls.Add(_sourceVideoPicBox); _form.Controls.Add(_echoVideoPicBox); Application.EnableVisualStyles(); ThreadPool.QueueUserWorkItem(delegate { Application.Run(_form); }); _form.FormClosing += (sender, e) => _isFormActivated = false; _form.Activated += (sender, e) => _isFormActivated = true; //_form.FormClosed += (sender, e) => // TODO. #endregion // Video sink and source to generate and consume VP8 video streams. var testPattern = new VideoTestPatternSource(new VpxVideoEncoder()); var vp8VideoSink = new VideoEncoderEndPoint(); #region Connect the video frames generated from the sink and source to the Windows form. testPattern.OnVideoSourceRawSample += (uint durationMilliseconds, int width, int height, byte[] sample, VideoPixelFormatsEnum pixelFormat) => { if (_isFormActivated) { _form?.BeginInvoke(new Action(() => { if (_form.Handle != IntPtr.Zero) { unsafe { fixed(byte *s = sample) { var bmpImage = new Bitmap(width, height, width * 3, System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr)s); _sourceVideoPicBox.Image = bmpImage; } } } })); } }; vp8VideoSink.OnVideoSinkDecodedSample += (byte[] bmp, uint width, uint height, int stride, VideoPixelFormatsEnum pixelFormat) => { if (_isFormActivated) { _form?.BeginInvoke(new Action(() => { if (_form.Handle != IntPtr.Zero) { unsafe { fixed(byte *s = bmp) { var bmpImage = new Bitmap((int)width, (int)height, stride, PixelFormat.Format24bppRgb, (IntPtr)s); _echoVideoPicBox.Image = bmpImage; } } } })); } }; #endregion await testPattern.StartVideo().ConfigureAwait(false); var pc = await CreatePeerConnection(testPattern, vp8VideoSink).ConfigureAwait(false); Console.WriteLine($"Sending offer to {SIGNALING_SERVER}."); var signaler = new HttpClient(); var offer = pc.createOffer(null); await pc.setLocalDescription(offer).ConfigureAwait(false); var content = new StringContent(offer.toJSON(), Encoding.UTF8, "application/json"); var response = await signaler.PostAsync($"{SIGNALING_SERVER}", content).ConfigureAwait(false); var answerStr = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (RTCSessionDescriptionInit.TryParse(answerStr, out var answerInit)) { var setAnswerResult = pc.setRemoteDescription(answerInit); if (setAnswerResult != SetDescriptionResultEnum.OK) { Console.WriteLine($"Set remote description failed {setAnswerResult}."); } } else { Console.WriteLine("Failed to parse SDP answer from signaling server."); } Console.WriteLine("Press any key to exit."); Console.ReadLine(); }
public Task SendSdp(string signalingId, string displayName, RTCSessionDescriptionInit sessionDescription) { return(Clients.Client(signalingId).SendAsync("Sdp", Context.ConnectionId, displayName, sessionDescription)); }
public async Task <RTCSessionDescriptionInit> GotOffer(RTCSessionDescriptionInit offer) { logger.LogTrace($"SDP offer received."); logger.LogTrace(offer.sdp); var pc = new RTCPeerConnection(); if (_presetIceAddresses != null) { foreach (var addr in _presetIceAddresses) { var rtpPort = pc.GetRtpChannel().RTPPort; var publicIPv4Candidate = new RTCIceCandidate(RTCIceProtocol.udp, addr, (ushort)rtpPort, RTCIceCandidateType.host); pc.addLocalIceCandidate(publicIPv4Candidate); } } MediaStreamTrack audioTrack = new MediaStreamTrack(SDPWellKnownMediaFormatsEnum.PCMU); pc.addTrack(audioTrack); MediaStreamTrack videoTrack = new MediaStreamTrack(new VideoFormat(VideoCodecsEnum.VP8, VP8_PAYLOAD_ID)); pc.addTrack(videoTrack); pc.OnRtpPacketReceived += (IPEndPoint rep, SDPMediaTypesEnum media, RTPPacket rtpPkt) => { pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); }; pc.OnTimeout += (mediaType) => logger.LogWarning($"Timeout for {mediaType}."); pc.oniceconnectionstatechange += (state) => logger.LogInformation($"ICE connection state changed to {state}."); pc.onsignalingstatechange += () => logger.LogInformation($"Signaling state changed to {pc.signalingState}."); pc.onconnectionstatechange += (state) => { logger.LogInformation($"Peer connection state changed to {state}."); if (state == RTCPeerConnectionState.failed) { pc.Close("ice failure"); } }; var setResult = pc.setRemoteDescription(offer); if (setResult == SetDescriptionResultEnum.OK) { var offerSdp = pc.createOffer(null); await pc.setLocalDescription(offerSdp); var answer = pc.createAnswer(null); logger.LogTrace($"SDP answer created."); logger.LogTrace(answer.sdp); return(answer); } else { logger.LogWarning($"Failed to set remote description {setResult}."); return(null); } }
static async Task <int> Main(string[] args) { Console.WriteLine("Starting webrtc echo test client."); string echoServerUrl = DEFAULT_ECHO_SERVER_URL; if (args?.Length > 0) { echoServerUrl = args[0]; } logger = AddConsoleLogger(LogEventLevel.Verbose); var pc = CreatePeerConnection(); var offer = pc.createOffer(null); await pc.setLocalDescription(offer); bool didConnect = false; TaskCompletionSource <int> connectResult = new TaskCompletionSource <int>(); pc.onconnectionstatechange += (state) => { logger.LogInformation($"Peer connection state changed to {state}."); if (state == RTCPeerConnectionState.disconnected || state == RTCPeerConnectionState.failed) { pc.Close("remote disconnection"); } else if (state == RTCPeerConnectionState.connected) { didConnect = true; pc.Close("normal"); } else if (state == RTCPeerConnectionState.closed) { connectResult.SetResult(didConnect ? SUCCESS_RESULT : FAILURE_RESULT); } }; logger.LogInformation($"Posting offer to {echoServerUrl}."); var httpClient = new HttpClient(); var content = new StringContent(offer.toJSON(), Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync(echoServerUrl, content); var answerStr = await response.Content.ReadAsStringAsync(); if (RTCSessionDescriptionInit.TryParse(answerStr, out var answerInit)) { var setAnswerResult = pc.setRemoteDescription(answerInit); if (setAnswerResult != SetDescriptionResultEnum.OK) { logger.LogWarning($"Set remote description failed {setAnswerResult}."); } } else { logger.LogWarning("Failed to parse SDP answer from echo server."); } var result = await connectResult.Task; logger.LogInformation($"Connection result {result}."); return(result); }
private static async Task RunCommand(Options options, bool noOptions) { // Plumbing code to facilitate a graceful exit. CancellationTokenSource exitCts = new CancellationTokenSource(); // Cancellation token to stop the SIP transport and RTP stream. //ManualResetEvent exitMre = new ManualResetEvent(false); logger = AddConsoleLogger(); // Start MDNS server. var mdnsServer = new ServiceDiscovery(); if (options.StunServer != null) { string[] fields = options.StunServer.Split(';'); _stunServer = new RTCIceServer { urls = fields[0], username = fields.Length > 1 ? fields[1] : null, credential = fields.Length > 2 ? fields[2] : null, credentialType = RTCIceCredentialType.password }; } _relayOnly = options.RelayOnly; if (!string.IsNullOrEmpty(options.IceTypes)) { options.IceTypes.Split().ToList().ForEach(x => { if (Enum.TryParse <RTCIceCandidateType>(x, out var iceType)) { _iceTypes.Add(iceType); } }); if (!_iceTypes.Any(x => x == RTCIceCandidateType.host)) { _offerOptions = new RTCOfferOptions { X_ExcludeIceCandidates = true }; _answerOptions = new RTCAnswerOptions { X_ExcludeIceCandidates = true }; } } if (!string.IsNullOrEmpty(options.AcceptIceTypes)) { options.AcceptIceTypes.Split().ToList().ForEach(x => { if (Enum.TryParse <RTCIceCandidateType>(x, out var iceType)) { _acceptIceTypes.Add(iceType); } }); } if (options.UseWebSocket || options.UseSecureWebSocket || noOptions) { // Start web socket. Console.WriteLine("Starting web socket server..."); _webSocketServer = new WebSocketServer(IPAddress.Any, WEBSOCKET_PORT, options.UseSecureWebSocket); if (options.UseSecureWebSocket) { _webSocketServer.SslConfiguration.ServerCertificate = new X509Certificate2(LOCALHOST_CERTIFICATE_PATH); _webSocketServer.SslConfiguration.CheckCertificateRevocation = false; } _webSocketServer.AddWebSocketService <WebRTCWebSocketPeer>("/", (peer) => { peer.OfferOptions = _offerOptions; if (_acceptIceTypes != null && _acceptIceTypes.Count > 0) { peer.FilterRemoteICECandidates = (init) => _acceptIceTypes.Any(x => x == RTCIceCandidate.Parse(init.candidate).type); } peer.CreatePeerConnection = CreatePeerConnection; }); _webSocketServer.Start(); Console.WriteLine($"Waiting for browser web socket connection to {_webSocketServer.Address}:{_webSocketServer.Port}..."); } else if (!string.IsNullOrWhiteSpace(options.WebSocketServer)) { // We are the client for a web socket server. The JSON signalling exchange still occurs the same way as when the web socket // server option is used except that as the web socket client we receive the SDP offer from the server. WebRTCWebSocketClient wsockClient = new WebRTCWebSocketClient(options.WebSocketServer, CreatePeerConnection); await wsockClient.Start(exitCts.Token); Console.WriteLine("web socket client started."); } else if (options.CreateJsonOffer) { var pc = await Createpc(null, _stunServer, _relayOnly); var offerSdp = pc.createOffer(null); await pc.setLocalDescription(offerSdp); Console.WriteLine(offerSdp.sdp); var offerJson = JsonConvert.SerializeObject(offerSdp, new Newtonsoft.Json.Converters.StringEnumConverter()); var offerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(offerJson)); Console.WriteLine(offerBase64); string remoteAnswerB64 = null; while (string.IsNullOrWhiteSpace(remoteAnswerB64)) { Console.Write("Remote Answer => "); remoteAnswerB64 = Console.ReadLine(); } string remoteAnswer = Encoding.UTF8.GetString(Convert.FromBase64String(remoteAnswerB64)); Console.WriteLine(remoteAnswer); RTCSessionDescriptionInit answerInit = JsonConvert.DeserializeObject <RTCSessionDescriptionInit>(remoteAnswer); Console.WriteLine($"Remote answer: {answerInit.sdp}"); var res = pc.setRemoteDescription(answerInit); if (res != SetDescriptionResultEnum.OK) { // No point continuing. Something will need to change and then try again. pc.Close("failed to set remote sdp"); } } else if (options.RestServer != null) { string[] fields = options.RestServer.Split(';'); if (fields.Length < 3) { throw new ArgumentException("The 'rest' option must contain 3 semi-colon separated fields, e.g. --rest=https://localhost:5001/api/webrtcsignal;myid;theirid."); } var webrtcRestPeer = new WebRTCRestSignalingPeer(fields[0], fields[1], fields[2], CreatePeerConnection); webrtcRestPeer.OfferOptions = _offerOptions; webrtcRestPeer.AnswerOptions = _answerOptions; if (_acceptIceTypes != null && _acceptIceTypes.Count > 0) { webrtcRestPeer.FilterRemoteICECandidates = (init) => _acceptIceTypes.Any(x => x == RTCIceCandidate.Parse(init.candidate).type); } await webrtcRestPeer.Start(exitCts); } _ = Task.Run(() => ProcessInput(exitCts)); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitCts.Cancel(); }; // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. exitCts.Token.WaitHandle.WaitOne(); Console.WriteLine(); Console.WriteLine("Exiting..."); _peerConnection?.Close("application exit"); _webSocketServer?.Stop(); Task.Delay(1000).Wait(); }
private static async Task RunCommand(Options options, bool noOptions) { // Plumbing code to facilitate a graceful exit. CancellationTokenSource exitCts = new CancellationTokenSource(); // Cancellation token to stop the SIP transport and RTP stream. //ManualResetEvent exitMre = new ManualResetEvent(false); AddConsoleLogger(); // Start MDNS server. var mdnsServer = new ServiceDiscovery(); if (options.StunServer != null) { string[] fields = options.StunServer.Split(';'); _stunServer = new RTCIceServer { urls = fields[0], username = fields.Length > 1 ? fields[1] : null, credential = fields.Length > 2 ? fields[2] : null, credentialType = RTCIceCredentialType.password }; } if (options.UseWebSocket || options.UseSecureWebSocket || noOptions) { // Start web socket. Console.WriteLine("Starting web socket server..."); _webSocketServer = new WebSocketServer(IPAddress.Any, WEBSOCKET_PORT, options.UseSecureWebSocket); if (options.UseSecureWebSocket) { _webSocketServer.SslConfiguration.ServerCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(LOCALHOST_CERTIFICATE_PATH); _webSocketServer.SslConfiguration.CheckCertificateRevocation = false; } //_webSocketServer.Log.Level = WebSocketSharp.LogLevel.Debug; _webSocketServer.AddWebSocketService <WebRtcClient>("/sendoffer", (client) => { client.WebSocketOpened += SendOffer; client.OnMessageReceived += WebSocketMessageReceived; }); _webSocketServer.AddWebSocketService <WebRtcClient>("/receiveoffer", (client) => { client.WebSocketOpened += ReceiveOffer; client.OnMessageReceived += WebSocketMessageReceived; }); _webSocketServer.Start(); Console.WriteLine($"Waiting for browser web socket connection to {_webSocketServer.Address}:{_webSocketServer.Port}..."); } else if (options.CreateJsonOffer) { var pc = Createpc(null, _stunServer); var offerSdp = pc.createOffer(null); await pc.setLocalDescription(offerSdp); Console.WriteLine(offerSdp.sdp); var offerJson = JsonConvert.SerializeObject(offerSdp, new Newtonsoft.Json.Converters.StringEnumConverter()); var offerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(offerJson)); Console.WriteLine(offerBase64); string remoteAnswerB64 = null; while (string.IsNullOrWhiteSpace(remoteAnswerB64)) { Console.Write("Remote Answer => "); remoteAnswerB64 = Console.ReadLine(); } string remoteAnswer = Encoding.UTF8.GetString(Convert.FromBase64String(remoteAnswerB64)); Console.WriteLine(remoteAnswer); RTCSessionDescriptionInit answerInit = JsonConvert.DeserializeObject <RTCSessionDescriptionInit>(remoteAnswer); Console.WriteLine($"Remote answer: {answerInit.sdp}"); var res = pc.setRemoteDescription(answerInit); if (res != SetDescriptionResultEnum.OK) { // No point continuing. Something will need to change and then try again. pc.Close("failed to set remote sdp"); } } else if (options.NodeDssServer != null) { _nodeDssUri = new Uri(options.NodeDssServer); _nodeDssclient = new HttpClient(); Console.WriteLine($"node-dss server successfully set to {_nodeDssUri}."); } _ = Task.Run(() => ProcessInput(exitCts)); // Ctrl-c will gracefully exit the call at any point. Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; exitCts.Cancel(); }; // Wait for a signal saying the call failed, was cancelled with ctrl-c or completed. exitCts.Token.WaitHandle.WaitOne(); Console.WriteLine(); Console.WriteLine("Exiting..."); _peerConnection?.Close("application exit"); _webSocketServer?.Stop(); Task.Delay(1000).Wait(); }