Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwasmjs.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qwasmjs_p.h"
5#include <qaudiodevice.h>
6#include <qcameradevice.h>
7
9
10
11Q_GLOBAL_STATIC(JsMediaInputStream, s_wasmMediaInputStreamInstance);
12
13JsMediaRecorder::JsMediaRecorder() = default;
14
15bool JsMediaRecorder::open(QIODevice::OpenMode mode)
16{
17 if (mode.testFlag(QIODevice::WriteOnly))
18 return false;
19 return QIODevice::open(mode);
20}
21
23{
24 return false;
25}
26
28{
29 return m_buffer.size();
30}
31
32bool JsMediaRecorder::seek(qint64 pos)
33{
34 if (pos >= size())
35 return false;
36 return QIODevice::seek(pos);
37}
38
39qint64 JsMediaRecorder::readData(char *data, qint64 maxSize)
40{
41 qint64 bytesToRead = qMin(maxSize, (qint64)m_buffer.size());
42 memcpy(data, m_buffer.constData(), bytesToRead);
43 m_buffer = m_buffer.right(m_buffer.size() - bytesToRead);
44 return bytesToRead;
45}
46
48{
49 Q_UNREACHABLE_RETURN(0);
50}
51
52void JsMediaRecorder::audioDataAvailable(emscripten::val aBlob, double timeCodeDifference)
53{
54 Q_UNUSED(timeCodeDifference)
55 if (aBlob.isUndefined() || aBlob.isNull()) {
56 qWarning() << "blob is null";
57 return;
58 }
59
60 auto fileReader = std::make_shared<qstdweb::FileReader>();
61
62 fileReader->onError([=](emscripten::val theError) {
63 emit streamError(QMediaRecorder::ResourceError,
64 QString::fromStdString(theError["message"].as<std::string>()));
65 });
66
67 fileReader->onAbort([=](emscripten::val) {
68 emit streamError(QMediaRecorder::ResourceError, QStringLiteral("File read aborted"));
69 });
70
71 fileReader->onLoad([=](emscripten::val) {
72 if (fileReader->val().isNull() || fileReader->val().isUndefined())
73 return;
74 qstdweb::ArrayBuffer result = fileReader->result();
75 if (result.val().isNull() || result.val().isUndefined())
76 return;
77
78 m_buffer.append(qstdweb::Uint8Array(result).copyToQByteArray());
79 emit readyRead();
80 });
81
82 fileReader->readAsArrayBuffer(qstdweb::Blob(aBlob));
83}
84
85void JsMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
86{
87 if (stream.isUndefined() || stream.isNull()) {
88 qWarning()<< "could not find MediaStream";
89 return;
90 }
91
92 emscripten::val navigator = emscripten::val::global("navigator");
93 emscripten::val mediaDevices = navigator["mediaDevices"];
94
95 // check which ones are supported
96 emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
97 // browsers only support some settings
98
99 emscripten::val videoParams = emscripten::val::object();
100 emscripten::val constraints = emscripten::val::object();
101 videoParams.set("resizeMode",std::string("crop-and-scale"));
102
103 if (m_needsCamera) {
104 if (settings.videoFrameRate() > 0)
105 videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
106 if (settings.videoResolution().height() > 0)
107 videoParams.set("height",
108 emscripten::val(settings.videoResolution().height())); // viewportHeight?
109 if (settings.videoResolution().width() > 0)
110 videoParams.set("width", emscripten::val(settings.videoResolution().width()));
111
112 constraints.set("video", videoParams); // only video here
113 }
114
115 emscripten::val audioParams = emscripten::val::object();
116 if (settings.audioSampleRate() > 0)
117 audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
118 if (settings.audioBitRate() > 0)
119 audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
120 if (settings.audioChannelCount() > 0)
121 audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
122
123 constraints.set("audio", audioParams); // only audio here
124
125 if (m_needsCamera && stream["active"].as<bool>()) {
126 emscripten::val videoTracks = emscripten::val::undefined();
127 videoTracks = stream.call<emscripten::val>("getVideoTracks");
128 if (videoTracks.isNull() || videoTracks.isUndefined()) {
129 qWarning() << "no video tracks";
130 return;
131 }
132 if (videoTracks["length"].as<int>() > 0) {
133 // try to apply the video options, async
134 qstdweb::Promise::make(videoTracks[0],
135 QStringLiteral("applyConstraints"), {
136 .thenFunc =
137 [this]([[maybe_unused]] emscripten::val result) {
138 startStreaming();
139 },
140 .catchFunc =
141 [this](emscripten::val theError) {
142 qWarning()
143 << theError["code"].as<int>()
144 << theError["message"].as<std::string>();
145 emit streamError(QMediaRecorder::ResourceError,
146 QString::fromStdString(theError["message"].as<std::string>()));
147 },
148 .finallyFunc = []() {},
149 },
150 constraints);
151 }
152 }
153}
154
156{
157 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
158 qWarning() << "could not find MediaRecorder";
159 return;
160 }
161 m_mediaRecorder.call<void>("pause");
162}
163
165{
166 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
167 qWarning() << "could not find MediaRecorder";
168 return;
169 }
170
171 m_mediaRecorder.call<void>("resume");
172}
173
175{
176 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
177 qWarning()<< "could not find MediaRecorder";
178 return;
179 }
180 if (m_mediaRecorder["state"].as<std::string>() == "recording")
181 m_mediaRecorder.call<void>("stop");
182
183}
184
186{
187 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
188 qWarning() << "could not find MediaStream";
189 return;
190 }
191
192 constexpr int sliceSizeInMs = 256;
193 // AudioWorklets uses 128 by default
194 m_mediaRecorder.call<void>("start", emscripten::val(sliceSizeInMs));
195}
196
197void JsMediaRecorder::setStream(emscripten::val stream)
198{
199 emscripten::val emMediaSettings = emscripten::val::object();
200 QMediaFormat::VideoCodec videoCodec = m_mediaSettings.videoCodec();
201 QMediaFormat::AudioCodec audioCodec = m_mediaSettings.audioCodec();
202 QMediaFormat::FileFormat fileFormat = m_mediaSettings.fileFormat();
203
204 // mime and codecs
205 QString mimeCodec;
206 if (!m_mediaSettings.mimeType().name().isEmpty()) {
207 mimeCodec = m_mediaSettings.mimeType().name();
208
209 if (videoCodec != QMediaFormat::VideoCodec::Unspecified)
210 mimeCodec += QStringLiteral(": codecs=");
211
212 if (audioCodec != QMediaFormat::AudioCodec::Unspecified) {
213 // TODO
214 }
215
216 if (fileFormat != QMediaFormat::UnspecifiedFormat)
217 mimeCodec += QMediaFormat::fileFormatName(m_mediaSettings.fileFormat());
218
219 emMediaSettings.set("mimeType", mimeCodec.toStdString());
220 }
221
222 if (m_mediaSettings.audioBitRate() > 0)
223 emMediaSettings.set("audioBitsPerSecond", emscripten::val(m_mediaSettings.audioBitRate()));
224
225 if (m_mediaSettings.videoBitRate() > 0)
226 emMediaSettings.set("videoBitsPerSecond", emscripten::val(m_mediaSettings.videoBitRate()));
227
228 // create the MediaRecorder, and set up data callback
229 m_mediaRecorder = emscripten::val::global("MediaRecorder").new_(stream, emMediaSettings);
230
231 if (m_mediaRecorder.isNull() || m_mediaRecorder.isUndefined()) {
232 qWarning() << "MediaRecorder could not be found";
233 return;
234 }
235 m_mediaRecorder.set("data-mediarecordercontext",
236 emscripten::val(quintptr(reinterpret_cast<void *>(this))));
237
238 if (!m_mediaStreamDataAvailable.isNull()) {
239 m_mediaStreamDataAvailable.reset();
240 m_mediaStreamStopped.reset();
241 m_mediaStreamError.reset();
242 m_mediaStreamStart.reset();
243 m_mediaStreamPause.reset();
244 m_mediaStreamResume.reset();
245 }
246
247 // dataavailable
248 auto callback = [](emscripten::val blob) {
249 if (blob.isUndefined() || blob.isNull()) {
250 qWarning() << "blob is null";
251 return;
252 }
253 if (blob["target"].isUndefined() || blob["target"].isNull())
254 return;
255 if (blob["data"].isUndefined() || blob["data"].isNull())
256 return;
257 if (blob["target"]["data-mediarecordercontext"].isUndefined()
258 || blob["target"]["data-mediarecordercontext"].isNull())
259 return;
260
261 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
262 blob["target"]["data-mediarecordercontext"].as<quintptr>());
263
264 if (recorder) {
265 const double timeCode =
266 blob.hasOwnProperty("timecode") ? blob["timecode"].as<double>() : 0;
267 recorder->audioDataAvailable(blob["data"], timeCode);
268 }
269 };
270
271 m_mediaStreamDataAvailable.reset(
272 new qstdweb::EventCallback(m_mediaRecorder, "dataavailable", callback));
273
274 // stopped
275 auto stoppedCallback = [this](emscripten::val event) {
276 if (event.isUndefined() || event.isNull()) {
277 qWarning() << "event is null";
278 return;
279 }
280 m_currentState = QMediaRecorder::StoppedState;
281 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
282 event["target"]["data-mediarecordercontext"].as<quintptr>());
283 emit recorder->stopped();
284 };
285
286 m_mediaStreamStopped.reset(
287 new qstdweb::EventCallback(m_mediaRecorder, "stop", stoppedCallback));
288
289 // error
290 auto errorCallback = [this](emscripten::val theError) {
291 if (theError.isUndefined() || theError.isNull()) {
292 qWarning() << "error is null";
293 return;
294 }
295
296 emit streamError(QMediaRecorder::ResourceError,
297 QString::fromStdString(theError["message"].as<std::string>()));
298 };
299
300 m_mediaStreamError.reset(new qstdweb::EventCallback(m_mediaRecorder, "error", errorCallback));
301
302 // start
303 auto startCallback = [this](emscripten::val event) {
304 if (event.isUndefined() || event.isNull()) {
305 qWarning() << "event is null";
306 return;
307 }
308
309 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
310 event["target"]["data-mediarecordercontext"].as<quintptr>());
311 m_currentState = QMediaRecorder::RecordingState;
312 emit recorder->started();
313 };
314
315 m_mediaStreamStart.reset(new qstdweb::EventCallback(m_mediaRecorder, "start", startCallback));
316
317 // pause
318 auto pauseCallback = [this](emscripten::val event) {
319 if (event.isUndefined() || event.isNull()) {
320 qWarning() << "event is null";
321 return;
322 }
323
324 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
325 event["target"]["data-mediarecordercontext"].as<quintptr>());
326 m_currentState = QMediaRecorder::PausedState;
327 emit recorder->paused();
328 };
329
330 m_mediaStreamPause.reset(new qstdweb::EventCallback(m_mediaRecorder, "pause", pauseCallback));
331
332 // resume
333 auto resumeCallback = [this](emscripten::val event) {
334 if (event.isUndefined() || event.isNull()) {
335 qWarning() << "event is null";
336 return;
337 }
338 m_currentState = QMediaRecorder::RecordingState;
339
340 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
341 event["target"]["data-mediarecordercontext"].as<quintptr>());
342 emit recorder->resumed();
343 };
344
345 m_mediaStreamResume.reset(
346 new qstdweb::EventCallback(m_mediaRecorder, "resume", resumeCallback));
347}
348
350{
351 return m_buffer.size();
352}
353
354
355JsMediaInputStream::JsMediaInputStream(QObject *parent)
356 : QObject{parent}
357{
358}
359
361
363{
364 return s_wasmMediaInputStreamInstance();
365}
366
367void JsMediaInputStream::setAudioStreamDevice(const std::string &id)
368{
369 if (!m_mediaStream.isUndefined() && !m_mediaStream.isNull()) {
370 if (!m_mediaStream.isNull() && !m_mediaStream.isUndefined()
371 && !m_mediaStream["getTracks"].isUndefined() && m_mediaStream["active"].as<bool>()) {
372 m_needsVideo = false;
373 m_needsAudio = true;
374 replaceMediaTrack(id);
375 }
376 }
377}
378
379void JsMediaInputStream::replaceMediaTrack(const std::string &id)
380{
381 qstdweb::PromiseCallbacks getUserMediaCallback{
382 // default
383 .thenFunc =
384 [this, id](emscripten::val newStream) {
385
386 std::string getTracksCommand;
387 if (m_needsAudio)
388 getTracksCommand = "getAudioTracks";
389 else
390 getTracksCommand = "getVideoTracks";
391
392 emscripten::val currentTracks = m_mediaStream.call<emscripten::val>(getTracksCommand.c_str());
393
394 if (!currentTracks.isUndefined() && currentTracks["length"].as<int>() > 0) {
395 emscripten::val currentTrackForType = currentTracks[0];
396 emscripten::val settings = currentTrackForType.call<emscripten::val>("getSettings");
397
398 if (!settings.isNull() && !settings.isUndefined()) {
399 if (settings["deviceId"].as<std::string>() != id) {
400 m_mediaStream.call<void>("removeTrack", currentTrackForType);
401 currentTrackForType.call<void>("stop");
402
403 emscripten::val newTracks = newStream.call<emscripten::val>(getTracksCommand.c_str());
404
405 m_mediaStream.call<void>("addTrack", newTracks[0]);
406
407 // stopMediaStream(stream); stopping this stream causes the track to stop :(
408 if (m_needsAudio)
409 emit mediaAudioStreamReady();
410 else
411 emit mediaVideoStreamReady();
412 }
413 }
414 } else { // we still need to add this track
415 qWarning() << " we still need to add this track";
416 }
417 },
418 .catchFunc =
419 [](emscripten::val error) {
420 qWarning()
421 << "replaceTrack getUserMedia failed."
422 << error["name"].as<std::string>()
423 << error["message"].as<std::string>();
424 }
425 };
426
427 emscripten::val mediaDevices = emscripten::val::global("navigator")["mediaDevices"];
428 qstdweb::Promise::make(mediaDevices, QStringLiteral("getUserMedia"),
429 std::move(getUserMediaCallback), setDeviceConstraints(id));
430}
431
433{
434 std::string deviceIdString = id;
435 if (deviceIdString.find("System") != std::string::npos)
436 deviceIdString.clear(); // no id is default/any device
437
438 emscripten::val constraints = emscripten::val::object();
439 if (m_needsAudio) {
440 emscripten::val audioConstraints = emscripten::val::object();
441 audioConstraints.set("audio", m_needsAudio);
442 if (!deviceIdString.empty()) {
443 emscripten::val exactDeviceId = emscripten::val::object();
444 exactDeviceId.set("exact", deviceIdString);
445 audioConstraints.set("deviceId", exactDeviceId);
446 }
447 constraints.set("audio", audioConstraints);
448 } else {
449 constraints.set("audio", false);
450 }
451
452 if (m_needsVideo) {
453 emscripten::val videoContraints = emscripten::val::object();
454 if (!deviceIdString.empty()) {
455 emscripten::val exactDeviceId = emscripten::val::object();
456 exactDeviceId.set("exact", deviceIdString);
457 videoContraints.set("deviceId", exactDeviceId);
458 }
459 videoContraints.set("resizeMode", std::string("crop-and-scale"));
460 constraints.set("video", videoContraints);
461 }
462 return constraints;
463}
464
465void JsMediaInputStream::setStreamDevice(const std::string &id)
466{
467 emscripten::val navigator = emscripten::val::global("navigator");
468 emscripten::val mediaDevices = navigator["mediaDevices"];
469
470 if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
471 qWarning() << "No media devices found";
472 return;
473 }
474
475 // decide if we need to replace a track here
476
477 if (!m_mediaStream.isNull() && !m_mediaStream.isUndefined())
478 m_active = m_mediaStream["active"].as<bool>();
479
480 if (m_active) { // if media stream is already active, we need to replace
481 replaceMediaTrack(id);
482 return;
483 }
484
485 qstdweb::PromiseCallbacks getUserMediaCallback{
486 // default
487 .thenFunc =
488 [this](emscripten::val stream) {
489 setupMediaStream(stream);
490 },
491 .catchFunc =
492 [](emscripten::val error) {
493 qWarning()
494 << "setStreamDevice getUserMedia fail"
495 << error["name"].as<std::string>()
496 << error["message"].as<std::string>();
497 }
498 };
499
500 // this prompts user for permissions
501 qstdweb::Promise::make(mediaDevices, QStringLiteral("getUserMedia"),
502 std::move(getUserMediaCallback), setDeviceConstraints(id));
503}
504
505void JsMediaInputStream::setupMediaStream(emscripten::val mStream)
506{
507 m_mediaStream = mStream;
508 m_active = mStream["active"].as<bool>();
509
510 auto activeStreamCallback = [=](emscripten::val) {
511 m_active = true;
512 emit activated(m_active);
513 };
514 m_activeStreamEvent.reset(new qstdweb::EventCallback(m_mediaStream, "active", activeStreamCallback));
515
516 auto inactiveStreamCallback = [=](emscripten::val) {
517 m_active = false;
518 emit activated(m_active);
519 };
520 m_inactiveStreamEvent.reset(new qstdweb::EventCallback(m_mediaStream, "inactive", inactiveStreamCallback));
521
522 emit mediaVideoStreamReady();
523}
524
525void JsMediaInputStream::stopMediaStream(emscripten::val mediaStream)
526{
527 if (!mediaStream.isNull() && !mediaStream.isUndefined() && !mediaStream["getTracks"].isUndefined()) {
528 emscripten::val tracks = mediaStream.call<emscripten::val>("getTracks");
529 if (!tracks.isUndefined() && tracks["length"].as<int>() > 0) {
530 for (int i = 0; i < tracks["length"].as<int>(); i++) {
531 tracks[i].call<void>("stop");
532 }
533 }
534 }
535 mediaStream = emscripten::val::undefined();
536 m_active = false;
537 }
538
539QT_END_NAMESPACE
void replaceMediaTrack(const std::string &id)
Definition qwasmjs.cpp:379
void setStreamDevice(const std::string &id)
Definition qwasmjs.cpp:465
void stopMediaStream(emscripten::val stream)
Definition qwasmjs.cpp:525
static JsMediaInputStream * instance()
Definition qwasmjs.cpp:362
void setAudioStreamDevice(const std::string &id)
Definition qwasmjs.cpp:367
emscripten::val setDeviceConstraints(const std::string &id)
Definition qwasmjs.cpp:432
qint64 writeData(const char *, qint64) override
Writes up to maxSize bytes from data to the device.
Definition qwasmjs.cpp:47
void startStreaming()
Definition qwasmjs.cpp:185
bool isSequential() const override
Returns true if this device is sequential; otherwise returns false.
Definition qwasmjs.cpp:22
qint64 size() const override
For open random-access devices, this function returns the size of the device.
Definition qwasmjs.cpp:27
void stopStream()
Definition qwasmjs.cpp:174
bool seek(qint64 pos) override
For random-access devices, this function sets the current position to pos, returning true on success,...
Definition qwasmjs.cpp:32
qint64 bytesAvailable() const override
Returns the number of bytes that are available for reading.
Definition qwasmjs.cpp:349
void setStream(emscripten::val stream)
Definition qwasmjs.cpp:197
bool open(QIODeviceBase::OpenMode mode) override
Opens the device and sets its OpenMode to mode.
Definition qwasmjs.cpp:15
void resumeStream()
Definition qwasmjs.cpp:164
qint64 readData(char *data, qint64 maxSize) override
Reads up to maxSize bytes from the device into data, and returns the number of bytes read or -1 if an...
Definition qwasmjs.cpp:39
void pauseStream()
Definition qwasmjs.cpp:155
Combined button and popup list for selecting options.