Skip to content

Commit e5155c0

Browse files
committed
fix: Fix black screen
Was mainly caused by a race condition while changing codec - Concurrent session guard (ExactReadableEndedError fix): - Add activeStartStreaming set to track running supervisor loops per IP - Add pendingStartStreaming map to queue the latest duplicate call instead of dropping it - Queued call fires automatically in the finally block - Better handles the codec switch timing - Codec flag race fix (h264SearchConfiguration "Invalid data" crash): - Capture this.useH265 into sessionIsH265 at the start of each runSession
1 parent 5b96d0d commit e5155c0

1 file changed

Lines changed: 39 additions & 9 deletions

File tree

src/api/android/scrcpy/ScrcpyServer.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)