FreeRDP
Loading...
Searching...
No Matches
av1_webcodecs.c
1
18/*
19 * freerdp_av1_decompress() runs on the graphics channel thread (a Web Worker) and needs the
20 * picture back synchronously, while VideoDecoder delivers frames asynchronously on the event
21 * loop of the thread that owns it. The worker therefore proxies each job to the main browser
22 * thread with emscripten_proxy_sync_with_ctx() and sleeps until the main thread has decoded
23 * the frame, copied it into the destination surface and called emscripten_proxy_finish().
24 *
25 * VideoFrame.copyTo() converts to the surface's RGB layout directly, so the browser (GPU or
26 * its native decoder) does both the decoding and the colour conversion, honouring the colour
27 * space signalled in the bitstream.
28 */
29
30#include <freerdp/config.h>
31
32#include <emscripten.h>
33#include <emscripten/proxying.h>
34#include <emscripten/threading.h>
35
36#include <winpr/assert.h>
37#include <freerdp/codec/color.h>
38
39#include "av1_webcodecs.h"
40
41typedef struct
42{
43 int handle;
44 const BYTE* src;
45 UINT32 size;
46 BYTE* dst;
47 const char* format;
48 UINT32 stride;
49 UINT32 width;
50 UINT32 height;
51 int key;
52 int profile;
53 int rc; /* 1: picture written, 0: no picture, <0: error */
54} av1_wc_job;
55
56/* clang-format off */
57EM_JS(int, av1_wc_js_available, (void), {
58 return (typeof VideoDecoder === "function" && typeof EncodedVideoChunk === "function") ? 1 : 0;
59});
60
61EM_JS(int, av1_wc_js_open, (void), {
62 const reg = (Module.freerdpAv1 = Module.freerdpAv1 || { next : 1, decoders : new Map() });
63 const handle = reg.next++;
64 reg.decoders.set(handle, { decoder : null, profile : -1, pending : null });
65 return handle;
66});
67
68EM_JS(void, av1_wc_js_close, (int handle), {
69 const reg = Module.freerdpAv1;
70 const d = reg?.decoders.get(handle);
71 if (!d)
72 return;
73 try { d.decoder?.close(); } catch (e) {}
74 reg.decoders.delete(handle);
75});
76
77/* Runs on the main browser thread; stores the result in *rc and completes asynchronously
78 * through _freerdp_av1_wc_finish. */
79EM_JS(void, av1_wc_js_decode, (void* ctx, int handle, const BYTE* src, UINT32 size, BYTE* dst,
80 const char* formatName, UINT32 stride, UINT32 width, UINT32 height,
81 int key, int profile, int* rc), {
82 src >>>= 0; size >>>= 0; dst >>>= 0; stride >>>= 0; width >>>= 0; height >>>= 0;
83 const format = UTF8ToString(formatName);
84 const finish = (r) => { HEAP32[rc >> 2] = r; _freerdp_av1_wc_finish(ctx); };
85 const d = Module.freerdpAv1?.decoders.get(handle);
86 if (!d)
87 return finish(-1);
88
89 /* (re)configure on key frames that carry a different profile (4:2:0 = 0, 4:4:4 = 1) */
90 if (!d.decoder || d.decoder.state === "closed" || (key && profile >= 0 && profile !== d.profile)) {
91 if (!key)
92 return finish(0); /* WebCodecs must start with a key frame: drop until one arrives */
93 try { d.decoder?.close(); } catch (e) {}
94 d.profile = profile;
95 d.decoder = new VideoDecoder({
96 output : (frame) => {
97 const p = d.pending;
98 d.pending = null;
99 if (!p) { frame.close(); return; }
100 clearTimeout(p.timer);
101 const w = Math.min(frame.displayWidth, p.width), h = Math.min(frame.displayHeight, p.height);
102 const view = HEAPU8.subarray(p.dst, p.dst + p.stride * h);
103 frame.copyTo(view, { rect : { x : 0, y : 0, width : w, height : h },
104 layout : [ { offset : 0, stride : p.stride } ], format : p.format })
105 .then(() => { frame.close(); p.finish(1); },
106 (e) => { frame.close(); console.error("[av1-webcodecs] copyTo", e); p.finish(-3); });
107 },
108 error : (e) => {
109 console.error("[av1-webcodecs] decoder error", e);
110 const p = d.pending;
111 d.pending = null;
112 if (p) { clearTimeout(p.timer); p.finish(-2); }
113 }
114 });
115 d.decoder.configure({ codec : profile === 1 ? "av01.1.13M.08" : "av01.0.13M.08",
116 optimizeForLatency : true, hardwareAcceleration : "no-preference" });
117 }
118
119 d.pending = { dst, format, stride, width, height, finish,
120 /* low-latency streams release each frame at once; never hang the worker if not */
121 timer : setTimeout(() => { if (d.pending) { d.pending = null; finish(0); } }, 1000) };
122 try {
123 const data = HEAPU8.slice(src, src + size); /* EncodedVideoChunk copies; avoid SAB views */
124 d.decoder.decode(new EncodedVideoChunk({ type : key ? "key" : "delta", timestamp : 0, data }));
125 } catch (e) {
126 console.error("[av1-webcodecs] decode", e);
127 clearTimeout(d.pending.timer);
128 d.pending = null;
129 finish(-2);
130 }
131});
132/* clang-format on */
133
134EMSCRIPTEN_KEEPALIVE void freerdp_av1_wc_finish(em_proxying_ctx* ctx)
135{
136 emscripten_proxy_finish(ctx);
137}
138
139static void main_available(void* arg)
140{
141 *(int*)arg = av1_wc_js_available();
142}
143
144static void main_open(void* arg)
145{
146 *(int*)arg = av1_wc_js_open();
147}
148
149static void main_close(void* arg)
150{
151 av1_wc_js_close(*(int*)arg);
152}
153
154static void main_decode(em_proxying_ctx* ctx, void* arg)
155{
156 av1_wc_job* job = arg;
157 WINPR_ASSERT(job);
158
159 av1_wc_js_decode(ctx, job->handle, job->src, job->size, job->dst, job->format, job->stride,
160 job->width, job->height, job->key, job->profile, &job->rc);
161}
162
163static BOOL on_main(void (*fn)(void*), void* arg)
164{
165 WINPR_ASSERT(fn);
166
167 if (emscripten_is_main_browser_thread())
168 {
169 fn(arg);
170 return TRUE;
171 }
172 return emscripten_proxy_sync(emscripten_proxy_get_system_queue(),
173 emscripten_main_runtime_thread_id(), fn, arg) == 1;
174}
175
176int av1_wc_open(void)
177{
178 int available = 0;
179 if (!on_main(main_available, &available) || !available)
180 return 0;
181 int handle = 0;
182 if (!on_main(main_open, &handle))
183 return 0;
184 return handle;
185}
186
187void av1_wc_close(int handle)
188{
189 if (handle > 0)
190 (void)on_main(main_close, &handle);
191}
192
193/* AV1 low overhead bitstream: a key frame carries a sequence header OBU (type 1), whose
194 * first 3 bits are seq_profile. Returns the profile, or -1 if there is no sequence header. */
195static int sequence_header_profile(const BYTE* p, UINT32 size)
196{
197 UINT32 pos = 0;
198 while (pos < size)
199 {
200 const BYTE header = p[pos];
201 const int type = (header >> 3) & 0x0f;
202 const int has_extension = (header >> 2) & 1;
203 const int has_size = (header >> 1) & 1;
204 pos += 1 + (UINT32)has_extension;
205 if (!has_size)
206 return (type == 1 && pos < size) ? (p[pos] >> 5) : -1;
207 UINT64 len = 0;
208 for (int i = 0; i < 8 && pos < size; i++)
209 {
210 const BYTE b = p[pos++];
211 len |= (UINT64)(b & 0x7f) << (7 * i);
212 if (!(b & 0x80))
213 break;
214 }
215 if (type == 1)
216 return (pos < size) ? (p[pos] >> 5) : -1;
217 if (len > size - pos)
218 return -1;
219 pos += (UINT32)len;
220 }
221 return -1;
222}
223
224static const char* copy_format(DWORD DstFormat)
225{
226 switch (DstFormat)
227 {
228 case PIXEL_FORMAT_BGRA32:
229 return "BGRA";
230 case PIXEL_FORMAT_BGRX32:
231 return "BGRX";
232 case PIXEL_FORMAT_RGBA32:
233 return "RGBA";
234 case PIXEL_FORMAT_RGBX32:
235 return "RGBX";
236 default:
237 return nullptr;
238 }
239}
240
241INT32 av1_wc_decode(int handle, const BYTE* pSrcData, UINT32 SrcSize, BYTE* pDstData,
242 DWORD DstFormat, UINT32 nDstStep, UINT32 nDstWidth, UINT32 nDstHeight)
243{
244 const char* format = copy_format(DstFormat);
245 if (!format)
246 return AV1_WC_UNSUPPORTED; /* copyTo() only writes 32 bit RGB: let dav1d handle it */
247 if (emscripten_is_main_browser_thread())
248 return -1; /* the main thread cannot block for its own asynchronous decode */
249
250 const int profile = sequence_header_profile(pSrcData, SrcSize);
251 av1_wc_job job = { .handle = handle,
252 .src = pSrcData,
253 .size = SrcSize,
254 .dst = pDstData,
255 .format = format,
256 .stride = nDstStep,
257 .width = nDstWidth,
258 .height = nDstHeight,
259 .key = profile >= 0,
260 .profile = profile,
261 .rc = -1 };
262 if (emscripten_proxy_sync_with_ctx(emscripten_proxy_get_system_queue(),
263 emscripten_main_runtime_thread_id(), main_decode, &job) != 1)
264 return -1;
265 return job.rc;
266}