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 <stdlib.h>
31#include <string.h>
32
33#include <emscripten.h>
34#include <emscripten/proxying.h>
35#include <emscripten/threading.h>
36
37#include <winpr/assert.h>
38#include <winpr/crt.h>
39#include <winpr/stream.h>
40
41#include <freerdp/types.h>
42#include <freerdp/codec/audio.h>
43
44#include "rdpsnd_main.h"
45
46typedef struct
47{
48 rdpsndDevicePlugin device;
49 UINT32 channels;
50 UINT32 rate;
51 UINT32 volume;
52 volatile int queued_ms; /* written by the main thread */
53} rdpsndEmscriptenPlugin;
54
55typedef struct
56{
57 rdpsndEmscriptenPlugin* plugin;
58 BYTE* data;
59 size_t size;
60} play_job;
61
62/* in rdpsnd_emscripten.js */
63extern void rdpsnd_web_play(BYTE* data, int size, int channels, int rate, int volume,
64 int* queued_ms);
65
66static void play_on_main(void* arg)
67{
68 play_job* job = arg;
69 WINPR_ASSERT(job);
70
71 rdpsndEmscriptenPlugin* p = job->plugin;
72 WINPR_ASSERT(p);
73
74 rdpsnd_web_play(job->data, (int)job->size, (int)p->channels, (int)p->rate,
75 (int)(p->volume & 0xffff), (int*)&p->queued_ms);
76 free(job->data);
77 free(job);
78}
79
80static BOOL rdpsnd_emscripten_format_supported(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device,
81 const AUDIO_FORMAT* format)
82{
83 WINPR_ASSERT(format);
84
85 /* Web Audio takes float samples at any rate: accept 16 bit PCM, mono or stereo */
86 return (format->wFormatTag == WAVE_FORMAT_PCM) && (format->wBitsPerSample == 16) &&
87 (format->nChannels >= 1) && (format->nChannels <= 2) && (format->nSamplesPerSec > 0);
88}
89
90static BOOL rdpsnd_emscripten_open(rdpsndDevicePlugin* device, const AUDIO_FORMAT* format,
91 WINPR_ATTR_UNUSED UINT32 latency)
92{
93 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
94 WINPR_ASSERT(p);
95
96 if (!format || !rdpsnd_emscripten_format_supported(device, format))
97 return FALSE;
98 p->channels = format->nChannels;
99 p->rate = format->nSamplesPerSec;
100 return TRUE;
101}
102
103static void rdpsnd_emscripten_close(WINPR_ATTR_UNUSED rdpsndDevicePlugin* device)
104{
105}
106
107static BOOL rdpsnd_emscripten_set_volume(rdpsndDevicePlugin* device, UINT32 value)
108{
109 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
110 WINPR_ASSERT(p);
111
112 p->volume = value;
113 return TRUE;
114}
115
116static UINT32 rdpsnd_emscripten_get_volume(rdpsndDevicePlugin* device)
117{
118 const rdpsndEmscriptenPlugin* p = (const rdpsndEmscriptenPlugin*)device;
119 WINPR_ASSERT(p);
120
121 return p->volume;
122}
123
124static UINT rdpsnd_emscripten_play(rdpsndDevicePlugin* device, const BYTE* data, size_t size)
125{
126 rdpsndEmscriptenPlugin* p = (rdpsndEmscriptenPlugin*)device;
127 WINPR_ASSERT(p);
128
129 if (!data || size == 0 || p->channels == 0)
130 return 0;
131
132 play_job* job = calloc(1, sizeof(play_job));
133 if (!job)
134 return 0;
135 job->data = malloc(size);
136 if (!job->data)
137 {
138 free(job);
139 return 0;
140 }
141 memcpy(job->data, data, size);
142 job->plugin = p;
143 job->size = size;
144
145 if (emscripten_is_main_browser_thread())
146 play_on_main(job);
147 else if (!emscripten_proxy_async(emscripten_proxy_get_system_queue(),
148 emscripten_main_runtime_thread_id(), play_on_main, job))
149 {
150 free(job->data);
151 free(job);
152 return 0;
153 }
154 /* latency reported to the server: what is already queued in the browser */
155 return (UINT)p->queued_ms;
156}
157
158static void drain_on_main(WINPR_ATTR_UNUSED void* arg)
159{
160}
161
162static void rdpsnd_emscripten_free(rdpsndDevicePlugin* device)
163{
164 /* Jobs queued by Play() still use the plugin. The main thread runs its queue in order, so
165 * once a synchronous no-op went through, all of them are done. */
166 if (emscripten_is_main_browser_thread())
167 emscripten_proxy_execute_queue(emscripten_proxy_get_system_queue());
168 else
169 (void)emscripten_proxy_sync(emscripten_proxy_get_system_queue(),
170 emscripten_main_runtime_thread_id(), drain_on_main, nullptr);
171 free(device);
172}
173
174FREERDP_ENTRY_POINT(UINT VCAPITYPE emscripten_freerdp_rdpsnd_client_subsystem_entry(
176{
177 WINPR_ASSERT(pEntryPoints);
178
179 rdpsndEmscriptenPlugin* p = calloc(1, sizeof(rdpsndEmscriptenPlugin));
180 if (!p)
181 return CHANNEL_RC_NO_MEMORY;
182
183 p->volume = UINT32_MAX;
184 p->device.Open = rdpsnd_emscripten_open;
185 p->device.FormatSupported = rdpsnd_emscripten_format_supported;
186 p->device.GetVolume = rdpsnd_emscripten_get_volume;
187 p->device.SetVolume = rdpsnd_emscripten_set_volume;
188 p->device.Play = rdpsnd_emscripten_play;
189 p->device.Close = rdpsnd_emscripten_close;
190 p->device.Free = rdpsnd_emscripten_free;
191 pEntryPoints->pRegisterRdpsndDevice(pEntryPoints->rdpsnd, &p->device);
192 return CHANNEL_RC_OK;
193}