Revision control

Copy as Markdown

// 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 "qwindowscamera_p.h"
#include "qsemaphore.h"
#include "qmutex.h"
#include <private/qcameradevice_p.h>
#include <private/qmemoryvideobuffer_p.h>
#include <private/qwindowsmultimediautils_p.h>
#include <private/qvideoframe_p.h>
#include <private/qcomobject_p.h>
#include <private/qwmf_support_p.h>
#include <QtCore/private/qsystemerror_p.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mferror.h>
#include <mfreadwrite.h>
#include <system_error>
#include <utility>
QT_BEGIN_NAMESPACE
namespace QFFmpeg {
qsizetype indexOfClosestCameraFormat(QSpan<const QCameraFormat> candidates,
const QCameraFormat &requested)
{
qsizetype closest = -1;
std::pair<float, float> closestDistance{};
for (qsizetype i = 0; i != candidates.size(); ++i) {
const QCameraFormat &candidate = candidates[i];
if (candidate.pixelFormat() != requested.pixelFormat()
|| candidate.resolution() != requested.resolution()) {
continue;
}
const std::pair distance{ qAbs(candidate.maxFrameRate() - requested.maxFrameRate()),
qAbs(candidate.minFrameRate() - requested.minFrameRate()) };
if (closest < 0 || distance < closestDistance) {
closest = i;
closestDistance = distance;
}
}
return closest;
}
namespace {
class CameraReaderCallback : public QComObject<IMFSourceReaderCallback>
{
public:
//from IMFSourceReaderCallback
STDMETHODIMP OnReadSample(HRESULT status, DWORD, DWORD, LONGLONG timestamp, IMFSample *sample) override;
STDMETHODIMP OnFlush(DWORD) override;
STDMETHODIMP OnEvent(DWORD, IMFMediaEvent *) override { return S_OK; }
void setActiveCamera(ActiveCamera *activeCamera)
{
QMutexLocker locker(&m_mutex);
m_activeCamera = activeCamera;
}
private:
// Destructor is not public. Caller should call Release.
~CameraReaderCallback() override = default;
ActiveCamera *m_activeCamera = nullptr;
QMutex m_mutex;
};
ComPtr<IMFSourceReader> createCameraReader(IMFMediaSource *mediaSource,
const ComPtr<CameraReaderCallback> &callback)
{
ComPtr<IMFSourceReader> sourceReader;
ComPtr<IMFAttributes> readerAttributes;
HRESULT hr = MFCreateAttributes(readerAttributes.GetAddressOf(), 1);
if (SUCCEEDED(hr)) {
hr = readerAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK, callback.Get());
if (SUCCEEDED(hr)) {
hr = MFCreateSourceReaderFromMediaSource(mediaSource, readerAttributes.Get(), sourceReader.GetAddressOf());
if (SUCCEEDED(hr))
return sourceReader;
}
}
qWarning() << "Failed to create camera IMFSourceReader" << hr;
return sourceReader;
}
ComPtr<IMFMediaSource> createCameraSource(const QString &deviceId)
{
ComPtr<IMFMediaSource> mediaSource;
ComPtr<IMFAttributes> sourceAttributes;
HRESULT hr = MFCreateAttributes(sourceAttributes.GetAddressOf(), 2);
if (SUCCEEDED(hr)) {
hr = sourceAttributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
if (SUCCEEDED(hr)) {
hr = sourceAttributes->SetString(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
reinterpret_cast<LPCWSTR>(deviceId.utf16()));
if (SUCCEEDED(hr)) {
hr = MFCreateDeviceSource(sourceAttributes.Get(), mediaSource.GetAddressOf());
if (SUCCEEDED(hr))
return mediaSource;
}
}
}
qWarning() << "Failed to create camera IMFMediaSource" << hr;
return mediaSource;
}
int calculateVideoFrameStride(IMFMediaType *videoType, int width)
{
Q_ASSERT(videoType);
GUID subtype = GUID_NULL;
HRESULT hr = videoType->GetGUID(MF_MT_SUBTYPE, &subtype);
if (SUCCEEDED(hr)) {
LONG stride = 0;
hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &stride);
if (SUCCEEDED(hr))
return int(qAbs(stride));
}
qWarning() << "Failed to calculate video stride" << QSystemError::windowsComString(hr);
return 0;
}
bool setCameraReaderFormat(IMFSourceReader *sourceReader, IMFMediaType *videoType)
{
Q_ASSERT(sourceReader);
Q_ASSERT(videoType);
HRESULT hr = sourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr,
videoType);
if (FAILED(hr))
qWarning() << "Failed to set video format" << QSystemError::windowsComString(hr);
return SUCCEEDED(hr);
}
ComPtr<IMFMediaType> findVideoType(IMFSourceReader *reader, const QCameraFormat &format)
{
std::vector<ComPtr<IMFMediaType>> mediaTypes;
std::vector<QCameraFormat> candidates;
for (DWORD i = 0;; ++i) {
ComPtr<IMFMediaType> mediaType;
HRESULT hr = reader->GetNativeMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, i,
mediaType.GetAddressOf());
if (FAILED(hr))
break;
const std::optional<QCameraFormat> candidate =
QWMF::cameraFormatFromMediaType(mediaType.Get());
if (!candidate)
continue;
mediaTypes.push_back(std::move(mediaType));
candidates.push_back(std::move(*candidate));
}
const qsizetype index = indexOfClosestCameraFormat(candidates, format);
if (index < 0)
return {};
return mediaTypes.at(index);
}
} // namespace
class ActiveCamera
{
public:
static std::unique_ptr<ActiveCamera> create(QWindowsCamera &wc, const QCameraDevice &device, const QCameraFormat &format)
{
auto ac = std::unique_ptr<ActiveCamera>(new ActiveCamera(wc));
ac->m_source = createCameraSource(QString::fromUtf8(device.id()));
if (!ac->m_source)
return {};
ac->m_readerCallback = makeComObject<CameraReaderCallback>();
ac->m_readerCallback->setActiveCamera(ac.get());
ac->m_reader = createCameraReader(ac->m_source.Get(), ac->m_readerCallback);
if (!ac->m_reader)
return {};
if (!ac->setFormat(format))
return {};
return ac;
}
bool setFormat(const QCameraFormat &format)
{
flush();
auto videoType = findVideoType(m_reader.Get(), format);
if (videoType) {
if (setCameraReaderFormat(m_reader.Get(), videoType.Get())) {
m_frameFormat = { format.resolution(), format.pixelFormat() };
m_frameFormat.setColorRange(QCameraFormatPrivate::getColorRange(format));
m_frameFormat.setColorSpace(QCameraFormatPrivate::getColorSpace(format));
m_videoFrameStride =
calculateVideoFrameStride(videoType.Get(), format.resolution().width());
}
}
m_reader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, nullptr, nullptr, nullptr,
nullptr);
return true;
}
void onReadSample(HRESULT status, LONGLONG timestamp, IMFSample *sample)
{
if (FAILED(status)) {
const std::string msg{ std::system_category().message(status) };
m_windowsCamera.updateError(QCamera::CameraError, QString::fromStdString(msg));
return;
}
if (sample) {
ComPtr<IMFMediaBuffer> mediaBuffer;
if (SUCCEEDED(sample->ConvertToContiguousBuffer(mediaBuffer.GetAddressOf()))) {
auto result = QWMF::withLockedBuffer(mediaBuffer,
[&](QSpan<BYTE> data, QSpan<BYTE> /*max*/) {
QByteArray bytes(data);
auto buffer = std::make_unique<QMemoryVideoBuffer>(std::move(bytes),
m_videoFrameStride);
QVideoFrame frame =
QVideoFramePrivate::createFrame(std::move(buffer), m_frameFormat);
// WMF uses 100-nanosecond units, Qt uses microseconds
frame.setStartTime(timestamp / 10);
LONGLONG duration = -1;
if (SUCCEEDED(sample->GetSampleDuration(&duration)))
frame.setEndTime((timestamp + duration) / 10);
return frame;
});
if (result)
Q_EMIT m_windowsCamera.newVideoFrame(result.value());
}
}
m_reader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0, nullptr,
nullptr, nullptr, nullptr);
}
void onFlush()
{
m_flushWait.release();
}
~ActiveCamera()
{
flush();
if (m_readerCallback)
m_readerCallback->setActiveCamera(nullptr);
}
private:
explicit ActiveCamera(QWindowsCamera &wc) : m_windowsCamera(wc), m_flushWait(0) { }
void flush()
{
if (m_reader && SUCCEEDED(m_reader->Flush(MF_SOURCE_READER_FIRST_VIDEO_STREAM))) {
m_flushWait.acquire();
}
}
QWindowsCamera &m_windowsCamera;
QSemaphore m_flushWait;
ComPtr<IMFMediaSource> m_source;
ComPtr<IMFSourceReader> m_reader;
ComPtr<CameraReaderCallback> m_readerCallback;
QVideoFrameFormat m_frameFormat;
int m_videoFrameStride = 0;
};
STDMETHODIMP CameraReaderCallback::OnReadSample(HRESULT status, DWORD, DWORD, LONGLONG timestamp, IMFSample *sample)
{
QMutexLocker locker(&m_mutex);
if (m_activeCamera)
m_activeCamera->onReadSample(status, timestamp, sample);
return S_OK;
}
STDMETHODIMP CameraReaderCallback::OnFlush(DWORD)
{
QMutexLocker locker(&m_mutex);
if (m_activeCamera)
m_activeCamera->onFlush();
return S_OK;
}
QWindowsCamera::QWindowsCamera(QCamera *camera)
: QPlatformCamera(camera)
{
m_cameraDevice = camera ? camera->cameraDevice() : QCameraDevice{};
}
QWindowsCamera::~QWindowsCamera()
{
QWindowsCamera::setActive(false);
}
void QWindowsCamera::setActive(bool active)
{
if (bool(m_active) == active)
return;
if (active) {
if (m_cameraDevice.isNull())
return;
if (m_cameraFormat.isNull())
m_cameraFormat = findBestCameraFormat(m_cameraDevice);
m_active = ActiveCamera::create(*this, m_cameraDevice, m_cameraFormat);
if (m_active)
Q_EMIT activeChanged(true);
} else {
m_active.reset();
Q_EMIT activeChanged(false);
}
}
void QWindowsCamera::setCamera(const QCameraDevice &camera)
{
bool active = bool(m_active);
if (active)
setActive(false);
m_cameraDevice = camera;
m_cameraFormat = {};
if (active)
setActive(true);
}
bool QWindowsCamera::setCameraFormat(const QCameraFormat &format)
{
if (format.isNull())
return false;
bool ok = m_active ? m_active->setFormat(format) : true;
if (ok)
m_cameraFormat = format;
return ok;
}
} // namespace QFFmpeg
QT_END_NAMESPACE