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 } else {
192 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has audio";
193 stream = static_cast<QWasmAudioInput *>(m_session->audioInput())->mediaStream();
194
195 if (stream.isNull() || stream.isUndefined()) {
196 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Audio input stream not found";
197 return;
198 }
199 m_jsMediaRecorderDevice->setNeedsAudio(true);
200 }
201 if (stream.isNull() || stream.isUndefined()) {
202 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "No input stream found";
203 return;
204 }
205 if (!m_jsMediaRecorderDevice->open(QIODeviceBase::ReadOnly)) {
206 qWarning() << "m_jsMediaRecorderDevice is not open";
207 return;
208 }
209
210 QObject::connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::readyRead, this, [this]() {
211
212 if (m_jsMediaRecorderDevice->bytesAvailable()) {
213
214 QByteArray mediaData = m_jsMediaRecorderDevice->read(m_jsMediaRecorderDevice->bytesAvailable());
215
216 m_durationMs = m_durationTimer->elapsed();
217 if (m_outputTarget->isOpen())
218 m_outputTarget->write(mediaData, mediaData.length());
219 // we've read everything
220 if (m_durationMs > 0) {
221 emit durationChanged(m_durationMs);
222 qCDebug(qWasmMediaRecorder) << "duration changed" << m_durationMs;
223 }
224 }
225 });
226
227 m_jsMediaRecorderDevice->setStream(stream);
228}
229
230void QWasmMediaRecorder::startAudioRecording()
231{
232 startStream();
233}
234
235void QWasmMediaRecorder::initMediaSettings()
236{
237 m_hasMediaSettings = true;
238}
239
240void QWasmMediaRecorder::setStream(emscripten::val stream)
241{
242 // set up what options we can
243 if (hasCamera())
244 setTrackContraints(m_mediaSettings, stream);
245 else
246 startStream();
247}
248
249// constraints are suggestions, as not all hardware supports all settings
250void QWasmMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
251{
252 qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO
253 << settings.audioSampleRate()
254 << settings.videoResolution();
255
256 if (stream.isUndefined() || stream.isNull()) {
257 qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaStream";
258 return;
259 }
260
261 emscripten::val navigator = emscripten::val::global("navigator");
262 emscripten::val mediaDevices = navigator["mediaDevices"];
263
264 // check which ones are supported
265 emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
266// browsers only support some settings
267
268 emscripten::val videoParams = emscripten::val::object();
269 emscripten::val constraints = emscripten::val::object();
270 videoParams.set("resizeMode",std::string("crop-and-scale"));
271
272 if (hasCamera()) {
273 if (settings.videoFrameRate() > 0)
274 videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
275 if (settings.videoResolution().height() > 0)
276 videoParams.set("height",
277 emscripten::val(settings.videoResolution().height())); // viewportHeight?
278 if (settings.videoResolution().width() > 0)
279 videoParams.set("width", emscripten::val(settings.videoResolution().width()));
280
281 constraints.set("video", videoParams); // only video here
282 }
283
284 emscripten::val audioParams = emscripten::val::object();
285 if (settings.audioSampleRate() > 0)
286 audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
287 if (settings.audioBitRate() > 0)
288 audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
289 if (settings.audioChannelCount() > 0)
290 audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
291
292 constraints.set("audio", audioParams); // only audio here
293
294 if (hasCamera() && stream["active"].as<bool>()) {
295 emscripten::val videoTracks = emscripten::val::undefined();
296 videoTracks = stream.call<emscripten::val>("getVideoTracks");
297 if (videoTracks.isNull() || videoTracks.isUndefined()) {
298 qCDebug(qWasmMediaRecorder) << "no video tracks";
299 return;
300 }
301 if (videoTracks["length"].as<int>() > 0) {
302 // try to apply the video options
303 qstdweb::Promise::make(videoTracks[0], QStringLiteral("applyConstraints"),
304 { .thenFunc =
305 [this](emscripten::val result) {
306 Q_UNUSED(result)
307 startStream();
308 },
309 .catchFunc =
310 [this](emscripten::val theError) {
311 qCDebug(qWasmMediaRecorder)
312 << theError["code"].as<int>()
313 << QString::fromStdString(theError["message"].as<std::string>());
314 updateError(QMediaRecorder::ResourceError,
315 QString::fromStdString(theError["message"].as<std::string>()));
316 },
317 .finallyFunc = []() {},
318 },
319 constraints);
320 }
321 }
322 startStream();
323}
324
325// this starts the recording stream
326void QWasmMediaRecorder::startStream()
327{
328 m_jsMediaRecorderDevice->startStreaming();
329}
330
331void QWasmMediaRecorder::setUpFileSink()
332{
333 QString m_targetFileName = outputLocation().toLocalFile();
334
335 QString suffix = m_mediaSettings.mimeType().preferredSuffix();
336 // what if no mimeType yet?
337
338 if (!m_mediaSettings.mimeType().isValid()) {
339 qWarning() << "mimetype not valid, using m4v";
340 suffix = QStringLiteral(".m4v");
341 }
342 if (m_targetFileName.isEmpty()) {
343 m_targetFileName = QDir::homePath() + u"/tmp."_s + suffix;
344 QPlatformMediaRecorder::setOutputLocation(m_targetFileName);
345 }
346
347 m_outputTarget = new QFile(m_targetFileName, this);
348 if (!m_outputTarget->open(QIODevice::WriteOnly)) {
349 qWarning() << "target file is not writable";
350 return;
351 }
352}
353
354QT_END_NAMESPACE
\inmodule QtMultimedia
QObject * parent
Definition qobject.h:73
QWasmVideoOutput * cameraOutput()
QWasmMediaRecorder(QMediaRecorder *parent)
qint64 duration() const override
void setCaptureSession(QPlatformMediaCaptureSession *session)
QMediaRecorder::RecorderState state() const override
void record(QMediaEncoderSettings &settings) override
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")