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
11JsMediaRecorder::JsMediaRecorder() = default;
12
13bool JsMediaRecorder::open(QIODevice::OpenMode mode)
14{
15 if (mode.testFlag(QIODevice::WriteOnly))
16 return false;
17 return QIODevice::open(mode);
18}
19
21{
22 return false;
23}
24
26{
27 return m_buffer.size();
28}
29
30bool JsMediaRecorder::seek(qint64 pos)
31{
32 if (pos >= size())
33 return false;
34 return QIODevice::seek(pos);
35}
36
37qint64 JsMediaRecorder::readData(char *data, qint64 maxSize)
38{
39 qint64 bytesToRead = qMin(maxSize, (qint64)m_buffer.size());
40 memcpy(data, m_buffer.constData(), bytesToRead);
41 m_buffer = m_buffer.right(m_buffer.size() - bytesToRead);
42 return bytesToRead;
43}
44
46{
47 Q_UNREACHABLE_RETURN(0);
48}
49
50void JsMediaRecorder::audioDataAvailable(emscripten::val aBlob, double timeCodeDifference)
51{
52 Q_UNUSED(timeCodeDifference)
53 if (aBlob.isUndefined() || aBlob.isNull()) {
54 qWarning() << "blob is null";
55 return;
56 }
57
58 auto fileReader = std::make_shared<qstdweb::FileReader>();
59
60 fileReader->onError([=](emscripten::val theError) {
61 emit streamError(QMediaRecorder::ResourceError,
62 QString::fromStdString(theError["message"].as<std::string>()));
63 });
64
65 fileReader->onAbort([=](emscripten::val) {
66 emit streamError(QMediaRecorder::ResourceError, QStringLiteral("File read aborted"));
67 });
68
69 fileReader->onLoad([=](emscripten::val) {
70 if (fileReader->val().isNull() || fileReader->val().isUndefined())
71 return;
72 qstdweb::ArrayBuffer result = fileReader->result();
73 if (result.val().isNull() || result.val().isUndefined())
74 return;
75
76 m_buffer.append(qstdweb::Uint8Array(result).copyToQByteArray());
77 emit readyRead();
78 });
79
80 fileReader->readAsArrayBuffer(qstdweb::Blob(aBlob));
81}
82
83void JsMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
84{
85 if (stream.isUndefined() || stream.isNull()) {
86 qWarning()<< "could not find MediaStream";
87 return;
88 }
89
90 emscripten::val navigator = emscripten::val::global("navigator");
91 emscripten::val mediaDevices = navigator["mediaDevices"];
92
93 // check which ones are supported
94 emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
95 // browsers only support some settings
96
97 emscripten::val videoParams = emscripten::val::object();
98 emscripten::val constraints = emscripten::val::object();
99 videoParams.set("resizeMode",std::string("crop-and-scale"));
100
101 if (m_needsCamera) {
102 if (settings.videoFrameRate() > 0)
103 videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
104 if (settings.videoResolution().height() > 0)
105 videoParams.set("height",
106 emscripten::val(settings.videoResolution().height())); // viewportHeight?
107 if (settings.videoResolution().width() > 0)
108 videoParams.set("width", emscripten::val(settings.videoResolution().width()));
109
110 constraints.set("video", videoParams); // only video here
111 }
112
113 emscripten::val audioParams = emscripten::val::object();
114 if (settings.audioSampleRate() > 0)
115 audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
116 if (settings.audioBitRate() > 0)
117 audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
118 if (settings.audioChannelCount() > 0)
119 audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
120
121 constraints.set("audio", audioParams); // only audio here
122
123 if (m_needsCamera && stream["active"].as<bool>()) {
124 emscripten::val videoTracks = emscripten::val::undefined();
125 videoTracks = stream.call<emscripten::val>("getVideoTracks");
126 if (videoTracks.isNull() || videoTracks.isUndefined()) {
127 qWarning() << "no video tracks";
128 return;
129 }
130 if (videoTracks["length"].as<int>() > 0) {
131 // try to apply the video options, async
132 qstdweb::Promise::make(videoTracks[0],
133 QStringLiteral("applyConstraints"), {
134 .thenFunc =
135 [this]([[maybe_unused]] emscripten::val result) {
136 startStreaming();
137 },
138 .catchFunc =
139 [this](emscripten::val theError) {
140 qWarning()
141 << theError["code"].as<int>()
142 << theError["message"].as<std::string>();
143 emit streamError(QMediaRecorder::ResourceError,
144 QString::fromStdString(theError["message"].as<std::string>()));
145 },
146 .finallyFunc = []() {},
147 },
148 constraints);
149 }
150 }
151}
152
154{
155 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
156 qWarning() << "could not find MediaRecorder";
157 return;
158 }
159 m_mediaRecorder.call<void>("pause");
160}
161
163{
164 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
165 qWarning() << "could not find MediaRecorder";
166 return;
167 }
168
169 m_mediaRecorder.call<void>("resume");
170}
171
173{
174 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
175 qWarning()<< "could not find MediaRecorder";
176 return;
177 }
178 if (m_mediaRecorder["state"].as<std::string>() == "recording")
179 m_mediaRecorder.call<void>("stop");
180
181}
182
184{
185 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
186 qWarning() << "could not find MediaStream";
187 return;
188 }
189
190 constexpr int sliceSizeInMs = 256;
191 // AudioWorklets uses 128 by default
192 m_mediaRecorder.call<void>("start", emscripten::val(sliceSizeInMs));
193}
194
195void JsMediaRecorder::setStream(emscripten::val stream)
196{
197 emscripten::val emMediaSettings = emscripten::val::object();
198 QMediaFormat::VideoCodec videoCodec = m_mediaSettings.videoCodec();
199 QMediaFormat::AudioCodec audioCodec = m_mediaSettings.audioCodec();
200 QMediaFormat::FileFormat fileFormat = m_mediaSettings.fileFormat();
201
202 // mime and codecs
203 QString mimeCodec;
204 if (!m_mediaSettings.mimeType().name().isEmpty()) {
205 mimeCodec = m_mediaSettings.mimeType().name();
206
207 if (videoCodec != QMediaFormat::VideoCodec::Unspecified)
208 mimeCodec += QStringLiteral(": codecs=");
209
210 if (audioCodec != QMediaFormat::AudioCodec::Unspecified) {
211 // TODO
212 }
213
214 if (fileFormat != QMediaFormat::UnspecifiedFormat)
215 mimeCodec += QMediaFormat::fileFormatName(m_mediaSettings.fileFormat());
216
217 emMediaSettings.set("mimeType", mimeCodec.toStdString());
218 }
219
220 if (m_mediaSettings.audioBitRate() > 0)
221 emMediaSettings.set("audioBitsPerSecond", emscripten::val(m_mediaSettings.audioBitRate()));
222
223 if (m_mediaSettings.videoBitRate() > 0)
224 emMediaSettings.set("videoBitsPerSecond", emscripten::val(m_mediaSettings.videoBitRate()));
225
226 // create the MediaRecorder, and set up data callback
227 m_mediaRecorder = emscripten::val::global("MediaRecorder").new_(stream, emMediaSettings);
228
229 if (m_mediaRecorder.isNull() || m_mediaRecorder.isUndefined()) {
230 qWarning() << "MediaRecorder could not be found";
231 return;
232 }
233 m_mediaRecorder.set("data-mediarecordercontext",
234 emscripten::val(quintptr(reinterpret_cast<void *>(this))));
235
236 if (!m_mediaStreamDataAvailable.isNull()) {
237 m_mediaStreamDataAvailable.reset();
238 m_mediaStreamStopped.reset();
239 m_mediaStreamError.reset();
240 m_mediaStreamStart.reset();
241 m_mediaStreamPause.reset();
242 m_mediaStreamResume.reset();
243 }
244
245 // dataavailable
246 auto callback = [](emscripten::val blob) {
247 if (blob.isUndefined() || blob.isNull()) {
248 qWarning() << "blob is null";
249 return;
250 }
251 if (blob["target"].isUndefined() || blob["target"].isNull())
252 return;
253 if (blob["data"].isUndefined() || blob["data"].isNull())
254 return;
255 if (blob["target"]["data-mediarecordercontext"].isUndefined()
256 || blob["target"]["data-mediarecordercontext"].isNull())
257 return;
258
259 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
260 blob["target"]["data-mediarecordercontext"].as<quintptr>());
261
262 if (recorder) {
263 const double timeCode =
264 blob.hasOwnProperty("timecode") ? blob["timecode"].as<double>() : 0;
265 recorder->audioDataAvailable(blob["data"], timeCode);
266 }
267 };
268
269 m_mediaStreamDataAvailable.reset(
270 new qstdweb::EventCallback(m_mediaRecorder, "dataavailable", callback));
271
272 // stopped
273 auto stoppedCallback = [this](emscripten::val event) {
274 if (event.isUndefined() || event.isNull()) {
275 qWarning() << "event is null";
276 return;
277 }
278 m_currentState = QMediaRecorder::StoppedState;
279 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
280 event["target"]["data-mediarecordercontext"].as<quintptr>());
281 emit recorder->stopped();
282 };
283
284 m_mediaStreamStopped.reset(
285 new qstdweb::EventCallback(m_mediaRecorder, "stop", stoppedCallback));
286
287 // error
288 auto errorCallback = [this](emscripten::val theError) {
289 if (theError.isUndefined() || theError.isNull()) {
290 qWarning() << "error is null";
291 return;
292 }
293
294 emit streamError(QMediaRecorder::ResourceError,
295 QString::fromStdString(theError["message"].as<std::string>()));
296 };
297
298 m_mediaStreamError.reset(new qstdweb::EventCallback(m_mediaRecorder, "error", errorCallback));
299
300 // start
301 auto startCallback = [this](emscripten::val event) {
302 if (event.isUndefined() || event.isNull()) {
303 qWarning() << "event is null";
304 return;
305 }
306
307 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
308 event["target"]["data-mediarecordercontext"].as<quintptr>());
309 m_currentState = QMediaRecorder::RecordingState;
310 emit recorder->started();
311 };
312
313 m_mediaStreamStart.reset(new qstdweb::EventCallback(m_mediaRecorder, "start", startCallback));
314
315 // pause
316 auto pauseCallback = [this](emscripten::val event) {
317 if (event.isUndefined() || event.isNull()) {
318 qWarning() << "event is null";
319 return;
320 }
321
322 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
323 event["target"]["data-mediarecordercontext"].as<quintptr>());
324 m_currentState = QMediaRecorder::PausedState;
325 emit recorder->paused();
326 };
327
328 m_mediaStreamPause.reset(new qstdweb::EventCallback(m_mediaRecorder, "pause", pauseCallback));
329
330 // resume
331 auto resumeCallback = [this](emscripten::val event) {
332 if (event.isUndefined() || event.isNull()) {
333 qWarning() << "event is null";
334 return;
335 }
336 m_currentState = QMediaRecorder::RecordingState;
337
338 JsMediaRecorder *recorder = reinterpret_cast<JsMediaRecorder *>(
339 event["target"]["data-mediarecordercontext"].as<quintptr>());
340 emit recorder->resumed();
341 };
342
343 m_mediaStreamResume.reset(
344 new qstdweb::EventCallback(m_mediaRecorder, "resume", resumeCallback));
345}
346
348{
349 return m_buffer.size();
350}
351
352
353JsMediaInputStream::JsMediaInputStream(QObject *parent)
354 : QObject{parent}
355{
356}
357
358void JsMediaInputStream::setStreamDevice(const std::string &id)
359{
360 emscripten::val navigator = emscripten::val::global("navigator");
361 emscripten::val mediaDevices = navigator["mediaDevices"];
362
363 if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
364 qWarning() << "No media devices found";
365 return;
366 }
367
368 qstdweb::PromiseCallbacks getUserMediaCallback{
369 // default
370 .thenFunc =
371 [this](emscripten::val stream) {
372 m_mediaStream = std::move(stream);
373 emit mediaStreamReady();
374 },
375 .catchFunc =
376 [](emscripten::val error) {
377 qWarning()
378 << "setStreamDevice getUserMedia fail"
379 << error["name"].as<std::string>()
380 << error["message"].as<std::string>();
381 }
382 };
383
384 emscripten::val constraints = emscripten::val::object();
385 if (m_needsAudio) {
386 emscripten::val audioConstraints = emscripten::val::object();
387 audioConstraints.set("audio", m_needsVideo); // formatting?
388 emscripten::val exactDeviceId = emscripten::val::object();
389 exactDeviceId.set("exact", id);
390 audioConstraints.set("deviceId", exactDeviceId);
391 constraints.set("audio", audioConstraints);
392 }
393
394 if (m_needsVideo) {
395 emscripten::val videoContraints = emscripten::val::object();
396 emscripten::val exactDeviceId = emscripten::val::object();
397 exactDeviceId.set("exact", id);
398 videoContraints.set("deviceId", exactDeviceId);
399 videoContraints.set("resizeMode", std::string("crop-and-scale"));
400 constraints.set("video", videoContraints);
401 }
402
403 // ???
404 // if (!id.empty()) {
405 // emscripten::val exactDeviceId = emscripten::val::object();
406 // exactDeviceId.set("exact", id);
407 // constraints.set("deviceId", exactDeviceId);
408 // }
409
410 // we do it this way as this prompts user for permissions
411 qstdweb::Promise::make(mediaDevices, QStringLiteral("getUserMedia"),
412 std::move(getUserMediaCallback), constraints);
413}
414
415void JsMediaInputStream::setupMediaStream(emscripten::val mStream)
416{
417 m_mediaStream = mStream;
418
419 auto activeStreamCallback = [=](emscripten::val event) {
420 m_active = true;
421 emit activated(m_active);
422 };
423 m_activeStreamEvent.reset(new qstdweb::EventCallback(m_mediaStream, "active", activeStreamCallback));
424
425 auto inactiveStreamCallback = [=](emscripten::val event) {
426 m_active = false;
427 emit activated(m_active);
428 };
429 m_inactiveStreamEvent.reset(new qstdweb::EventCallback(m_mediaStream, "inactive", inactiveStreamCallback));
430}
431
432QT_END_NAMESPACE
void setStreamDevice(const std::string &id)
Definition qwasmjs.cpp:358
qint64 writeData(const char *, qint64) override
Writes up to maxSize bytes from data to the device.
Definition qwasmjs.cpp:45
void startStreaming()
Definition qwasmjs.cpp:183
bool isSequential() const override
Returns true if this device is sequential; otherwise returns false.
Definition qwasmjs.cpp:20
qint64 size() const override
For open random-access devices, this function returns the size of the device.
Definition qwasmjs.cpp:25
void stopStream()
Definition qwasmjs.cpp:172
bool seek(qint64 pos) override
For random-access devices, this function sets the current position to pos, returning true on success,...
Definition qwasmjs.cpp:30
qint64 bytesAvailable() const override
Returns the number of bytes that are available for reading.
Definition qwasmjs.cpp:347
void setStream(emscripten::val stream)
Definition qwasmjs.cpp:195
bool open(QIODeviceBase::OpenMode mode) override
Opens the device and sets its OpenMode to mode.
Definition qwasmjs.cpp:13
void resumeStream()
Definition qwasmjs.cpp:162
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:37
void pauseStream()
Definition qwasmjs.cpp:153