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