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