11import { useEffect , useState , useMemo , useRef } from "react" ;
22import PlayerScreenCanvas from "./PlayerScreenCanvas.tsx" ;
3- import {
4- VideoFrameRenderer ,
5- WebGLVideoFrameRenderer ,
6- BitmapVideoFrameRenderer ,
7- WebCodecsVideoDecoder ,
8- } from "@yume-chan/scrcpy-decoder-webcodecs" ;
93import { getLogger } from "@logtape/logtape" ;
104import { ScrcpyMediaStreamPacket , ScrcpyVideoCodecId } from "@yume-chan/scrcpy" ;
115const host : string = window . location . hostname ;
@@ -41,19 +35,6 @@ const deserializeData = (serializedData: string) => {
4135 }
4236} ;
4337
44- function createVideoFrameRenderer ( ) : VideoFrameRenderer {
45-
46- if ( WebGLVideoFrameRenderer . isSupported ) {
47- logger . debug ( "[SCRCPY] Using WebGLVideoFrameRenderer" ) ;
48- return new WebGLVideoFrameRenderer ( ) ;
49- } else {
50- logger . warn ( "[SCRCPY] WebGL isn't supported... " ) ;
51- }
52-
53- logger . debug ( "[SCRCPY] Using fallback BitmapVideoFrameRenderer" ) ;
54- return new BitmapVideoFrameRenderer ( ) ;
55- }
56-
5738interface VideoStreamManagerProps {
5839 needsInteractivity ?: boolean ;
5940 selectedCanvas ?: string ;
@@ -92,6 +73,8 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
9273 const isDecoderHasConfig = new Map < string , boolean > ( ) ;
9374 // Tracks the codec each decoder was created with — used to detect mid-stream codec changes
9475 const streamIsH265 = new Map < string , boolean > ( ) ;
76+ // Map of worker instances for each stream
77+ const decoderWorkers = useRef < Map < string , Worker > > ( new Map ( ) ) ;
9578
9679
9780
@@ -111,10 +94,7 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
11194 }
11295 // Prepare video stream =======================
11396
114- const renderer : VideoFrameRenderer = createVideoFrameRenderer ( ) ;
115-
116- // get the canvas from the renderer (renderer as any is used to ensure ts knows that canvas is a property of the renderer)
117- const canvas = ( renderer as any ) . canvas as HTMLCanvasElement
97+ const canvas = document . createElement ( "canvas" ) ;
11898
11999 // Catch cases with non IP devices (USB
120100 const canvasId : string =
@@ -136,6 +116,24 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
136116 }
137117 }
138118
119+ const offscreenCanvas = canvas . transferControlToOffscreen ( ) ;
120+
121+ // Clean up any existing worker
122+ if ( decoderWorkers . current . has ( deviceId ) ) {
123+ decoderWorkers . current . get ( deviceId ) ?. terminate ( ) ;
124+ decoderWorkers . current . delete ( deviceId ) ;
125+ }
126+
127+ // Create a new web worker to handle the stream
128+ const worker = new Worker ( new URL ( "../../workers/scrcpyDecoder.ts" , import . meta. url ) , { type : "module" } ) ;
129+ decoderWorkers . current . set ( deviceId , worker ) ;
130+
131+ worker . addEventListener ( "message" , ( e ) => {
132+ if ( e . data . type === 'sizeChanged' ) {
133+ logger . debug ( `[Scrcpy] Size changed for ${ deviceId } : ${ e . data . width } x${ e . data . height } ` ) ;
134+ }
135+ } ) ;
136+
139137 // Create the ReadableStream BEFORE the async codec check.
140138 // new ReadableStream() calls start() synchronously, so the real controller is placed
141139 // in readableControllers before this function ever suspends at the first await.
@@ -152,6 +150,13 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
152150 cancel ( ) {
153151 readableControllers . delete ( deviceId ) ;
154152 isDecoderHasConfig . delete ( deviceId ) ;
153+
154+ // Terminate the worker
155+ if ( decoderWorkers . current . has ( deviceId ) ) {
156+ decoderWorkers . current . get ( deviceId ) ?. terminate ( ) ;
157+ decoderWorkers . current . delete ( deviceId ) ;
158+ }
159+
155160 // Use the canvas already in scope — canvasList captures a stale closure
156161 // (React state at render time) so canvasList[deviceId] is always undefined here.
157162 canvas . remove ( ) ;
@@ -173,24 +178,24 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
173178 if ( useH265 && ! supported . supported ) {
174179 logger . warn ( "[Scrcpy-VideoStreamManager] Should decode h265, but not compatible, waiting for new stream to start..." ) ;
175180 readableControllers . delete ( deviceId ) ;
181+ worker . terminate ( ) ;
182+ decoderWorkers . current . delete ( deviceId ) ;
176183 return ;
177184 }
178185
179186 if ( supported . supported || ! useH265 ) {
180- const decoder = new WebCodecsVideoDecoder ( {
181- codec : useH265 ? ScrcpyVideoCodecId . H265 : ScrcpyVideoCodecId . H264 ,
182- renderer : renderer ,
183- // Firefox on Linux has no hardware H264 WebCodecs path; "prefer-software" enables
184- // the software decoder (OpenH264) and avoids "encoding not supported" errors.
185- // H265 keeps "no-preference": Chrome only supports H265 via hardware, so forcing
186- // software would cause "OperationError: Unsupported configuration".
187- hardwareAcceleration : useH265 ? "no-preference" : "prefer-software" ,
188- } ) ;
189-
190- // Feed the scrcpy stream to the video decoder
191- void stream . pipeTo ( decoder . writable ) . catch ( ( err ) => {
192- logger . error ( "[Scrcpy] Error piping to decoder writable stream: {err}" , { err } ) ;
193- } ) ;
187+ const codec = useH265 ? ScrcpyVideoCodecId . H265 : ScrcpyVideoCodecId . H264 ;
188+
189+ // Pass objects and stream to worker
190+ worker . postMessage (
191+ {
192+ codec,
193+ canvas : offscreenCanvas ,
194+ stream,
195+ useH265
196+ } ,
197+ [ offscreenCanvas , stream ]
198+ ) ;
194199 } else {
195200 logger . error ( "[Scrcpy] Error piping to decoder writable stream" ) ;
196201 }
@@ -223,6 +228,12 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
223228 // Clearing these forces newVideoStream() to recreate the decoder on the next config packet.
224229 readableControllers . delete ( streamId ) ;
225230 isDecoderHasConfig . delete ( streamId ) ;
231+
232+ // Terminate the worker
233+ if ( decoderWorkers . current . has ( streamId ) ) {
234+ decoderWorkers . current . get ( streamId ) ?. terminate ( ) ;
235+ decoderWorkers . current . delete ( streamId ) ;
236+ }
226237 }
227238
228239 const ws = new WebSocket ( `ws://${ host } :${ port } /stream/${ streamId } ` ) ;
@@ -303,6 +314,13 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
303314 readableControllers . delete ( streamId ) ;
304315 isDecoderHasConfig . delete ( streamId ) ;
305316 streamIsH265 . delete ( streamId ) ;
317+
318+ // Terminate the worker
319+ if ( decoderWorkers . current . has ( streamId ) ) {
320+ decoderWorkers . current . get ( streamId ) ?. terminate ( ) ;
321+ decoderWorkers . current . delete ( streamId ) ;
322+ }
323+
306324 // Remove canvas from DOM and React state
307325 const canvas = canvasRefs . current . get ( streamId ) ;
308326 if ( canvas ) {
@@ -396,6 +414,8 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
396414 cleanedUp = true ;
397415 controlSocket ?. close ( ) ;
398416 deviceSockets . forEach ( ws => ws . close ( ) ) ;
417+ decoderWorkers . current . forEach ( worker => worker . terminate ( ) ) ;
418+ decoderWorkers . current . clear ( ) ;
399419 } ;
400420 } , [ ] ) ;
401421
0 commit comments