Skip to content

Commit 86fd2c7

Browse files
fix: fallback to MessagePort when ReadableStream transfer is unsupported
Safari does not support transferring ReadableStream objects directly to Web Workers via postMessage. This commit adds a feature detection check to see if transferring a ReadableStream is supported by the browser. If supported (like in Chrome and Firefox), it continues to transfer the stream directly for optimal performance. If unsupported (like in Safari), it falls back to using a MessageChannel to stream chunks manually to the worker, where the stream is reconstructed. Also adds checks for VideoDecoder availability to prevent errors in unsupported environments. Co-authored-by: RoiArthurB <16764085+RoiArthurB@users.noreply.github.com>
1 parent 944e3b2 commit 86fd2c7

2 files changed

Lines changed: 123 additions & 34 deletions

File tree

src/components/WebSocketManager/VideoStreamManager.tsx

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
165165
},
166166
});
167167

168+
if (typeof VideoDecoder === 'undefined') {
169+
logger.warn("[Scrcpy-VideoStreamManager] WebCodecs API (VideoDecoder) is not available in this browser, aborting stream");
170+
readableControllers.delete(deviceId);
171+
worker.terminate();
172+
decoderWorkers.current.delete(deviceId);
173+
return;
174+
}
175+
168176
await VideoDecoder.isConfigSupported({
169177
// Check if h265 is supported
170178
codec: "hev1.1.60.L153.B0.0.0.0.0.0",
@@ -180,16 +188,64 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
180188
if (supported.supported || !useH265) {
181189
const codec = useH265 ? ScrcpyVideoCodecId.H265 : ScrcpyVideoCodecId.H264;
182190

183-
// Pass objects and stream to worker
184-
worker.postMessage(
185-
{
186-
codec,
187-
canvas: offscreenCanvas,
188-
stream,
189-
useH265
190-
},
191-
[offscreenCanvas, stream]
192-
);
191+
// Check if browser supports transferring ReadableStream
192+
let canTransferStream = false;
193+
try {
194+
const { port1 } = new MessageChannel();
195+
const testStream = new ReadableStream();
196+
port1.postMessage(testStream, [testStream]);
197+
canTransferStream = true;
198+
} catch (e) {
199+
canTransferStream = false;
200+
}
201+
202+
if (canTransferStream) {
203+
// Pass objects and stream to worker directly
204+
worker.postMessage(
205+
{
206+
codec,
207+
canvas: offscreenCanvas,
208+
stream,
209+
useH265,
210+
type: 'direct'
211+
},
212+
[offscreenCanvas, stream]
213+
);
214+
} else {
215+
// Fallback for browsers that don't support transferring ReadableStream (like Safari)
216+
logger.info("[Scrcpy-VideoStreamManager] ReadableStream transfer not supported, using MessageChannel fallback");
217+
const { port1, port2 } = new MessageChannel();
218+
worker.postMessage(
219+
{ codec, canvas: offscreenCanvas, port: port2, useH265, type: 'port' },
220+
[offscreenCanvas, port2]
221+
);
222+
223+
const reader = stream.getReader();
224+
(async () => {
225+
try {
226+
while (true) {
227+
const { done, value } = await reader.read();
228+
if (done) {
229+
port1.postMessage({ done: true });
230+
break;
231+
}
232+
const transferables: Transferable[] = [];
233+
// Clone the buffer to avoid detaching it if it's needed elsewhere,
234+
// or just send the value. In Firefox, detaching was causing issues.
235+
// However, since we fallback to MessageChannel ONLY when ReadableStream transfer fails,
236+
// Firefox (which supports stream transfer) will use the direct path above.
237+
if (value?.data instanceof Uint8Array) {
238+
transferables.push(value.data.buffer);
239+
}
240+
port1.postMessage({ done: false, value }, transferables);
241+
}
242+
} catch {
243+
port1.postMessage({ done: true });
244+
} finally {
245+
port1.close();
246+
}
247+
})();
248+
}
193249
} else {
194250
logger.error("[Scrcpy] Error piping to decoder writable stream");
195251
}
@@ -211,6 +267,7 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
211267
// Reconnects automatically after 1 s on unexpected close.
212268
function connectDeviceSocket(streamId: string) {
213269
if (cleanedUp) return;
270+
if (typeof VideoDecoder === 'undefined') return;
214271

215272
// Prevent the stale socket's onclose from firing a reconnect when we replace it
216273
const existing = deviceSockets.get(streamId);
@@ -339,32 +396,34 @@ const VideoStreamManager = ({ needsInteractivity, selectedCanvas, hideInfos }: V
339396

340397
// Send browser's codecs compatibility
341398
socket.onopen = async () => {
342-
let supportH264: boolean, supportH265: boolean, supportAv1: boolean;
343-
// Check if h264 is supported
344-
await VideoDecoder.isConfigSupported({ codec: "avc1.4D401E" }).then((r) => {
345-
supportH264 = r.supported!;
346-
logger.info("[SCRCPY] Supports h264: {supportH264}", { supportH264 });
347-
})
348-
349-
// Check if h265 is supported
350-
await VideoDecoder.isConfigSupported({ codec: "hev1.1.60.L153.B0.0.0.0.0.0" }).then((r) => {
351-
supportH265 = r.supported!;
352-
logger.info("[SCRCPY] Supports h265 {supportH265}", { supportH265 });
353-
})
354-
355-
// Check if AV1 is supported
356-
await VideoDecoder.isConfigSupported({ codec: "av01.0.05M.08" }).then((r) => {
357-
supportAv1 = r.supported!;
358-
logger.info("[SCRCPY] Supports AV1 {supportAv1}", { supportAv1 });
359-
})
399+
let supportH264 = false, supportH265 = false, supportAv1 = false;
400+
401+
if (typeof VideoDecoder === 'undefined') {
402+
logger.warn("[SCRCPY] WebCodecs API not available, reporting no codec support");
403+
} else {
404+
// Check if h264 is supported
405+
await VideoDecoder.isConfigSupported({ codec: "avc1.4D401E" }).then((r) => {
406+
supportH264 = r.supported!;
407+
logger.info("[SCRCPY] Supports h264: {supportH264}", { supportH264 });
408+
})
409+
410+
// Check if h265 is supported
411+
await VideoDecoder.isConfigSupported({ codec: "hev1.1.60.L153.B0.0.0.0.0.0" }).then((r) => {
412+
supportH265 = r.supported!;
413+
logger.info("[SCRCPY] Supports h265 {supportH265}", { supportH265 });
414+
})
415+
416+
// Check if AV1 is supported
417+
await VideoDecoder.isConfigSupported({ codec: "av01.0.05M.08" }).then((r) => {
418+
supportAv1 = r.supported!;
419+
logger.info("[SCRCPY] Supports AV1 {supportAv1}", { supportAv1 });
420+
})
421+
}
360422

361423
socket.send(JSON.stringify({
362424
"type": "codecVideo",
363-
// @ts-expect-error
364425
"h264": supportH264,
365-
// @ts-expect-error
366426
"h265": supportH265,
367-
// @ts-expect-error
368427
"av1": supportAv1,
369428
}));
370429
}

src/workers/scrcpyDecoder.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
} from "@yume-chan/scrcpy-decoder-webcodecs";
77

88
self.addEventListener("message", (e) => {
9-
const { codec, canvas, stream, useH265 } = e.data as {
9+
const { codec, canvas, stream, port, useH265, type } = e.data as {
1010
codec: ScrcpyVideoCodecId;
1111
canvas: OffscreenCanvas;
12-
stream: ReadableStream<ScrcpyMediaStreamPacket>;
12+
stream?: ReadableStream<ScrcpyMediaStreamPacket>;
13+
port?: MessagePort;
1314
useH265: boolean;
15+
type: 'direct' | 'port';
1416
};
1517

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

33-
void stream.pipeTo(decoder.writable).catch((err) => {
35+
let activeStream: ReadableStream<ScrcpyMediaStreamPacket>;
36+
37+
if (type === 'direct' && stream) {
38+
activeStream = stream;
39+
} else if (type === 'port' && port) {
40+
// Reconstruct a ReadableStream from the MessagePort (Safari doesn't support
41+
// transferring ReadableStream directly via postMessage).
42+
activeStream = new ReadableStream<ScrcpyMediaStreamPacket>({
43+
start(controller) {
44+
port.onmessage = ({ data }) => {
45+
if (data.done) {
46+
controller.close();
47+
port.close();
48+
} else {
49+
controller.enqueue(data.value as ScrcpyMediaStreamPacket);
50+
}
51+
};
52+
port.start();
53+
},
54+
cancel() {
55+
port.close();
56+
},
57+
});
58+
} else {
59+
console.error("[Worker] Invalid stream transfer type or missing stream/port.");
60+
return;
61+
}
62+
63+
void activeStream.pipeTo(decoder.writable).catch((err) => {
3464
console.error("[Worker] Error piping to decoder writable stream:", err);
3565
});
3666
});

0 commit comments

Comments
 (0)