FreeRDP
Loading...
Searching...
No Matches
rdpsnd_emscripten.c
1
18/*
19 * Play() runs on the audio channel thread. It copies the PCM samples and hands them to the
20 * main browser thread asynchronously, so it never waits for the busy UI thread. The main thread
21 * converts them to float and schedules them back to back with AudioBufferSourceNode.start(),
22 * and keeps the amount of queued audio (ms) in shared memory, which Play() reports to the server
23 * as latency. Browsers only allow audio after a user gesture: a page that creates and resumes
24 * Module.freerdpAudioContext in its connect click handler gets sound at once. Otherwise the backend
25 * creates the context itself, and samples are dropped until the user interacts with the page.
26 */
27
28#include <freerdp/config.h>
29
30#include <stdint.h>
31#include <stdlib.h>
32#include <string.h>
33
34#include <emscripten.h>
35#include <emscripten/proxying.h>
36#include <emscripten/threading.h>
37
38#include <winpr/assert.h>
39#include <winpr/crt.h>
40#include <winpr/stream.h>
41
42#include <freerdp/types.h>
43#include <freerdp/codec/audio.h>
44
45#include "rdpsnd_main.h"
46
47typedef struct
48{
49 rdpsndDevicePlugin device;
50 UINT32 channels;
51 UINT32 rate;
52 UINT32 volume;
53 volatile int queued_ms; /* written by the main thread */
54 AUDIO_FORMAT asked; /* the last format passed to FormatSupported() */
55} rdpsndEmscriptenPlugin;
56
57/*
58 * The format the server sends on the wire, for the web page's Info panel:
59 * [0] wFormatTag (WAVE_FORMAT_UNKNOWN = no audio yet), [1] nSamplesPerSec, [2] nChannels.
60 *
61 * Open() only sees the PCM format the device plays: for Opus, AAC or ADPCM rdpsnd opens the
62 * device with a PCM copy of the wire format and decodes with freerdp_dsp. The wire format is
63 * what rdpsnd asked FormatSupported() about right before Open() (rdpsnd_ensure_device_is_open),
64 * and the PCM copy keeps its rate and channels. Open() takes the asked format only when rate
65 * and channels match, and otherwise publishes the format it opened with.
66 */
67static uint32_t web_audio_format[3];
68
69EMSCRIPTEN_KEEPALIVE uint32_t* freerdp_web_audio_format(void);
70EMSCRIPTEN_KEEPALIVE uint32_t* freerdp_web_audio_format(void)
71{
72 return web_audio_format;
73}
74
75static void web_audio_format_set(UINT16 tag, UINT32 rate, UINT16 channels)
76{
77 __atomic_store_n(&web_audio_format[1], rate, __ATOMIC_RELAXED);
78 __atomic_store_n(&web_audio_format[2], channels, __ATOMIC_RELAXED);
79 __atomic_store_n(&web_audio_format[0], tag, __ATOMIC_RELEASE);
80}
81
82typedef struct
83{
84 rdpsndEmscriptenPlugin* plugin;
85 BYTE* data;
86 size_t size;
87} play_job;
88
89/* in rdpsnd_emscripten.js */
90extern void rdpsnd_web_play(BYTE* data, int size, int channels, int rate, int volume,
91 int* queued_ms);
92
93static void play_on_main(void* arg)
94{
95 play_job* job = arg;
96 WINPR_ASSERT(job);
97
98 rdpsndEmscriptenPlugin* p = job->plugin;
99 WINPR_ASSERT(p);
100
101 rdpsnd_web_play(job->data, (int)job->size, (int)p->channels, (int)p->rate,
102 (int)(p->volume & 0xffff), (int*)&p->queued_ms);
103 free(job->data);
104 free(job);
105}
106
107WINPR_ATTR_NODISCARD
108static BOOL pcm_playable(const AUDIO_FORMAT* format)
109{
110 WINPR_ASSERT(format);
111
112 /* Web Audio takes float samples at any rate: accept 16 bit PCM, mono or stereo */
113 return (format->wFormatTag == WAVE_FORMAT_PCM) && (format->wBitsPerSample == 16) &&
114 (format->nChannels >= 1) && (format->nChannels <= 2) && (format->nSamplesPerSec > 0);
115}
116
117WINPR_ATTR_NODISCARD
118static BOOL rdpsnd_emscripten_format_supported(rdpsndDevicePlugin* device,
119 const AUDIO_FORMAT* format)
120{
121 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
122 WINPR_ASSERT(p);
123 WINPR_ASSERT(format);
124
125 p->asked = *format;
126 p->asked.data = nullptr;
127 return pcm_playable(format);
128}
129
130static BOOL rdpsnd_emscripten_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format,
131 WINPR_ATTR_UNUSED UINT32 latency)
132{
133 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
134 WINPR_ASSERT(p);
135
136 if (!format || !pcm_playable(format))
137 return FALSE;
138 p->channels = format->nChannels;
139 p->rate = format->nSamplesPerSec;
140
141 const AUDIO_FORMAT* wire = format;
142 if ((p->asked.wFormatTag != WAVE_FORMAT_UNKNOWN) &&
143 (p->asked.nSamplesPerSec == format->nSamplesPerSec) &&
144 (p->asked.nChannels == format->nChannels))
145 wire = &p->asked;
146 web_audio_format_set(wire->wFormatTag, wire->nSamplesPerSec, wire->nChannels);
147 return TRUE;
148}
149
150static void rdpsnd_emscripten_close(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device)
151{
152}
153
154static BOOL rdpsnd_emscripten_set_volume(rdpsndDevicePlugin* device, UINT32 value)
155{
156 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
157 WINPR_ASSERT(p);
158
159 p->volume = value;
160 return TRUE;
161}
162
163static UINT32 rdpsnd_emscripten_get_volume(rdpsndDevicePlugin* device)
164{
165 const rdpsndEmscriptenPlugin* p = (const rdpsndEmscriptenPlugin*)device;
166 WINPR_ASSERT(p);
167
168 return p->volume;
169}
170
171static UINT rdpsnd_emscripten_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size)
172{
173 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
174 WINPR_ASSERT(p);
175
176 if (!data || size == 0 || p->channels == 0)
177 return 0;
178
179 play_job* job = calloc(1, sizeof(play_job));
180 if (!job)
181 return 0;
182 job->data = malloc(size);
183 if (!job->data)
184 {
185 free(job);
186 return 0;
187 }
188 memcpy(job->data, data, size);
189 job->plugin = p;
190 job->size = size;
191
192 if (emscripten_is_main_browser_thread())
193 play_on_main(job);
194 else if (!emscripten_proxy_async(emscripten_proxy_get_system_queue(),
195 emscripten_main_runtime_thread_id(), play_on_main, job))
196 {
197 free(job->data);
198 free(job);
199 return 0;
200 }
201 /* latency reported to the server: what is already queued in the browser */
202 return (UINT)p->queued_ms;
203}
204
205static void drain_on_main(WINPR_ATTR_UNUSED void* arg)
206{
207}
208
209static void rdpsnd_emscripten_free(rdpsndDevicePlugin* device)
210{
211 /* Jobs queued by Play() still use the plugin. The main thread runs its queue in order, so
212 * once a synchronous no-op went through, all of them are done. */
213 if (emscripten_is_main_browser_thread())
214 emscripten_proxy_execute_queue(emscripten_proxy_get_system_queue());
215 else
216 (void)emscripten_proxy_sync(emscripten_proxy_get_system_queue(),
217 emscripten_main_runtime_thread_id(), drain_on_main, nullptr);
218 free(device);
219}
220
221FREERDP_ENTRY_POINT(UINT VCAPITYPE emscripten_freerdp_rdpsnd_client_subsystem_entry(
223{
224 WINPR_ASSERT(pEntryPoints);
225
226 rdpsndEmscriptenPlugin* p = calloc(1, sizeof(rdpsndEmscriptenPlugin));
227 if (!p)
228 return CHANNEL_RC_NO_MEMORY;
229
230 p->volume = UINT32_MAX;
231 web_audio_format_set(WAVE_FORMAT_UNKNOWN, 0, 0); /* a new session: no audio yet */
232 p->device.Open = rdpsnd_emscripten_open;
233 p->device.FormatSupported = rdpsnd_emscripten_format_supported;
234 p->device.GetVolume = rdpsnd_emscripten_get_volume;
235 p->device.SetVolume = rdpsnd_emscripten_set_volume;
236 p->device.Play = rdpsnd_emscripten_play;
237 p->device.Close = rdpsnd_emscripten_close;
238 p->device.Free = rdpsnd_emscripten_free;
239 pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &p->device);
240 return CHANNEL_RC_OK;
241}