Revision control

++ // Copyright (C) 2022 The Qt Company Ltd and/or its subsidiary(-ies).
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qwasmcamera_p.h"
++
++ #include "qwasmmediacapturesession_p.h"
++ #include <common/qwasmvideooutput_p.h>
++
++ #include <QtMultimedia/qcameradevice.h>
++ #include <QtMultimedia/qmediadevices.h>
++ #include <QtMultimedia/private/qcameradevice_p.h>
++ #include <QtMultimedia/private/qmemoryvideobuffer_p.h>
++ #include <QtMultimedia/private/qmultimedia_ranges_p.h>
++ #include <QtMultimedia/private/qplatformvideodevices_p.h>
++ #include <QtMultimedia/private/qplatformvideosink_p.h>
++ #include <QtMultimedia/private/qvideotexturehelper_p.h>
++ #include <QtMultimedia/private/qwasmmediadevices_p.h>
++ #include <QtCore/qtimer.h>
++ #include <QtCore/quuid.h>
++ #include <QtCore/private/qstdweb_p.h>
++
++ #include <emscripten/val.h>
++ #include <emscripten/bind.h>
++ #include <emscripten/html5.h>
++
++ Q_LOGGING_CATEGORY(qWasmCamera, "qt.multimedia.wasm.camera")
++
++ namespace ranges = QtMultimediaPrivate::ranges;
++
++ QWasmCamera::QWasmCamera(QCamera *camera)
++ : QPlatformCamera(camera),
++ m_cameraOutput(new QWasmVideoOutput),
++ m_cameraIsReady(false)
++ {
++ connect(this, &QWasmCamera::cameraIsReady, this, [this]() {
++ m_cameraIsReady = true;
++ if (m_cameraShouldStartActive) {
++ QTimer::singleShot(50, this, [this]() {
++ setActive(true);
++ });
++ }
++ });
++ }
++
++ QWasmCamera::~QWasmCamera() = default;
++
++ bool QWasmCamera::isActive() const
++ {
++ return m_cameraActive;
++ }
++
++ void QWasmCamera::setActive(bool active)
++ {
++ if (m_cameraActive == active)
++ return;
++ if (!m_CaptureSession) {
++ updateError(QCamera::CameraError, QStringLiteral("video surface error"));
++ m_shouldBeActive = true;
++ return;
++ }
++ if (m_cameraActive && !active)
++ m_cameraOutput->stop();
++
++ m_shouldBeActive = active;
++
++ if (!m_cameraIsReady) {
++ m_cameraShouldStartActive = true;
++ return;
++ }
++
++ QVideoSink *sink = m_CaptureSession->videoSink();
++ if (!sink) {
++ qWarning() << Q_FUNC_INFO << "sink not ready";
++ return;
++ }
++
++ m_cameraOutput->setSurface(m_CaptureSession->videoSink());
++ m_cameraActive = active;
++ m_shouldBeActive = false;
++
++ if (active)
++ updateCameraFeatures();
++ Q_EMIT activeChanged(active);
++ if (m_CaptureSession->imageCapture()) {
++ if (active) {
++ m_readyChangedConnection = connect(cameraOutput(), &QWasmVideoOutput::readyChanged, this, [this] () {
++ m_CaptureSession->setReadyForCapture(true);
++ });
++ }
++ }
++ if (!active && m_readyChangedConnection) {
++ QObject::disconnect(m_readyChangedConnection);
++ }
++
++ if (m_cameraActive)
++ m_cameraOutput->start();
++ }
++
++ void QWasmCamera::setCamera(const QCameraDevice &camera)
++ {
++ if (camera.id().isEmpty() || (m_cameraDev.id() == camera.id())) {
++ return;
++ }
++
++ const bool wasActive = m_cameraActive;
++ if (wasActive)
++ m_cameraOutput->stop();
++
++ m_cameraOutput->setVideoMode(QWasmVideoOutput::Camera);
++
++ constexpr QSize initialSize(0, 0);
++ constexpr QRect initialRect(QPoint(0, 0), initialSize);
++ m_cameraOutput->createVideoElement(
++ QUuid::createUuid().toString(QUuid::WithoutBraces).toStdString()); // videoElementId
++ m_cameraOutput->doElementCallbacks();
++ m_cameraOutput->updateVideoElementGeometry(initialRect);
++
++ const auto cameras = QMediaDevices::videoInputs();
++
++ if (ranges::contains(cameras, camera)) {
++ m_cameraDev = camera;
++ createCamera(m_cameraDev);
++ Q_EMIT cameraIsReady();
++ if (wasActive)
++ m_cameraOutput->start();
++ return;
++ }
++
++ if (cameras.count() > 0) {
++ m_cameraDev = camera;
++ createCamera(m_cameraDev);
++ Q_EMIT cameraIsReady();
++ if (wasActive)
++ m_cameraOutput->start();
++ } else {
++ updateError(QCamera::CameraError, QStringLiteral("Failed to find a camera"));
++ }
++ }
++
++ bool QWasmCamera::setCameraFormat(const QCameraFormat &format)
++ {
++ m_cameraFormat = format;
++ m_cameraOutput->setVideoConstraints(format.resolution(), format.minFrameRate(), format.maxFrameRate());
++ return true;
++ }
++
++ void QWasmCamera::setCaptureSession(QPlatformMediaCaptureSession *session)
++ {
++ QWasmMediaCaptureSession *captureSession = static_cast<QWasmMediaCaptureSession *>(session);
++ if (m_CaptureSession == captureSession)
++ return;
++
++ m_CaptureSession = captureSession;
++
++ if (m_shouldBeActive)
++ setActive(true);
++ }
++
++ void QWasmCamera::setFocusMode(QCamera::FocusMode mode)
++ {
++ if (!isFocusModeSupported(mode))
++ return;
++
++ static constexpr std::string_view focusModeString = "focusMode";
++ if (mode == QCamera::FocusModeManual)
++ m_cameraOutput->setDeviceSetting(focusModeString.data(), emscripten::val("manual"));
++ if (mode == QCamera::FocusModeAuto)
++ m_cameraOutput->setDeviceSetting(focusModeString.data(), emscripten::val("continuous"));
++ focusModeChanged(mode);
++ }
++
++ bool QWasmCamera::isFocusModeSupported(QCamera::FocusMode mode) const
++ {
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return false;
++
++ emscripten::val focusMode = caps["focusMode"];
++ if (focusMode.isUndefined())
++ return false;
++
++ std::vector<std::string> focalModes;
++
++ for (int i = 0; i < focusMode["length"].as<int>(); i++)
++ focalModes.push_back(focusMode[i].as<std::string>());
++
++ // Do we need to take into account focusDistance
++ // it is not always available, and what distance
++ // would be far/near
++
++ bool found = false;
++ switch (mode) {
++ case QCamera::FocusModeAuto:
++ return ranges::contains(focalModes, "continuous")
++ || ranges::contains(focalModes, "single-shot");
++ case QCamera::FocusModeAutoNear:
++ case QCamera::FocusModeAutoFar:
++ case QCamera::FocusModeHyperfocal:
++ case QCamera::FocusModeInfinity:
++ break;
++ case QCamera::FocusModeManual:
++ found = ranges::contains(focalModes, "manual");
++ };
++ return found;
++ }
++
++ void QWasmCamera::setTorchMode(QCamera::TorchMode mode)
++ {
++ if (!isTorchModeSupported(mode))
++ return;
++
++ if (m_wasmTorchMode == mode)
++ return;
++
++ static constexpr std::string_view torchModeString = "torchMode";
++ bool hasChanged = false;
++ switch (mode) {
++ case QCamera::TorchOff:
++ m_cameraOutput->setDeviceSetting(torchModeString.data(), emscripten::val(false));
++ hasChanged = true;
++ break;
++ case QCamera::TorchOn:
++ m_cameraOutput->setDeviceSetting(torchModeString.data(), emscripten::val(true));
++ hasChanged = true;
++ break;
++ case QCamera::TorchAuto:
++ break;
++ };
++ m_wasmTorchMode = mode;
++ if (hasChanged)
++ torchModeChanged(m_wasmTorchMode);
++ }
++
++ bool QWasmCamera::isTorchModeSupported(QCamera::TorchMode mode) const
++ {
++ if (!m_cameraIsReady)
++ return false;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return false;
++
++ emscripten::val exposureMode = caps["torch"];
++ if (exposureMode.isUndefined())
++ return false;
++
++ return (mode != QCamera::TorchAuto);
++ }
++
++ void QWasmCamera::setExposureMode(QCamera::ExposureMode mode)
++ {
++ // TODO manually come up with exposureTime values ?
++ if (!isExposureModeSupported(mode))
++ return;
++
++ if (m_wasmExposureMode == mode)
++ return;
++
++ bool hasChanged = false;
++ static constexpr std::string_view exposureModeString = "exposureMode";
++ switch (mode) {
++ case QCamera::ExposureManual:
++ m_cameraOutput->setDeviceSetting(exposureModeString.data(), emscripten::val("manual"));
++ hasChanged = true;
++ break;
++ case QCamera::ExposureAuto:
++ m_cameraOutput->setDeviceSetting(exposureModeString.data(), emscripten::val("continuous"));
++ hasChanged = true;
++ break;
++ default:
++ break;
++ };
++
++ if (hasChanged) {
++ m_wasmExposureMode = mode;
++ exposureModeChanged(m_wasmExposureMode);
++ }
++ }
++
++ bool QWasmCamera::isExposureModeSupported(QCamera::ExposureMode mode) const
++ {
++ if (!m_cameraIsReady)
++ return false;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return false;
++
++ emscripten::val exposureMode = caps["exposureMode"];
++ if (exposureMode.isUndefined())
++ return false;
++
++ std::vector<std::string> exposureModes;
++
++ for (int i = 0; i < exposureMode["length"].as<int>(); i++)
++ exposureModes.push_back(exposureMode[i].as<std::string>());
++
++ bool found = false;
++ switch (mode) {
++ case QCamera::ExposureAuto:
++ found = ranges::contains(exposureModes, "continuous");
++ break;
++ case QCamera::ExposureManual:
++ found = ranges::contains(exposureModes, "manual");
++ break;
++ default:
++ break;
++ };
++
++ return found;
++ }
++
++ void QWasmCamera::setExposureCompensation(float bias)
++ {
++ if (!m_cameraIsReady)
++ return;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return;
++
++ emscripten::val exposureComp = caps["exposureCompensation"];
++ if (exposureComp.isUndefined())
++ return;
++ if (m_wasmExposureCompensation == bias)
++ return;
++
++ static constexpr std::string_view exposureCompensationModeString = "exposureCompensation";
++ m_cameraOutput->setDeviceSetting(exposureCompensationModeString.data(), emscripten::val(bias));
++ m_wasmExposureCompensation = bias;
++ Q_EMIT exposureCompensationChanged(m_wasmExposureCompensation);
++ }
++
++ void QWasmCamera::setManualExposureTime(float secs)
++ {
++ if (m_wasmExposureTime == secs)
++ return;
++
++ if (!m_cameraIsReady)
++ return;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ emscripten::val exposureTime = caps["exposureTime"];
++ if (exposureTime.isUndefined())
++ return;
++ static constexpr std::string_view exposureTimeString = "exposureTime";
++ m_cameraOutput->setDeviceSetting(exposureTimeString.data(), emscripten::val(secs));
++ m_wasmExposureTime = secs;
++ Q_EMIT exposureTimeChanged(m_wasmExposureTime);
++ }
++
++ int QWasmCamera::isoSensitivity() const
++ {
++ if (!m_cameraIsReady)
++ return 0;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return false;
++
++ emscripten::val isoSpeed = caps["iso"];
++ if (isoSpeed.isUndefined())
++ return 0;
++
++ return isoSpeed.as<double>();
++ }
++
++ void QWasmCamera::setManualIsoSensitivity(int sens)
++ {
++ if (!m_cameraIsReady)
++ return;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return;
++
++ emscripten::val isoSpeed = caps["iso"];
++ if (isoSpeed.isUndefined())
++ return;
++ if (m_wasmIsoSensitivity == sens)
++ return;
++ static constexpr std::string_view isoString = "iso";
++ m_cameraOutput->setDeviceSetting(isoString.data(), emscripten::val(sens));
++ m_wasmIsoSensitivity = sens;
++ Q_EMIT isoSensitivityChanged(m_wasmIsoSensitivity);
++ }
++
++ bool QWasmCamera::isWhiteBalanceModeSupported(QCamera::WhiteBalanceMode mode) const
++ {
++ if (!m_cameraIsReady)
++ return false;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return false;
++
++ emscripten::val whiteBalanceMode = caps["whiteBalanceMode"];
++ if (whiteBalanceMode.isUndefined())
++ return false;
++
++ if (mode == QCamera::WhiteBalanceAuto || mode == QCamera::WhiteBalanceManual)
++ return true;
++
++ return false;
++ }
++
++ void QWasmCamera::setWhiteBalanceMode(QCamera::WhiteBalanceMode mode)
++ {
++ if (!isWhiteBalanceModeSupported(mode))
++ return;
++
++ if (m_wasmWhiteBalanceMode == mode)
++ return;
++
++ bool hasChanged = false;
++ static constexpr std::string_view whiteBalanceModeString = "whiteBalanceMode";
++ switch (mode) {
++ case QCamera::WhiteBalanceAuto:
++ m_cameraOutput->setDeviceSetting(whiteBalanceModeString.data(), emscripten::val("auto"));
++ hasChanged = true;
++ break;
++ case QCamera::WhiteBalanceManual:
++ m_cameraOutput->setDeviceSetting(whiteBalanceModeString.data(), emscripten::val("manual"));
++ hasChanged = true;
++ break;
++ default:
++ break;
++ };
++
++ if (hasChanged) {
++ m_wasmWhiteBalanceMode = mode;
++ Q_EMIT whiteBalanceModeChanged(m_wasmWhiteBalanceMode);
++ }
++ }
++
++ void QWasmCamera::setColorTemperature(int temperature)
++ {
++ if (!m_cameraIsReady)
++ return;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return;
++
++ emscripten::val whiteBalanceMode = caps["colorTemperature"];
++ if (whiteBalanceMode.isUndefined())
++ return;
++ if(m_wasmColorTemperature == temperature)
++ return;
++
++ static constexpr std::string_view colorBalanceString = "colorTemperature";
++ m_cameraOutput->setDeviceSetting(colorBalanceString.data(), emscripten::val(temperature));
++ m_wasmColorTemperature = temperature;
++ colorTemperatureChanged(m_wasmColorTemperature);
++ }
++
++ void QWasmCamera::createCamera(const QCameraDevice &camera)
++ {
++ m_cameraOutput->addCameraSourceElement(camera.id().toStdString());
++ }
++
++ void QWasmCamera::updateCameraFeatures()
++ {
++ if (!m_cameraIsReady)
++ return;
++
++ emscripten::val caps = m_cameraOutput->getDeviceCapabilities();
++ if (caps.isUndefined())
++ return;
++
++ QCamera::Features cameraFeatures;
++
++ if (!caps["colorTemperature"].isUndefined())
++ cameraFeatures |= QCamera::Feature::ColorTemperature;
++
++ if (!caps["exposureCompensation"].isUndefined())
++ cameraFeatures |= QCamera::Feature::ExposureCompensation;
++
++ if (!caps["iso"].isUndefined())
++ cameraFeatures |= QCamera::Feature::IsoSensitivity;
++
++ if (!caps["exposureTime"].isUndefined())
++ cameraFeatures |= QCamera::Feature::ManualExposureTime;
++
++ if (!caps["focusDistance"].isUndefined())
++ cameraFeatures |= QCamera::Feature::FocusDistance;
++
++ supportedFeaturesChanged(cameraFeatures);
++ updateVideoFormats(caps);
++ }
++
++ void QWasmCamera::updateVideoFormats(const emscripten::val &cababilities)
++ {
++ // Use synthetic standard res and framerates. Webaudio
++ // only allows real camera format from a live camera stream
++ // camera constraints are only a suggestion anyway
++ emscripten::val widthCapabilities = cababilities["width"];
++ emscripten::val heightCapabilities = cababilities["height"];
++ emscripten::val frameRateCapabilities = cababilities["frameRate"];
++
++ if (widthCapabilities.isUndefined() || heightCapabilities.isUndefined()
++ || frameRateCapabilities.isUndefined())
++ return;
++
++ const int maxWidth = widthCapabilities["max"].as<int>();
++ const int maxHeight = heightCapabilities["max"].as<int>();
++ const float minFrameRate = frameRateCapabilities["min"].as<double>();
++ const float maxFrameRate = frameRateCapabilities["max"].as<double>();
++
++ static const QSize standardResolutions[] = {
++ { 3840, 2160 }, // 4k
++ { 2560, 1440 }, // 1440p
++ { 2048, 1080 }, // 2k
++ { 1920, 1080 }, // FHD
++ { 1280, 720 }, // HD
++ { 640, 480 }, // SD
++ };
++ static const float standardFrameRates[] = { 15.f, 24.f, 25.f, 29.97f, 30.f,
++ 60.f, 100.f, 120.f };
++
++ QList<QCameraFormat> formats;
++ for (const QSize &resolution : standardResolutions) {
++ if (resolution.width() > maxWidth || resolution.height() > maxHeight)
++ continue;
++ for (float rate : standardFrameRates) {
++ if (rate < minFrameRate || rate > maxFrameRate + 0.5f)
++ continue;
++ auto *oneFormat = new QCameraFormatPrivate{
++ QSharedData(),
++ QVideoFrameFormat::Format_RGBA8888,
++ resolution,
++ rate, rate
++ };
++ formats << oneFormat->create();
++ }
++ }
++
++ if (formats.isEmpty())
++ return;
++
++ QCameraDevicePrivate *devicePrivate = QCameraDevicePrivate::handle(m_cameraDev);
++ devicePrivate->videoFormats = formats;
++ }
++