From 0a73a7f2fc23dbd71c6b926ddde7b77273d3abd5 Mon Sep 17 00:00:00 2001 From: RoiArthurB Date: Thu, 5 Mar 2026 17:37:22 +0700 Subject: [PATCH 1/5] refactor: Replace shared video WebSocket with per-device sockets to fix backpressure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Android device now gets a dedicated WebSocket at /stream/:deviceIp (IP only, port stripped for stability across ADB reconnects). All packets (config then data) flow in order on the same socket, eliminating config/data ordering races. The existing ws('/*') becomes a lightweight control channel that only carries codec negotiation and stream_available announcements — no video data. Clients open a per-device socket when they receive an announcement, and reconnect automatically after 1s on close. The upgrade handler validates against activeStreams dynamically so there is no hardcoded stream count. Each stream gets an independent 8 MB backpressure budget with safeSend() dropping frames per-stream instead of globally. Co-Authored-By: Claude Sonnet 4.6 --- src/api/android/scrcpy/ScrcpyServer.ts | 125 ++++++++++++++++-- .../WebSocketManager/VideoStreamManager.tsx | 119 +++++++++++------ 2 files changed, 192 insertions(+), 52 deletions(-) diff --git a/src/api/android/scrcpy/ScrcpyServer.ts b/src/api/android/scrcpy/ScrcpyServer.ts index 809ce7c..5a67882 100644 --- a/src/api/android/scrcpy/ScrcpyServer.ts +++ b/src/api/android/scrcpy/ScrcpyServer.ts @@ -37,8 +37,11 @@ export class ScrcpyServer { // ======================= // WebSocket private wsServer!: TemplatedApp; - private wsClients: Set>; - private maxBackpressure: number = 8 * 1024 * 1024; // 8 MB per socket + private wsClients: Set>; // control channel: codec negotiation + stream_available announcements + private streamClients: Map>>; // per-device data sockets, keyed by device IP + private activeStreams: Set; // device IPs with a live scrcpy session (used to validate /stream/:id upgrades) + private scrcpyClientsByIp: Map>>; // for triggering config reset on new device socket + // Previously: private maxBackpressure = 8 * 1024 * 1024 — now inlined in each ws handler below private scrcpyClients: AdbScrcpyClient>[] = watchList([], () => { logger.debug("Scrcpy clients changed, restarting all video streams"); @@ -64,6 +67,9 @@ export class ScrcpyServer { this.adbManager = adbManager; this.wsClients = new Set>(); + this.streamClients = new Map(); + this.activeStreams = new Set(); + this.scrcpyClientsByIp = new Map(); const host = process.env.WEB_APPLICATION_HOST || '0.0.0.0'; const port = parseInt(process.env.VIDEO_WS_PORT || '8082', 10); @@ -87,17 +93,19 @@ export class ScrcpyServer { compression: uWS.SHARED_COMPRESSOR, // Enable compression maxPayloadLength: 256 * 1024, // 256 KB: Adjust based on expected video bitrate & 6 video streams // When backpressure exceeds this, uWS *drops the connection* - maxBackpressure: this.maxBackpressure, + maxBackpressure: 8 * 1024 * 1024, // 8 MB — previously this.maxBackpressure idleTimeout: 100, // 100 seconds (<2min) timeout // Send pings to uphold a stable connection sendPingsAutomatically: true, open: (ws) => { this.wsClients.add(ws); - logger.debug("Web view connected"); + logger.debug("Control client connected"); - // Restart every video stream on new client to have them all sync - this.resentAllConfigPackage(true).then(() => logger.debug("Streams restarted for new client connected")); + // Announce all currently active streams so the client can open their per-device data sockets + for (const ip of this.activeStreams) { + ws.send(JSON.stringify({ type: "stream_available", streamId: ip }), false, false); + } }, drain: (ws) => { @@ -172,6 +180,60 @@ export class ScrcpyServer { } } }); + + // Per-device data sockets: all packets (config + data) for a device flow here in order. + // Clients open this after receiving a stream_available announcement on the control socket above. + this.wsServer.ws<{ streamId: string }>('/stream/:id', { + compression: uWS.DISABLED, + maxPayloadLength: 256 * 1024, + maxBackpressure: 8 * 1024 * 1024, // 8 MB per stream — previously this.maxBackpressure + idleTimeout: 100, + sendPingsAutomatically: true, + + upgrade: (res, req, context) => { + const ip = req.getParameter(0); + if (!this.activeStreams.has(ip)) { + res.writeStatus('404 Not Found').end('Stream not found'); + return; + } + res.upgrade<{ streamId: string }>( + { streamId: ip }, + req.getHeader('sec-websocket-key'), + req.getHeader('sec-websocket-protocol'), + req.getHeader('sec-websocket-extensions'), + context + ); + }, + + open: (ws) => { + const { streamId } = ws.getUserData(); + if (!this.streamClients.has(streamId)) { + this.streamClients.set(streamId, new Set()); + } + this.streamClients.get(streamId)!.add(ws); + logger.debug(`[${streamId}] Device socket connected`); + + // Trigger a config reset so this client receives a fresh config + keyframe + const scrcpyClient = this.scrcpyClientsByIp.get(streamId); + if (scrcpyClient) { + this.resentConfigPackage(scrcpyClient) + .then(() => logger.debug(`[${streamId}] Config resent for new device socket client`)); + } + }, + + drain: (ws) => { + const { streamId } = ws.getUserData(); + logger + .getChild("Drain") + .info(`[${streamId}] Backpressure drained, streaming should be back to normal, buffered: ${ws.getBufferedAmount()}`); + }, + + close: (ws, code, message) => { + const { streamId } = ws.getUserData(); + this.streamClients.get(streamId)?.delete(ws); + logger.info(`[${streamId}] Device socket closed. Code: ${code}, Reason: ${Buffer.from(message).toString()}`); + }, + }); } async loadScrcpyServer() { @@ -210,6 +272,8 @@ export class ScrcpyServer { logger.debug(`Starting scrcpy stream with ${useH265 ? "h265" : "h264"} codec`) try { + const streamIp = adbConnection.serial.split(':')[0]; // device IP, port stripped — stable across ADB reconnects + if (this.server == null) { await this.loadScrcpyServer(); } @@ -258,6 +322,8 @@ export class ScrcpyServer { client.exited .then(() => { logger.info(`Scrcpy server exited for ${adbConnection.serial}`); + this.activeStreams.delete(streamIp); + this.scrcpyClientsByIp.delete(streamIp); // Remove client from array const index = this.scrcpyClients.indexOf(client); if (index > -1) { @@ -275,6 +341,8 @@ export class ScrcpyServer { logger.error(`Unexpected exit error for ${adbConnection.serial}: {error}`, { error }); } + this.activeStreams.delete(streamIp); + this.scrcpyClientsByIp.delete(streamIp); // Remove client from array const index = this.scrcpyClients.indexOf(client); if (index > -1) { @@ -296,9 +364,13 @@ export class ScrcpyServer { ); } - // Store the controller of new client + // Register stream as active and store the controller logger.debug(`Saving new scrcpy client ${adbConnection.serial}`); + this.activeStreams.add(streamIp); + this.scrcpyClientsByIp.set(streamIp, client); this.scrcpyClients.push(client); + // Announce to all connected control clients so they open /stream/:streamIp + this.broadcastToClients(JSON.stringify({ type: "stream_available", streamId: streamIp })); // Print output of Scrcpy server client.output.pipeTo( @@ -324,13 +396,13 @@ export class ScrcpyServer { case "configuration": { // Handle configuration packet const newStreamConfig = JSON.stringify({ - streamId: adbConnection.serial, + streamId: streamIp, h265: useH265, type: "configuration", data: Buffer.from(packet.data).toString('base64'), // Convert Uint8Array to Base64 string }); - // Save packet for clients after this first packet emission - myself.broadcastToClients(newStreamConfig); + // Send to all clients on this device's data socket + myself.broadcastToStream(streamIp, newStreamConfig); logger.trace("Sending configuration frame {newStreamConfig}", { newStreamConfig }) // It is sent only once while opening the video stream and set the renderer @@ -339,10 +411,11 @@ export class ScrcpyServer { break; case "data": - // Handle data packet - myself.broadcastToClients( + // Handle data packet — sent on the dedicated per-device socket + myself.broadcastToStream( + streamIp, JSON.stringify({ - streamId: adbConnection.serial, + streamId: streamIp, h265: useH265, type: "data", keyframe: packet.keyframe, @@ -440,6 +513,32 @@ export class ScrcpyServer { return gotError; } + private safeSend(ws: uWS.WebSocket<{ streamId: string }>, packetJson: string): void { + const customLogger = logger.getChild("Drain"); + const { streamId } = ws.getUserData(); + + if (ws.getBufferedAmount() > 6 * 1024 * 1024) { // 6 MB threshold → 2 MB room before maxBackpressure + customLogger.warn(`[${streamId}] Dropping frame — client too slow`); + return; + } + + /* Possible returned values: + 0 : OK + 1 : Backpressure built up (but still queued) + 2 : Message dropped — backpressure limit exceeded + - The last one is the only interesting one + */ + if (ws.send(packetJson, false, true) === 2) { + customLogger.error(`[${streamId}] Video stream frame dropped...`); + } + } + + broadcastToStream(ip: string, packetJson: string): void { + this.streamClients.get(ip)?.forEach((client) => { + this.safeSend(client, packetJson); + }); + } + broadcastToClients(packetJson: string): void { const customLogger: Logger = logger.getChild("Drain"); this.wsClients.forEach((client) => { diff --git a/src/components/WebSocketManager/VideoStreamManager.tsx b/src/components/WebSocketManager/VideoStreamManager.tsx index b8573bd..b2f3b58 100644 --- a/src/components/WebSocketManager/VideoStreamManager.tsx +++ b/src/components/WebSocketManager/VideoStreamManager.tsx @@ -196,7 +196,70 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // ------------------------------------------------------------------------------------------------------------------- useEffect(() => { - // Open the WebSocket connection + let cleanedUp = false; + const deviceSockets = new Map(); + + // Opens a dedicated socket for a device at /stream/:deviceIp. + // All packets (config then data) arrive here in order — no split-channel ordering issues. + // Reconnects automatically after 1 s on unexpected close. + function connectDeviceSocket(streamId: string) { + if (cleanedUp) return; + + // Prevent the stale socket's onclose from firing a reconnect when we replace it + const existing = deviceSockets.get(streamId); + if (existing && existing.readyState < WebSocket.CLOSING) { + existing.onclose = null; + existing.close(); + } + + const ws = new WebSocket(`ws://${host}:${port}/stream/${streamId}`); + deviceSockets.set(streamId, ws); + + ws.onmessage = (event) => { + // Deserialize the message and enqueue the data into the readable stream + const deserializedData = deserializeData(event.data); + + // Create stream if new stream + if (!readableControllers.has(deserializedData!.streamId)) { + newVideoStream(deserializedData!.streamId, deserializedData!.useH265); + } + + const controller = readableControllers.get(deserializedData!.streamId); + + // Since we set very early the entry before the controller exists, + // this catch potential race conditions where controller do not exists + if (controller != undefined) { + // Enqueue data package to decoder stream + if (deserializedData!.packet) { + if ( + isDecoderHasConfig.get(deserializedData!.streamId) && + deserializedData!.packet.type == "data" + ) { + controller!.enqueue(deserializedData!.packet); + // Ensure starting stream with a configuration package holding keyframe + } else if ( + //\!isDecoderHasConfig.get(deserializedData!.streamId) && + deserializedData!.packet.type == "configuration" + ) { + controller!.enqueue(deserializedData!.packet); + isDecoderHasConfig.set(deserializedData!.streamId, true); + } + } else { + logger.warn("[Scrcpy] Error piping to decoder writable stream, closing controller..."); + controller!.close(); + } + } + }; + + ws.onclose = () => { + if (!cleanedUp) { + logger.info(`[Scrcpy-VideoStreamManager] Device socket for ${streamId} closed, reconnecting in 1s...`); + setTimeout(() => connectDeviceSocket(streamId), 1000); + } + }; + } + + // Control socket: codec negotiation (client→server) + stream_available announcements (server→client) const socket = new WebSocket("ws://" + host + ":" + port); // Send browser's codecs compatibility socket.onopen = async () => { @@ -204,7 +267,7 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // Check if h264 is supported await VideoDecoder.isConfigSupported({ codec: "avc1.4D401E" }).then((r) => { supportH264 = r.supported!; - logger.info("[SCRCPY] Supports h264: {supportsH264}", { supportH264 }); + logger.info("[SCRCPY] Supports h264: {supportH264}", { supportH264 }); }) // Check if h265 is supported @@ -221,54 +284,32 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V socket.send(JSON.stringify({ "type": "codecVideo", - // @ts-expect-error + // @ts-expect-error "h264": supportH264, - // @ts-expect-error + // @ts-expect-error "h265": supportH265, - // @ts-expect-error + // @ts-expect-error "av1": supportAv1, })); } - // Handle incoming WebSocket messages + // Handle stream_available announcements — open a dedicated socket per device socket.onmessage = (event) => { - // Deserialize the message and enqueue the data into the readable stream - const deserializedData = deserializeData(event.data); - - // Create stream if new stream - if (!readableControllers.has(deserializedData!.streamId)) { - newVideoStream(deserializedData!.streamId, deserializedData!.useH265); - } - - const controller = readableControllers.get(deserializedData!.streamId); - - // Since we set very early the entry before the controller exists, - // this catch potential race conditions where controller do not exists - if (controller != undefined) { - // Enqueue data package to decoder stream - if (deserializedData!.packet) { - if ( - isDecoderHasConfig.get(deserializedData!.streamId) && - deserializedData!.packet.type == "data" - ) { - controller!.enqueue(deserializedData!.packet); - // Ensure starting stream with a configuration package holding keyframe - } else if ( - //\!isDecoderHasConfig.get(deserializedData!.streamId) && - deserializedData!.packet.type == "configuration" - ) { - controller!.enqueue(deserializedData!.packet); - isDecoderHasConfig.set(deserializedData!.streamId, true); - } - } else { - logger.warn("[Scrcpy] Error piping to decoder writable stream, closing controller..."); - controller!.close(); - } + const data = JSON.parse(event.data); + if (data.type === "stream_available") { + logger.info(`[Scrcpy-VideoStreamManager] Stream available: ${data.streamId}`); + connectDeviceSocket(data.streamId); } }; socket.onclose = () => { - logger.info("[Scrcpy-VideoStreamManager] Closing readable"); + logger.info("[Scrcpy-VideoStreamManager] Control socket closed"); + }; + + return () => { + cleanedUp = true; + socket.close(); + deviceSockets.forEach(ws => ws.close()); }; }, []); From d4e5f8d0e9d6e3b6fe8e7a7e0ecf0d9f6b497d2c Mon Sep 17 00:00:00 2001 From: RoiArthurB Date: Thu, 5 Mar 2026 18:01:25 +0700 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20Resolve=20codec-switch=20crash=20and?= =?UTF-8?q?=20stale=20canvas=20on=20h265=E2=86=92h264=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Null out `existing.onmessage` before closing the old device socket so queued h265 packets in the receive buffer don't feed a newly created h264 decoder, eliminating the `Invalid data at h265SearchConfiguration` crash - Add `streamIsH265` map to detect mid-stream codec changes and reset decoder state when a config packet arrives with a different codec - Remove stale canvas element via `querySelector('canvas')?.remove()` before appending the new one in PlayerScreenCanvas, so the DOM never holds two overlapping canvases after a stream restart --- .../WebSocketManager/PlayerScreenCanvas.tsx | 4 ++-- .../WebSocketManager/VideoStreamManager.tsx | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/WebSocketManager/PlayerScreenCanvas.tsx b/src/components/WebSocketManager/PlayerScreenCanvas.tsx index 751a4e8..7ca5ba2 100644 --- a/src/components/WebSocketManager/PlayerScreenCanvas.tsx +++ b/src/components/WebSocketManager/PlayerScreenCanvas.tsx @@ -47,6 +47,7 @@ const PlayerScreenCanvas = ({ canvas, id, isPlaceholder, hideInfos, isLimitingWi if (showPopup) { if (popupref.current) { + popupref.current.querySelector('canvas')?.remove(); popupref.current.appendChild(canvas); canvas.classList.add("max-h-[95dvh]") canvas.classList.add("max-w-[95dvw]") @@ -63,8 +64,7 @@ const PlayerScreenCanvas = ({ canvas, id, isPlaceholder, hideInfos, isLimitingWi canvas.classList.add("max-w-[95dvw]") } - - + canvasref.current.querySelector('canvas')?.remove(); canvasref.current.appendChild(canvas); } } diff --git a/src/components/WebSocketManager/VideoStreamManager.tsx b/src/components/WebSocketManager/VideoStreamManager.tsx index b2f3b58..1c627d9 100644 --- a/src/components/WebSocketManager/VideoStreamManager.tsx +++ b/src/components/WebSocketManager/VideoStreamManager.tsx @@ -89,6 +89,8 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V ReadableStreamDefaultController | undefined >(); const isDecoderHasConfig = new Map(); + // Tracks the codec each decoder was created with — used to detect mid-stream codec changes + const streamIsH265 = new Map(); @@ -208,8 +210,13 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // Prevent the stale socket's onclose from firing a reconnect when we replace it const existing = deviceSockets.get(streamId); if (existing && existing.readyState < WebSocket.CLOSING) { + existing.onmessage = null; // prevent queued messages from old socket being processed after replacement existing.onclose = null; existing.close(); + // Reset decoder state: the stream is restarting, possibly with a different codec. + // Clearing these forces newVideoStream() to recreate the decoder on the next config packet. + readableControllers.delete(streamId); + isDecoderHasConfig.delete(streamId); } const ws = new WebSocket(`ws://${host}:${port}/stream/${streamId}`); @@ -219,6 +226,19 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // Deserialize the message and enqueue the data into the readable stream const deserializedData = deserializeData(event.data); + // Detect codec change on every configuration packet, regardless of channel ordering. + // The server can switch codec (h265↔h264) and restart streams; the new config packet + // may arrive on the old device socket before stream_available fires on the control + // socket, so we can't rely on connectDeviceSocket having run first. + if (deserializedData!.packet.type === "configuration") { + const knownCodec = streamIsH265.get(deserializedData!.streamId); + if (knownCodec !== undefined && knownCodec !== deserializedData!.useH265) { + readableControllers.delete(deserializedData!.streamId); + isDecoderHasConfig.delete(deserializedData!.streamId); + } + streamIsH265.set(deserializedData!.streamId, deserializedData!.useH265); + } + // Create stream if new stream if (!readableControllers.has(deserializedData!.streamId)) { newVideoStream(deserializedData!.streamId, deserializedData!.useH265); From 63b37e597554d67ed353f19b71f17bdc9d0509cd Mon Sep 17 00:00:00 2001 From: RoiArthurB Date: Fri, 6 Mar 2026 09:26:16 +0700 Subject: [PATCH 3/5] fix: Fix Firefox h264 WebCodecs failure and stream_available race on codec switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Client (VideoStreamManager.tsx): - Pass hardwareAcceleration: "prefer-software" to WebCodecsVideoDecoder for h264 so Firefox uses its software decoder (OpenH264) instead of failing on the hardware WebCodecs path, fixing "DOMException: The given encoding is not supported" Server (ScrcpyServer.ts): - Remove TinyH264Decoder videoCodecOptions restriction — encoding profile/level is now left to the Android encoder since decoding is done by WebCodecs in the browser (not TinyH264), removing an unnecessary Baseline-only constraint - Clear activeStreams + scrcpyClientsByIp immediately when a codec switch starts so control clients reconnecting during the transition get no stale stream_available announcements - Guard exited handlers with scrcpyClientsByIp.get(streamIp) === client check so a departing client's async exit callback never wipes the new client's registration, which was the root cause of stream_available being lost after a codec switch --- src/api/android/scrcpy/ScrcpyServer.ts | 36 +++++++++++-------- .../WebSocketManager/VideoStreamManager.tsx | 4 +++ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/api/android/scrcpy/ScrcpyServer.ts b/src/api/android/scrcpy/ScrcpyServer.ts index 5a67882..2b47145 100644 --- a/src/api/android/scrcpy/ScrcpyServer.ts +++ b/src/api/android/scrcpy/ScrcpyServer.ts @@ -3,9 +3,8 @@ import path from "path"; import { ReadableStream } from "@yume-chan/stream-extra"; import { Adb } from "@yume-chan/adb"; -import { DefaultServerPath, ScrcpyMediaStreamPacket, ScrcpyCodecOptions } from "@yume-chan/scrcpy"; +import { DefaultServerPath, ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy"; import { AdbScrcpyClient, AdbScrcpyExitedError, AdbScrcpyOptions3_3_3 } from "@yume-chan/adb-scrcpy"; -import { TinyH264Decoder } from "@yume-chan/scrcpy-decoder-tinyh264"; import uWS, { TemplatedApp } from "uWebSockets.js"; import {getLogger, Logger} from "@logtape/logtape"; @@ -14,8 +13,6 @@ import { AdbManager } from "../adb/AdbManager.ts"; // Override the log function const logger = getLogger(["android", "ScrcpyServer"]); -const H264Capabilities = TinyH264Decoder.capabilities.h264; - // Starts with optimistic settings let useH265: boolean = true; // Switch to true if at least 1 client doesn't h265 @@ -134,6 +131,11 @@ export class ScrcpyServer { // Reset video streams if codec changed ! if (previousCodec != useH265) { logger.warn(`Restarting streams with new codec (${useH265 ? "h265" : "h264"})`); + // Clear active streams immediately so control clients that reconnect + // during the transition don't receive stale stream_available announcements. + // The exited handlers on old clients will now find no matching entry to delete. + this.activeStreams.clear(); + this.scrcpyClientsByIp.clear(); for (const client of this.scrcpyClients) { await client.controller!.close(); await client.close(); @@ -251,10 +253,9 @@ export class ScrcpyServer { async startStreaming(adbConnection: Adb, deviceModel: string, flipWidth: boolean = false): Promise { const scrcpyOptions = new AdbScrcpyOptions3_3_3({ // scrcpy options - videoCodecOptions: new ScrcpyCodecOptions({ // Ensure Meta Quest compatibility - profile: H264Capabilities.maxProfile, - level: H264Capabilities.maxLevel, - }), + // No videoCodecOptions: let the Android encoder choose its own profile/level. + // Previously restricted to TinyH264's Baseline caps, but decoding is now done + // by WebCodecs in the browser which supports any H264/H265 profile. videoCodec: (useH265 ? "h265" : "h264"), // Video settings video: true, @@ -322,9 +323,15 @@ export class ScrcpyServer { client.exited .then(() => { logger.info(`Scrcpy server exited for ${adbConnection.serial}`); - this.activeStreams.delete(streamIp); - this.scrcpyClientsByIp.delete(streamIp); - // Remove client from array + // Guard: only clean up the IP entry if THIS client is still the registered + // one. During a codec switch a new client may have already taken over the + // same IP, and we must not wipe its registration. + if (this.scrcpyClientsByIp.get(streamIp) === client) { + this.activeStreams.delete(streamIp); + this.scrcpyClientsByIp.delete(streamIp); + } + // Removing from the flat list is always safe (indexOf will simply return -1 + // if the list was already cleared during a codec switch). const index = this.scrcpyClients.indexOf(client); if (index > -1) { this.scrcpyClients.splice(index, 1); @@ -341,9 +348,10 @@ export class ScrcpyServer { logger.error(`Unexpected exit error for ${adbConnection.serial}: {error}`, { error }); } - this.activeStreams.delete(streamIp); - this.scrcpyClientsByIp.delete(streamIp); - // Remove client from array + if (this.scrcpyClientsByIp.get(streamIp) === client) { + this.activeStreams.delete(streamIp); + this.scrcpyClientsByIp.delete(streamIp); + } const index = this.scrcpyClients.indexOf(client); if (index > -1) { this.scrcpyClients.splice(index, 1); diff --git a/src/components/WebSocketManager/VideoStreamManager.tsx b/src/components/WebSocketManager/VideoStreamManager.tsx index 1c627d9..62b74d3 100644 --- a/src/components/WebSocketManager/VideoStreamManager.tsx +++ b/src/components/WebSocketManager/VideoStreamManager.tsx @@ -156,6 +156,10 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V const decoder = new WebCodecsVideoDecoder({ codec: useH265 ? ScrcpyVideoCodecId.H265 : ScrcpyVideoCodecId.H264, renderer: renderer, + // Firefox on Linux has no hardware H264 WebCodecs path; "prefer-software" enables + // the software decoder (OpenH264) and avoids "encoding not supported" errors. + // H265 keeps "no-preference" so hardware acceleration is used when available. + hardwareAcceleration: useH265 ? "no-preference" : "prefer-software", }); //TODO fix ce log logger.log("[Scrcpy-VideoStreamManager] Decoder for {useH265} ? \"h265\" : \"h264\", loaded", { useH265: "h265" }); // Create new ReadableStream used for scrcpy decoding From 0cacc6eaf21cf34162217ea97f06f0c71c38f8f0 Mon Sep 17 00:00:00 2001 From: RoiArthurB Date: Fri, 6 Mar 2026 09:48:17 +0700 Subject: [PATCH 4/5] chore: Cleanup refactored branch - Cleaning some weird vibe-coded left-over - Simplify un-necessary `myself` in the backend - Entirely remove TinyH264 dependencies - All fully managed by upgraded WebCodecs - Rebuilt package-lock without chained dependencies - Remove old comments --- package-lock.json | 408 +++++++++++------- package.json | 1 - src/api/android/scrcpy/ScrcpyServer.ts | 30 +- .../WebSocketManager/VideoStreamManager.tsx | 12 - vite.config.ts | 3 +- 5 files changed, 267 insertions(+), 187 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6fcd446..ebeb561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@yume-chan/adb": "^2.5.1", "@yume-chan/adb-scrcpy": "^2.3.2", "@yume-chan/adb-server-node-tcp": "^2.5.2", - "@yume-chan/scrcpy-decoder-tinyh264": "^2.1.0", "@yume-chan/scrcpy-decoder-webcodecs": "^2.5.0", "@yume-chan/stream-extra": "2.1.0", "autoprefixer": "^10.4.20", @@ -1938,6 +1937,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1951,6 +1953,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1964,6 +1969,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1977,6 +1985,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1990,6 +2001,9 @@ "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2003,6 +2017,9 @@ "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2016,6 +2033,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2029,6 +2049,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2042,6 +2065,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2055,6 +2081,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2068,6 +2097,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2081,6 +2113,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2094,6 +2129,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2383,9 +2421,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", - "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3522,7 +3560,6 @@ "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -3543,12 +3580,11 @@ } }, "node_modules/bare-os": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz", - "integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -3559,7 +3595,6 @@ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } @@ -3570,7 +3605,6 @@ "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "streamx": "^2.21.0", "teex": "^1.0.1" @@ -3594,7 +3628,6 @@ "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -3674,6 +3707,31 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3930,9 +3988,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001776", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001776.tgz", + "integrity": "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==", "funding": [ { "type": "opencollective", @@ -4540,9 +4598,9 @@ } }, "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4727,53 +4785,12 @@ } }, "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==", + "license": "BSD", "dependencies": { - "safe-buffer": "~5.1.0" + "readable-stream": "~1.1.9" } }, "node_modules/ee-first": { @@ -4783,9 +4800,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "license": "ISC" }, "node_modules/emittery": { @@ -6400,9 +6417,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "license": "ISC" }, "node_modules/for-each": { @@ -6460,9 +6477,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -6773,9 +6790,9 @@ } }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -7951,9 +7968,9 @@ } }, "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "license": "MIT" }, "node_modules/isexe": { @@ -8789,6 +8806,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9325,39 +9348,6 @@ "duplexer2": "0.0.2" } }, - "node_modules/multipipe/node_modules/duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha512-+AWBwjGadtksxjOQSFDhPNQbed7icNXApT4+2BNpsXzcCBiInq2H9XW0O8sfHFaPmnQRs7cg/P0fAr2IWQSW0g==", - "license": "BSD", - "dependencies": { - "readable-stream": "~1.1.9" - } - }, - "node_modules/multipipe/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/multipipe/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/multipipe/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "license": "MIT" - }, "node_modules/multistream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", @@ -9383,6 +9373,31 @@ "readable-stream": "^3.6.0" } }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/multistream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/mute-stream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", @@ -9517,9 +9532,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/normalize-path": { @@ -10019,9 +10034,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -10209,6 +10224,31 @@ "dev": true, "license": "ISC" }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/prebuild-install/node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -10339,9 +10379,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -10601,18 +10641,15 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, "node_modules/readdirp": { @@ -10995,6 +11032,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -11032,6 +11076,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -11615,14 +11666,10 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" }, "node_modules/string-length": { "version": "4.0.2", @@ -12021,9 +12068,9 @@ } }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -12038,9 +12085,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", "dev": true, "license": "MIT", "dependencies": { @@ -12053,13 +12100,14 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -12095,7 +12143,6 @@ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "streamx": "^2.12.5" } @@ -12741,9 +12788,9 @@ } }, "node_modules/typescript-eslint/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -12863,6 +12910,56 @@ "node-int64": "^0.4.0" } }, + "node_modules/unzipper/node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -13155,6 +13252,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/which-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", diff --git a/package.json b/package.json index ede6ef0..bab55ad 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "@yume-chan/adb": "^2.5.1", "@yume-chan/adb-scrcpy": "^2.3.2", "@yume-chan/adb-server-node-tcp": "^2.5.2", - "@yume-chan/scrcpy-decoder-tinyh264": "^2.1.0", "@yume-chan/scrcpy-decoder-webcodecs": "^2.5.0", "@yume-chan/stream-extra": "2.1.0", "autoprefixer": "^10.4.20", diff --git a/src/api/android/scrcpy/ScrcpyServer.ts b/src/api/android/scrcpy/ScrcpyServer.ts index 2b47145..3646699 100644 --- a/src/api/android/scrcpy/ScrcpyServer.ts +++ b/src/api/android/scrcpy/ScrcpyServer.ts @@ -38,21 +38,17 @@ export class ScrcpyServer { private streamClients: Map>>; // per-device data sockets, keyed by device IP private activeStreams: Set; // device IPs with a live scrcpy session (used to validate /stream/:id upgrades) private scrcpyClientsByIp: Map>>; // for triggering config reset on new device socket - // Previously: private maxBackpressure = 8 * 1024 * 1024 — now inlined in each ws handler below + + private maxBackpressure: number = 8 * 1024 * 1024; // 8 MB private scrcpyClients: AdbScrcpyClient>[] = watchList([], () => { logger.debug("Scrcpy clients changed, restarting all video streams"); this.resentAllConfigPackage(); - });//[]; + }); // ======================= // Scrcpy server - declare server: Buffer; //ArrayBuffer; - - // ======================= - // Scrcpy stream - //@ts-expect-error this value is used line 321 - private scrcpyStreamConfig!: string; + declare server: Buffer; private adbManager!: AdbManager; @@ -72,7 +68,7 @@ export class ScrcpyServer { const port = parseInt(process.env.VIDEO_WS_PORT || '8082', 10); try { - this.wsServer = uWS.App(); //new WebSocketServer({ host, port }); + this.wsServer = uWS.App(); logger.info(`Creating video stream server on: ws://${host}:${port}`); } catch (e) { logger.error('Failed to create a new websocket {e}', { e }); @@ -90,7 +86,7 @@ export class ScrcpyServer { compression: uWS.SHARED_COMPRESSOR, // Enable compression maxPayloadLength: 256 * 1024, // 256 KB: Adjust based on expected video bitrate & 6 video streams // When backpressure exceeds this, uWS *drops the connection* - maxBackpressure: 8 * 1024 * 1024, // 8 MB — previously this.maxBackpressure + maxBackpressure: this.maxBackpressure, idleTimeout: 100, // 100 seconds (<2min) timeout // Send pings to uphold a stable connection sendPingsAutomatically: true, @@ -188,13 +184,13 @@ export class ScrcpyServer { this.wsServer.ws<{ streamId: string }>('/stream/:id', { compression: uWS.DISABLED, maxPayloadLength: 256 * 1024, - maxBackpressure: 8 * 1024 * 1024, // 8 MB per stream — previously this.maxBackpressure + maxBackpressure: this.maxBackpressure, // 8 MB per stream idleTimeout: 100, sendPingsAutomatically: true, upgrade: (res, req, context) => { const ip = req.getParameter(0); - if (!this.activeStreams.has(ip)) { + if (!ip || !this.activeStreams.has(ip)) { res.writeStatus('404 Not Found').end('Stream not found'); return; } @@ -254,8 +250,7 @@ export class ScrcpyServer { const scrcpyOptions = new AdbScrcpyOptions3_3_3({ // scrcpy options // No videoCodecOptions: let the Android encoder choose its own profile/level. - // Previously restricted to TinyH264's Baseline caps, but decoding is now done - // by WebCodecs in the browser which supports any H264/H265 profile. + // Decoding is done by WebCodecs in the browser, which supports any H264/H265 profile. videoCodec: (useH265 ? "h265" : "h264"), // Video settings video: true, @@ -284,13 +279,11 @@ export class ScrcpyServer { logger.debug(`Sync adb with ${adbConnection.serial}`); const sync = await adbConnection.sync(); try { - const myself = this; - await sync.write({ filename: DefaultServerPath, file: new ReadableStream({ start: (controller) => { - controller.enqueue(new Uint8Array(myself.server)); + controller.enqueue(new Uint8Array(this.server)); controller.close(); }, }), @@ -412,9 +405,6 @@ export class ScrcpyServer { // Send to all clients on this device's data socket myself.broadcastToStream(streamIp, newStreamConfig); logger.trace("Sending configuration frame {newStreamConfig}", { newStreamConfig }) - - // It is sent only once while opening the video stream and set the renderer - myself.scrcpyStreamConfig = newStreamConfig; } break; diff --git a/src/components/WebSocketManager/VideoStreamManager.tsx b/src/components/WebSocketManager/VideoStreamManager.tsx index 62b74d3..0a1a560 100644 --- a/src/components/WebSocketManager/VideoStreamManager.tsx +++ b/src/components/WebSocketManager/VideoStreamManager.tsx @@ -94,15 +94,6 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V - /** - * Creates a new ReadableStream for receiving and decoding H.264 video data associated with a specific device. - * - * This function initializes a ReadableStream that serves as the entry point for raw H.264 video data from a given device. - * It also sets up a TinyH264Decoder instance and pipes the ReadableStream's output to the decoder's writable stream. - * The decoded video frames are then rendered to an element referenced by `videoContainerRef`. - * - * @returns A ReadableStream that can be enqueued with data stream - */ async function newVideoStream(deviceId: string, useH265: boolean = false) { // Avoid having controller creation hell if connection is too fast @@ -161,7 +152,6 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V // H265 keeps "no-preference" so hardware acceleration is used when available. hardwareAcceleration: useH265 ? "no-preference" : "prefer-software", }); - //TODO fix ce log logger.log("[Scrcpy-VideoStreamManager] Decoder for {useH265} ? \"h265\" : \"h264\", loaded", { useH265: "h265" }); // Create new ReadableStream used for scrcpy decoding const stream = new ReadableStream({ start(controller) { @@ -260,9 +250,7 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V deserializedData!.packet.type == "data" ) { controller!.enqueue(deserializedData!.packet); - // Ensure starting stream with a configuration package holding keyframe } else if ( - //\!isDecoderHasConfig.get(deserializedData!.streamId) && deserializedData!.packet.type == "configuration" ) { controller!.enqueue(deserializedData!.packet); diff --git a/vite.config.ts b/vite.config.ts index 359673f..7548930 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,8 +23,7 @@ export default defineConfig(({ mode }) => { preview: serverConfig, server: serverConfig, optimizeDeps: { - exclude: ["@yume-chan/adb-scrcpy", "@yume-chan/stream-extra", "@yume-chan/scrcpy-decoder-tinyh264"], - include: ['@yume-chan/scrcpy-decoder-tinyh264 > yuv-buffer', '@yume-chan/scrcpy-decoder-tinyh264 > yuv-canvas'] + exclude: ["@yume-chan/adb-scrcpy", "@yume-chan/stream-extra"], }, define: { 'process.env.MONITOR_WS_PORT': JSON.stringify(env.MONITOR_WS_PORT), From 9f48425f90ff72b81e420ae4fe7b9c6ce70366ed Mon Sep 17 00:00:00 2001 From: RoiArthurB Date: Fri, 6 Mar 2026 10:26:18 +0700 Subject: [PATCH 5/5] chore: Fix code smells from SonarQube --- src/api/android/scrcpy/ScrcpyServer.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/api/android/scrcpy/ScrcpyServer.ts b/src/api/android/scrcpy/ScrcpyServer.ts index 3646699..cb6b145 100644 --- a/src/api/android/scrcpy/ScrcpyServer.ts +++ b/src/api/android/scrcpy/ScrcpyServer.ts @@ -33,13 +33,13 @@ function watchList(list: T[], onChange: () => void): T[] { export class ScrcpyServer { // ======================= // WebSocket - private wsServer!: TemplatedApp; - private wsClients: Set>; // control channel: codec negotiation + stream_available announcements - private streamClients: Map>>; // per-device data sockets, keyed by device IP - private activeStreams: Set; // device IPs with a live scrcpy session (used to validate /stream/:id upgrades) - private scrcpyClientsByIp: Map>>; // for triggering config reset on new device socket + private readonly wsServer!: TemplatedApp; + private readonly wsClients: Set>; // control channel: codec negotiation + stream_available announcements + private readonly streamClients: Map>>; // per-device data sockets, keyed by device IP + private readonly activeStreams: Set; // device IPs with a live scrcpy session (used to validate /stream/:id upgrades) + private readonly scrcpyClientsByIp: Map>>; // for triggering config reset on new device socket - private maxBackpressure: number = 8 * 1024 * 1024; // 8 MB + private readonly maxBackpressure: number = 8 * 1024 * 1024; // 8 MB private scrcpyClients: AdbScrcpyClient>[] = watchList([], () => { logger.debug("Scrcpy clients changed, restarting all video streams"); @@ -50,7 +50,7 @@ export class ScrcpyServer { // Scrcpy server declare server: Buffer; - private adbManager!: AdbManager; + private readonly adbManager!: AdbManager; constructor(adbManager: AdbManager) { // Set global variables