@@ -253,24 +253,54 @@ export class ScrcpyServer {
253253 return ;
254254 }
255255
256- while ( true ) {
257- const shouldRestart = await this . runSession ( adbConnection , streamIp , deviceModel , flipWidth ) ;
258- if ( ! shouldRestart ) break ;
259- logger . info ( `[${ streamIp } ] Restarting stream in 1s...` ) ;
260- await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
256+ // Prevent two supervisor loops from running concurrently for the same IP
257+ // (would cause two AdbScrcpyClient.start() calls to race on the same ADB
258+ // connection → ExactReadableEndedError). Instead of dropping the duplicate,
259+ // queue it so it runs as soon as the current session fully exits.
260+ // Only the latest pending call is kept — older ones are overwritten.
261+ if ( this . activeStartStreaming . has ( streamIp ) ) {
262+ logger . debug ( `[${ streamIp } ] startStreaming already active, queuing for after current session exits` ) ;
263+ this . pendingStartStreaming . set ( streamIp , ( ) => {
264+ void this . startStreaming ( adbConnection , deviceModel , flipWidth ) ;
265+ } ) ;
266+ return ;
267+ }
268+ this . activeStartStreaming . add ( streamIp ) ;
269+
270+ try {
271+ while ( true ) {
272+ const shouldRestart = await this . runSession ( adbConnection , streamIp , deviceModel , flipWidth ) ;
273+ if ( ! shouldRestart ) break ;
274+ logger . info ( `[${ streamIp } ] Restarting stream in 1s...` ) ;
275+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
276+ }
277+ } finally {
278+ this . activeStartStreaming . delete ( streamIp ) ;
279+ // Run the latest pending call now that the session slot is free.
280+ const pending = this . pendingStartStreaming . get ( streamIp ) ;
281+ if ( pending ) {
282+ this . pendingStartStreaming . delete ( streamIp ) ;
283+ pending ( ) ;
284+ }
261285 }
262286 }
263287
264288 // Runs one scrcpy session from start to finish.
265289 // Returns true → caller should restart (unexpected crash or natural exit).
266290 // Returns false → caller should stop (intentional abort, e.g. codec switch).
267291 private async runSession ( adbConnection : Adb , streamIp : string , deviceModel : string , flipWidth : boolean ) : Promise < boolean > {
292+ // Capture the codec once at session start. this.useH265 can change mid-session
293+ // (when a new client with different capabilities connects), but the write handler must
294+ // always tag packets with the codec this session actually encodes — mixing the flag
295+ // with data from a different codec causes h264SearchConfiguration "Invalid data" crashes.
296+ const sessionIsH265 = this . useH265 ; // snapshot: this.useH265 may change during the session
297+
268298 // Build fresh options each session so a codec switch is picked up correctly.
269299 const scrcpyOptions = new AdbScrcpyOptions3_3_3 ( {
270300 // scrcpy options
271301 // No videoCodecOptions: let the Android encoder choose its own profile/level.
272302 // Decoding is done by WebCodecs in the browser, which supports any H264/H265 profile.
273- videoCodec : ( useH265 ? "h265" : "h264" ) ,
303+ videoCodec : ( sessionIsH265 ? "h265" : "h264" ) ,
274304 // Video settings
275305 video : true ,
276306 maxSize : 1570 ,
@@ -284,7 +314,7 @@ export class ScrcpyServer {
284314 } ,
285315 // Spoofing version, there's only bug-fixing between .3 and .4 so should be safe
286316 { version : "3.3.4" } ) ;
287- logger . debug ( `[${ streamIp } ] Starting scrcpy stream with ${ useH265 ? "h265" : "h264" } codec` ) ;
317+ logger . debug ( `[${ streamIp } ] Starting scrcpy stream with ${ sessionIsH265 ? "h265" : "h264" } codec` ) ;
288318
289319 let client : AdbScrcpyClient < AdbScrcpyOptions3_3_3 < true > > | undefined ;
290320
@@ -369,7 +399,7 @@ export class ScrcpyServer {
369399 { // Handle configuration packet
370400 const newStreamConfig = JSON . stringify ( {
371401 streamId : streamIp ,
372- h265 : useH265 ,
402+ h265 : sessionIsH265 ,
373403 type : "configuration" ,
374404 data : Buffer . from ( packet . data ) . toString ( 'base64' ) , // Convert Uint8Array to Base64 string
375405 } ) ;
@@ -385,7 +415,7 @@ export class ScrcpyServer {
385415 streamIp ,
386416 JSON . stringify ( {
387417 streamId : streamIp ,
388- h265 : useH265 ,
418+ h265 : sessionIsH265 ,
389419 type : "data" ,
390420 keyframe : packet . keyframe ,
391421 // @ts -expect-error
0 commit comments