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