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
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/qplatformaudiodevices_p.h>
7#include <private/qplatformmediaintegration_p.h>
10
11#include <private/qstdweb_p.h>
12#include <QtCore/QIODevice>
13#include <QFile>
14#include <QDir>
15#include <QTimer>
16#include <QDebug>
17
19
20using namespace Qt::Literals;
21
22Q_LOGGING_CATEGORY(qWasmMediaRecorder, "qt.multimedia.wasm.mediarecorder")
23
26{
27 m_durationTimer.reset(new QElapsedTimer());
28 QPlatformMediaIntegration::instance()->audioDevices(); // initialize getUserMedia
29
30 m_jsMediaRecorderDevice.reset(new JsMediaRecorder());
31
32 connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::started, this,
33 [this]() {
34 m_isRecording = true;
35 m_durationTimer->start();
36 emit stateChanged(QMediaRecorder::RecordingState);
37 });
38
39 connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::stopped, this,
40 [this]() {
41 m_isRecording = false;
42 m_durationMs = m_durationTimer->elapsed();
43 emit durationChanged(m_durationMs);
44
45 m_durationTimer->invalidate();
46 emit stateChanged(QMediaRecorder::StoppedState);
47 });
48
49 connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::paused, this,
50 [this]() {
51 m_isRecording = false;
52 m_durationMs = m_durationTimer->elapsed();
53 emit durationChanged(m_durationMs);
54
55 m_durationTimer->invalidate();
56 emit stateChanged(QMediaRecorder::PausedState);
57 });
58
59 connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::resumed, this,
60 [this]() {
61 m_isRecording = true;
62 m_durationTimer->start();
63 });
64
65 connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::streamError, this,
66 [this](QMediaRecorder::Error errorCode, const QString &errorMessage) {
67 updateError(errorCode, errorMessage);
68 emit stateChanged(state());
69 });
70}
71
72QWasmMediaRecorder::~QWasmMediaRecorder()
73{
74 if (m_outputTarget->isOpen())
75 m_outputTarget->close();
76
77 if (m_jsMediaRecorderDevice->isOpen()) {
78 qWarning() << " bytes still available" << m_jsMediaRecorderDevice->bytesAvailable();
79 m_jsMediaRecorderDevice->close();
80 }
81
82 if (!m_mediaRecorder.isNull()) {
83 m_mediaStreamDataAvailable.reset(nullptr);
84 m_mediaStreamStopped.reset(nullptr);
85 m_mediaStreamError.reset(nullptr);
86 m_mediaStreamStart.reset(nullptr);
87 }
88}
89
90bool QWasmMediaRecorder::isLocationWritable(const QUrl &location) const
91{
92 return location.isValid() && (location.isLocalFile() || location.isRelative());
93}
94
96{
97 return m_jsMediaRecorderDevice->currentState();
98}
99
101{ // milliseconds
102 return m_durationMs;
103}
104
105void QWasmMediaRecorder::record(QMediaEncoderSettings &settings)
106{
107 if (!m_session)
108 return;
109
110 m_mediaSettings = settings;
111 if (!m_jsMediaRecorderDevice->open(QIODeviceBase::ReadOnly)) {
112 qWarning() << "m_jsMediaRecorderDevice is not open";
113 return;
114 }
115
116 initUserMedia();
117 m_jsMediaRecorderDevice->startStreaming();
118}
119
121{
122 if (!m_session) {
123 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "could not find MediaRecorder";
124 return;
125 }
126 m_jsMediaRecorderDevice->pauseStream();
127}
128
130{
131 if (!m_session) {
132 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
133 return;
134 }
135 m_jsMediaRecorderDevice->resumeStream();
136}
137
139{
140 if (!m_session) {
141 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
142 return;
143 }
144 m_jsMediaRecorderDevice->stopStream();
145}
146
147void QWasmMediaRecorder::setCaptureSession(QPlatformMediaCaptureSession *session)
148{
149 m_session = static_cast<QWasmMediaCaptureSession *>(session);
150}
151
152bool QWasmMediaRecorder::hasCamera() const
153{
154 return m_session && m_session->camera();
155}
156
157void QWasmMediaRecorder::initUserMedia()
158{
159 setUpFileSink();
160 emscripten::val navigator = emscripten::val::global("navigator");
161 emscripten::val mediaDevices = navigator["mediaDevices"];
162
163 if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
164 qCDebug(qWasmMediaRecorder) << "MediaDevices are undefined or null";
165 return;
166 }
167
168 if (!m_session)
169 return;
170 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << m_session;
171
172 emscripten::val stream = emscripten::val::undefined();
173 if (hasCamera()) {
174 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has camera";
175 QWasmCamera *wasmCamera = reinterpret_cast<QWasmCamera *>(m_session->camera());
176
177 if (wasmCamera) {
178 m_jsMediaRecorderDevice->setNeedsCamera(true);
179 emscripten::val m_video = wasmCamera->cameraOutput()->surfaceElement();
180 if (m_video.isNull() || m_video.isUndefined()) {
181 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "video element not found";
182 return;
183 }
184
185 stream = m_video["srcObject"];
186 if (stream.isNull() || stream.isUndefined()) {
187 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Video input stream not found";
188 return;
189 }
190 }
191 }
192 if (m_session->hasAudio()) {
193 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has audio";
194 stream = static_cast<QWasmAudioInput *>(m_session->audioInput())->mediaStream();
195
196 if (stream.isNull() || stream.isUndefined()) {
197 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Audio input stream not found";
198 return;
199 }
200 m_jsMediaRecorderDevice->setNeedsAudio(m_session->hasAudio());
201 }
202 if (stream.isNull() || stream.isUndefined()) {
203 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "No input stream found";
204 return;
205 }
206 if (!m_jsMediaRecorderDevice->open(QIODeviceBase::ReadOnly)) {
207 qWarning() << "m_jsMediaRecorderDevice is not open";
208 return;
209 }
210
211 QObject::connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::readyRead, this, [this]() {
212
213 if (m_jsMediaRecorderDevice->bytesAvailable()) {
214
215 QByteArray mediaData = m_jsMediaRecorderDevice->read(m_jsMediaRecorderDevice->bytesAvailable());
216
217 m_durationMs = m_durationTimer->elapsed();
218 if (m_outputTarget->isOpen())
219 m_outputTarget->write(mediaData, mediaData.length());
220 // we've read everything
221 if (m_durationMs > 0) {
222 emit durationChanged(m_durationMs);
223 qCDebug(qWasmMediaRecorder) << "duration changed" << m_durationMs;
224 }
225 }
226 });
227
228 m_jsMediaRecorderDevice->setStream(stream);
229}
230
231void QWasmMediaRecorder::startAudioRecording()
232{
233 startStream();
234}
235
236void QWasmMediaRecorder::initMediaSettings()
237{
238 m_hasMediaSettings = true;
239}
240
241void QWasmMediaRecorder::setStream(emscripten::val stream)
242{
243 // set up what options we can
244 if (hasCamera())
245 setTrackContraints(m_mediaSettings, stream);
246 else
247 startStream();
248}
249
250// constraints are suggestions, as not all hardware supports all settings
251void QWasmMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
252{
253 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO
254 << settings.audioSampleRate()
255 << settings.videoResolution();
256
257 if (stream.isUndefined() || stream.isNull()) {
258 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaStream";
259 return;
260 }
261
262 emscripten::val navigator = emscripten::val::global("navigator");
263 emscripten::val mediaDevices = navigator["mediaDevices"];
264
265 // check which ones are supported
266 emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
267// browsers only support some settings
268
269 emscripten::val videoParams = emscripten::val::object();
270 emscripten::val constraints = emscripten::val::object();
271 videoParams.set("resizeMode",std::string("crop-and-scale"));
272
273 if (hasCamera()) {
274 if (settings.videoFrameRate() > 0)
275 videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
276 if (settings.videoResolution().height() > 0)
277 videoParams.set("height",
278 emscripten::val(settings.videoResolution().height())); // viewportHeight?
279 if (settings.videoResolution().width() > 0)
280 videoParams.set("width", emscripten::val(settings.videoResolution().width()));
281
282 constraints.set("video", videoParams); // only video here
283 }
284
285 emscripten::val audioParams = emscripten::val::object();
286 if (settings.audioSampleRate() > 0)
287 audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
288 if (settings.audioBitRate() > 0)
289 audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
290 if (settings.audioChannelCount() > 0)
291 audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
292
293 constraints.set("audio", audioParams); // only audio here
294
295 if (hasCamera() && stream["active"].as<bool>()) {
296 emscripten::val videoTracks = emscripten::val::undefined();
297 videoTracks = stream.call<emscripten::val>("getVideoTracks");
298 if (videoTracks.isNull() || videoTracks.isUndefined()) {
299 qCDebug(qWasmMediaRecorder) << "no video tracks";
300 return;
301 }
302 if (videoTracks["length"].as<int>() > 0) {
303 // try to apply the video options
304 qstdweb::Promise::make(videoTracks[0], QStringLiteral("applyConstraints"),
305 { .thenFunc =
306 [this](emscripten::val result) {
307 Q_UNUSED(result)
308 startStream();
309 },
310 .catchFunc =
311 [this](emscripten::val theError) {
312 qCDebug(qWasmMediaRecorder)
313 << theError["code"].as<int>()
314 << QString::fromStdString(theError["message"].as<std::string>());
315 updateError(QMediaRecorder::ResourceError,
316 QString::fromStdString(theError["message"].as<std::string>()));
317 },
318 .finallyFunc = []() {},
319 },
320 constraints);
321 }
322 }
323 startStream();
324}
325
326// this starts the recording stream
327void QWasmMediaRecorder::startStream()
328{
329 m_jsMediaRecorderDevice->startStreaming();
330}
331
332void QWasmMediaRecorder::setUpFileSink()
333{
334 QString m_targetFileName = outputLocation().toLocalFile();
335
336 QString suffix = m_mediaSettings.mimeType().preferredSuffix();
337 // what if no mimeType yet?
338
339 if (!m_mediaSettings.mimeType().isValid()) {
340 qWarning() << "mimetype not valid, using m4v";
341 suffix = QStringLiteral(".m4v");
342 }
343 if (m_targetFileName.isEmpty()) {
344 m_targetFileName = QDir::homePath() + u"/tmp."_s + suffix;
345 QPlatformMediaRecorder::setOutputLocation(QUrl::fromLocalFile(m_targetFileName));
346 }
347
348 m_outputTarget = new QFile(m_targetFileName, this);
349 if (!m_outputTarget->open(QIODevice::WriteOnly)) {
350 qWarning() << "target file is not writable";
351 return;
352 }
353}
354
355QT_END_NAMESPACE
\inmodule QtMultimedia
QObject * parent
Definition qobject.h:74
QWasmVideoOutput * cameraOutput()
QWasmMediaRecorder(QMediaRecorder *parent)
qint64 duration() const override
void setCaptureSession(QPlatformMediaCaptureSession *session)
QMediaRecorder::RecorderState state() const override
void record(QMediaEncoderSettings &settings) override
Combined button and popup list for selecting options.
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")