Qt 6.x
The Qt SDK
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
qwasmmediarecorder.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 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
6#include <private/qplatformmediadevices_p.h>
7#include "qwasmcamera_p.h"
8#include "qwasmaudioinput_p.h"
9
10#include <private/qstdweb_p.h>
11#include <QtCore/QIODevice>
12#include <QFile>
13#include <QTimer>
14#include <QDebug>
15
17
18Q_LOGGING_CATEGORY(qWasmMediaRecorder, "qt.multimedia.wasm.mediarecorder")
19
22{
23 m_durationTimer.reset(new QElapsedTimer());
24 QPlatformMediaDevices::instance(); // initialize getUserMedia
25}
26
28{
29 if (m_outputTarget->isOpen())
30 m_outputTarget->close();
31
32 if (!m_mediaRecorder.isNull()) {
33 m_mediaStreamDataAvailable.reset(nullptr);
34 m_mediaStreamStopped.reset(nullptr);
35 m_mediaStreamError.reset(nullptr);
36 m_mediaStreamStart.reset(nullptr);
37 }
38}
39
41{
42 return location.isValid() && (location.isLocalFile() || location.isRelative());
43}
44
46{
48
49 if (!m_mediaRecorder.isUndefined()) {
50 std::string state = m_mediaRecorder["state"].as<std::string>();
51 if (state == "recording")
52 recordingState = QMediaRecorder::RecordingState;
53 else if (state == "paused")
54 recordingState = QMediaRecorder::PausedState;
55 }
56 return recordingState;
57}
58
60{ // milliseconds
61 return m_durationMs;
62}
63
65{
66 if (!m_session)
67 return;
68
69 m_mediaSettings = settings;
70 initUserMedia();
71}
72
74{
75 if (!m_session || (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull())) {
76 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "could not find MediaRecorder";
77 return;
78 }
79 m_mediaRecorder.call<void>("pause");
81}
82
84{
85 if (!m_session || (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull())) {
86 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
87 return;
88 }
89
90 m_mediaRecorder.call<void>("resume");
92}
93
95{
96 if (!m_session || (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull())) {
97 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
98 return;
99 }
100 if (m_mediaRecorder["state"].as<std::string>() == "recording")
101 m_mediaRecorder.call<void>("stop");
102}
103
105{
106 m_session = static_cast<QWasmMediaCaptureSession *>(session);
107}
108
109bool QWasmMediaRecorder::hasCamera() const
110{
111 return m_session && m_session->camera();
112}
113
114void QWasmMediaRecorder::initUserMedia()
115{
116 setUpFileSink();
117 emscripten::val navigator = emscripten::val::global("navigator");
118 emscripten::val mediaDevices = navigator["mediaDevices"];
119
120 if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
121 qCDebug(qWasmMediaRecorder) << "MediaDevices are undefined or null";
122 return;
123 }
124
125 if (!m_session)
126 return;
127 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << m_session;
128
129 emscripten::val stream = emscripten::val::undefined();
130 if (hasCamera()) {
131 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has camera";
132 QWasmCamera *wasmCamera = reinterpret_cast<QWasmCamera *>(m_session->camera());
133
134 if (wasmCamera) {
135 emscripten::val m_video = wasmCamera->cameraOutput()->surfaceElement();
136 if (m_video.isNull() || m_video.isUndefined()) {
137 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "video element not found";
138 return;
139 }
140
141 stream = m_video["srcObject"];
142 if (stream.isNull() || stream.isUndefined()) {
143 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Video input stream not found";
144 return;
145 }
146 }
147 } else {
148 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has audio";
149 stream = static_cast<QWasmAudioInput *>(m_session->audioInput())->mediaStream();
150
151 if (stream.isNull() || stream.isUndefined()) {
152 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Audio input stream not found";
153 return;
154 }
155 }
156 if (stream.isNull() || stream.isUndefined()) {
157 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "No input stream found";
158 return;
159 }
160
161 setStream(stream);
162}
163
164void QWasmMediaRecorder::startAudioRecording()
165{
166 startStream();
167}
168
169void QWasmMediaRecorder::setStream(emscripten::val stream)
170{
171 emscripten::val emMediaSettings = emscripten::val::object();
172 QMediaFormat::VideoCodec videoCodec = m_mediaSettings.videoCodec();
173 QMediaFormat::AudioCodec audioCodec = m_mediaSettings.audioCodec();
175
176 // mime and codecs
177 QString mimeCodec;
178 if (!m_mediaSettings.mimeType().name().isEmpty()) {
179 mimeCodec = m_mediaSettings.mimeType().name();
180
182 mimeCodec += QStringLiteral(": codecs=");
183
184 if (audioCodec != QMediaFormat::AudioCodec::Unspecified) {
185 // TODO
186 }
187
189 mimeCodec += QMediaFormat::fileFormatName(m_mediaSettings.fileFormat());
190
191 emMediaSettings.set("mimeType", mimeCodec.toStdString());
192 }
193
194 if (m_mediaSettings.audioBitRate() > 0)
195 emMediaSettings.set("audioBitsPerSecond", emscripten::val(m_mediaSettings.audioBitRate()));
196
197 if (m_mediaSettings.videoBitRate() > 0)
198 emMediaSettings.set("videoBitsPerSecond", emscripten::val(m_mediaSettings.videoBitRate()));
199
200 // create the MediaRecorder, and set up data callback
201 m_mediaRecorder = emscripten::val::global("MediaRecorder").new_(stream, emMediaSettings);
202
203 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "m_mediaRecorder state:"
204 << QString::fromStdString(m_mediaRecorder["state"].as<std::string>());
205
206 if (m_mediaRecorder.isNull() || m_mediaRecorder.isUndefined()) {
207 qWarning() << "MediaRecorder could not be found";
208 return;
209 }
210 m_mediaRecorder.set("data-mediarecordercontext",
211 emscripten::val(quintptr(reinterpret_cast<void *>(this))));
212
213 if (!m_mediaStreamDataAvailable.isNull()) {
214 m_mediaStreamDataAvailable.reset();
215 m_mediaStreamStopped.reset();
216 m_mediaStreamError.reset();
217 m_mediaStreamStart.reset();
218 m_mediaStreamPause.reset();
219 m_mediaStreamResume.reset();
220 }
221
222 // dataavailable
223 auto callback = [](emscripten::val blob) {
224 if (blob.isUndefined() || blob.isNull()) {
225 qCDebug(qWasmMediaRecorder) << "blob is null";
226 return;
227 }
228 if (blob["target"].isUndefined() || blob["target"].isNull())
229 return;
230 if (blob["data"].isUndefined() || blob["data"].isNull())
231 return;
232 if (blob["target"]["data-mediarecordercontext"].isUndefined()
233 || blob["target"]["data-mediarecordercontext"].isNull())
234 return;
235
236 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
237 blob["target"]["data-mediarecordercontext"].as<quintptr>());
238
239 if (recorder) {
240 const double timeCode =
241 blob.hasOwnProperty("timecode") ? blob["timecode"].as<double>() : 0;
242 recorder->audioDataAvailable(blob["data"], timeCode);
243 }
244 };
245
246 m_mediaStreamDataAvailable.reset(
247 new qstdweb::EventCallback(m_mediaRecorder, "dataavailable", callback));
248
249 // stopped
250 auto stoppedCallback = [](emscripten::val event) {
251 if (event.isUndefined() || event.isNull()) {
252 qCDebug(qWasmMediaRecorder) << "event is null";
253 return;
254 }
255 qCDebug(qWasmMediaRecorder)
256 << "STOPPED: state changed"
257 << QString::fromStdString(event["target"]["state"].as<std::string>());
258
259 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
260 event["target"]["data-mediarecordercontext"].as<quintptr>());
261
262 if (recorder) {
263 recorder->m_isRecording = false;
264 recorder->m_durationTimer->invalidate();
265
266 emit recorder->stateChanged(recorder->state());
267 }
268 };
269
270 m_mediaStreamStopped.reset(
271 new qstdweb::EventCallback(m_mediaRecorder, "stop", stoppedCallback));
272
273 // error
274 auto errorCallback = [](emscripten::val theError) {
275 if (theError.isUndefined() || theError.isNull()) {
276 qCDebug(qWasmMediaRecorder) << "error is null";
277 return;
278 }
279 qCDebug(qWasmMediaRecorder)
280 << theError["code"].as<int>()
281 << QString::fromStdString(theError["message"].as<std::string>());
282
283 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
284 theError["target"]["data-mediarecordercontext"].as<quintptr>());
285
286 if (recorder) {
288 QString::fromStdString(theError["message"].as<std::string>()));
289 emit recorder->stateChanged(recorder->state());
290 }
291 };
292
293 m_mediaStreamError.reset(new qstdweb::EventCallback(m_mediaRecorder, "error", errorCallback));
294
295 // start
296 auto startCallback = [](emscripten::val event) {
297 if (event.isUndefined() || event.isNull()) {
298 qCDebug(qWasmMediaRecorder) << "event is null";
299 return;
300 }
301
302 qCDebug(qWasmMediaRecorder)
303 << "START: state changed"
304 << QString::fromStdString(event["target"]["state"].as<std::string>());
305
306 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
307 event["target"]["data-mediarecordercontext"].as<quintptr>());
308
309 if (recorder) {
310 recorder->m_isRecording = true;
311 recorder->m_durationTimer->start();
312 emit recorder->stateChanged(recorder->state());
313 }
314 };
315
316 m_mediaStreamStart.reset(new qstdweb::EventCallback(m_mediaRecorder, "start", startCallback));
317
318 // pause
319 auto pauseCallback = [](emscripten::val event) {
320 if (event.isUndefined() || event.isNull()) {
321 qCDebug(qWasmMediaRecorder) << "event is null";
322 return;
323 }
324
325 qCDebug(qWasmMediaRecorder)
326 << "pause: state changed"
327 << QString::fromStdString(event["target"]["state"].as<std::string>());
328
329 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
330 event["target"]["data-mediarecordercontext"].as<quintptr>());
331
332 if (recorder) {
333 recorder->m_isRecording = true;
334 recorder->m_durationTimer->start();
335 emit recorder->stateChanged(recorder->state());
336 }
337 };
338
339 m_mediaStreamPause.reset(new qstdweb::EventCallback(m_mediaRecorder, "pause", pauseCallback));
340
341 // resume
342 auto resumeCallback = [](emscripten::val event) {
343 if (event.isUndefined() || event.isNull()) {
344 qCDebug(qWasmMediaRecorder) << "event is null";
345 return;
346 }
347
348 qCDebug(qWasmMediaRecorder)
349 << "resume: state changed"
350 << QString::fromStdString(event["target"]["state"].as<std::string>());
351
352 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
353 event["target"]["data-mediarecordercontext"].as<quintptr>());
354
355 if (recorder) {
356 recorder->m_isRecording = true;
357 recorder->m_durationTimer->start();
358 emit recorder->stateChanged(recorder->state());
359 }
360 };
361
362 m_mediaStreamResume.reset(
363 new qstdweb::EventCallback(m_mediaRecorder, "resume", resumeCallback));
364
365 // set up what options we can
366 if (hasCamera())
367 setTrackContraints(m_mediaSettings, stream);
368 else
369 startStream();
370}
371
372void QWasmMediaRecorder::audioDataAvailable(emscripten::val blob, double timeCodeDifference)
373{
374 Q_UNUSED(timeCodeDifference)
375 if (blob.isUndefined() || blob.isNull()) {
376 qCDebug(qWasmMediaRecorder) << "blob is null";
377 return;
378 }
379
380 auto fileReader = std::make_shared<qstdweb::FileReader>();
381
382 fileReader->onError([=](emscripten::val theError) {
384 QString::fromStdString(theError["message"].as<std::string>()));
385 });
386
387 fileReader->onAbort([=](emscripten::val) {
389 });
390
391 fileReader->onLoad([=](emscripten::val) {
392 if (fileReader->val().isNull() || fileReader->val().isUndefined())
393 return;
394 qstdweb::ArrayBuffer result = fileReader->result();
395 if (result.val().isNull() || result.val().isUndefined())
396 return;
398
399 if (m_isRecording && !fileContent.isEmpty()) {
400 m_durationMs = m_durationTimer->elapsed();
401 if (m_outputTarget->isOpen())
402 m_outputTarget->write(fileContent, fileContent.length());
403 // we've read everything
404 emit durationChanged(m_durationMs);
405 qCDebug(qWasmMediaRecorder) << "duration changed" << m_durationMs;
406 }
407 });
408
409 fileReader->readAsArrayBuffer(qstdweb::Blob(blob));
410}
411
412// constraints are suggestions, as not all hardware supports all settings
413void QWasmMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
414{
415 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << settings.audioSampleRate();
416
417 if (stream.isUndefined() || stream.isNull()) {
418 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaStream";
419 return;
420 }
421
422 emscripten::val navigator = emscripten::val::global("navigator");
423 emscripten::val mediaDevices = navigator["mediaDevices"];
424
425 // check which ones are supported
426 // emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
427 // browsers only support some settings
428
429 emscripten::val videoParams = emscripten::val::object();
430 emscripten::val constraints = emscripten::val::object();
431
432 if (hasCamera()) {
433 if (settings.videoFrameRate() > 0)
434 videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
435 if (settings.videoResolution().height() > 0)
436 videoParams.set("height",
437 emscripten::val(settings.videoResolution().height())); // viewportHeight?
438 if (settings.videoResolution().width() > 0)
439 videoParams.set("width", emscripten::val(settings.videoResolution().width()));
440
441 constraints.set("video", videoParams); // only video here
442 }
443
444 emscripten::val audioParams = emscripten::val::object();
445 if (settings.audioSampleRate() > 0)
446 audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
447 if (settings.audioBitRate() > 0)
448 audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
449 if (settings.audioChannelCount() > 0)
450 audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
451
452 constraints.set("audio", audioParams); // only audio here
453
454 if (hasCamera() && stream["active"].as<bool>()) {
455 emscripten::val videoTracks = emscripten::val::undefined();
456 videoTracks = stream.call<emscripten::val>("getVideoTracks");
457 if (videoTracks.isNull() || videoTracks.isUndefined()) {
458 qCDebug(qWasmMediaRecorder) << "no video tracks";
459 return;
460 }
461 if (videoTracks["length"].as<int>() > 0) {
462 // try to apply the video options
463 qstdweb::Promise::make(videoTracks[0], QStringLiteral("applyConstraints"),
464 { .thenFunc =
465 [this](emscripten::val result) {
467 startStream();
468 },
469 .catchFunc =
470 [this](emscripten::val theError) {
471 qWarning() << "setting video params failed error";
472 qCDebug(qWasmMediaRecorder)
473 << theError["code"].as<int>()
474 << QString::fromStdString(theError["message"].as<std::string>());
475 error(QMediaRecorder::ResourceError, QString::fromStdString(theError["message"].as<std::string>()));
476 } },
477 constraints);
478 }
479 }
480}
481
482// this starts the recording stream
483void QWasmMediaRecorder::startStream()
484{
485 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
486 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "could not find MediaStream";
487 return;
488 }
489 qCDebug(qWasmMediaRecorder) << "m_mediaRecorder state:" <<
490 QString::fromStdString(m_mediaRecorder["state"].as<std::string>());
491
492 constexpr int sliceSizeInMs = 250; // TODO find what size is best
493 m_mediaRecorder.call<void>("start", emscripten::val(sliceSizeInMs));
494
495 /* this method can optionally be passed a timeslice argument with a value in milliseconds.
496 * If this is specified, the media will be captured in separate chunks of that duration,
497 * rather than the default behavior of recording the media in a single large chunk.*/
498
500}
501
502void QWasmMediaRecorder::setUpFileSink()
503{
504 QString m_targetFileName = outputLocation().toLocalFile();
505 QString suffix = m_mediaSettings.mimeType().preferredSuffix();
506 if (m_targetFileName.isEmpty()) {
507 m_targetFileName = "/home/web_user/tmp." + suffix;
509 }
510
511 m_outputTarget = new QFile(m_targetFileName, this);
512 if (!m_outputTarget->open(QIODevice::WriteOnly)) {
513 qWarning() << "target file is not writable";
514 return;
515 }
516}
517
AVFCameraSession * m_session
AVCaptureDeviceInput * audioInput() const
\inmodule QtCore
Definition qbytearray.h:57
qsizetype length() const noexcept
Same as size().
Definition qbytearray.h:479
bool isEmpty() const noexcept
Returns true if the byte array has size 0; otherwise returns false.
Definition qbytearray.h:106
\inmodule QtCore
qint64 elapsed() const noexcept
Returns the number of milliseconds since this QElapsedTimer was last started.
\inmodule QtCore
Definition qfile.h:93
virtual bool open(QIODeviceBase::OpenMode mode)
Opens the device and sets its OpenMode to mode.
bool isOpen() const
Returns true if the device is open; otherwise returns false.
qint64 write(const char *data, qint64 len)
Writes at most maxSize bytes of data from data to the device.
virtual void close()
First emits aboutToClose(), then closes the device and sets its OpenMode to NotOpen.
QMediaFormat::AudioCodec audioCodec() const
QMediaFormat::FileFormat fileFormat() const
QMediaFormat::VideoCodec videoCodec() const
AudioCodec
\qmlproperty enumeration QtMultimedia::mediaFormat::fileFormat
FileFormat
Describes the container format used in a multimedia file or stream.
VideoCodec
\qmlproperty enumeration QtMultimedia::mediaFormat::audioCodec
static Q_INVOKABLE QString fileFormatName(FileFormat fileFormat)
\qmlmethod string QtMultimedia::mediaFormat::fileFormatName(fileFormat) Returns a string based name f...
\inmodule QtMultimedia
QMediaRecorder::Error error
Returns the current error state.
RecorderState
\qmlproperty enumeration QtMultimedia::MediaRecorder::recorderState
QString preferredSuffix
the preferred suffix for the MIME type
Definition qmimetype.h:38
QString name
the name of the MIME type
Definition qmimetype.h:29
static QPlatformMediaDevices * instance()
void stateChanged(QMediaRecorder::RecorderState state)
QMediaRecorder::Error error() const
void durationChanged(qint64 position)
virtual void setOutputLocation(const QUrl &location)
bool isNull() const noexcept
Returns true if this object refers to \nullptr.
void reset(T *other=nullptr) noexcept(noexcept(Cleanup::cleanup(std::declval< T * >())))
Deletes the existing object it is pointing to (if any), and sets its pointer to other.
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
static QString fromStdString(const std::string &s)
Definition qstring.h:1322
std::string toStdString() const
Returns a std::string object with the data contained in this QString.
Definition qstring.h:1319
bool isEmpty() const
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:1083
\inmodule QtCore
Definition qurl.h:94
QString toLocalFile() const
Returns the path of this URL formatted as a local file path.
Definition qurl.cpp:3411
QWasmVideoOutput * cameraOutput()
QPlatformCamera * camera() override
qint64 duration() const override
bool isLocationWritable(const QUrl &location) const override
void setCaptureSession(QPlatformMediaCaptureSession *session)
QMediaRecorder::RecorderState state() const override
void record(QMediaEncoderSettings &settings) override
emscripten::val surfaceElement()
void onAbort(const std::function< void(emscripten::val)> &onAbort)
Definition qstdweb.cpp:639
ArrayBuffer result() const
Definition qstdweb.cpp:617
void onLoad(const std::function< void(emscripten::val)> &onLoad)
Definition qstdweb.cpp:627
void readAsArrayBuffer(const Blob &blob) const
Definition qstdweb.cpp:622
void onError(const std::function< void(emscripten::val)> &onError)
Definition qstdweb.cpp:633
emscripten::val val()
Definition qstdweb.cpp:645
QByteArray copyToQByteArray() const
Definition qstdweb.cpp:712
QMediaRecorder * recorder
Definition camera.cpp:20
Combined button and popup list for selecting options.
bool isNull(const T &t)
void make(emscripten::val target, QString methodName, PromiseCallbacks callbacks, Args... args)
Definition qstdweb_p.h:182
#define Q_FUNC_INFO
EGLStreamKHR stream
QMediaFormat::FileFormat fileFormat
#define qWarning
Definition qlogging.h:162
#define Q_LOGGING_CATEGORY(name,...)
#define qCDebug(category,...)
GLint location
struct _cl_event * event
GLuint64EXT * result
[6]
#define QStringLiteral(str)
#define emit
#define Q_UNUSED(x)
size_t quintptr
Definition qtypes.h:72
long long qint64
Definition qtypes.h:55
QSettings settings("MySoft", "Star Runner")
[0]
IUIAutomationTreeWalker __RPC__deref_out_opt IUIAutomationElement ** parent