diff --git a/src/plugins/multimedia/CMakeLists.txt b/src/plugins/multimedia/CMakeLists.txt index 5bc39c1f8..c74f2ff4c 100644 --- a/src/plugins/multimedia/CMakeLists.txt +++ b/src/plugins/multimedia/CMakeLists.txt @@ -2,7 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_ffmpeg) - add_subdirectory(ffmpeg) + add_subdirectory(v4l2) endif() if(QT_FEATURE_gstreamer) add_subdirectory(gstreamer) diff --git a/src/plugins/multimedia/v4l2/CMakeLists.txt b/src/plugins/multimedia/v4l2/CMakeLists.txt new file mode 100644 index 000000000..f20612c29 --- /dev/null +++ b/src/plugins/multimedia/v4l2/CMakeLists.txt @@ -0,0 +1,24 @@ +qt_internal_add_plugin(QFFmpegMediaPlugin + OUTPUT_NAME ffmpegmediaplugin + PLUGIN_TYPE multimedia + SOURCES + qffmpegmediametadata.cpp qffmpegmediametadata_p.h + qffmpegvideosink.cpp qffmpegvideosink_p.h + qffmpegmediaformatinfo.cpp qffmpegmediaformatinfo_p.h + qffmpegmediaintegration.cpp qffmpegmediaintegration_p.h + qffmpegimagecapture.cpp qffmpegimagecapture_p.h + qffmpegmediacapturesession.cpp qffmpegmediacapturesession_p.h + DEFINES + QT_COMPILING_FFMPEG + LIBRARIES + Qt::MultimediaPrivate + Qt::CorePrivate +) + +qt_internal_extend_target(QFFmpegMediaPlugin CONDITION QT_FEATURE_linux_v4l + SOURCES + qv4l2camera.cpp qv4l2camera_p.h + qv4l2filedescriptor.cpp qv4l2filedescriptor_p.h + qv4l2memorytransfer.cpp qv4l2memorytransfer_p.h + qv4l2cameradevices.cpp qv4l2cameradevices_p.h +) diff --git a/src/plugins/multimedia/v4l2/ffmpeg.json b/src/plugins/multimedia/v4l2/ffmpeg.json new file mode 100644 index 000000000..d8e7e4456 --- /dev/null +++ b/src/plugins/multimedia/v4l2/ffmpeg.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "ffmpeg" ] +} diff --git a/src/plugins/multimedia/v4l2/qffmpegimagecapture.cpp b/src/plugins/multimedia/v4l2/qffmpegimagecapture.cpp new file mode 100644 index 000000000..9ee4e1db8 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegimagecapture.cpp @@ -0,0 +1,269 @@ +// Copyright (C) 2016 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 "qffmpegimagecapture_p.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcImageCapture, "qt.multimedia.imageCapture") + +QFFmpegImageCapture::QFFmpegImageCapture(QImageCapture *parent) + : QPlatformImageCapture(parent) +{ +} + +QFFmpegImageCapture::~QFFmpegImageCapture() +{ +} + +bool QFFmpegImageCapture::isReadyForCapture() const +{ + return m_isReadyForCapture; +} + +static const char *extensionForFormat(QImageCapture::FileFormat format) +{ + const char *fmt = "jpg"; + switch (format) { + case QImageCapture::UnspecifiedFormat: + case QImageCapture::JPEG: + fmt = "jpg"; + break; + case QImageCapture::PNG: + fmt = "png"; + break; + case QImageCapture::WebP: + fmt = "webp"; + break; + case QImageCapture::Tiff: + fmt = "tiff"; + break; + } + return fmt; +} + +int QFFmpegImageCapture::capture(const QString &fileName) +{ + QString path = QMediaStorageLocation::generateFileName(fileName, QStandardPaths::PicturesLocation, QLatin1String(extensionForFormat(m_settings.format()))); + return doCapture(path); +} + +int QFFmpegImageCapture::captureToBuffer() +{ + return doCapture(QString()); +} + +int QFFmpegImageCapture::doCapture(const QString &fileName) +{ + qCDebug(qLcImageCapture) << "do capture"; + if (!m_session) { + //emit error in the next event loop, + //so application can associate it with returned request id. + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(int, -1), + Q_ARG(int, QImageCapture::ResourceError), + Q_ARG(QString, QPlatformImageCapture::msgImageCaptureNotSet())); + + qCDebug(qLcImageCapture) << "error 1"; + return -1; + } + if (!m_camera) { + //emit error in the next event loop, + //so application can associate it with returned request id. + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(int, -1), + Q_ARG(int, QImageCapture::ResourceError), + Q_ARG(QString,tr("No camera available."))); + + qCDebug(qLcImageCapture) << "error 2"; + return -1; + } + if (passImage) { + //emit error in the next event loop, + //so application can associate it with returned request id. + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(int, -1), + Q_ARG(int, QImageCapture::NotReadyError), + Q_ARG(QString, QPlatformImageCapture::msgCameraNotReady())); + + qCDebug(qLcImageCapture) << "error 3"; + return -1; + } + m_lastId++; + + pendingImages.enqueue({m_lastId, fileName, QMediaMetaData{}}); + // let one image pass the pipeline + passImage = true; + + updateReadyForCapture(); + return m_lastId; +} + +void QFFmpegImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session) +{ + auto *captureSession = static_cast(session); + if (m_session == captureSession) + return; + + if (m_session) { + disconnect(m_session, nullptr, this, nullptr); + m_lastId = 0; + pendingImages.clear(); + passImage = false; + cameraActive = false; + } + + m_session = captureSession; + if (m_session) + connect(m_session, &QPlatformMediaCaptureSession::cameraChanged, this, &QFFmpegImageCapture::onCameraChanged); + + onCameraChanged(); + updateReadyForCapture(); +} + +void QFFmpegImageCapture::updateReadyForCapture() +{ + bool ready = m_session && !passImage && cameraActive; + if (ready == m_isReadyForCapture) + return; + m_isReadyForCapture = ready; + emit readyForCaptureChanged(m_isReadyForCapture); +} + +void QFFmpegImageCapture::cameraActiveChanged(bool active) +{ + qCDebug(qLcImageCapture) << "cameraActiveChanged" << cameraActive << active; + if (cameraActive == active) + return; + cameraActive = active; + qCDebug(qLcImageCapture) << "isReady" << isReadyForCapture(); + updateReadyForCapture(); +} + +void QFFmpegImageCapture::newVideoFrame(const QVideoFrame &frame) +{ + if (!passImage) + return; + + passImage = false; + Q_ASSERT(!pendingImages.isEmpty()); + auto pending = pendingImages.dequeue(); + + emit imageExposed(pending.id); + // ### Add metadata from the AVFrame + emit imageMetadataAvailable(pending.id, pending.metaData); + emit imageAvailable(pending.id, frame); + QImage image = frame.toImage(); + if (m_settings.resolution().isValid() && m_settings.resolution() != image.size()) + image = image.scaled(m_settings.resolution()); + + emit imageCaptured(pending.id, image); + if (!pending.filename.isEmpty()) { + const char *fmt = nullptr; + switch (m_settings.format()) { + case QImageCapture::UnspecifiedFormat: + case QImageCapture::JPEG: + fmt = "jpeg"; + break; + case QImageCapture::PNG: + fmt = "png"; + break; + case QImageCapture::WebP: + fmt = "webp"; + break; + case QImageCapture::Tiff: + fmt = "tiff"; + break; + } + int quality = -1; + switch (m_settings.quality()) { + case QImageCapture::VeryLowQuality: + quality = 25; + break; + case QImageCapture::LowQuality: + quality = 50; + break; + case QImageCapture::NormalQuality: + break; + case QImageCapture::HighQuality: + quality = 75; + break; + case QImageCapture::VeryHighQuality: + quality = 99; + break; + } + + QImageWriter writer(pending.filename, fmt); + writer.setQuality(quality); + + if (writer.write(image)) { + emit imageSaved(pending.id, pending.filename); + } else { + QImageCapture::Error err = QImageCapture::ResourceError; + if (writer.error() == QImageWriter::UnsupportedFormatError) + err = QImageCapture::FormatError; + emit error(pending.id, err, writer.errorString()); + } + } + updateReadyForCapture(); +} + +void QFFmpegImageCapture::onCameraChanged() +{ + auto *camera = m_session ? m_session->camera() : nullptr; + if (m_camera == camera) + return; + + if (m_camera) + disconnect(m_camera); + + m_camera = camera; + + if (camera) { + cameraActiveChanged(camera->isActive()); + connect(camera, &QPlatformCamera::activeChanged, this, &QFFmpegImageCapture::cameraActiveChanged); + connect(camera, &QPlatformCamera::newVideoFrame, this, &QFFmpegImageCapture::newVideoFrame); + } else { + cameraActiveChanged(false); + } +} + +QImageEncoderSettings QFFmpegImageCapture::imageSettings() const +{ + return m_settings; +} + +void QFFmpegImageCapture::setImageSettings(const QImageEncoderSettings &settings) +{ + auto s = settings; + const auto supportedFormats = QPlatformMediaIntegration::instance()->formatInfo()->imageFormats; + if (supportedFormats.isEmpty()) { + emit error(-1, QImageCapture::FormatError, "No image formats supported, can't capture."); + return; + } + if (s.format() == QImageCapture::UnspecifiedFormat) { + auto f = QImageCapture::JPEG; + if (!supportedFormats.contains(f)) + f = supportedFormats.first(); + s.setFormat(f); + } else if (!supportedFormats.contains(settings.format())) { + emit error(-1, QImageCapture::FormatError, "Image format not supported."); + return; + } + + m_settings = settings; +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qffmpegimagecapture_p.h b/src/plugins/multimedia/v4l2/qffmpegimagecapture_p.h new file mode 100644 index 000000000..de54fe7cb --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegimagecapture_p.h @@ -0,0 +1,72 @@ +// Copyright (C) 2016 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 + + +#ifndef QFFMPEGIMAGECAPTURE_H +#define QFFMPEGIMAGECAPTURE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include "qffmpegmediacapturesession_p.h" + +#include + +QT_BEGIN_NAMESPACE + +class QFFmpegImageCapture : public QPlatformImageCapture + +{ + Q_OBJECT +public: + QFFmpegImageCapture(QImageCapture *parent); + virtual ~QFFmpegImageCapture(); + + bool isReadyForCapture() const override; + int capture(const QString &fileName) override; + int captureToBuffer() override; + + QImageEncoderSettings imageSettings() const override; + void setImageSettings(const QImageEncoderSettings &settings) override; + + void setCaptureSession(QPlatformMediaCaptureSession *session); + + void updateReadyForCapture(); + +public Q_SLOTS: + void cameraActiveChanged(bool active); + void newVideoFrame(const QVideoFrame &frame); + void onCameraChanged(); + +private: + int doCapture(const QString &fileName); + + QFFmpegMediaCaptureSession *m_session = nullptr; + int m_lastId = 0; + QImageEncoderSettings m_settings; + QPlatformCamera *m_camera = nullptr; + + struct PendingImage { + int id; + QString filename; + QMediaMetaData metaData; + }; + + QQueue pendingImages; + bool passImage = false; + bool cameraActive = false; + bool m_isReadyForCapture = false; +}; + +QT_END_NAMESPACE + +#endif // QGSTREAMERCAPTURECORNTROL_H diff --git a/src/plugins/multimedia/v4l2/qffmpegmediacapturesession.cpp b/src/plugins/multimedia/v4l2/qffmpegmediacapturesession.cpp new file mode 100644 index 000000000..b6865761c --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediacapturesession.cpp @@ -0,0 +1,114 @@ +// Copyright (C) 2016 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 "qffmpegmediacapturesession_p.h" + +#include "private/qplatformaudioinput_p.h" +#include "private/qplatformaudiooutput_p.h" +#include "qffmpegimagecapture_p.h" +#include "private/qplatformcamera_p.h" +#include "qvideosink.h" + +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcMediaCapture, "qt.multimedia.capture") + + + +QFFmpegMediaCaptureSession::QFFmpegMediaCaptureSession() +{ +} + +QFFmpegMediaCaptureSession::~QFFmpegMediaCaptureSession() +{ +} + +QPlatformCamera *QFFmpegMediaCaptureSession::camera() +{ + return m_camera; +} + +void QFFmpegMediaCaptureSession::setCamera(QPlatformCamera *camera) +{ + if (m_camera == camera) + return; + if (m_camera) { + m_camera->disconnect(this); + m_camera->setCaptureSession(nullptr); + } + + m_camera = camera; + + if (m_camera) { + connect(m_camera, &QPlatformCamera::newVideoFrame, this, &QFFmpegMediaCaptureSession::newVideoFrame); + m_camera->setCaptureSession(this); + } + + emit cameraChanged(); +} + +QPlatformImageCapture *QFFmpegMediaCaptureSession::imageCapture() +{ + return m_imageCapture; +} + +void QFFmpegMediaCaptureSession::setImageCapture(QPlatformImageCapture *imageCapture) +{ + if (m_imageCapture == imageCapture) + return; + + if (m_imageCapture) + m_imageCapture->setCaptureSession(nullptr); + + m_imageCapture = static_cast(imageCapture); + + if (m_imageCapture) + m_imageCapture->setCaptureSession(this); + + emit imageCaptureChanged(); +} + +void QFFmpegMediaCaptureSession::setMediaRecorder(QPlatformMediaRecorder *recorder) +{ + return; +} + +QPlatformMediaRecorder *QFFmpegMediaCaptureSession::mediaRecorder() +{ + return nullptr; +} + +void QFFmpegMediaCaptureSession::setAudioInput(QPlatformAudioInput *input) +{ + if (m_audioInput == input) + return; + + m_audioInput = input; +} + +void QFFmpegMediaCaptureSession::setVideoPreview(QVideoSink *sink) +{ + if (m_videoSink == sink) + return; + + m_videoSink = sink; +} + +void QFFmpegMediaCaptureSession::setAudioOutput(QPlatformAudioOutput *output) +{ + if (m_audioOutput == output) + return; + + m_audioOutput = output; +} + +void QFFmpegMediaCaptureSession::newVideoFrame(const QVideoFrame &frame) +{ + if (m_videoSink) + m_videoSink->setVideoFrame(frame); +} + + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qffmpegmediacapturesession_p.h b/src/plugins/multimedia/v4l2/qffmpegmediacapturesession_p.h new file mode 100644 index 000000000..858a537cc --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediacapturesession_p.h @@ -0,0 +1,63 @@ +// Copyright (C) 2016 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 + +#ifndef QFFMPEGMEDIACAPTURESESSION_H +#define QFFMPEGMEDIACAPTURESESSION_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QFFmpegMediaRecorder; +class QFFmpegImageCapture; +class QVideoFrame; + +class QFFmpegMediaCaptureSession : public QPlatformMediaCaptureSession +{ + Q_OBJECT + +public: + QFFmpegMediaCaptureSession(); + virtual ~QFFmpegMediaCaptureSession(); + + QPlatformCamera *camera() override; + void setCamera(QPlatformCamera *camera) override; + + QPlatformImageCapture *imageCapture() override; + void setImageCapture(QPlatformImageCapture *imageCapture) override; + + QPlatformMediaRecorder *mediaRecorder() override; + void setMediaRecorder(QPlatformMediaRecorder *recorder) override; + + void setAudioInput(QPlatformAudioInput *input) override; + QPlatformAudioInput *audioInput() { return m_audioInput; } + + void setVideoPreview(QVideoSink *sink) override; + void setAudioOutput(QPlatformAudioOutput *output) override; + +public Q_SLOTS: + void newVideoFrame(const QVideoFrame &frame); + +private: + QPlatformCamera *m_camera = nullptr; + QPlatformAudioInput *m_audioInput = nullptr; + QFFmpegImageCapture *m_imageCapture = nullptr; + QPlatformAudioOutput *m_audioOutput = nullptr; + QVideoSink *m_videoSink = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QGSTREAMERCAPTURESERVICE_H diff --git a/src/plugins/multimedia/v4l2/qffmpegmediaformatinfo.cpp b/src/plugins/multimedia/v4l2/qffmpegmediaformatinfo.cpp new file mode 100644 index 000000000..00b838d50 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediaformatinfo.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2021 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 "qffmpegmediaformatinfo_p.h" +#include "qaudioformat.h" +#include "qimagewriter.h" + +QT_BEGIN_NAMESPACE + +QFFmpegMediaFormatInfo::QFFmpegMediaFormatInfo() +{ + // Add image formats we support. We currently simply use Qt's built-in image write + // to save images. That doesn't give us HDR support or support for larger bit depths, + // but most cameras can currently not generate those anyway. + const auto imgFormats = QImageWriter::supportedImageFormats(); + for (const auto &f : imgFormats) { + if (f == "png") + imageFormats.append(QImageCapture::PNG); + else if (f == "jpeg") + imageFormats.append(QImageCapture::JPEG); + else if (f == "tiff") + imageFormats.append(QImageCapture::Tiff); + else if (f == "webp") + imageFormats.append(QImageCapture::WebP); + } + +} + +QFFmpegMediaFormatInfo::~QFFmpegMediaFormatInfo() = default; + + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qffmpegmediaformatinfo_p.h b/src/plugins/multimedia/v4l2/qffmpegmediaformatinfo_p.h new file mode 100644 index 000000000..e34005bbf --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediaformatinfo_p.h @@ -0,0 +1,34 @@ +// Copyright (C) 2021 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 + +#ifndef QFFmpegMediaFormatInfo_H +#define QFFmpegMediaFormatInfo_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QFFmpegMediaFormatInfo : public QPlatformMediaFormatInfo +{ +public: + QFFmpegMediaFormatInfo(); + ~QFFmpegMediaFormatInfo(); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/multimedia/v4l2/qffmpegmediaintegration.cpp b/src/plugins/multimedia/v4l2/qffmpegmediaintegration.cpp new file mode 100644 index 000000000..63a8ff196 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediaintegration.cpp @@ -0,0 +1,131 @@ +// Copyright (C) 2021 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 +#include +#include "qffmpegmediaintegration_p.h" +#include "qffmpegmediaformatinfo_p.h" +#include "qffmpegvideosink_p.h" +#include "qffmpegmediacapturesession_p.h" +#include "qffmpegimagecapture_p.h" + +#ifdef Q_OS_MACOS +#include +#endif + +#ifdef Q_OS_DARWIN +#include "qavfcamera_p.h" +#elif defined(Q_OS_WINDOWS) +#include "../windows/mediacapture/qwindowscamera_p.h" +#include "../windows/qwindowsvideodevices_p.h" +#endif + +#if QT_CONFIG(linux_v4l) +#include "qv4l2camera_p.h" +#include "qv4l2cameradevices_p.h" +#endif + +QT_BEGIN_NAMESPACE + +class QFFmpegMediaPlugin : public QPlatformMediaPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QPlatformMediaPlugin_iid FILE "ffmpeg.json") + +public: + QFFmpegMediaPlugin() + : QPlatformMediaPlugin() + {} + + QPlatformMediaIntegration* create(const QString &name) override + { + if (name == QLatin1String("ffmpeg")) + return new QFFmpegMediaIntegration; + return nullptr; + } +}; + +QFFmpegMediaIntegration::QFFmpegMediaIntegration() + : QPlatformMediaIntegration(QLatin1String("ffmpeg")) +{ +#ifndef QT_NO_DEBUG + qDebug() << "Available HW decoding frameworks:"; + AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE; + while ((type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE) + qDebug() << " " << av_hwdevice_get_type_name(type); +#endif +} + +QPlatformMediaFormatInfo *QFFmpegMediaIntegration::createFormatInfo() +{ + return new QFFmpegMediaFormatInfo(); +} + +QPlatformVideoDevices *QFFmpegMediaIntegration::createVideoDevices() +{ +#if defined(Q_OS_ANDROID) + return new QAndroidVideoDevices(this); +#elif QT_CONFIG(linux_v4l) + return new QV4L2CameraDevices(this); +#elif defined Q_OS_DARWIN + return new QAVFVideoDevices(this); +#elif defined(Q_OS_WINDOWS) + return new QWindowsVideoDevices(this); +#else + return nullptr; +#endif +} + +QMaybe QFFmpegMediaIntegration::createCaptureSession() +{ + return new QFFmpegMediaCaptureSession(); +} + +QMaybe QFFmpegMediaIntegration::createCamera(QCamera *camera) +{ +#ifdef Q_OS_DARWIN + return new QAVFCamera(camera); +#elif QT_CONFIG(linux_v4l) + return new QV4L2Camera(camera); +#elif defined(Q_OS_WINDOWS) + return new QWindowsCamera(camera); +#else + Q_UNUSED(camera); + return nullptr;//new QFFmpegCamera(camera); +#endif +} + +QMaybe QFFmpegMediaIntegration::createImageCapture(QImageCapture *imageCapture) +{ + return new QFFmpegImageCapture(imageCapture); +} + +QMaybe QFFmpegMediaIntegration::createVideoSink(QVideoSink *sink) +{ + return new QFFmpegVideoSink(sink); +} + +#ifdef Q_OS_ANDROID +Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) +{ + static bool initialized = false; + if (initialized) + return JNI_VERSION_1_6; + initialized = true; + + QT_USE_NAMESPACE + void *environment; + if (vm->GetEnv(&environment, JNI_VERSION_1_6)) + return JNI_ERR; + + // setting our javavm into ffmpeg. + if (av_jni_set_java_vm(vm, nullptr)) + return JNI_ERR; + + return JNI_VERSION_1_6; +} +#endif + +QT_END_NAMESPACE + +#include "qffmpegmediaintegration.moc" diff --git a/src/plugins/multimedia/v4l2/qffmpegmediaintegration_p.h b/src/plugins/multimedia/v4l2/qffmpegmediaintegration_p.h new file mode 100644 index 000000000..13b81ecc2 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediaintegration_p.h @@ -0,0 +1,47 @@ +// Copyright (C) 2021 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 + +#ifndef QGSTREAMERINTEGRATION_H +#define QGSTREAMERINTEGRATION_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QFFmpegMediaFormatInfo; + +class QFFmpegMediaIntegration : public QPlatformMediaIntegration +{ +public: + QFFmpegMediaIntegration(); + + static QFFmpegMediaIntegration *instance() + { + return static_cast(QPlatformMediaIntegration::instance()); + } + + QMaybe createCaptureSession() override; + QMaybe createCamera(QCamera *) override; + QMaybe createImageCapture(QImageCapture *) override; + QMaybe createVideoSink(QVideoSink *sink) override; + +protected: + QPlatformMediaFormatInfo *createFormatInfo() override; + + QPlatformVideoDevices *createVideoDevices() override; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/multimedia/v4l2/qffmpegmediametadata.cpp b/src/plugins/multimedia/v4l2/qffmpegmediametadata.cpp new file mode 100644 index 000000000..dda577d44 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediametadata.cpp @@ -0,0 +1,72 @@ +// 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 "qffmpegmediametadata_p.h" +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +namespace { + +struct { + const char *tag; + QMediaMetaData::Key key; +} ffmpegTagToMetaDataKey[] = { + { "title", QMediaMetaData::Title }, + { "comment", QMediaMetaData::Comment }, + { "description", QMediaMetaData::Description }, + { "genre", QMediaMetaData::Genre }, + { "date", QMediaMetaData::Date }, + { "year", QMediaMetaData::Date }, + { "creation_time", QMediaMetaData::Date }, + + { "language", QMediaMetaData::Language }, + + { "copyright", QMediaMetaData::Copyright }, + + // Music + { "album", QMediaMetaData::AlbumTitle }, + { "album_artist", QMediaMetaData::AlbumArtist }, + { "artist", QMediaMetaData::ContributingArtist }, + { "track", QMediaMetaData::TrackNumber }, + + // Movie + { "performer", QMediaMetaData::LeadPerformer }, + + { nullptr, QMediaMetaData::Title } +}; + +} + +static QMediaMetaData::Key tagToKey(const char *tag) +{ + auto *map = ffmpegTagToMetaDataKey; + while (map->tag) { + if (!strcmp(map->tag, tag)) + return map->key; + ++map; + } + return QMediaMetaData::Key(-1); +} + +static const char *keyToTag(QMediaMetaData::Key key) +{ + auto *map = ffmpegTagToMetaDataKey; + while (map->tag) { + if (map->key == key) + return map->tag; + ++map; + } + return nullptr; +} + +QByteArray QFFmpegMetaData::value(const QMediaMetaData &metaData, QMediaMetaData::Key key) +{ + return {}; +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qffmpegmediametadata_p.h b/src/plugins/multimedia/v4l2/qffmpegmediametadata_p.h new file mode 100644 index 000000000..95b069b64 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegmediametadata_p.h @@ -0,0 +1,30 @@ +// 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 + +#ifndef QFFMPEGMEDIAMETADATA_H +#define QFFMPEGMEDIAMETADATA_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +class QFFmpegMetaData : public QMediaMetaData +{ +public: + static QByteArray value(const QMediaMetaData &metaData, QMediaMetaData::Key key); +}; + +QT_END_NAMESPACE + +#endif // QFFMPEGMEDIAMETADATA_H diff --git a/src/plugins/multimedia/v4l2/qffmpegvideosink.cpp b/src/plugins/multimedia/v4l2/qffmpegvideosink.cpp new file mode 100644 index 000000000..93e7ceeed --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegvideosink.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2021 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 + +QT_BEGIN_NAMESPACE + +QFFmpegVideoSink::QFFmpegVideoSink(QVideoSink *sink) + : QPlatformVideoSink(sink) +{ +} + +void QFFmpegVideoSink::setVideoFrame(const QVideoFrame &frame) +{ + QPlatformVideoSink::setVideoFrame(frame); +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qffmpegvideosink_p.h b/src/plugins/multimedia/v4l2/qffmpegvideosink_p.h new file mode 100644 index 000000000..cbaa810d7 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qffmpegvideosink_p.h @@ -0,0 +1,39 @@ +// Copyright (C) 2021 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 + +#ifndef QFFMPEGVIDEOSINK_H +#define QFFMPEGVIDEOSINK_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +//#include + +QT_BEGIN_NAMESPACE + +// Required for QDoc workaround +class QString; + +class QFFmpegVideoSink : public QPlatformVideoSink +{ + Q_OBJECT + +public: + QFFmpegVideoSink(QVideoSink *sink); + + void setVideoFrame(const QVideoFrame &frame) override; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/plugins/multimedia/v4l2/qv4l2camera.cpp b/src/plugins/multimedia/v4l2/qv4l2camera.cpp new file mode 100644 index 000000000..1ba05364d --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2camera.cpp @@ -0,0 +1,707 @@ +// Copyright (C) 2021 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 "qv4l2camera_p.h" +#include "qv4l2filedescriptor_p.h" +#include "qv4l2memorytransfer_p.h" + +#include +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +static Q_LOGGING_CATEGORY(qLcV4L2Camera, "qt.multimedia.ffmpeg.v4l2camera"); + +static const struct { + QVideoFrameFormat::PixelFormat fmt; + uint32_t v4l2Format; +} formatMap[] = { + // ### How do we handle V4L2_PIX_FMT_H264 and V4L2_PIX_FMT_MPEG4? + { QVideoFrameFormat::Format_YUV420P, V4L2_PIX_FMT_YUV420 }, + { QVideoFrameFormat::Format_YUV422P, V4L2_PIX_FMT_YUV422P }, + { QVideoFrameFormat::Format_YUYV, V4L2_PIX_FMT_YUYV }, + { QVideoFrameFormat::Format_UYVY, V4L2_PIX_FMT_UYVY }, + { QVideoFrameFormat::Format_XBGR8888, V4L2_PIX_FMT_XBGR32 }, + { QVideoFrameFormat::Format_XRGB8888, V4L2_PIX_FMT_XRGB32 }, + { QVideoFrameFormat::Format_ABGR8888, V4L2_PIX_FMT_ABGR32 }, + { QVideoFrameFormat::Format_ARGB8888, V4L2_PIX_FMT_ARGB32 }, + { QVideoFrameFormat::Format_BGRX8888, V4L2_PIX_FMT_BGR32 }, + { QVideoFrameFormat::Format_RGBX8888, V4L2_PIX_FMT_RGB32 }, + { QVideoFrameFormat::Format_BGRA8888, V4L2_PIX_FMT_BGRA32 }, + { QVideoFrameFormat::Format_RGBA8888, V4L2_PIX_FMT_RGBA32 }, + { QVideoFrameFormat::Format_Y8, V4L2_PIX_FMT_GREY }, + { QVideoFrameFormat::Format_Y16, V4L2_PIX_FMT_Y16 }, + { QVideoFrameFormat::Format_NV12, V4L2_PIX_FMT_NV12 }, + { QVideoFrameFormat::Format_NV21, V4L2_PIX_FMT_NV21 }, + { QVideoFrameFormat::Format_Jpeg, V4L2_PIX_FMT_MJPEG }, + { QVideoFrameFormat::Format_Jpeg, V4L2_PIX_FMT_JPEG }, + { QVideoFrameFormat::Format_Invalid, 0 }, +}; + +QVideoFrameFormat::PixelFormat formatForV4L2Format(uint32_t v4l2Format) +{ + auto *f = formatMap; + while (f->v4l2Format) { + if (f->v4l2Format == v4l2Format) + return f->fmt; + ++f; + } + return QVideoFrameFormat::Format_Invalid; +} + +uint32_t v4l2FormatForPixelFormat(QVideoFrameFormat::PixelFormat format) +{ + auto *f = formatMap; + while (f->v4l2Format) { + if (f->fmt == format) + return f->v4l2Format; + ++f; + } + return 0; +} + +QV4L2Camera::QV4L2Camera(QCamera *camera) + : QPlatformCamera(camera) +{ +} + +QV4L2Camera::~QV4L2Camera() +{ + stopCapturing(); + closeV4L2Fd(); +} + +bool QV4L2Camera::isActive() const +{ + return m_active; +} + +void QV4L2Camera::setActive(bool active) +{ + if (m_active == active) + return; + if (m_cameraDevice.isNull() && active) + return; + + if (m_cameraFormat.isNull()) + resolveCameraFormat({}); + + m_active = active; + if (m_active) + startCapturing(); + else + stopCapturing(); + + emit newVideoFrame({}); + + emit activeChanged(active); +} + +void QV4L2Camera::setCamera(const QCameraDevice &camera) +{ + if (m_cameraDevice == camera) + return; + + stopCapturing(); + closeV4L2Fd(); + + m_cameraDevice = camera; + resolveCameraFormat({}); + + initV4L2Controls(); + + if (m_active) + startCapturing(); +} + +bool QV4L2Camera::setCameraFormat(const QCameraFormat &format) +{ + if (!format.isNull() && !m_cameraDevice.videoFormats().contains(format)) + return false; + + if (!resolveCameraFormat(format)) + return true; + + if (m_active) { + stopCapturing(); + closeV4L2Fd(); + + initV4L2Controls(); + startCapturing(); + } + + return true; +} + +bool QV4L2Camera::resolveCameraFormat(const QCameraFormat &format) +{ + auto fmt = format; + if (fmt.isNull()) + fmt = findBestCameraFormat(m_cameraDevice); + + if (fmt == m_cameraFormat) + return false; + + m_cameraFormat = fmt; + return true; +} + +void QV4L2Camera::setFocusMode(QCamera::FocusMode mode) +{ + if (mode == focusMode()) + return; + + bool focusDist = supportedFeatures() & QCamera::Feature::FocusDistance; + if (!focusDist && !m_v4l2Info.rangedFocus) + return; + + switch (mode) { + default: + case QCamera::FocusModeAuto: + setV4L2Parameter(V4L2_CID_FOCUS_AUTO, 1); + if (m_v4l2Info.rangedFocus) + setV4L2Parameter(V4L2_CID_AUTO_FOCUS_RANGE, V4L2_AUTO_FOCUS_RANGE_AUTO); + break; + case QCamera::FocusModeAutoNear: + setV4L2Parameter(V4L2_CID_FOCUS_AUTO, 1); + if (m_v4l2Info.rangedFocus) + setV4L2Parameter(V4L2_CID_AUTO_FOCUS_RANGE, V4L2_AUTO_FOCUS_RANGE_MACRO); + else if (focusDist) + setV4L2Parameter(V4L2_CID_FOCUS_ABSOLUTE, m_v4l2Info.minFocus); + break; + case QCamera::FocusModeAutoFar: + setV4L2Parameter(V4L2_CID_FOCUS_AUTO, 1); + if (m_v4l2Info.rangedFocus) + setV4L2Parameter(V4L2_CID_AUTO_FOCUS_RANGE, V4L2_AUTO_FOCUS_RANGE_INFINITY); + break; + case QCamera::FocusModeInfinity: + setV4L2Parameter(V4L2_CID_FOCUS_AUTO, 0); + setV4L2Parameter(V4L2_CID_FOCUS_ABSOLUTE, m_v4l2Info.maxFocus); + break; + case QCamera::FocusModeManual: + setV4L2Parameter(V4L2_CID_FOCUS_AUTO, 0); + setFocusDistance(focusDistance()); + break; + } + focusModeChanged(mode); +} + +void QV4L2Camera::setFocusDistance(float d) +{ + int distance = m_v4l2Info.minFocus + int((m_v4l2Info.maxFocus - m_v4l2Info.minFocus) * d); + setV4L2Parameter(V4L2_CID_FOCUS_ABSOLUTE, distance); + focusDistanceChanged(d); +} + +void QV4L2Camera::zoomTo(float factor, float) +{ + if (m_v4l2Info.maxZoom == m_v4l2Info.minZoom) + return; + factor = qBound(1., factor, 2.); + int zoom = m_v4l2Info.minZoom + (factor - 1.) * (m_v4l2Info.maxZoom - m_v4l2Info.minZoom); + setV4L2Parameter(V4L2_CID_ZOOM_ABSOLUTE, zoom); + zoomFactorChanged(factor); +} + +bool QV4L2Camera::isFocusModeSupported(QCamera::FocusMode mode) const +{ + if (supportedFeatures() & QCamera::Feature::FocusDistance && + (mode == QCamera::FocusModeManual || mode == QCamera::FocusModeAutoNear || mode == QCamera::FocusModeInfinity)) + return true; + + return mode == QCamera::FocusModeAuto; +} + +void QV4L2Camera::setFlashMode(QCamera::FlashMode mode) +{ + if (!m_v4l2Info.flashSupported || mode == QCamera::FlashOn) + return; + setV4L2Parameter(V4L2_CID_FLASH_LED_MODE, mode == QCamera::FlashAuto ? V4L2_FLASH_LED_MODE_FLASH : V4L2_FLASH_LED_MODE_NONE); + flashModeChanged(mode); +} + +bool QV4L2Camera::isFlashModeSupported(QCamera::FlashMode mode) const +{ + if (m_v4l2Info.flashSupported && mode == QCamera::FlashAuto) + return true; + return mode == QCamera::FlashOff; +} + +bool QV4L2Camera::isFlashReady() const +{ + struct v4l2_queryctrl queryControl; + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_AUTO_WHITE_BALANCE; + + return m_v4l2FileDescriptor && m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl); +} + +void QV4L2Camera::setTorchMode(QCamera::TorchMode mode) +{ + if (!m_v4l2Info.torchSupported || mode == QCamera::TorchOn) + return; + setV4L2Parameter(V4L2_CID_FLASH_LED_MODE, mode == QCamera::TorchOn ? V4L2_FLASH_LED_MODE_TORCH : V4L2_FLASH_LED_MODE_NONE); + torchModeChanged(mode); +} + +bool QV4L2Camera::isTorchModeSupported(QCamera::TorchMode mode) const +{ + if (mode == QCamera::TorchOn) + return m_v4l2Info.torchSupported; + return mode == QCamera::TorchOff; +} + +void QV4L2Camera::setExposureMode(QCamera::ExposureMode mode) +{ + if (m_v4l2Info.autoExposureSupported && m_v4l2Info.manualExposureSupported) { + if (mode != QCamera::ExposureAuto && mode != QCamera::ExposureManual) + return; + int value = QCamera::ExposureAuto ? V4L2_EXPOSURE_AUTO : V4L2_EXPOSURE_MANUAL; + setV4L2Parameter(V4L2_CID_EXPOSURE_AUTO, value); + exposureModeChanged(mode); + return; + } +} + +bool QV4L2Camera::isExposureModeSupported(QCamera::ExposureMode mode) const +{ + if (mode == QCamera::ExposureAuto) + return true; + if (m_v4l2Info.manualExposureSupported && m_v4l2Info.autoExposureSupported) + return mode == QCamera::ExposureManual; + return false; +} + +void QV4L2Camera::setExposureCompensation(float compensation) +{ + if ((m_v4l2Info.minExposureAdjustment != 0 || m_v4l2Info.maxExposureAdjustment != 0)) { + int value = qBound(m_v4l2Info.minExposureAdjustment, (int)(compensation * 1000), + m_v4l2Info.maxExposureAdjustment); + setV4L2Parameter(V4L2_CID_AUTO_EXPOSURE_BIAS, value); + exposureCompensationChanged(value/1000.); + return; + } +} + +void QV4L2Camera::setManualIsoSensitivity(int iso) +{ + if (!(supportedFeatures() & QCamera::Feature::IsoSensitivity)) + return; + setV4L2Parameter(V4L2_CID_ISO_SENSITIVITY_AUTO, iso <= 0 ? V4L2_ISO_SENSITIVITY_AUTO : V4L2_ISO_SENSITIVITY_MANUAL); + if (iso > 0) { + iso = qBound(minIso(), iso, maxIso()); + setV4L2Parameter(V4L2_CID_ISO_SENSITIVITY, iso); + } + return; +} + +int QV4L2Camera::isoSensitivity() const +{ + if (!(supportedFeatures() & QCamera::Feature::IsoSensitivity)) + return -1; + return getV4L2Parameter(V4L2_CID_ISO_SENSITIVITY); +} + +void QV4L2Camera::setManualExposureTime(float secs) +{ + if (m_v4l2Info.manualExposureSupported && m_v4l2Info.autoExposureSupported) { + int exposure = + qBound(m_v4l2Info.minExposure, qRound(secs * 10000.), m_v4l2Info.maxExposure); + setV4L2Parameter(V4L2_CID_EXPOSURE_ABSOLUTE, exposure); + exposureTimeChanged(exposure/10000.); + return; + } +} + +float QV4L2Camera::exposureTime() const +{ + return getV4L2Parameter(V4L2_CID_EXPOSURE_ABSOLUTE)/10000.; +} + +bool QV4L2Camera::isWhiteBalanceModeSupported(QCamera::WhiteBalanceMode mode) const +{ + if (m_v4l2Info.autoWhiteBalanceSupported && m_v4l2Info.colorTemperatureSupported) + return true; + + return mode == QCamera::WhiteBalanceAuto; +} + +void QV4L2Camera::setWhiteBalanceMode(QCamera::WhiteBalanceMode mode) +{ + Q_ASSERT(isWhiteBalanceModeSupported(mode)); + + int temperature = colorTemperatureForWhiteBalance(mode); + int t = setV4L2ColorTemperature(temperature); + if (t == 0) + mode = QCamera::WhiteBalanceAuto; + whiteBalanceModeChanged(mode); +} + +void QV4L2Camera::setColorTemperature(int temperature) +{ + if (temperature == 0) { + setWhiteBalanceMode(QCamera::WhiteBalanceAuto); + return; + } + + Q_ASSERT(isWhiteBalanceModeSupported(QCamera::WhiteBalanceManual)); + + int t = setV4L2ColorTemperature(temperature); + if (t) + colorTemperatureChanged(t); +} + +void QV4L2Camera::readFrame() +{ + Q_ASSERT(m_memoryTransfer); + + auto buffer = m_memoryTransfer->dequeueBuffer(); + if (!buffer) { + qCWarning(qLcV4L2Camera) << "Cannot take buffer"; + + if (errno == ENODEV) { + // camera got removed while being active + stopCapturing(); + closeV4L2Fd(); + } + + return; + } + + auto videoBuffer = new QMemoryVideoBuffer(buffer->data, m_bytesPerLine); + QVideoFrame frame(videoBuffer, frameFormat()); + + auto &v4l2Buffer = buffer->v4l2Buffer; + + if (m_firstFrameTime.tv_sec == -1) + m_firstFrameTime = v4l2Buffer.timestamp; + qint64 secs = v4l2Buffer.timestamp.tv_sec - m_firstFrameTime.tv_sec; + qint64 usecs = v4l2Buffer.timestamp.tv_usec - m_firstFrameTime.tv_usec; + frame.setStartTime(secs*1000000 + usecs); + frame.setEndTime(frame.startTime() + m_frameDuration); + + emit newVideoFrame(frame); + + if (!m_memoryTransfer->enqueueBuffer(v4l2Buffer.index)) + qCWarning(qLcV4L2Camera) << "Cannot add buffer"; +} + +void QV4L2Camera::setCameraBusy() +{ + m_cameraBusy = true; + updateError(QCamera::CameraError, QLatin1String("Camera is in use")); +} + +void QV4L2Camera::initV4L2Controls() +{ + m_v4l2Info = {}; + QCamera::Features features; + + const QByteArray deviceName = m_cameraDevice.id(); + Q_ASSERT(!deviceName.isEmpty()); + + closeV4L2Fd(); + + const int descriptor = qt_safe_open(deviceName.constData(), O_RDWR); + if (descriptor == -1) { + qCWarning(qLcV4L2Camera) << "Unable to open the camera" << deviceName + << "for read to query the parameter info:" + << qt_error_string(errno); + updateError(QCamera::CameraError, QLatin1String("Cannot open camera")); + return; + } + + m_v4l2FileDescriptor = std::make_shared(descriptor); + + qCDebug(qLcV4L2Camera) << "FD=" << descriptor; + + struct v4l2_queryctrl queryControl; + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_AUTO_WHITE_BALANCE; + + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.autoWhiteBalanceSupported = true; + setV4L2Parameter(V4L2_CID_AUTO_WHITE_BALANCE, true); + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_WHITE_BALANCE_TEMPERATURE; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.minColorTemp = queryControl.minimum; + m_v4l2Info.maxColorTemp = queryControl.maximum; + m_v4l2Info.colorTemperatureSupported = true; + features |= QCamera::Feature::ColorTemperature; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_EXPOSURE_AUTO; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.autoExposureSupported = true; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_EXPOSURE_ABSOLUTE; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.manualExposureSupported = true; + m_v4l2Info.minExposure = queryControl.minimum; + m_v4l2Info.maxExposure = queryControl.maximum; + features |= QCamera::Feature::ManualExposureTime; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_AUTO_EXPOSURE_BIAS; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.minExposureAdjustment = queryControl.minimum; + m_v4l2Info.maxExposureAdjustment = queryControl.maximum; + features |= QCamera::Feature::ExposureCompensation; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_ISO_SENSITIVITY_AUTO; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + queryControl.id = V4L2_CID_ISO_SENSITIVITY; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + features |= QCamera::Feature::IsoSensitivity; + minIsoChanged(queryControl.minimum); + maxIsoChanged(queryControl.minimum); + } + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_FOCUS_ABSOLUTE; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.minExposureAdjustment = queryControl.minimum; + m_v4l2Info.maxExposureAdjustment = queryControl.maximum; + features |= QCamera::Feature::FocusDistance; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_AUTO_FOCUS_RANGE; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.rangedFocus = true; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_FLASH_LED_MODE; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.flashSupported = queryControl.minimum <= V4L2_FLASH_LED_MODE_FLASH + && queryControl.maximum >= V4L2_FLASH_LED_MODE_FLASH; + m_v4l2Info.torchSupported = queryControl.minimum <= V4L2_FLASH_LED_MODE_TORCH + && queryControl.maximum >= V4L2_FLASH_LED_MODE_TORCH; + } + + ::memset(&queryControl, 0, sizeof(queryControl)); + queryControl.id = V4L2_CID_ZOOM_ABSOLUTE; + if (m_v4l2FileDescriptor->call(VIDIOC_QUERYCTRL, &queryControl)) { + m_v4l2Info.minZoom = queryControl.minimum; + m_v4l2Info.maxZoom = queryControl.maximum; + } + // zoom factors are in arbitrary units, so we simply normalize them to go from 1 to 2 + // if they are different + minimumZoomFactorChanged(1); + maximumZoomFactorChanged(m_v4l2Info.minZoom != m_v4l2Info.maxZoom ? 2 : 1); + + supportedFeaturesChanged(features); +} + +void QV4L2Camera::closeV4L2Fd() +{ + Q_ASSERT(!m_memoryTransfer); + + m_v4l2Info = {}; + m_cameraBusy = false; + m_v4l2FileDescriptor = nullptr; +} + +int QV4L2Camera::setV4L2ColorTemperature(int temperature) +{ + struct v4l2_control control; + ::memset(&control, 0, sizeof(control)); + + if (m_v4l2Info.autoWhiteBalanceSupported) { + setV4L2Parameter(V4L2_CID_AUTO_WHITE_BALANCE, temperature == 0 ? true : false); + } else if (temperature == 0) { + temperature = 5600; + } + + if (temperature != 0 && m_v4l2Info.colorTemperatureSupported) { + temperature = qBound(m_v4l2Info.minColorTemp, temperature, m_v4l2Info.maxColorTemp); + if (!setV4L2Parameter( + V4L2_CID_WHITE_BALANCE_TEMPERATURE, + qBound(m_v4l2Info.minColorTemp, temperature, m_v4l2Info.maxColorTemp))) + temperature = 0; + } else { + temperature = 0; + } + + return temperature; +} + +bool QV4L2Camera::setV4L2Parameter(quint32 id, qint32 value) +{ + v4l2_control control{ id, value }; + if (!m_v4l2FileDescriptor->call(VIDIOC_S_CTRL, &control)) { + qWarning() << "Unable to set the V4L2 Parameter" << Qt::hex << id << "to" << value << qt_error_string(errno); + return false; + } + return true; +} + +int QV4L2Camera::getV4L2Parameter(quint32 id) const +{ + struct v4l2_control control{id, 0}; + if (!m_v4l2FileDescriptor->call(VIDIOC_G_CTRL, &control)) { + qWarning() << "Unable to get the V4L2 Parameter" << Qt::hex << id << qt_error_string(errno); + return 0; + } + return control.value; +} + +void QV4L2Camera::setV4L2CameraFormat() +{ + if (m_v4l2Info.formatInitialized || !m_v4l2FileDescriptor) + return; + + Q_ASSERT(!m_cameraFormat.isNull()); + qCDebug(qLcV4L2Camera) << "XXXXX" << this << m_cameraDevice.id() << m_cameraFormat.pixelFormat() + << m_cameraFormat.resolution(); + + v4l2_format fmt = {}; + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + auto size = m_cameraFormat.resolution(); + fmt.fmt.pix.width = size.width(); + fmt.fmt.pix.height = size.height(); + fmt.fmt.pix.pixelformat = v4l2FormatForPixelFormat(m_cameraFormat.pixelFormat()); + fmt.fmt.pix.field = V4L2_FIELD_ANY; + + qCDebug(qLcV4L2Camera) << "setting camera format to" << size << fmt.fmt.pix.pixelformat; + + if (!m_v4l2FileDescriptor->call(VIDIOC_S_FMT, &fmt)) { + if (errno == EBUSY) { + setCameraBusy(); + return; + } + qWarning() << "Couldn't set video format on v4l2 camera" << strerror(errno); + } + + m_v4l2Info.formatInitialized = true; + m_cameraBusy = false; + + m_bytesPerLine = fmt.fmt.pix.bytesperline; + m_imageSize = std::max(fmt.fmt.pix.sizeimage, m_bytesPerLine * fmt.fmt.pix.height); + + switch (v4l2_colorspace(fmt.fmt.pix.colorspace)) { + default: + case V4L2_COLORSPACE_DCI_P3: + m_colorSpace = QVideoFrameFormat::ColorSpace_Undefined; + break; + case V4L2_COLORSPACE_REC709: + m_colorSpace = QVideoFrameFormat::ColorSpace_BT709; + break; + case V4L2_COLORSPACE_JPEG: + m_colorSpace = QVideoFrameFormat::ColorSpace_AdobeRgb; + break; + case V4L2_COLORSPACE_SRGB: + // ##### is this correct??? + m_colorSpace = QVideoFrameFormat::ColorSpace_BT601; + break; + case V4L2_COLORSPACE_BT2020: + m_colorSpace = QVideoFrameFormat::ColorSpace_BT2020; + break; + } + + v4l2_streamparm streamParam = {}; + streamParam.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + streamParam.parm.capture.capability = V4L2_CAP_TIMEPERFRAME; + auto [num, den] = qRealToFraction(1./m_cameraFormat.maxFrameRate()); + streamParam.parm.capture.timeperframe = { (uint)num, (uint)den }; + m_v4l2FileDescriptor->call(VIDIOC_S_PARM, &streamParam); + + m_frameDuration = 1000000 * streamParam.parm.capture.timeperframe.numerator + / streamParam.parm.capture.timeperframe.denominator; +} + +void QV4L2Camera::initV4L2MemoryTransfer() +{ + if (m_cameraBusy) + return; + + Q_ASSERT(!m_memoryTransfer); + + m_memoryTransfer = makeUserPtrMemoryTransfer(m_v4l2FileDescriptor, m_imageSize); + + if (m_memoryTransfer) + return; + + if (errno == EBUSY) { + setCameraBusy(); + return; + } + + qCDebug(qLcV4L2Camera) << "Cannot init V4L2_MEMORY_USERPTR; trying V4L2_MEMORY_MMAP"; + + m_memoryTransfer = makeMMapMemoryTransfer(m_v4l2FileDescriptor); + + if (!m_memoryTransfer) { + qCWarning(qLcV4L2Camera) << "Cannot init v4l2 memory transfer," << qt_error_string(errno); + updateError(QCamera::CameraError, QLatin1String("Cannot init V4L2 memory transfer")); + } +} + +void QV4L2Camera::stopCapturing() +{ + if (!m_memoryTransfer || !m_v4l2FileDescriptor) + return; + + m_notifier = nullptr; + + if (!m_v4l2FileDescriptor->stopStream()) { + // TODO: handle the case carefully to avoid possible memory corruption + if (errno != ENODEV) + qWarning() << "failed to stop capture"; + } + + m_memoryTransfer = nullptr; + m_cameraBusy = false; +} + +void QV4L2Camera::startCapturing() +{ + if (!m_v4l2FileDescriptor) + return; + + setV4L2CameraFormat(); + initV4L2MemoryTransfer(); + + if (m_cameraBusy || !m_memoryTransfer) + return; + + if (!m_v4l2FileDescriptor->startStream()) { + qWarning() << "Couldn't start v4l2 camera stream"; + return; + } + + m_notifier = + std::make_unique(m_v4l2FileDescriptor->get(), QSocketNotifier::Read); + connect(m_notifier.get(), &QSocketNotifier::activated, this, &QV4L2Camera::readFrame); + + m_firstFrameTime = { -1, -1 }; +} + +QVideoFrameFormat QV4L2Camera::frameFormat() const +{ + auto result = QPlatformCamera::frameFormat(); + result.setColorSpace(m_colorSpace); + return result; +} + +QT_END_NAMESPACE + +#include "moc_qv4l2camera_p.cpp" diff --git a/src/plugins/multimedia/v4l2/qv4l2camera_p.h b/src/plugins/multimedia/v4l2/qv4l2camera_p.h new file mode 100644 index 000000000..79cc3cfa9 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2camera_p.h @@ -0,0 +1,133 @@ +// Copyright (C) 2021 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 + +#ifndef QV4L2CAMERA_H +#define QV4L2CAMERA_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QV4L2FileDescriptor; +class QV4L2MemoryTransfer; +class QSocketNotifier; + +struct V4L2CameraInfo +{ + bool formatInitialized = false; + + bool autoWhiteBalanceSupported = false; + bool colorTemperatureSupported = false; + bool autoExposureSupported = false; + bool manualExposureSupported = false; + bool flashSupported = false; + bool torchSupported = false; + qint32 minColorTemp = 5600; // Daylight... + qint32 maxColorTemp = 5600; + qint32 minExposure = 0; + qint32 maxExposure = 0; + qint32 minExposureAdjustment = 0; + qint32 maxExposureAdjustment = 0; + qint32 minFocus = 0; + qint32 maxFocus = 0; + qint32 rangedFocus = false; + + int minZoom = 0; + int maxZoom = 0; +}; + +QVideoFrameFormat::PixelFormat formatForV4L2Format(uint32_t v4l2Format); +uint32_t v4l2FormatForPixelFormat(QVideoFrameFormat::PixelFormat format); + +class Q_MULTIMEDIA_EXPORT QV4L2Camera : public QPlatformCamera +{ + Q_OBJECT + +public: + explicit QV4L2Camera(QCamera *parent); + ~QV4L2Camera(); + + bool isActive() const override; + void setActive(bool active) override; + + void setCamera(const QCameraDevice &camera) override; + bool setCameraFormat(const QCameraFormat &format) override; + bool resolveCameraFormat(const QCameraFormat &format); + + bool isFocusModeSupported(QCamera::FocusMode mode) const override; + void setFocusMode(QCamera::FocusMode /*mode*/) override; + +// void setCustomFocusPoint(const QPointF &/*point*/) override; + void setFocusDistance(float) override; + void zoomTo(float /*newZoomFactor*/, float /*rate*/ = -1.) override; + + void setFlashMode(QCamera::FlashMode /*mode*/) override; + bool isFlashModeSupported(QCamera::FlashMode mode) const override; + bool isFlashReady() const override; + + void setTorchMode(QCamera::TorchMode /*mode*/) override; + bool isTorchModeSupported(QCamera::TorchMode mode) const override; + + void setExposureMode(QCamera::ExposureMode) override; + bool isExposureModeSupported(QCamera::ExposureMode mode) const override; + void setExposureCompensation(float) override; + int isoSensitivity() const override; + void setManualIsoSensitivity(int) override; + void setManualExposureTime(float) override; + float exposureTime() const override; + + bool isWhiteBalanceModeSupported(QCamera::WhiteBalanceMode mode) const override; + void setWhiteBalanceMode(QCamera::WhiteBalanceMode /*mode*/) override; + void setColorTemperature(int /*temperature*/) override; + + QVideoFrameFormat frameFormat() const override; + +private Q_SLOTS: + void readFrame(); + +private: + void setCameraBusy(); + void initV4L2Controls(); + void closeV4L2Fd(); + int setV4L2ColorTemperature(int temperature); + bool setV4L2Parameter(quint32 id, qint32 value); + int getV4L2Parameter(quint32 id) const; + + void setV4L2CameraFormat(); + void initV4L2MemoryTransfer(); + void startCapturing(); + void stopCapturing(); + +private: + bool m_active = false; + QCameraDevice m_cameraDevice; + + std::unique_ptr m_notifier; + std::unique_ptr m_memoryTransfer; + std::shared_ptr m_v4l2FileDescriptor; + + V4L2CameraInfo m_v4l2Info; + + timeval m_firstFrameTime = { -1, -1 }; + quint32 m_bytesPerLine = 0; + quint32 m_imageSize = 0; + QVideoFrameFormat::ColorSpace m_colorSpace = QVideoFrameFormat::ColorSpace_Undefined; + qint64 m_frameDuration = -1; + bool m_cameraBusy = false; +}; + +QT_END_NAMESPACE + +#endif // QV4L2CAMERA_H diff --git a/src/plugins/multimedia/v4l2/qv4l2cameradevices.cpp b/src/plugins/multimedia/v4l2/qv4l2cameradevices.cpp new file mode 100644 index 000000000..44abde914 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2cameradevices.cpp @@ -0,0 +1,168 @@ +// Copyright (C) 2023 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 "qv4l2cameradevices_p.h" +#include "qv4l2filedescriptor_p.h" +#include "qv4l2camera_p.h" + +#include +#include + +#include +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +static Q_LOGGING_CATEGORY(qLcV4L2CameraDevices, "qt.multimedia.ffmpeg.v4l2cameradevices"); + +static bool areCamerasEqual(QList a, QList b) +{ + auto areCamerasDataEqual = [](const QCameraDevice &a, const QCameraDevice &b) { + Q_ASSERT(QCameraDevicePrivate::handle(a)); + Q_ASSERT(QCameraDevicePrivate::handle(b)); + return *QCameraDevicePrivate::handle(a) == *QCameraDevicePrivate::handle(b); + }; + + return std::equal(a.cbegin(), a.cend(), b.cbegin(), b.cend(), areCamerasDataEqual); +} + +QV4L2CameraDevices::QV4L2CameraDevices(QPlatformMediaIntegration *integration) + : QPlatformVideoDevices(integration) +{ + m_deviceWatcher.addPath(QLatin1String("/dev")); + connect(&m_deviceWatcher, &QFileSystemWatcher::directoryChanged, this, + &QV4L2CameraDevices::checkCameras); + doCheckCameras(); +} + +QList QV4L2CameraDevices::videoDevices() const +{ + return m_cameras; +} + +void QV4L2CameraDevices::checkCameras() +{ + if (doCheckCameras()) + emit videoInputsChanged(); +} + +bool QV4L2CameraDevices::doCheckCameras() +{ + QList newCameras; + + QDir dir(QLatin1String("/dev")); + const auto devices = dir.entryList(QDir::System); + + bool first = true; + + for (auto device : devices) { + // qCDebug(qLcV4L2Camera) << "device:" << device; + if (!device.startsWith(QLatin1String("video"))) + continue; + + QByteArray file = QFile::encodeName(dir.filePath(device)); + const int fd = open(file.constData(), O_RDONLY); + if (fd < 0) + continue; + + auto fileCloseGuard = qScopeGuard([fd]() { close(fd); }); + + v4l2_fmtdesc formatDesc = {}; + + struct v4l2_capability cap; + if (xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) + continue; + + if (cap.device_caps & V4L2_CAP_META_CAPTURE) + continue; + if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) + continue; + if (!(cap.capabilities & V4L2_CAP_STREAMING)) + continue; + + auto camera = std::make_unique(); + + camera->id = file; + camera->description = QString::fromUtf8((const char *)cap.card); + qCDebug(qLcV4L2CameraDevices) << "found camera" << camera->id << camera->description; + + formatDesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + while (!xioctl(fd, VIDIOC_ENUM_FMT, &formatDesc)) { + auto pixelFmt = formatForV4L2Format(formatDesc.pixelformat); + qCDebug(qLcV4L2CameraDevices) << " " << pixelFmt; + + if (pixelFmt == QVideoFrameFormat::Format_Invalid) { + ++formatDesc.index; + continue; + } + + qCDebug(qLcV4L2CameraDevices) << "frame sizes:"; + v4l2_frmsizeenum frameSize = {}; + frameSize.pixel_format = formatDesc.pixelformat; + + while (!xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frameSize)) { + ++frameSize.index; + if (frameSize.type != V4L2_FRMSIZE_TYPE_DISCRETE) + continue; + + QSize resolution(frameSize.discrete.width, frameSize.discrete.height); + float min = 1e10; + float max = 0; + + v4l2_frmivalenum frameInterval = {}; + frameInterval.pixel_format = formatDesc.pixelformat; + frameInterval.width = frameSize.discrete.width; + frameInterval.height = frameSize.discrete.height; + + while (!xioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frameInterval)) { + ++frameInterval.index; + if (frameInterval.type != V4L2_FRMIVAL_TYPE_DISCRETE) + continue; + float rate = float(frameInterval.discrete.denominator) + / float(frameInterval.discrete.numerator); + if (rate > max) + max = rate; + if (rate < min) + min = rate; + } + + qCDebug(qLcV4L2CameraDevices) << " " << resolution << min << max; + + if (min <= max) { + auto fmt = std::make_unique(); + fmt->pixelFormat = pixelFmt; + fmt->resolution = resolution; + fmt->minFrameRate = min; + fmt->maxFrameRate = max; + camera->videoFormats.append(fmt.release()->create()); + camera->photoResolutions.append(resolution); + } + } + + ++formatDesc.index; + } + + if (camera->videoFormats.empty()) + continue; + + // first camera is default + camera->isDefault = std::exchange(first, false); + + newCameras.append(camera.release()->create()); + } + + if (areCamerasEqual(m_cameras, newCameras)) + return false; + + m_cameras = std::move(newCameras); + return true; +} + +QT_END_NAMESPACE + +#include "moc_qv4l2cameradevices_p.cpp" diff --git a/src/plugins/multimedia/v4l2/qv4l2cameradevices_p.h b/src/plugins/multimedia/v4l2/qv4l2cameradevices_p.h new file mode 100644 index 000000000..ce424d3b6 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2cameradevices_p.h @@ -0,0 +1,46 @@ +// Copyright (C) 2023 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 + +#ifndef QV4L2CAMERADEVICES_P_H +#define QV4L2CAMERADEVICES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +class QV4L2CameraDevices : public QPlatformVideoDevices +{ + Q_OBJECT +public: + QV4L2CameraDevices(QPlatformMediaIntegration *integration); + + QList videoDevices() const override; + +public Q_SLOTS: + void checkCameras(); + +private: + bool doCheckCameras(); + +private: + QList m_cameras; + QFileSystemWatcher m_deviceWatcher; +}; + +QT_END_NAMESPACE + +#endif // QV4L2CAMERADEVICES_P_H diff --git a/src/plugins/multimedia/v4l2/qv4l2filedescriptor.cpp b/src/plugins/multimedia/v4l2/qv4l2filedescriptor.cpp new file mode 100644 index 000000000..7f7b099c7 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2filedescriptor.cpp @@ -0,0 +1,71 @@ +// Copyright (C) 2023 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 "qv4l2filedescriptor_p.h" + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +int xioctl(int fd, int request, void *arg) +{ + int res; + + do { + res = ::ioctl(fd, request, arg); + } while (res == -1 && EINTR == errno); + + return res; +} + +QV4L2FileDescriptor::QV4L2FileDescriptor(int descriptor) : m_descriptor(descriptor) +{ + Q_ASSERT(descriptor >= 0); +} + +QV4L2FileDescriptor::~QV4L2FileDescriptor() +{ + qt_safe_close(m_descriptor); +} + +bool QV4L2FileDescriptor::call(int request, void *arg) const +{ + return ::xioctl(m_descriptor, request, arg) >= 0; +} + +bool QV4L2FileDescriptor::requestBuffers(quint32 memoryType, quint32 &buffersCount) const +{ + v4l2_requestbuffers req = {}; + req.count = buffersCount; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = memoryType; + + if (!call(VIDIOC_REQBUFS, &req)) + return false; + + buffersCount = req.count; + return true; +} + +bool QV4L2FileDescriptor::startStream() +{ + int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (!call(VIDIOC_STREAMON, &type)) + return false; + + m_streamStarted = true; + return true; +} + +bool QV4L2FileDescriptor::stopStream() +{ + int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + auto result = call(VIDIOC_STREAMOFF, &type); + m_streamStarted = false; + return result; +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qv4l2filedescriptor_p.h b/src/plugins/multimedia/v4l2/qv4l2filedescriptor_p.h new file mode 100644 index 000000000..1058c7a82 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2filedescriptor_p.h @@ -0,0 +1,50 @@ +// Copyright (C) 2023 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 + +#ifndef QV4L2FILEDESCRIPTOR_P_H +#define QV4L2FILEDESCRIPTOR_P_H + +#include + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +int xioctl(int fd, int request, void *arg); + +class QV4L2FileDescriptor +{ +public: + QV4L2FileDescriptor(int descriptor); + + ~QV4L2FileDescriptor(); + + bool call(int request, void *arg) const; + + int get() const { return m_descriptor; } + + bool requestBuffers(quint32 memoryType, quint32 &buffersCount) const; + + bool startStream(); + + bool stopStream(); + + bool streamStarted() const { return m_streamStarted; } + +private: + int m_descriptor; + bool m_streamStarted = false; +}; + +QT_END_NAMESPACE + +#endif // QV4L2FILEDESCRIPTOR_P_H diff --git a/src/plugins/multimedia/v4l2/qv4l2memorytransfer.cpp b/src/plugins/multimedia/v4l2/qv4l2memorytransfer.cpp new file mode 100644 index 000000000..32ee4f8f8 --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2memorytransfer.cpp @@ -0,0 +1,223 @@ +// Copyright (C) 2023 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 "qv4l2memorytransfer_p.h" +#include "qv4l2filedescriptor_p.h" + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +static Q_LOGGING_CATEGORY(qLcV4L2MemoryTransfer, "qt.multimedia.ffmpeg.v4l2camera.memorytransfer"); + +namespace { + +v4l2_buffer makeV4l2Buffer(quint32 memoryType, quint32 index = 0) +{ + v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = memoryType; + buf.index = index; + return buf; +} + +class UserPtrMemoryTransfer : public QV4L2MemoryTransfer +{ +public: + static QV4L2MemoryTransferUPtr create(QV4L2FileDescriptorPtr fileDescriptor, quint32 imageSize) + { + quint32 buffersCount = 2; + if (!fileDescriptor->requestBuffers(V4L2_MEMORY_USERPTR, buffersCount)) { + qCWarning(qLcV4L2MemoryTransfer) << "Cannot request V4L2_MEMORY_USERPTR buffers"; + return {}; + } + + std::unique_ptr result( + new UserPtrMemoryTransfer(std::move(fileDescriptor), buffersCount, imageSize)); + + return result->enqueueBuffers() ? std::move(result) : nullptr; + } + + std::optional dequeueBuffer() override + { + auto v4l2Buffer = makeV4l2Buffer(V4L2_MEMORY_USERPTR); + if (!fileDescriptor().call(VIDIOC_DQBUF, &v4l2Buffer)) + return {}; + + Q_ASSERT(v4l2Buffer.index < m_byteArrays.size()); + Q_ASSERT(!m_byteArrays[v4l2Buffer.index].isEmpty()); + + return Buffer{ v4l2Buffer, std::move(m_byteArrays[v4l2Buffer.index]) }; + } + + bool enqueueBuffer(quint32 index) override + { + Q_ASSERT(index < m_byteArrays.size()); + Q_ASSERT(m_byteArrays[index].isEmpty()); + + auto buf = makeV4l2Buffer(V4L2_MEMORY_USERPTR, index); + static_assert(sizeof(decltype(buf.m.userptr)) == sizeof(size_t), "Not compatible sizes"); + + m_byteArrays[index] = QByteArray(static_cast(m_imageSize), Qt::Uninitialized); + + buf.m.userptr = (decltype(buf.m.userptr))m_byteArrays[index].data(); + buf.length = m_byteArrays[index].size(); + + if (!fileDescriptor().call(VIDIOC_QBUF, &buf)) { + qWarning() << "Couldn't add V4L2 buffer" << errno << strerror(errno) << index; + return false; + } + + return true; + } + + quint32 buffersCount() const override { return static_cast(m_byteArrays.size()); } + +private: + UserPtrMemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor, quint32 buffersCount, + quint32 imageSize) + : QV4L2MemoryTransfer(std::move(fileDescriptor)), + m_imageSize(imageSize), + m_byteArrays(buffersCount) + { + } + +private: + quint32 m_imageSize; + std::vector m_byteArrays; +}; + +class MMapMemoryTransfer : public QV4L2MemoryTransfer +{ +public: + struct MemorySpan + { + void *data = nullptr; + size_t size = 0; + bool inQueue = false; + }; + + static QV4L2MemoryTransferUPtr create(QV4L2FileDescriptorPtr fileDescriptor) + { + quint32 buffersCount = 2; + if (!fileDescriptor->requestBuffers(V4L2_MEMORY_MMAP, buffersCount)) { + qCWarning(qLcV4L2MemoryTransfer) << "Cannot request V4L2_MEMORY_MMAP buffers"; + return {}; + } + + std::unique_ptr result( + new MMapMemoryTransfer(std::move(fileDescriptor))); + + return result->init(buffersCount) ? std::move(result) : nullptr; + } + + bool init(quint32 buffersCount) + { + for (quint32 index = 0; index < buffersCount; ++index) { + auto buf = makeV4l2Buffer(V4L2_MEMORY_MMAP, index); + + if (!fileDescriptor().call(VIDIOC_QUERYBUF, &buf)) { + qWarning() << "Can't map buffer" << index; + return false; + } + + auto mappedData = mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, + fileDescriptor().get(), buf.m.offset); + + if (mappedData == MAP_FAILED) { + qWarning() << "mmap failed" << index << buf.length << buf.m.offset; + return false; + } + + m_spans.push_back(MemorySpan{ mappedData, buf.length, false }); + } + + m_spans.shrink_to_fit(); + + return enqueueBuffers(); + } + + ~MMapMemoryTransfer() override + { + for (const auto &span : m_spans) + munmap(span.data, span.size); + } + + std::optional dequeueBuffer() override + { + auto v4l2Buffer = makeV4l2Buffer(V4L2_MEMORY_MMAP); + if (!fileDescriptor().call(VIDIOC_DQBUF, &v4l2Buffer)) + return {}; + + const auto index = v4l2Buffer.index; + + Q_ASSERT(index < m_spans.size()); + + auto &span = m_spans[index]; + + Q_ASSERT(span.inQueue); + span.inQueue = false; + + return Buffer{ v4l2Buffer, + QByteArray(reinterpret_cast(span.data), span.size) }; + } + + bool enqueueBuffer(quint32 index) override + { + Q_ASSERT(index < m_spans.size()); + Q_ASSERT(!m_spans[index].inQueue); + + auto buf = makeV4l2Buffer(V4L2_MEMORY_MMAP, index); + if (!fileDescriptor().call(VIDIOC_QBUF, &buf)) + return false; + + m_spans[index].inQueue = true; + return true; + } + + quint32 buffersCount() const override { return static_cast(m_spans.size()); } + +private: + using QV4L2MemoryTransfer::QV4L2MemoryTransfer; + +private: + std::vector m_spans; +}; +} // namespace + +QV4L2MemoryTransfer::QV4L2MemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor) + : m_fileDescriptor(std::move(fileDescriptor)) +{ + Q_ASSERT(m_fileDescriptor); + Q_ASSERT(!m_fileDescriptor->streamStarted()); +} + +QV4L2MemoryTransfer::~QV4L2MemoryTransfer() +{ + Q_ASSERT(!m_fileDescriptor->streamStarted()); // to avoid possible corruptions +} + +bool QV4L2MemoryTransfer::enqueueBuffers() +{ + for (quint32 i = 0; i < buffersCount(); ++i) + if (!enqueueBuffer(i)) + return false; + + return true; +} + +QV4L2MemoryTransferUPtr makeUserPtrMemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor, + quint32 imageSize) +{ + return UserPtrMemoryTransfer::create(std::move(fileDescriptor), imageSize); +} + +QV4L2MemoryTransferUPtr makeMMapMemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor) +{ + return MMapMemoryTransfer::create(std::move(fileDescriptor)); +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/v4l2/qv4l2memorytransfer_p.h b/src/plugins/multimedia/v4l2/qv4l2memorytransfer_p.h new file mode 100644 index 000000000..6b5e3913f --- /dev/null +++ b/src/plugins/multimedia/v4l2/qv4l2memorytransfer_p.h @@ -0,0 +1,66 @@ +// Copyright (C) 2023 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 + +#ifndef QV4L2MEMORYTRANSFER_P_H +#define QV4L2MEMORYTRANSFER_P_H + +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +class QV4L2FileDescriptor; +using QV4L2FileDescriptorPtr = std::shared_ptr; + +class QV4L2MemoryTransfer +{ +public: + struct Buffer + { + v4l2_buffer v4l2Buffer = {}; + QByteArray data; + }; + + QV4L2MemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor); + + virtual ~QV4L2MemoryTransfer(); + + virtual std::optional dequeueBuffer() = 0; + + virtual bool enqueueBuffer(quint32 index) = 0; + + virtual quint32 buffersCount() const = 0; + +protected: + bool enqueueBuffers(); + + const QV4L2FileDescriptor &fileDescriptor() const { return *m_fileDescriptor; } + +private: + QV4L2FileDescriptorPtr m_fileDescriptor; +}; + +using QV4L2MemoryTransferUPtr = std::unique_ptr; + +QV4L2MemoryTransferUPtr makeUserPtrMemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor, + quint32 imageSize); + +QV4L2MemoryTransferUPtr makeMMapMemoryTransfer(QV4L2FileDescriptorPtr fileDescriptor); + +QT_END_NAMESPACE + +#endif // QV4L2MEMORYTRANSFER_P_H