@@ -165,6 +165,14 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
165165 } ,
166166 } ) ;
167167
168+ if ( typeof VideoDecoder === 'undefined' ) {
169+ logger . warn ( "[Scrcpy-VideoStreamManager] WebCodecs API (VideoDecoder) is not available in this browser, aborting stream" ) ;
170+ readableControllers . delete ( deviceId ) ;
171+ worker . terminate ( ) ;
172+ decoderWorkers . current . delete ( deviceId ) ;
173+ return ;
174+ }
175+
168176 await VideoDecoder . isConfigSupported ( {
169177 // Check if h265 is supported
170178 codec : "hev1.1.60.L153.B0.0.0.0.0.0" ,
@@ -180,16 +188,64 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
180188 if ( supported . supported || ! useH265 ) {
181189 const codec = useH265 ? ScrcpyVideoCodecId . H265 : ScrcpyVideoCodecId . H264 ;
182190
183- // Pass objects and stream to worker
184- worker . postMessage (
185- {
186- codec,
187- canvas : offscreenCanvas ,
188- stream,
189- useH265
190- } ,
191- [ offscreenCanvas , stream ]
192- ) ;
191+ // Check if browser supports transferring ReadableStream
192+ let canTransferStream = false ;
193+ try {
194+ const { port1 } = new MessageChannel ( ) ;
195+ const testStream = new ReadableStream ( ) ;
196+ port1 . postMessage ( testStream , [ testStream ] ) ;
197+ canTransferStream = true ;
198+ } catch ( e ) {
199+ canTransferStream = false ;
200+ }
201+
202+ if ( canTransferStream ) {
203+ // Pass objects and stream to worker directly
204+ worker . postMessage (
205+ {
206+ codec,
207+ canvas : offscreenCanvas ,
208+ stream,
209+ useH265,
210+ type : 'direct'
211+ } ,
212+ [ offscreenCanvas , stream ]
213+ ) ;
214+ } else {
215+ // Fallback for browsers that don't support transferring ReadableStream (like Safari)
216+ logger . info ( "[Scrcpy-VideoStreamManager] ReadableStream transfer not supported, using MessageChannel fallback" ) ;
217+ const { port1, port2 } = new MessageChannel ( ) ;
218+ worker . postMessage (
219+ { codec, canvas : offscreenCanvas , port : port2 , useH265, type : 'port' } ,
220+ [ offscreenCanvas , port2 ]
221+ ) ;
222+
223+ const reader = stream . getReader ( ) ;
224+ ( async ( ) => {
225+ try {
226+ while ( true ) {
227+ const { done, value } = await reader . read ( ) ;
228+ if ( done ) {
229+ port1 . postMessage ( { done : true } ) ;
230+ break ;
231+ }
232+ const transferables : Transferable [ ] = [ ] ;
233+ // Clone the buffer to avoid detaching it if it's needed elsewhere,
234+ // or just send the value. In Firefox, detaching was causing issues.
235+ // However, since we fallback to MessageChannel ONLY when ReadableStream transfer fails,
236+ // Firefox (which supports stream transfer) will use the direct path above.
237+ if ( value ?. data instanceof Uint8Array ) {
238+ transferables . push ( value . data . buffer ) ;
239+ }
240+ port1 . postMessage ( { done : false , value } , transferables ) ;
241+ }
242+ } catch {
243+ port1 . postMessage ( { done : true } ) ;
244+ } finally {
245+ port1 . close ( ) ;
246+ }
247+ } ) ( ) ;
248+ }
193249 } else {
194250 logger . error ( "[Scrcpy] Error piping to decoder writable stream" ) ;
195251 }
@@ -211,6 +267,7 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
211267 // Reconnects automatically after 1 s on unexpected close.
212268 function connectDeviceSocket ( streamId : string ) {
213269 if ( cleanedUp ) return ;
270+ if ( typeof VideoDecoder === 'undefined' ) return ;
214271
215272 // Prevent the stale socket's onclose from firing a reconnect when we replace it
216273 const existing = deviceSockets . get ( streamId ) ;
@@ -339,32 +396,34 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
339396
340397 // Send browser's codecs compatibility
341398 socket . onopen = async ( ) => {
342- let supportH264 : boolean , supportH265 : boolean , supportAv1 : boolean ;
343- // Check if h264 is supported
344- await VideoDecoder . isConfigSupported ( { codec : "avc1.4D401E" } ) . then ( ( r ) => {
345- supportH264 = r . supported ! ;
346- logger . info ( "[SCRCPY] Supports h264: {supportH264}" , { supportH264 } ) ;
347- } )
348-
349- // Check if h265 is supported
350- await VideoDecoder . isConfigSupported ( { codec : "hev1.1.60.L153.B0.0.0.0.0.0" } ) . then ( ( r ) => {
351- supportH265 = r . supported ! ;
352- logger . info ( "[SCRCPY] Supports h265 {supportH265}" , { supportH265 } ) ;
353- } )
354-
355- // Check if AV1 is supported
356- await VideoDecoder . isConfigSupported ( { codec : "av01.0.05M.08" } ) . then ( ( r ) => {
357- supportAv1 = r . supported ! ;
358- logger . info ( "[SCRCPY] Supports AV1 {supportAv1}" , { supportAv1 } ) ;
359- } )
399+ let supportH264 = false , supportH265 = false , supportAv1 = false ;
400+
401+ if ( typeof VideoDecoder === 'undefined' ) {
402+ logger . warn ( "[SCRCPY] WebCodecs API not available, reporting no codec support" ) ;
403+ } else {
404+ // Check if h264 is supported
405+ await VideoDecoder . isConfigSupported ( { codec : "avc1.4D401E" } ) . then ( ( r ) => {
406+ supportH264 = r . supported ! ;
407+ logger . info ( "[SCRCPY] Supports h264: {supportH264}" , { supportH264 } ) ;
408+ } )
409+
410+ // Check if h265 is supported
411+ await VideoDecoder . isConfigSupported ( { codec : "hev1.1.60.L153.B0.0.0.0.0.0" } ) . then ( ( r ) => {
412+ supportH265 = r . supported ! ;
413+ logger . info ( "[SCRCPY] Supports h265 {supportH265}" , { supportH265 } ) ;
414+ } )
415+
416+ // Check if AV1 is supported
417+ await VideoDecoder . isConfigSupported ( { codec : "av01.0.05M.08" } ) . then ( ( r ) => {
418+ supportAv1 = r . supported ! ;
419+ logger . info ( "[SCRCPY] Supports AV1 {supportAv1}" , { supportAv1 } ) ;
420+ } )
421+ }
360422
361423 socket . send ( JSON . stringify ( {
362424 "type" : "codecVideo" ,
363- // @ts -expect-error
364425 "h264" : supportH264 ,
365- // @ts -expect-error
366426 "h265" : supportH265 ,
367- // @ts -expect-error
368427 "av1" : supportAv1 ,
369428 } ) ) ;
370429 }
0 commit comments