Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qwasmmediarecorder_p.h"
++
++ #include "qwasmmediacapturesession_p.h"
++ #include "qwasmcamera_p.h"
++ #include "qwasmaudioinput_p.h"
++
++ #include <QtMultimedia/private/qplatformaudiodevices_p.h>
++ #include <QtMultimedia/private/qplatformmediaintegration_p.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qdir.h>
++ #include <QtCore/qfile.h>
++ #include <QtCore/qiodevice.h>
++ #include <QtCore/qtimer.h>
++ #include <QtCore/private/qstdweb_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::Literals;
++
++ Q_LOGGING_CATEGORY(qWasmMediaRecorder, "qt.multimedia.wasm.mediarecorder")
++
++ QWasmMediaRecorder::QWasmMediaRecorder(QMediaRecorder *parent)
++ : QPlatformMediaRecorder(parent)
++ {
++ m_durationTimer.reset(new QElapsedTimer());
++ QPlatformMediaIntegration::instance()->audioDevices(); // initialize getUserMedia
++
++ m_jsMediaRecorderDevice.reset(new JsMediaRecorder());
++
++ connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::started, this,
++ [this]() {
++ m_isRecording = true;
++ m_durationTimer->start();
++ Q_EMIT stateChanged(QMediaRecorder::RecordingState);
++ });
++
++ connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::stopped, this,
++ [this]() {
++ m_isRecording = false;
++ m_durationMs = m_durationTimer->elapsed();
++ Q_EMIT durationChanged(m_durationMs);
++
++ m_durationTimer->invalidate();
++ Q_EMIT stateChanged(QMediaRecorder::StoppedState);
++ });
++
++ connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::paused, this,
++ [this]() {
++ m_isRecording = false;
++ m_durationMs = m_durationTimer->elapsed();
++ Q_EMIT durationChanged(m_durationMs);
++
++ m_durationTimer->invalidate();
++ Q_EMIT stateChanged(QMediaRecorder::PausedState);
++ });
++
++ connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::resumed, this,
++ [this]() {
++ m_isRecording = true;
++ m_durationTimer->start();
++ });
++
++ connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::streamError, this,
++ [this](QMediaRecorder::Error errorCode, const QString &errorMessage) {
++ updateError(errorCode, errorMessage);
++ Q_EMIT stateChanged(state());
++ });
++ }
++
++ QWasmMediaRecorder::~QWasmMediaRecorder()
++ {
++ if (m_outputTarget->isOpen())
++ m_outputTarget->close();
++
++ if (m_jsMediaRecorderDevice->isOpen()) {
++ m_jsMediaRecorderDevice->close();
++ }
++
++ if (!m_mediaRecorder.isNull()) {
++ m_mediaStreamDataAvailable.reset(nullptr);
++ m_mediaStreamStopped.reset(nullptr);
++ m_mediaStreamError.reset(nullptr);
++ m_mediaStreamStart.reset(nullptr);
++ }
++ }
++
++ bool QWasmMediaRecorder::isLocationWritable(const QUrl &location) const
++ {
++ return location.isValid() && (location.isLocalFile() || location.isRelative());
++ }
++
++ QMediaRecorder::RecorderState QWasmMediaRecorder::state() const
++ {
++ return m_jsMediaRecorderDevice->currentState();
++ }
++
++ qint64 QWasmMediaRecorder::duration() const
++ { // milliseconds
++ return m_durationMs;
++ }
++
++ void QWasmMediaRecorder::record(QMediaEncoderSettings &settings)
++ {
++ if (!m_session)
++ return;
++
++ m_mediaSettings = settings;
++ if (!m_jsMediaRecorderDevice->open(QIODeviceBase::ReadOnly)) {
++ qWarning() << "m_jsMediaRecorderDevice is not open";
++ return;
++ }
++
++ initUserMedia();
++ m_jsMediaRecorderDevice->startStreaming();
++ }
++
++ void QWasmMediaRecorder::pause()
++ {
++ if (!m_session) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "could not find MediaRecorder";
++ return;
++ }
++ m_jsMediaRecorderDevice->pauseStream();
++ }
++
++ void QWasmMediaRecorder::resume()
++ {
++ if (!m_session) {
++ qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
++ return;
++ }
++ m_jsMediaRecorderDevice->resumeStream();
++ }
++
++ void QWasmMediaRecorder::stop()
++ {
++ if (!m_session) {
++ qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaRecorder";
++ return;
++ }
++ m_jsMediaRecorderDevice->stopStream();
++ }
++
++ void QWasmMediaRecorder::setCaptureSession(QPlatformMediaCaptureSession *session)
++ {
++ m_session = static_cast<QWasmMediaCaptureSession *>(session);
++ }
++
++ bool QWasmMediaRecorder::hasCamera() const
++ {
++ return m_session && m_session->camera();
++ }
++
++ void QWasmMediaRecorder::initUserMedia()
++ {
++ setUpFileSink();
++ emscripten::val navigator = emscripten::val::global("navigator");
++ emscripten::val mediaDevices = navigator["mediaDevices"];
++
++ if (mediaDevices.isNull() || mediaDevices.isUndefined()) {
++ qCDebug(qWasmMediaRecorder) << "MediaDevices are undefined or null";
++ return;
++ }
++
++ if (!m_session)
++ return;
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << m_session;
++
++ emscripten::val stream = emscripten::val::undefined();
++ if (hasCamera()) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has camera";
++ QWasmCamera *wasmCamera = reinterpret_cast<QWasmCamera *>(m_session->camera());
++
++ if (wasmCamera) {
++ m_jsMediaRecorderDevice->setNeedsCamera(true);
++ emscripten::val m_video = wasmCamera->cameraOutput()->surfaceElement();
++ if (m_video.isNull() || m_video.isUndefined()) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "video element not found";
++ return;
++ }
++
++ stream = m_video["srcObject"];
++ if (stream.isNull() || stream.isUndefined()) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Video input stream not found";
++ return;
++ }
++ }
++ } else if (m_session->hasAudio()) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "has audio";
++ stream = static_cast<QWasmAudioInput *>(m_session->audioInput())->mediaStream();
++
++ if (stream.isNull() || stream.isUndefined()) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "Audio input stream not found";
++ return;
++ }
++ m_jsMediaRecorderDevice->setNeedsAudio(m_session->hasAudio());
++ }
++ if (stream.isNull() || stream.isUndefined()) {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO << "No input stream found";
++ return;
++ }
++ if (!m_jsMediaRecorderDevice->open(QIODeviceBase::ReadOnly)) {
++ qWarning() << "m_jsMediaRecorderDevice is not open";
++ return;
++ }
++
++ QObject::connect(m_jsMediaRecorderDevice.get(), &JsMediaRecorder::readyRead, this, [this]() {
++
++ if (m_jsMediaRecorderDevice->bytesAvailable()) {
++
++ QByteArray mediaData = m_jsMediaRecorderDevice->read(m_jsMediaRecorderDevice->bytesAvailable());
++
++ m_durationMs = m_durationTimer->elapsed();
++ if (m_outputTarget->isOpen())
++ m_outputTarget->write(mediaData.constData(), mediaData.length());
++ // we've read everything
++ if (m_durationMs > 0) {
++ Q_EMIT durationChanged(m_durationMs);
++ qCDebug(qWasmMediaRecorder) << "duration changed" << m_durationMs;
++ }
++ }
++ });
++
++ m_jsMediaRecorderDevice->setStream(stream);
++ }
++
++ void QWasmMediaRecorder::startAudioRecording()
++ {
++ startStream();
++ }
++
++ void QWasmMediaRecorder::initMediaSettings()
++ {
++ m_hasMediaSettings = true;
++ }
++
++ void QWasmMediaRecorder::setStream(emscripten::val stream)
++ {
++ // set up what options we can
++ if (hasCamera())
++ setTrackContraints(m_mediaSettings, stream);
++ else
++ startStream();
++ }
++
++ // constraints are suggestions, as not all hardware supports all settings
++ void QWasmMediaRecorder::setTrackContraints(QMediaEncoderSettings &settings, emscripten::val stream)
++ {
++ qCDebug(qWasmMediaRecorder) << Q_FUNC_INFO
++ << settings.audioSampleRate()
++ << settings.videoResolution();
++
++ if (stream.isUndefined() || stream.isNull()) {
++ qCDebug(qWasmMediaRecorder)<< Q_FUNC_INFO << "could not find MediaStream";
++ return;
++ }
++
++ emscripten::val navigator = emscripten::val::global("navigator");
++ emscripten::val mediaDevices = navigator["mediaDevices"];
++
++ // check which ones are supported
++ emscripten::val allConstraints = mediaDevices.call<emscripten::val>("getSupportedConstraints");
++ // browsers only support some settings
++
++ emscripten::val videoParams = emscripten::val::object();
++ emscripten::val constraints = emscripten::val::object();
++ videoParams.set("resizeMode",std::string("crop-and-scale"));
++
++ if (hasCamera()) {
++ if (settings.videoFrameRate() > 0)
++ videoParams.set("frameRate", emscripten::val(settings.videoFrameRate()));
++ if (settings.videoResolution().height() > 0)
++ videoParams.set("height",
++ emscripten::val(settings.videoResolution().height())); // viewportHeight?
++ if (settings.videoResolution().width() > 0)
++ videoParams.set("width", emscripten::val(settings.videoResolution().width()));
++
++ constraints.set("video", videoParams); // only video here
++ }
++
++ emscripten::val audioParams = emscripten::val::object();
++ if (settings.audioSampleRate() > 0)
++ audioParams.set("sampleRate", emscripten::val(settings.audioSampleRate())); // may not work
++ if (settings.audioBitRate() > 0)
++ audioParams.set("sampleSize", emscripten::val(settings.audioBitRate())); // may not work
++ if (settings.audioChannelCount() > 0)
++ audioParams.set("channelCount", emscripten::val(settings.audioChannelCount()));
++
++ constraints.set("audio", audioParams); // only audio here
++
++ if (hasCamera() && stream["active"].as<bool>()) {
++ emscripten::val videoTracks = emscripten::val::undefined();
++ videoTracks = stream.call<emscripten::val>("getVideoTracks");
++ if (videoTracks.isNull() || videoTracks.isUndefined()) {
++ qCDebug(qWasmMediaRecorder) << "no video tracks";
++ return;
++ }
++ if (videoTracks["length"].as<int>() > 0) {
++ // try to apply the video options
++ qstdweb::Promise::make(videoTracks[0], QStringLiteral("applyConstraints"),
++ { .thenFunc =
++ [this](emscripten::val result) {
++ Q_UNUSED(result)
++ startStream();
++ },
++ .catchFunc =
++ [this](emscripten::val theError) {
++ qCDebug(qWasmMediaRecorder)
++ << theError["code"].as<int>()
++ << QString::fromStdString(theError["message"].as<std::string>());
++ updateError(QMediaRecorder::ResourceError,
++ QString::fromStdString(theError["message"].as<std::string>()));
++ },
++ .finallyFunc = []() {},
++ },
++ constraints);
++ }
++ }
++ startStream();
++ }
++
++ // this starts the recording stream
++ void QWasmMediaRecorder::startStream()
++ {
++ m_jsMediaRecorderDevice->startStreaming();
++ }
++
++ void QWasmMediaRecorder::setUpFileSink()
++ {
++ QString m_targetFileName = outputLocation().toLocalFile();
++
++ QString suffix = m_mediaSettings.mimeType().preferredSuffix();
++ // what if no mimeType yet?
++
++ if (!m_mediaSettings.mimeType().isValid()) {
++ qWarning() << "mimetype not valid, using m4v";
++ suffix = QStringLiteral(".m4v");
++ }
++ if (m_targetFileName.isEmpty()) {
++ m_targetFileName = QDir::homePath() + u"/tmp."_s + suffix;
++ QPlatformMediaRecorder::setOutputLocation(QUrl::fromLocalFile(m_targetFileName));
++ }
++
++ m_outputTarget = new QFile(m_targetFileName, this);
++ if (!m_outputTarget->open(QIODevice::WriteOnly)) {
++ qWarning() << "target file is not writable";
++ return;
++ }
++ }
++
++ QT_END_NAMESPACE