Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
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 <private/qplatformmediaintegration_p.h>
8#include "qwasmcamera_p.h"
9#include "qwasmaudioinput_p.h"
10
11#include <private/qstdweb_p.h>
12#include <QtCore/QIODevice>
13#include <QFile>
14#include <QTimer>
15#include <QDebug>
16
18
19Q_LOGGING_CATEGORY(qWasmMediaRecorder, "qt.multimedia.wasm.mediarecorder")
20
23{
24 m_durationTimer.reset(new QElapsedTimer());
25 QPlatformMediaIntegration::instance()->mediaDevices(); // initialize getUserMedia
26}
27
29{
30 if (m_outputTarget->isOpen())
31 m_outputTarget->close();
32
33 if (!m_mediaRecorder.isNull()) {
34 m_mediaStreamDataAvailable.reset(nullptr);
35 m_mediaStreamStopped.reset(nullptr);
36 m_mediaStreamError.reset(nullptr);
37 m_mediaStreamStart.reset(nullptr);
38 }
39}
40
42{
43 return location.isValid() && (location.isLocalFile() || location.isRelative());
44}
45
47{
49
50 if (!m_mediaRecorder.isUndefined()) {
51 std::string state = m_mediaRecorder["state"].as<std::string>();
52 if (state == "recording")
53 recordingState = QMediaRecorder::RecordingState;
54 else if (state == "paused")
55 recordingState = QMediaRecorder::PausedState;
56 }
57 return recordingState;
58}
59
61{ // milliseconds
62 return m_durationMs;
63}
64
66{
67 if (!m_session)
68 return;
69
70 m_mediaSettings = settings;
71 initUserMedia();
72}
73
75{
76 if (!m_session || (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull())) {
77 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "could not find MediaRecorder";
78 return;
79 }
80 m_mediaRecorder.call<void>("pause");
82}
83
85{
86 if (!m_session || (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull())) {
87 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
88 return;
89 }
90
91 m_mediaRecorder.call<void>("resume");
93}
94
96{
97 if (!m_session || (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull())) {
98 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
99 return;
100 }
101 if (m_mediaRecorder["state"].as<std::string>() == "recording")
102 m_mediaRecorder.call<void>("stop");
103}
104
106{
107 m_session = static_cast<QWasmMediaCaptureSession *>(session);
108}
109
110bool QWasmMediaRecorder::hasCamera() const
111{
112 return m_session && m_session->camera();
113}
114
115void QWasmMediaRecorder::initUserMedia()
116{
117 setUpFileSink();
118 emscripten::val navigator = emscripten::val::global("navigator");
119 emscripten::val mediaDevices = navigator["mediaDevices"];
120
121 if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
122 qCDebug(qWasmMediaRecorder) << "MediaDevices are undefined or null";
123 return;
124 }
125
126 if (!m_session)
127 return;
128 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << m_session;
129
130 emscripten::val stream = emscripten::val::undefined();
131 if (hasCamera()) {
132 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has camera";
133 QWasmCamera *wasmCamera = reinterpret_cast<QWasmCamera *>(m_session->camera());
134
135 if (wasmCamera) {
136 emscripten::val m_video = wasmCamera->cameraOutput()->surfaceElement();
137 if (m_video.isNull() || m_video.isUndefined()) {
138 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "video element not found";
139 return;
140 }
141
142 stream = m_video["srcObject"];
143 if (stream.isNull() || stream.isUndefined()) {
144 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Video input stream not found";
145 return;
146 }
147 }
148 } else {
149 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has audio";
150 stream = static_cast<QWasmAudioInput *>(m_session->audioInput())->mediaStream();
151
152 if (stream.isNull() || stream.isUndefined()) {
153 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Audio input stream not found";
154 return;
155 }
156 }
157 if (stream.isNull() || stream.isUndefined()) {
158 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "No input stream found";
159 return;
160 }
161
162 setStream(stream);
163}
164
165void QWasmMediaRecorder::startAudioRecording()
166{
167 startStream();
168}
169
170void QWasmMediaRecorder::setStream(emscripten::val stream)
171{
172 emscripten::val emMediaSettings = emscripten::val::object();
173 QMediaFormat::VideoCodec videoCodec = m_mediaSettings.videoCodec();
174 QMediaFormat::AudioCodec audioCodec = m_mediaSettings.audioCodec();
176
177 // mime and codecs
178 QString mimeCodec;
179 if (!m_mediaSettings.mimeType().name().isEmpty()) {
180 mimeCodec = m_mediaSettings.mimeType().name();
181
183 mimeCodec += QStringLiteral(": codecs=");
184
185 if (audioCodec != QMediaFormat::AudioCodec::Unspecified) {
186 // TODO
187 }
188
190 mimeCodec += QMediaFormat::fileFormatName(m_mediaSettings.fileFormat());
191
192 emMediaSettings.set("mimeType", mimeCodec.toStdString());
193 }
194
195 if (m_mediaSettings.audioBitRate() > 0)
196 emMediaSettings.set("audioBitsPerSecond", emscripten::val(m_mediaSettings.audioBitRate()));
197
198 if (m_mediaSettings.videoBitRate() > 0)
199 emMediaSettings.set("videoBitsPerSecond", emscripten::val(m_mediaSettings.videoBitRate()));
200
201 // create the MediaRecorder, and set up data callback
202 m_mediaRecorder = emscripten::val::global("MediaRecorder").new_(stream, emMediaSettings);
203
204 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "m_mediaRecorder state:"
205 << QString::fromStdString(m_mediaRecorder["state"].as<std::string>());
206
207 if (m_mediaRecorder.isNull() || m_mediaRecorder.isUndefined()) {
208 qWarning() << "MediaRecorder could not be found";
209 return;
210 }
211 m_mediaRecorder.set("data-mediarecordercontext",
212 emscripten::val(quintptr(reinterpret_cast<void *>(this))));
213
214 if (!m_mediaStreamDataAvailable.isNull()) {
215 m_mediaStreamDataAvailable.reset();
216 m_mediaStreamStopped.reset();
217 m_mediaStreamError.reset();
218 m_mediaStreamStart.reset();
219 m_mediaStreamPause.reset();
220 m_mediaStreamResume.reset();
221 }
222
223 // dataavailable
224 auto callback = [](emscripten::val blob) {
225 if (blob.isUndefined() || blob.isNull()) {
226 qCDebug(qWasmMediaRecorder) << "blob is null";
227 return;
228 }
229 if (blob["target"].isUndefined() || blob["target"].isNull())
230 return;
231 if (blob["data"].isUndefined() || blob["data"].isNull())
232 return;
233 if (blob["target"]["data-mediarecordercontext"].isUndefined()
234 || blob["target"]["data-mediarecordercontext"].isNull())
235 return;
236
237 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
238 blob["target"]["data-mediarecordercontext"].as<quintptr>());
239
240 if (recorder) {
241 const double timeCode =
242 blob.hasOwnProperty("timecode") ? blob["timecode"].as<double>() : 0;
243 recorder->audioDataAvailable(blob["data"], timeCode);
244 }
245 };
246
247 m_mediaStreamDataAvailable.reset(
248 new qstdweb::EventCallback(m_mediaRecorder, "dataavailable", callback));
249
250 // stopped
251 auto stoppedCallback = [](emscripten::val event) {
252 if (event.isUndefined() || event.isNull()) {
253 qCDebug(qWasmMediaRecorder) << "event is null";
254 return;
255 }
256 qCDebug(qWasmMediaRecorder)
257 << "STOPPED: state changed"
258 << QString::fromStdString(event["target"]["state"].as<std::string>());
259
260 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
261 event["target"]["data-mediarecordercontext"].as<quintptr>());
262
263 if (recorder) {
264 recorder->m_isRecording = false;
265 recorder->m_durationTimer->invalidate();
266
267 emit recorder->stateChanged(recorder->state());
268 }
269 };
270
271 m_mediaStreamStopped.reset(
272 new qstdweb::EventCallback(m_mediaRecorder, "stop", stoppedCallback));
273
274 // error
275 auto errorCallback = [](emscripten::val theError) {
276 if (theError.isUndefined() || theError.isNull()) {
277 qCDebug(qWasmMediaRecorder) << "error is null";
278 return;
279 }
280 qCDebug(qWasmMediaRecorder)
281 << theError["code"].as<int>()
282 << QString::fromStdString(theError["message"].as<std::string>());
283
284 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
285 theError["target"]["data-mediarecordercontext"].as<quintptr>());
286
287 if (recorder) {
289 QString::fromStdString(theError["message"].as<std::string>()));
290 emit recorder->stateChanged(recorder->state());
291 }
292 };
293
294 m_mediaStreamError.reset(new qstdweb::EventCallback(m_mediaRecorder, "error", errorCallback));
295
296 // start
297 auto startCallback = [](emscripten::val event) {
298 if (event.isUndefined() || event.isNull()) {
299 qCDebug(qWasmMediaRecorder) << "event is null";
300 return;
301 }
302
303 qCDebug(qWasmMediaRecorder)
304 << "START: state changed"
305 << QString::fromStdString(event["target"]["state"].as<std::string>());
306
307 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
308 event["target"]["data-mediarecordercontext"].as<quintptr>());
309
310 if (recorder) {
311 recorder->m_isRecording = true;
312 recorder->m_durationTimer->start();
313 emit recorder->stateChanged(recorder->state());
314 }
315 };
316
317 m_mediaStreamStart.reset(new qstdweb::EventCallback(m_mediaRecorder, "start", startCallback));
318
319 // pause
320 auto pauseCallback = [](emscripten::val event) {
321 if (event.isUndefined() || event.isNull()) {
322 qCDebug(qWasmMediaRecorder) << "event is null";
323 return;
324 }
325
326 qCDebug(qWasmMediaRecorder)
327 << "pause: state changed"
328 << QString::fromStdString(event["target"]["state"].as<std::string>());
329
330 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
331 event["target"]["data-mediarecordercontext"].as<quintptr>());
332
333 if (recorder) {
334 recorder->m_isRecording = true;
335 recorder->m_durationTimer->start();
336 emit recorder->stateChanged(recorder->state());
337 }
338 };
339
340 m_mediaStreamPause.reset(new qstdweb::EventCallback(m_mediaRecorder, "pause", pauseCallback));
341
342 // resume
343 auto resumeCallback = [](emscripten::val event) {
344 if (event.isUndefined() || event.isNull()) {
345 qCDebug(qWasmMediaRecorder) << "event is null";
346 return;
347 }
348
349 qCDebug(qWasmMediaRecorder)
350 << "resume: state changed"
351 << QString::fromStdString(event["target"]["state"].as<std::string>());
352
353 QWasmMediaRecorder *recorder = reinterpret_cast<QWasmMediaRecorder *>(
354 event["target"]["data-mediarecordercontext"].as<quintptr>());
355
356 if (recorder) {
357 recorder->m_isRecording = true;
358 recorder->m_durationTimer->start();
359 emit recorder->stateChanged(recorder->state());
360 }
361 };
362
363 m_mediaStreamResume.reset(
364 new qstdweb::EventCallback(m_mediaRecorder, "resume", resumeCallback));
365
366 // set up what options we can
367 if (hasCamera())
368 setTrackContraints(m_mediaSettings, stream);
369 else
370 startStream();
371}
372
373void QWasmMediaRecorder::audioDataAvailable(emscripten::val blob, double timeCodeDifference)
374{
375 Q_UNUSED(timeCodeDifference)
376 if (blob.isUndefined() || blob.isNull()) {
377 qCDebug(qWasmMediaRecorder) << "blob is null";
378 return;
379 }
380
381 auto fileReader = std::make_shared<qstdweb::FileReader>();
382
383 fileReader->onError([=](emscripten::val theError) {
385 QString::fromStdString(theError["message"].as<std::string>()));
386 });
387
388 fileReader->onAbort([=](emscripten::val) {
390 });
391
392 fileReader->onLoad([=](emscripten::val) {
393 if (fileReader->val().isNull() || fileReader->val().isUndefined())
394 return;
395 qstdweb::ArrayBuffer result = fileReader->result();
396 if (result.val().isNull() || result.val().isUndefined())
397 return;
399
400 if (m_isRecording && !fileContent.isEmpty()) {
401 m_durationMs = m_durationTimer->elapsed();
402 if (m_outputTarget->isOpen())
403 m_outputTarget->write(fileContent, fileContent.length());
404 // we've read everything
405 emit durationChanged(m_durationMs);
406 qCDebug(qWasmMediaRecorder) << "duration changed" << m_durationMs;
407 }
408 });
409
410 fileReader->readAsArrayBuffer(qstdweb::Blob(blob));
411}
412
413// constraints are suggestions, as not all hardware supports all settings
414void QWasmMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
415{
416 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << settings.audioSampleRate();
417
418 if (stream.isUndefined() || stream.isNull()) {
419 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaStream";
420 return;
421 }
422
423 emscripten::val navigator = emscripten::val::global("navigator");
424 emscripten::val mediaDevices = navigator["mediaDevices"];
425
426 // check which ones are supported
427 // emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
428 // browsers only support some settings
429
430 emscripten::val videoParams = emscripten::val::object();
431 emscripten::val constraints = emscripten::val::object();
432
433 if (hasCamera()) {
434 if (settings.videoFrameRate() > 0)
435 videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
436 if (settings.videoResolution().height() > 0)
437 videoParams.set("height",
438 emscripten::val(settings.videoResolution().height())); // viewportHeight?
439 if (settings.videoResolution().width() > 0)
440 videoParams.set("width", emscripten::val(settings.videoResolution().width()));
441
442 constraints.set("video", videoParams); // only video here
443 }
444
445 emscripten::val audioParams = emscripten::val::object();
446 if (settings.audioSampleRate() > 0)
447 audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
448 if (settings.audioBitRate() > 0)
449 audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
450 if (settings.audioChannelCount() > 0)
451 audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
452
453 constraints.set("audio", audioParams); // only audio here
454
455 if (hasCamera() && stream["active"].as<bool>()) {
456 emscripten::val videoTracks = emscripten::val::undefined();
457 videoTracks = stream.call<emscripten::val>("getVideoTracks");
458 if (videoTracks.isNull() || videoTracks.isUndefined()) {
459 qCDebug(qWasmMediaRecorder) << "no video tracks";
460 return;
461 }
462 if (videoTracks["length"].as<int>() > 0) {
463 // try to apply the video options
464 qstdweb::Promise::make(videoTracks[0], QStringLiteral("applyConstraints"),
465 { .thenFunc =
466 [this](emscripten::val result) {
468 startStream();
469 },
470 .catchFunc =
471 [this](emscripten::val theError) {
472 qWarning() << "setting video params failed error";
473 qCDebug(qWasmMediaRecorder)
474 << theError["code"].as<int>()
475 << QString::fromStdString(theError["message"].as<std::string>());
477 QString::fromStdString(theError["message"].as<std::string>()));
478 } },
479 constraints);
480 }
481 }
482}
483
484// this starts the recording stream
485void QWasmMediaRecorder::startStream()
486{
487 if (m_mediaRecorder.isUndefined() || m_mediaRecorder.isNull()) {
488 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "could not find MediaStream";
489 return;
490 }
491 qCDebug(qWasmMediaRecorder) << "m_mediaRecorder state:" <<
492 QString::fromStdString(m_mediaRecorder["state"].as<std::string>());
493
494 constexpr int sliceSizeInMs = 250; // TODO find what size is best
495 m_mediaRecorder.call<void>("start", emscripten::val(sliceSizeInMs));
496
497 /* this method can optionally be passed a timeslice argument with a value in milliseconds.
498 * If this is specified, the media will be captured in separate chunks of that duration,
499 * rather than the default behavior of recording the media in a single large chunk.*/
500
502}
503
504void QWasmMediaRecorder::setUpFileSink()
505{
506 QString m_targetFileName = outputLocation().toLocalFile();
507 QString suffix = m_mediaSettings.mimeType().preferredSuffix();
508 if (m_targetFileName.isEmpty()) {
509 m_targetFileName = "/home/web_user/tmp." + suffix;
511 }
512
513 m_outputTarget = new QFile(m_targetFileName, this);
514 if (!m_outputTarget->open(QIODevice::WriteOnly)) {
515 qWarning() << "target file is not writable";
516 return;
517 }
518}
519
AVFCameraSession * m_session
AVCaptureDeviceInput * audioInput() const
\inmodule QtCore
Definition qbytearray.h:57
\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
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 QPlatformMediaIntegration * instance()
void updateError(QMediaRecorder::Error error, const QString &errorString)
void stateChanged(QMediaRecorder::RecorderState state)
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:129
static QString fromStdString(const std::string &s)
Definition qstring.h:1447
bool isEmpty() const noexcept
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:192
\inmodule QtCore
Definition qurl.h:94
QString toLocalFile() const
Returns the path of this URL formatted as a local file path.
Definition qurl.cpp:3425
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:592
ArrayBuffer result() const
Definition qstdweb.cpp:570
void onLoad(const std::function< void(emscripten::val)> &onLoad)
Definition qstdweb.cpp:580
void readAsArrayBuffer(const Blob &blob) const
Definition qstdweb.cpp:575
void onError(const std::function< void(emscripten::val)> &onError)
Definition qstdweb.cpp:586
emscripten::val val() const
Definition qstdweb.cpp:598
QByteArray copyToQByteArray() const
Definition qstdweb.cpp:672
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:221
#define Q_FUNC_INFO
EGLStreamKHR stream
QMediaFormat::FileFormat fileFormat
#define qWarning
Definition qlogging.h:166
#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:167
long long qint64
Definition qtypes.h:60
QSettings settings("MySoft", "Star Runner")
[0]