Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 90 additions & 31 deletions src/components/WebSocketManager/VideoStreamManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@
},
});

if (typeof VideoDecoder === 'undefined') {
logger.warn("[Scrcpy-VideoStreamManager] WebCodecs API (VideoDecoder) is not available in this browser, aborting stream");
readableControllers.delete(deviceId);
worker.terminate();
decoderWorkers.current.delete(deviceId);
return;
}

await VideoDecoder.isConfigSupported({
// Check if h265 is supported
codec: "hev1.1.60.L153.B0.0.0.0.0.0",
Expand All @@ -180,16 +188,64 @@
if (supported.supported || !useH265) {
const codec = useH265 ? ScrcpyVideoCodecId.H265 : ScrcpyVideoCodecId.H264;

// Pass objects and stream to worker
worker.postMessage(
{
codec,
canvas: offscreenCanvas,
stream,
useH265
},
[offscreenCanvas, stream]
);
// Check if browser supports transferring ReadableStream
let canTransferStream = false;
try {
const { port1 } = new MessageChannel();
const testStream = new ReadableStream();
port1.postMessage(testStream, [testStream]);
canTransferStream = true;
} catch (e) {
canTransferStream = false;
}

Check warning on line 200 in src/components/WebSocketManager/VideoStreamManager.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=project-SIMPLE_simple.webplatform&issues=AZ0n9yyAassvNkiMpIot&open=AZ0n9yyAassvNkiMpIot&pullRequest=137

if (canTransferStream) {
// Pass objects and stream to worker directly
worker.postMessage(
{
codec,
canvas: offscreenCanvas,
stream,
useH265,
type: 'direct'
},
[offscreenCanvas, stream]
);
} else {
// Fallback for browsers that don't support transferring ReadableStream (like Safari)
logger.info("[Scrcpy-VideoStreamManager] ReadableStream transfer not supported, using MessageChannel fallback");
const { port1, port2 } = new MessageChannel();
worker.postMessage(
{ codec, canvas: offscreenCanvas, port: port2, useH265, type: 'port' },
[offscreenCanvas, port2]
);

const reader = stream.getReader();
(async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
port1.postMessage({ done: true });
break;
}
const transferables: Transferable[] = [];
// Clone the buffer to avoid detaching it if it's needed elsewhere,
// or just send the value. In Firefox, detaching was causing issues.
// However, since we fallback to MessageChannel ONLY when ReadableStream transfer fails,
// Firefox (which supports stream transfer) will use the direct path above.
if (value?.data instanceof Uint8Array) {
transferables.push(value.data.buffer);
}
port1.postMessage({ done: false, value }, transferables);
}
} catch {
port1.postMessage({ done: true });
} finally {
port1.close();
}
})();
}
} else {
logger.error("[Scrcpy] Error piping to decoder writable stream");
}
Expand All @@ -211,6 +267,7 @@
// Reconnects automatically after 1 s on unexpected close.
function connectDeviceSocket(streamId: string) {
if (cleanedUp) return;
if (typeof VideoDecoder === 'undefined') return;

// Prevent the stale socket's onclose from firing a reconnect when we replace it
const existing = deviceSockets.get(streamId);
Expand Down Expand Up @@ -339,32 +396,34 @@

// Send browser's codecs compatibility
socket.onopen = async () => {
let supportH264: boolean, supportH265: boolean, supportAv1: boolean;
// Check if h264 is supported
await VideoDecoder.isConfigSupported({ codec: "avc1.4D401E" }).then((r) => {
supportH264 = r.supported!;
logger.info("[SCRCPY] Supports h264: {supportH264}", { supportH264 });
})

// Check if h265 is supported
await VideoDecoder.isConfigSupported({ codec: "hev1.1.60.L153.B0.0.0.0.0.0" }).then((r) => {
supportH265 = r.supported!;
logger.info("[SCRCPY] Supports h265 {supportH265}", { supportH265 });
})

// Check if AV1 is supported
await VideoDecoder.isConfigSupported({ codec: "av01.0.05M.08" }).then((r) => {
supportAv1 = r.supported!;
logger.info("[SCRCPY] Supports AV1 {supportAv1}", { supportAv1 });
})
let supportH264 = false, supportH265 = false, supportAv1 = false;

if (typeof VideoDecoder === 'undefined') {
logger.warn("[SCRCPY] WebCodecs API not available, reporting no codec support");
} else {
// Check if h264 is supported
await VideoDecoder.isConfigSupported({ codec: "avc1.4D401E" }).then((r) => {
supportH264 = r.supported!;
logger.info("[SCRCPY] Supports h264: {supportH264}", { supportH264 });
})

// Check if h265 is supported
await VideoDecoder.isConfigSupported({ codec: "hev1.1.60.L153.B0.0.0.0.0.0" }).then((r) => {
supportH265 = r.supported!;
logger.info("[SCRCPY] Supports h265 {supportH265}", { supportH265 });
})

// Check if AV1 is supported
await VideoDecoder.isConfigSupported({ codec: "av01.0.05M.08" }).then((r) => {
supportAv1 = r.supported!;
logger.info("[SCRCPY] Supports AV1 {supportAv1}", { supportAv1 });
})
}

socket.send(JSON.stringify({
"type": "codecVideo",
// @ts-expect-error
"h264": supportH264,
// @ts-expect-error
"h265": supportH265,
// @ts-expect-error
"av1": supportAv1,
}));
}
Expand Down
36 changes: 33 additions & 3 deletions src/workers/scrcpyDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
} from "@yume-chan/scrcpy-decoder-webcodecs";

self.addEventListener("message", (e) => {
const { codec, canvas, stream, useH265 } = e.data as {
const { codec, canvas, stream, port, useH265, type } = e.data as {
codec: ScrcpyVideoCodecId;
canvas: OffscreenCanvas;
stream: ReadableStream<ScrcpyMediaStreamPacket>;
stream?: ReadableStream<ScrcpyMediaStreamPacket>;
port?: MessagePort;
useH265: boolean;
type: 'direct' | 'port';
};

let renderer;
Expand All @@ -30,7 +32,35 @@ self.addEventListener("message", (e) => {
postMessage({ type: 'sizeChanged', width, height });
});

void stream.pipeTo(decoder.writable).catch((err) => {
let activeStream: ReadableStream<ScrcpyMediaStreamPacket>;

if (type === 'direct' && stream) {
activeStream = stream;
} else if (type === 'port' && port) {
// Reconstruct a ReadableStream from the MessagePort (Safari doesn't support
// transferring ReadableStream directly via postMessage).
activeStream = new ReadableStream<ScrcpyMediaStreamPacket>({
start(controller) {
port.onmessage = ({ data }) => {
if (data.done) {
controller.close();
port.close();
} else {
controller.enqueue(data.value as ScrcpyMediaStreamPacket);
}
};
port.start();
},
cancel() {
port.close();
},
});
} else {
console.error("[Worker] Invalid stream transfer type or missing stream/port.");
return;
}

void activeStream.pipeTo(decoder.writable).catch((err) => {
console.error("[Worker] Error piping to decoder writable stream:", err);
});
});
Loading