@midscene/android-playground 1.10.4 → 1.10.5-beta-20260714101501.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/bin.mjs +93 -50
- package/dist/es/index.mjs +93 -50
- package/dist/lib/bin.js +93 -50
- package/dist/lib/index.js +93 -50
- package/dist/types/scrcpy-preview-status.d.ts +8 -0
- package/dist/types/scrcpy-server.d.ts +2 -0
- package/package.json +5 -5
- package/static/index.html +1 -1
- package/static/static/css/index.2b6b43ad.css +2 -0
- package/static/static/css/index.2b6b43ad.css.map +1 -0
- package/static/static/js/{445.b67c87ae.js → 890.0322764c.js} +3 -3
- package/static/static/js/{445.b67c87ae.js.LICENSE.txt → 890.0322764c.js.LICENSE.txt} +0 -2
- package/static/static/js/890.0322764c.js.map +1 -0
- package/static/static/js/index.e8830865.js +960 -0
- package/static/static/js/index.e8830865.js.map +1 -0
- package/static/static/css/index.1293237f.css +0 -2
- package/static/static/css/index.1293237f.css.map +0 -1
- package/static/static/js/445.b67c87ae.js.map +0 -1
- package/static/static/js/index.4506e773.js +0 -960
- package/static/static/js/index.4506e773.js.map +0 -1
- /package/static/static/js/{index.4506e773.js.LICENSE.txt → index.e8830865.js.LICENSE.txt} +0 -0
package/dist/es/bin.mjs
CHANGED
|
@@ -146,6 +146,14 @@ const androidPlaygroundPlatform = definePlaygroundPlatform({
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
});
|
|
149
|
+
function buildScrcpyPreviewErrorEvent(reason, sessionId, message) {
|
|
150
|
+
return {
|
|
151
|
+
reason,
|
|
152
|
+
sessionId,
|
|
153
|
+
message,
|
|
154
|
+
recoverable: 'process-exited' === reason || 'startup-timeout' === reason || 'stream-ended' === reason || 'stream-read-failed' === reason
|
|
155
|
+
};
|
|
156
|
+
}
|
|
149
157
|
function getScrcpyPreviewStatusMessage(phase) {
|
|
150
158
|
switch(phase){
|
|
151
159
|
case 'connecting-device':
|
|
@@ -224,6 +232,11 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
224
232
|
'::1',
|
|
225
233
|
'[::1]'
|
|
226
234
|
]);
|
|
235
|
+
const MAX_SCRCPY_OUTPUT_LINES = 100;
|
|
236
|
+
function appendBoundedScrcpyOutput(outputLines, line, maxLines = MAX_SCRCPY_OUTPUT_LINES) {
|
|
237
|
+
outputLines.push(line);
|
|
238
|
+
if (outputLines.length > maxLines) outputLines.splice(0, outputLines.length - maxLines);
|
|
239
|
+
}
|
|
227
240
|
function isPrivateIP(hostname) {
|
|
228
241
|
return LOOPBACK_HOSTS.has(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) || /^100\.(6[4-9]|[7-9]\d|1[0-2]\d)\./.test(hostname);
|
|
229
242
|
}
|
|
@@ -385,8 +398,48 @@ class ScrcpyServer {
|
|
|
385
398
|
setupSocketHandlers() {
|
|
386
399
|
this.io.on('connection', async (socket)=>{
|
|
387
400
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
388
|
-
let
|
|
401
|
+
let activeSession = null;
|
|
402
|
+
let sessionGeneration = 0;
|
|
389
403
|
let adb = null;
|
|
404
|
+
const closeScrcpySession = async (reason, sessionToClose = activeSession)=>{
|
|
405
|
+
if (!sessionToClose) return;
|
|
406
|
+
sessionToClose.closeReason ??= reason;
|
|
407
|
+
if (activeSession === sessionToClose) activeSession = null;
|
|
408
|
+
try {
|
|
409
|
+
debugPage('closing scrcpy session %s, reason: %s', sessionToClose.id, reason);
|
|
410
|
+
await sessionToClose.client.close();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error(`failed to close scrcpy client (${reason}):`, error);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const emitPreviewError = (session, reason, message)=>{
|
|
416
|
+
if (session.failureReported || session.closeReason) return;
|
|
417
|
+
session.failureReported = true;
|
|
418
|
+
debugPage('scrcpy session %s failed (%s), recent output:\n%s', session.id, reason, session.outputLines.join('\n'));
|
|
419
|
+
if (socket.connected) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(reason, session.id, message));
|
|
420
|
+
};
|
|
421
|
+
const monitorScrcpySession = (session)=>{
|
|
422
|
+
(async ()=>{
|
|
423
|
+
try {
|
|
424
|
+
const reader = session.client.output.getReader();
|
|
425
|
+
while(true){
|
|
426
|
+
const { done, value } = await reader.read();
|
|
427
|
+
if (done) break;
|
|
428
|
+
for (const line of String(value).split(/\r?\n/))if (line) {
|
|
429
|
+
appendBoundedScrcpyOutput(session.outputLines, line);
|
|
430
|
+
debugPage('scrcpy[%s]: %s', session.id, line);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (!session.closeReason) debugPage('failed reading scrcpy output for %s: %s', session.id, error);
|
|
435
|
+
}
|
|
436
|
+
})();
|
|
437
|
+
Promise.resolve(session.client.exited).then(()=>{
|
|
438
|
+
emitPreviewError(session, 'process-exited', 'Scrcpy service exited. Recovering preview.');
|
|
439
|
+
}).catch((error)=>{
|
|
440
|
+
emitPreviewError(session, 'process-exited', `Scrcpy service exited unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
|
|
441
|
+
});
|
|
442
|
+
};
|
|
390
443
|
const emitPreviewStatus = (phase)=>{
|
|
391
444
|
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
392
445
|
};
|
|
@@ -413,10 +466,8 @@ class ScrcpyServer {
|
|
|
413
466
|
socket.on('switch-device', async (deviceId)=>{
|
|
414
467
|
debugPage('received client request to switch device:', deviceId);
|
|
415
468
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
scrcpyClient = null;
|
|
419
|
-
}
|
|
469
|
+
sessionGeneration += 1;
|
|
470
|
+
await closeScrcpySession('switch device');
|
|
420
471
|
this.currentDeviceId = deviceId;
|
|
421
472
|
debugPage('device switched to:', deviceId);
|
|
422
473
|
socket.emit('device-switched', {
|
|
@@ -435,21 +486,34 @@ class ScrcpyServer {
|
|
|
435
486
|
});
|
|
436
487
|
socket.on('connect-device', async (options = {})=>{
|
|
437
488
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
489
|
+
const generation = sessionGeneration + 1;
|
|
490
|
+
sessionGeneration = generation;
|
|
491
|
+
const sessionId = `${socket.id}:${generation}`;
|
|
492
|
+
const isCurrentGeneration = ()=>socket.connected && sessionGeneration === generation;
|
|
438
493
|
try {
|
|
439
494
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
495
|
+
await closeScrcpySession('new connect-device request');
|
|
440
496
|
emitPreviewStatus('connecting-device');
|
|
441
497
|
const requestedDeviceId = resolveRequestedDeviceId(options, this.currentDeviceId);
|
|
442
498
|
if (requestedDeviceId) this.currentDeviceId = requestedDeviceId;
|
|
443
499
|
adb = await this.getAdb(requestedDeviceId);
|
|
444
500
|
if (!adb) {
|
|
445
501
|
console.error('no available device found');
|
|
446
|
-
socket.emit('error',
|
|
447
|
-
message: 'No device found'
|
|
448
|
-
});
|
|
502
|
+
socket.emit('preview-error', buildScrcpyPreviewErrorEvent('adb-unavailable', sessionId, 'No Android device is available.'));
|
|
449
503
|
return;
|
|
450
504
|
}
|
|
505
|
+
if (!isCurrentGeneration()) return;
|
|
451
506
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
452
|
-
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
507
|
+
const scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
508
|
+
if (!isCurrentGeneration()) return void await scrcpyClient.close();
|
|
509
|
+
const session = {
|
|
510
|
+
client: scrcpyClient,
|
|
511
|
+
failureReported: false,
|
|
512
|
+
id: sessionId,
|
|
513
|
+
outputLines: []
|
|
514
|
+
};
|
|
515
|
+
activeSession = session;
|
|
516
|
+
monitorScrcpySession(session);
|
|
453
517
|
debugPage('scrcpy service started successfully');
|
|
454
518
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
455
519
|
const type = typeof scrcpyClient[name];
|
|
@@ -469,6 +533,7 @@ class ScrcpyServer {
|
|
|
469
533
|
emitPreviewStatus('waiting-for-video');
|
|
470
534
|
videoStream = scrcpyClient.videoStream;
|
|
471
535
|
}
|
|
536
|
+
if (!isCurrentGeneration() || activeSession !== session) return void await closeScrcpySession('stale video stream', session);
|
|
472
537
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
473
538
|
const metadata = videoStream.metadata || {};
|
|
474
539
|
debugPage('original metadata: %s', metadata);
|
|
@@ -485,14 +550,16 @@ class ScrcpyServer {
|
|
|
485
550
|
socket.emit('video-metadata', metadata);
|
|
486
551
|
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
487
552
|
const { stream } = videoStream;
|
|
553
|
+
const streamSession = session;
|
|
488
554
|
const reader = stream.getReader();
|
|
489
555
|
const processStream = async ()=>{
|
|
490
556
|
try {
|
|
491
557
|
while(true){
|
|
492
558
|
const { done, value } = await reader.read();
|
|
493
559
|
if (done) break;
|
|
560
|
+
if (!isCurrentGeneration() || activeSession !== streamSession) return;
|
|
494
561
|
const frameType = value.type || 'data';
|
|
495
|
-
socket.emit('video-data', {
|
|
562
|
+
socket.volatile.emit('video-data', {
|
|
496
563
|
data: value.data,
|
|
497
564
|
type: frameType,
|
|
498
565
|
timestamp: Date.now(),
|
|
@@ -501,63 +568,39 @@ class ScrcpyServer {
|
|
|
501
568
|
}
|
|
502
569
|
} catch (error) {
|
|
503
570
|
console.error('error processing video stream:', error);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
});
|
|
571
|
+
emitPreviewError(streamSession, 'stream-read-failed', `Scrcpy video stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
572
|
+
closeScrcpySession('video stream error', streamSession);
|
|
507
573
|
return;
|
|
508
574
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
});
|
|
512
|
-
if (scrcpyClient) {
|
|
513
|
-
try {
|
|
514
|
-
await scrcpyClient.close();
|
|
515
|
-
} catch (closeError) {
|
|
516
|
-
console.error('failed to close scrcpy client after stream ended:', closeError);
|
|
517
|
-
}
|
|
518
|
-
scrcpyClient = null;
|
|
519
|
-
}
|
|
575
|
+
emitPreviewError(streamSession, 'stream-ended', 'Scrcpy video stream ended. Recovering preview.');
|
|
576
|
+
closeScrcpySession('video stream ended', streamSession);
|
|
520
577
|
};
|
|
521
578
|
processStream();
|
|
522
579
|
} else {
|
|
523
580
|
console.error('scrcpyClient object does not have videoStream property');
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
581
|
+
emitPreviewError(session, 'unknown', 'Video stream is not available in the scrcpy client.');
|
|
582
|
+
await closeScrcpySession('video stream unavailable', session);
|
|
583
|
+
return;
|
|
527
584
|
}
|
|
528
585
|
} catch (error) {
|
|
529
586
|
console.error('error processing video stream:', error);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
});
|
|
587
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Video stream processing error: ${error.message}`);
|
|
588
|
+
await closeScrcpySession('video stream setup error', session);
|
|
533
589
|
}
|
|
534
590
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
535
591
|
} catch (error) {
|
|
536
592
|
console.error('failed to connect device:', error);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
scrcpyClient = null;
|
|
544
|
-
}
|
|
545
|
-
socket.emit('error', {
|
|
546
|
-
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
547
|
-
});
|
|
593
|
+
const session = activeSession;
|
|
594
|
+
if (session?.id === sessionId) {
|
|
595
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Failed to connect device: ${error?.message || 'Unknown error'}`);
|
|
596
|
+
await closeScrcpySession('connect-device error', session);
|
|
597
|
+
} else if (isCurrentGeneration()) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', sessionId, `Failed to connect device: ${error?.message || 'Unknown error'}`));
|
|
548
598
|
}
|
|
549
599
|
});
|
|
550
600
|
socket.on('disconnect', async (reason)=>{
|
|
551
601
|
debugPage('client disconnected, id: %s, reason: %s', socket.id, reason);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
debugPage('closing scrcpy client');
|
|
555
|
-
await scrcpyClient.close();
|
|
556
|
-
} catch (error) {
|
|
557
|
-
console.error('failed to close scrcpy client:', error);
|
|
558
|
-
}
|
|
559
|
-
scrcpyClient = null;
|
|
560
|
-
}
|
|
602
|
+
sessionGeneration += 1;
|
|
603
|
+
await closeScrcpySession(`socket disconnect: ${reason}`);
|
|
561
604
|
});
|
|
562
605
|
sendDevicesList();
|
|
563
606
|
});
|
package/dist/es/index.mjs
CHANGED
|
@@ -146,6 +146,14 @@ const androidPlaygroundPlatform = definePlaygroundPlatform({
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
});
|
|
149
|
+
function buildScrcpyPreviewErrorEvent(reason, sessionId, message) {
|
|
150
|
+
return {
|
|
151
|
+
reason,
|
|
152
|
+
sessionId,
|
|
153
|
+
message,
|
|
154
|
+
recoverable: 'process-exited' === reason || 'startup-timeout' === reason || 'stream-ended' === reason || 'stream-read-failed' === reason
|
|
155
|
+
};
|
|
156
|
+
}
|
|
149
157
|
function getScrcpyPreviewStatusMessage(phase) {
|
|
150
158
|
switch(phase){
|
|
151
159
|
case 'connecting-device':
|
|
@@ -224,6 +232,11 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
224
232
|
'::1',
|
|
225
233
|
'[::1]'
|
|
226
234
|
]);
|
|
235
|
+
const MAX_SCRCPY_OUTPUT_LINES = 100;
|
|
236
|
+
function appendBoundedScrcpyOutput(outputLines, line, maxLines = MAX_SCRCPY_OUTPUT_LINES) {
|
|
237
|
+
outputLines.push(line);
|
|
238
|
+
if (outputLines.length > maxLines) outputLines.splice(0, outputLines.length - maxLines);
|
|
239
|
+
}
|
|
227
240
|
function isPrivateIP(hostname) {
|
|
228
241
|
return LOOPBACK_HOSTS.has(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || /^192\.168\./.test(hostname) || /^100\.(6[4-9]|[7-9]\d|1[0-2]\d)\./.test(hostname);
|
|
229
242
|
}
|
|
@@ -385,8 +398,48 @@ class ScrcpyServer {
|
|
|
385
398
|
setupSocketHandlers() {
|
|
386
399
|
this.io.on('connection', async (socket)=>{
|
|
387
400
|
debugPage('client connected, id: %s, client address: %s', socket.id, socket.handshake.address);
|
|
388
|
-
let
|
|
401
|
+
let activeSession = null;
|
|
402
|
+
let sessionGeneration = 0;
|
|
389
403
|
let adb = null;
|
|
404
|
+
const closeScrcpySession = async (reason, sessionToClose = activeSession)=>{
|
|
405
|
+
if (!sessionToClose) return;
|
|
406
|
+
sessionToClose.closeReason ??= reason;
|
|
407
|
+
if (activeSession === sessionToClose) activeSession = null;
|
|
408
|
+
try {
|
|
409
|
+
debugPage('closing scrcpy session %s, reason: %s', sessionToClose.id, reason);
|
|
410
|
+
await sessionToClose.client.close();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error(`failed to close scrcpy client (${reason}):`, error);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const emitPreviewError = (session, reason, message)=>{
|
|
416
|
+
if (session.failureReported || session.closeReason) return;
|
|
417
|
+
session.failureReported = true;
|
|
418
|
+
debugPage('scrcpy session %s failed (%s), recent output:\n%s', session.id, reason, session.outputLines.join('\n'));
|
|
419
|
+
if (socket.connected) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(reason, session.id, message));
|
|
420
|
+
};
|
|
421
|
+
const monitorScrcpySession = (session)=>{
|
|
422
|
+
(async ()=>{
|
|
423
|
+
try {
|
|
424
|
+
const reader = session.client.output.getReader();
|
|
425
|
+
while(true){
|
|
426
|
+
const { done, value } = await reader.read();
|
|
427
|
+
if (done) break;
|
|
428
|
+
for (const line of String(value).split(/\r?\n/))if (line) {
|
|
429
|
+
appendBoundedScrcpyOutput(session.outputLines, line);
|
|
430
|
+
debugPage('scrcpy[%s]: %s', session.id, line);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (!session.closeReason) debugPage('failed reading scrcpy output for %s: %s', session.id, error);
|
|
435
|
+
}
|
|
436
|
+
})();
|
|
437
|
+
Promise.resolve(session.client.exited).then(()=>{
|
|
438
|
+
emitPreviewError(session, 'process-exited', 'Scrcpy service exited. Recovering preview.');
|
|
439
|
+
}).catch((error)=>{
|
|
440
|
+
emitPreviewError(session, 'process-exited', `Scrcpy service exited unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
|
|
441
|
+
});
|
|
442
|
+
};
|
|
390
443
|
const emitPreviewStatus = (phase)=>{
|
|
391
444
|
socket.emit('preview-status', buildScrcpyPreviewStatusEvent(phase));
|
|
392
445
|
};
|
|
@@ -413,10 +466,8 @@ class ScrcpyServer {
|
|
|
413
466
|
socket.on('switch-device', async (deviceId)=>{
|
|
414
467
|
debugPage('received client request to switch device:', deviceId);
|
|
415
468
|
try {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
scrcpyClient = null;
|
|
419
|
-
}
|
|
469
|
+
sessionGeneration += 1;
|
|
470
|
+
await closeScrcpySession('switch device');
|
|
420
471
|
this.currentDeviceId = deviceId;
|
|
421
472
|
debugPage('device switched to:', deviceId);
|
|
422
473
|
socket.emit('device-switched', {
|
|
@@ -435,21 +486,34 @@ class ScrcpyServer {
|
|
|
435
486
|
});
|
|
436
487
|
socket.on('connect-device', async (options = {})=>{
|
|
437
488
|
const { ScrcpyVideoCodecId } = await import("@yume-chan/scrcpy");
|
|
489
|
+
const generation = sessionGeneration + 1;
|
|
490
|
+
sessionGeneration = generation;
|
|
491
|
+
const sessionId = `${socket.id}:${generation}`;
|
|
492
|
+
const isCurrentGeneration = ()=>socket.connected && sessionGeneration === generation;
|
|
438
493
|
try {
|
|
439
494
|
debugPage('received device connection request, options: %s, client id: %s', options, socket.id);
|
|
495
|
+
await closeScrcpySession('new connect-device request');
|
|
440
496
|
emitPreviewStatus('connecting-device');
|
|
441
497
|
const requestedDeviceId = resolveRequestedDeviceId(options, this.currentDeviceId);
|
|
442
498
|
if (requestedDeviceId) this.currentDeviceId = requestedDeviceId;
|
|
443
499
|
adb = await this.getAdb(requestedDeviceId);
|
|
444
500
|
if (!adb) {
|
|
445
501
|
console.error('no available device found');
|
|
446
|
-
socket.emit('error',
|
|
447
|
-
message: 'No device found'
|
|
448
|
-
});
|
|
502
|
+
socket.emit('preview-error', buildScrcpyPreviewErrorEvent('adb-unavailable', sessionId, 'No Android device is available.'));
|
|
449
503
|
return;
|
|
450
504
|
}
|
|
505
|
+
if (!isCurrentGeneration()) return;
|
|
451
506
|
debugPage('starting scrcpy service, device id: %s', this.currentDeviceId);
|
|
452
|
-
scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
507
|
+
const scrcpyClient = await this.startScrcpy(adb, options, emitPreviewStatus);
|
|
508
|
+
if (!isCurrentGeneration()) return void await scrcpyClient.close();
|
|
509
|
+
const session = {
|
|
510
|
+
client: scrcpyClient,
|
|
511
|
+
failureReported: false,
|
|
512
|
+
id: sessionId,
|
|
513
|
+
outputLines: []
|
|
514
|
+
};
|
|
515
|
+
activeSession = session;
|
|
516
|
+
monitorScrcpySession(session);
|
|
453
517
|
debugPage('scrcpy service started successfully');
|
|
454
518
|
debugPage('check scrcpyClient object structure: %s', Object.getOwnPropertyNames(scrcpyClient).map((name)=>{
|
|
455
519
|
const type = typeof scrcpyClient[name];
|
|
@@ -469,6 +533,7 @@ class ScrcpyServer {
|
|
|
469
533
|
emitPreviewStatus('waiting-for-video');
|
|
470
534
|
videoStream = scrcpyClient.videoStream;
|
|
471
535
|
}
|
|
536
|
+
if (!isCurrentGeneration() || activeSession !== session) return void await closeScrcpySession('stale video stream', session);
|
|
472
537
|
debugPage('video stream fetched successfully, metadata: %s', videoStream.metadata);
|
|
473
538
|
const metadata = videoStream.metadata || {};
|
|
474
539
|
debugPage('original metadata: %s', metadata);
|
|
@@ -485,14 +550,16 @@ class ScrcpyServer {
|
|
|
485
550
|
socket.emit('video-metadata', metadata);
|
|
486
551
|
debugPage('video-metadata event sent to client, id: %s, timeout budget: %ss', socket.id, Math.round(SCRCPY_PREVIEW_METADATA_TIMEOUT_MS / 1000));
|
|
487
552
|
const { stream } = videoStream;
|
|
553
|
+
const streamSession = session;
|
|
488
554
|
const reader = stream.getReader();
|
|
489
555
|
const processStream = async ()=>{
|
|
490
556
|
try {
|
|
491
557
|
while(true){
|
|
492
558
|
const { done, value } = await reader.read();
|
|
493
559
|
if (done) break;
|
|
560
|
+
if (!isCurrentGeneration() || activeSession !== streamSession) return;
|
|
494
561
|
const frameType = value.type || 'data';
|
|
495
|
-
socket.emit('video-data', {
|
|
562
|
+
socket.volatile.emit('video-data', {
|
|
496
563
|
data: value.data,
|
|
497
564
|
type: frameType,
|
|
498
565
|
timestamp: Date.now(),
|
|
@@ -501,63 +568,39 @@ class ScrcpyServer {
|
|
|
501
568
|
}
|
|
502
569
|
} catch (error) {
|
|
503
570
|
console.error('error processing video stream:', error);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
});
|
|
571
|
+
emitPreviewError(streamSession, 'stream-read-failed', `Scrcpy video stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
572
|
+
closeScrcpySession('video stream error', streamSession);
|
|
507
573
|
return;
|
|
508
574
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
});
|
|
512
|
-
if (scrcpyClient) {
|
|
513
|
-
try {
|
|
514
|
-
await scrcpyClient.close();
|
|
515
|
-
} catch (closeError) {
|
|
516
|
-
console.error('failed to close scrcpy client after stream ended:', closeError);
|
|
517
|
-
}
|
|
518
|
-
scrcpyClient = null;
|
|
519
|
-
}
|
|
575
|
+
emitPreviewError(streamSession, 'stream-ended', 'Scrcpy video stream ended. Recovering preview.');
|
|
576
|
+
closeScrcpySession('video stream ended', streamSession);
|
|
520
577
|
};
|
|
521
578
|
processStream();
|
|
522
579
|
} else {
|
|
523
580
|
console.error('scrcpyClient object does not have videoStream property');
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
581
|
+
emitPreviewError(session, 'unknown', 'Video stream is not available in the scrcpy client.');
|
|
582
|
+
await closeScrcpySession('video stream unavailable', session);
|
|
583
|
+
return;
|
|
527
584
|
}
|
|
528
585
|
} catch (error) {
|
|
529
586
|
console.error('error processing video stream:', error);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
});
|
|
587
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Video stream processing error: ${error.message}`);
|
|
588
|
+
await closeScrcpySession('video stream setup error', session);
|
|
533
589
|
}
|
|
534
590
|
if (scrcpyClient?.controller) socket.emit('control-ready');
|
|
535
591
|
} catch (error) {
|
|
536
592
|
console.error('failed to connect device:', error);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
scrcpyClient = null;
|
|
544
|
-
}
|
|
545
|
-
socket.emit('error', {
|
|
546
|
-
message: `Failed to connect device: ${error?.message || 'Unknown error'}`
|
|
547
|
-
});
|
|
593
|
+
const session = activeSession;
|
|
594
|
+
if (session?.id === sessionId) {
|
|
595
|
+
emitPreviewError(session, error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', `Failed to connect device: ${error?.message || 'Unknown error'}`);
|
|
596
|
+
await closeScrcpySession('connect-device error', session);
|
|
597
|
+
} else if (isCurrentGeneration()) socket.emit('preview-error', buildScrcpyPreviewErrorEvent(error?.message?.includes('Timed out') ? 'startup-timeout' : 'unknown', sessionId, `Failed to connect device: ${error?.message || 'Unknown error'}`));
|
|
548
598
|
}
|
|
549
599
|
});
|
|
550
600
|
socket.on('disconnect', async (reason)=>{
|
|
551
601
|
debugPage('client disconnected, id: %s, reason: %s', socket.id, reason);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
debugPage('closing scrcpy client');
|
|
555
|
-
await scrcpyClient.close();
|
|
556
|
-
} catch (error) {
|
|
557
|
-
console.error('failed to close scrcpy client:', error);
|
|
558
|
-
}
|
|
559
|
-
scrcpyClient = null;
|
|
560
|
-
}
|
|
602
|
+
sessionGeneration += 1;
|
|
603
|
+
await closeScrcpySession(`socket disconnect: ${reason}`);
|
|
561
604
|
});
|
|
562
605
|
sendDevicesList();
|
|
563
606
|
});
|