@@ -10,30 +10,53 @@ import type {
1010
1111const HAVE_CURRENT_DATA = 2 ;
1212const WEBRTC_CONTROL_CHANNEL_LABEL = "simdeck-control" ;
13+ const WEBRTC_REALTIME_INPUT_CHANNEL_LABEL = "simdeck-input" ;
14+ const WEBRTC_TELEMETRY_CHANNEL_LABEL = "simdeck-telemetry" ;
1315const WEBRTC_FIRST_FRAME_TIMEOUT_MS = 10000 ;
1416const WEBRTC_STALLED_FRAME_TIMEOUT_MS = 8000 ;
1517const WEBRTC_REMOTE_DISCONNECTED_GRACE_MS = 3000 ;
18+ const WEBRTC_REALTIME_INPUT_BUFFER_LIMIT = 1024 ;
1619
1720let activeWebRtcControlChannel : RTCDataChannel | null = null ;
21+ let activeWebRtcRealtimeInputChannel : RTCDataChannel | null = null ;
22+ let activeWebRtcTelemetryChannel : RTCDataChannel | null = null ;
1823let activeStreamClient : StreamWorkerClient | null = null ;
1924
2025export type StreamBackend = "webrtc" ;
2126
2227export function sendWebRtcControlMessage ( encoded : string ) : boolean {
23- if ( activeWebRtcControlChannel ?. readyState !== "open" ) {
28+ return sendDataChannelMessage ( activeWebRtcControlChannel , encoded ) ;
29+ }
30+
31+ export function sendWebRtcRealtimeControlMessage ( encoded : string ) : boolean {
32+ if ( activeWebRtcRealtimeInputChannel ?. readyState !== "open" ) {
2433 return false ;
2534 }
26- activeWebRtcControlChannel . send ( encoded ) ;
35+ if (
36+ activeWebRtcRealtimeInputChannel . bufferedAmount >
37+ WEBRTC_REALTIME_INPUT_BUFFER_LIMIT
38+ ) {
39+ return true ;
40+ }
41+ activeWebRtcRealtimeInputChannel . send ( encoded ) ;
2742 return true ;
2843}
2944
3045export function sendWebRtcClientStats ( stats : unknown ) : boolean {
31- if ( activeWebRtcControlChannel ?. readyState !== "open" ) {
32- return false ;
33- }
34- activeWebRtcControlChannel . send (
46+ return sendDataChannelMessage (
47+ activeWebRtcTelemetryChannel ,
3548 JSON . stringify ( { stats, type : "clientStats" } ) ,
3649 ) ;
50+ }
51+
52+ function sendDataChannelMessage (
53+ channel : RTCDataChannel | null ,
54+ encoded : string ,
55+ ) : boolean {
56+ if ( channel ?. readyState !== "open" ) {
57+ return false ;
58+ }
59+ channel . send ( encoded ) ;
3760 return true ;
3861}
3962
@@ -67,9 +90,11 @@ class WebRtcStreamClient implements StreamClientBackend {
6790 private lastVideoFrameAt = 0 ;
6891 private peerConnection : RTCPeerConnection | null = null ;
6992 private reconnectTimeout = 0 ;
93+ private realtimeInputChannel : RTCDataChannel | null = null ;
7094 private remoteMode = false ;
7195 private reportedVideoConfig = false ;
7296 private shouldReconnect = false ;
97+ private telemetryChannel : RTCDataChannel | null = null ;
7398 private stats : StreamStats = createEmptyStreamStats ( ) ;
7499 private video : HTMLVideoElement | null = null ;
75100 private videoFrameCallback = 0 ;
@@ -134,6 +159,34 @@ class WebRtcStreamClient implements StreamClientBackend {
134159 activeWebRtcControlChannel = null ;
135160 }
136161 } ) ;
162+ const realtimeInputChannel = peerConnection . createDataChannel (
163+ WEBRTC_REALTIME_INPUT_CHANNEL_LABEL ,
164+ {
165+ maxRetransmits : 0 ,
166+ ordered : false ,
167+ } ,
168+ ) ;
169+ this . realtimeInputChannel = realtimeInputChannel ;
170+ activeWebRtcRealtimeInputChannel = realtimeInputChannel ;
171+ realtimeInputChannel . addEventListener ( "close" , ( ) => {
172+ if ( activeWebRtcRealtimeInputChannel === realtimeInputChannel ) {
173+ activeWebRtcRealtimeInputChannel = null ;
174+ }
175+ } ) ;
176+ const telemetryChannel = peerConnection . createDataChannel (
177+ WEBRTC_TELEMETRY_CHANNEL_LABEL ,
178+ {
179+ maxRetransmits : 0 ,
180+ ordered : false ,
181+ } ,
182+ ) ;
183+ this . telemetryChannel = telemetryChannel ;
184+ activeWebRtcTelemetryChannel = telemetryChannel ;
185+ telemetryChannel . addEventListener ( "close" , ( ) => {
186+ if ( activeWebRtcTelemetryChannel === telemetryChannel ) {
187+ activeWebRtcTelemetryChannel = null ;
188+ }
189+ } ) ;
137190
138191 peerConnection . ontrack = ( event ) => {
139192 if ( generation !== this . connectGeneration ) {
@@ -223,7 +276,7 @@ class WebRtcStreamClient implements StreamClientBackend {
223276 }
224277 } ;
225278
226- const offer = await peerConnection . createOffer ( ) ;
279+ const offer = safariBaselineH264Offer ( await peerConnection . createOffer ( ) ) ;
227280 if ( generation !== this . connectGeneration ) {
228281 return ;
229282 }
@@ -292,6 +345,16 @@ class WebRtcStreamClient implements StreamClientBackend {
292345 activeWebRtcControlChannel = null ;
293346 }
294347 this . controlChannel = null ;
348+ this . realtimeInputChannel ?. close ( ) ;
349+ if ( activeWebRtcRealtimeInputChannel === this . realtimeInputChannel ) {
350+ activeWebRtcRealtimeInputChannel = null ;
351+ }
352+ this . realtimeInputChannel = null ;
353+ this . telemetryChannel ?. close ( ) ;
354+ if ( activeWebRtcTelemetryChannel === this . telemetryChannel ) {
355+ activeWebRtcTelemetryChannel = null ;
356+ }
357+ this . telemetryChannel = null ;
295358 this . peerConnection ?. close ( ) ;
296359 this . peerConnection = null ;
297360 }
@@ -716,6 +779,26 @@ function configureReceiverCodecPreferences(transceiver: RTCRtpTransceiver) {
716779 ] ) ;
717780}
718781
782+ function safariBaselineH264Offer (
783+ offer : RTCSessionDescriptionInit ,
784+ ) : RTCSessionDescriptionInit {
785+ if ( ! isSafariBrowser ( ) || ! offer . sdp ) {
786+ return offer ;
787+ }
788+ return {
789+ ...offer ,
790+ sdp : offer . sdp . replace (
791+ / ( a = f m t p : \d + .* p r o f i l e - l e v e l - i d = ) [ 0 - 9 a - f A - F ] { 6 } / g,
792+ "$142e01f" ,
793+ ) ,
794+ } ;
795+ }
796+
797+ function isSafariBrowser ( ) : boolean {
798+ const ua = navigator . userAgent ;
799+ return / S a f a r i \/ / . test ( ua ) && ! / C h r o m e \/ | C h r o m i u m \/ | C r i O S \/ | F x i O S \/ / . test ( ua ) ;
800+ }
801+
719802function iceServers ( health ?: HealthResponse | null ) : RTCIceServer [ ] {
720803 const params = new URLSearchParams ( window . location . search ) ;
721804 const queryValue = params . get ( "iceServers" ) ;
0 commit comments