Revision control

Copy as Markdown

// Copyright (C) 2026 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 "qohoscamerasession_p.h"
#include "common/qohosvideooutput_p.h"
#include "qohosglobal_p.h"
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qstandardpaths.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qmutex.h>
#include <QtCore/qset.h>
#include <QtCore/qthread.h>
#include <QtCore/qthreadpool.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/qimage.h>
#include <QtMultimedia/qvideosink.h>
#include <fcntl.h>
#include <unistd.h>
#include <private/qcameradevice_p.h>
#include <private/qmediastoragelocation_p.h>
#include <private/qmemoryvideobuffer_p.h>
#include <private/qvideoframe_p.h>
#include <multimedia/image_framework/image/image_native.h>
#include <multimedia/image_framework/image/image_packer_native.h>
#include <multimedia/image_framework/image/image_source_native.h>
#include <native_buffer/native_buffer.h>
#include <native_window/external_window.h>
QT_BEGIN_NAMESPACE
namespace {
constexpr int32_t kImageReceiverCapacity = 4;
constexpr const char *kJpegMimeType = "image/jpeg";
// OHOS phones only allow one Camera_Input open at a time. Other platforms
// auto-preempt the previously-open camera when a new one is opened, but on
// OHOS OH_CameraInput_Open fails with CAMERA_OPERATION_NOT_ALLOWED if any
// other input is still alive in this process. Track live sessions so a
// starting session can stop the others first.
//
// The mutex is recursive because stopSession() -> releaseSession() removes
// the session from liveSessions() under the same lock, and preemption iterates
// while holding it (so a pointer can't be destroyed mid-iteration).
QRecursiveMutex &liveSessionMutex()
{
static QRecursiveMutex m;
return m;
}
QSet<QOhosCameraSession *> &liveSessions()
{
static QSet<QOhosCameraSession *> s;
return s;
}
QVideoFrameFormat::PixelFormat pixelFormatFor(Camera_Format format)
{
switch (format) {
case CAMERA_FORMAT_RGBA_8888:
return QVideoFrameFormat::Format_RGBA8888;
case CAMERA_FORMAT_YUV_420_SP:
return QVideoFrameFormat::Format_NV12;
case CAMERA_FORMAT_JPEG:
return QVideoFrameFormat::Format_Jpeg;
default:
break;
}
return QVideoFrameFormat::Format_Invalid;
}
int qualityToInt(QImageCapture::Quality q)
{
switch (q) {
case QImageCapture::VeryLowQuality:
return 25;
case QImageCapture::LowQuality:
return 50;
case QImageCapture::HighQuality:
return 90;
case QImageCapture::VeryHighQuality:
return 100;
case QImageCapture::NormalQuality:
default:
return 75;
}
}
QByteArray encodeNativeImageToJpeg(OH_ImageNative *image, int quality)
{
uint32_t *components = nullptr;
size_t componentCount = 0;
if (OH_ImageNative_GetComponentTypes(image, nullptr, &componentCount) != IMAGE_SUCCESS
|| componentCount == 0) {
return {};
}
std::vector<uint32_t> types(componentCount);
components = types.data();
if (OH_ImageNative_GetComponentTypes(image, &components, &componentCount) != IMAGE_SUCCESS)
return {};
OH_NativeBuffer *nativeBuffer = nullptr;
if (OH_ImageNative_GetByteBuffer(image, types[0], &nativeBuffer) != IMAGE_SUCCESS
|| !nativeBuffer) {
return {};
}
OH_NativeBuffer_Config bufferConfig{};
OH_NativeBuffer_GetConfig(nativeBuffer, &bufferConfig);
void *mapped = nullptr;
if (OH_NativeBuffer_Map(nativeBuffer, &mapped) != 0 || !mapped)
return {};
size_t bufferSize = 0;
OH_ImageNative_GetBufferSize(image, types[0], &bufferSize);
QByteArray result;
OH_ImageSourceNative *source = nullptr;
if (OH_ImageSourceNative_CreateFromData(static_cast<uint8_t *>(mapped), bufferSize, &source)
== IMAGE_SUCCESS
&& source) {
OH_ImagePackerNative *packer = nullptr;
if (OH_ImagePackerNative_Create(&packer) == IMAGE_SUCCESS && packer) {
OH_PackingOptions *options = nullptr;
if (OH_PackingOptions_Create(&options) == IMAGE_SUCCESS && options) {
Image_MimeType mime{ const_cast<char *>(kJpegMimeType),
std::strlen(kJpegMimeType) };
OH_PackingOptions_SetMimeType(options, &mime);
OH_PackingOptions_SetQuality(options, uint32_t(quality));
size_t outSize = bufferSize * 2 + 1024;
result.resize(int(outSize));
if (OH_ImagePackerNative_PackToDataFromImageSource(
packer, options, source,
reinterpret_cast<uint8_t *>(result.data()), &outSize)
== IMAGE_SUCCESS) {
result.resize(int(outSize));
} else {
result.clear();
}
OH_PackingOptions_Release(options);
}
OH_ImagePackerNative_Release(packer);
}
OH_ImageSourceNative_Release(source);
}
OH_NativeBuffer_Unmap(nativeBuffer);
return result;
}
void imageArriveCallbackTrampoline(OH_ImageReceiverNative * /*receiver*/, void *userData)
{
auto *session = static_cast<QOhosCameraSession *>(userData);
if (!session)
return;
QMetaObject::invokeMethod(session, &QOhosCameraSession::onCapturedImageAvailable,
Qt::QueuedConnection);
}
} // namespace
QOhosCameraSession::QOhosCameraSession(QObject *parent) : QObject(parent)
{
if (qApp) {
connect(qApp, &QGuiApplication::applicationStateChanged, this,
&QOhosCameraSession::onApplicationStateChanged);
}
}
void QOhosCameraSession::onApplicationStateChanged()
{
// HarmonyOS revokes camera access in the background and the session is left
// dead with no notification, so tear it down and build a new one on return.
if (QGuiApplication::applicationState() == Qt::ApplicationActive) {
if (m_restoreOnForeground) {
m_restoreOnForeground = false;
setActive(true);
}
} else if (m_active) {
setActive(false);
m_restoreOnForeground = true;
}
}
QOhosCameraSession::~QOhosCameraSession()
{
{
QMutexLocker lock{ &liveSessionMutex() };
liveSessions().remove(this);
}
releaseSession();
if (m_supportedDevices) {
OH_CameraManager_DeleteSupportedCameras(m_manager, m_supportedDevices,
m_supportedDeviceCount);
m_supportedDevices = nullptr;
}
if (m_manager) {
OH_Camera_DeleteCameraManager(m_manager);
m_manager = nullptr;
}
}
void QOhosCameraSession::setCamera(const QCameraDevice &camera)
{
if (m_cameraDevice == camera)
return;
const bool wasActive = m_active;
if (wasActive)
setActive(false);
m_cameraDevice = camera;
if (wasActive)
setActive(true);
}
void QOhosCameraSession::setCameraFormat(const QCameraFormat &format)
{
m_cameraFormat = format;
}
void QOhosCameraSession::setVideoSink(QVideoSink *sink)
{
if (m_videoSink == sink)
return;
m_videoSink = sink;
if (m_videoOutput)
m_videoOutput.reset();
m_videoOutput = std::make_unique<QOhosVideoOutput>(
sink, QOhosVideoOutput::ContentSource::Camera, this);
connect(m_videoOutput.get(), &QOhosVideoOutput::surfaceReady, this,
&QOhosCameraSession::onSurfaceReady);
}
void QOhosCameraSession::setActive(bool active)
{
if (m_active == active)
return;
// Starting is refused while the application is in the background, so keep
// the request and act on it once the application is active again.
if (active && qApp && QGuiApplication::applicationState() != Qt::ApplicationActive) {
m_restoreOnForeground = true;
return;
}
m_restoreOnForeground = false;
if (active) {
if (!startSession()) {
m_pendingStart = true;
return;
}
} else {
m_pendingStart = false;
stopSession();
}
m_active = active;
Q_EMIT activeChanged(active);
emitReadyForCaptureChanged();
}
void QOhosCameraSession::setImageSettings(const QImageEncoderSettings &settings)
{
m_imageSettings = settings;
}
int QOhosCameraSession::capture(const QString &fileName, bool toBuffer)
{
const int id = ++m_lastCaptureId;
if (!m_active || !m_photoOutput) {
Q_EMIT imageCaptureError(id, QImageCapture::NotReadyError,
tr("Camera not ready for capture"));
return id;
}
if (m_captureInProgress) {
Q_EMIT imageCaptureError(id, QImageCapture::NotReadyError,
tr("Capture already in progress"));
return id;
}
m_pendingCaptureId = id;
m_pendingCaptureFileName = fileName;
m_pendingCaptureToBuffer = toBuffer;
m_captureInProgress = true;
emitReadyForCaptureChanged();
if (OH_PhotoOutput_Capture(m_photoOutput) != CAMERA_OK) {
m_captureInProgress = false;
Q_EMIT imageCaptureError(id, QImageCapture::ResourceError,
tr("OH_PhotoOutput_Capture failed"));
emitReadyForCaptureChanged();
}
return id;
}
void QOhosCameraSession::onSurfaceReady()
{
if (m_pendingStart && !m_active) {
m_pendingStart = false;
if (startSession()) {
m_active = true;
Q_EMIT activeChanged(true);
emitReadyForCaptureChanged();
}
return;
}
// Rebuild when the surface the session was started with has been replaced.
if (!m_active || !m_videoOutput)
return;
const QByteArray surfaceId = m_videoOutput->surfaceId();
if (surfaceId.isEmpty() || surfaceId == m_previewSurfaceId)
return;
m_active = false;
stopSession();
if (startSession()) {
m_active = true;
Q_EMIT activeChanged(true);
emitReadyForCaptureChanged();
}
}
void QOhosCameraSession::onCapturedImageAvailable()
{
if (!m_imageReceiver)
return;
OH_ImageNative *image = nullptr;
if (OH_ImageReceiverNative_ReadLatestImage(m_imageReceiver, &image) != IMAGE_SUCCESS
|| !image) {
m_captureInProgress = false;
Q_EMIT imageCaptureError(m_pendingCaptureId, QImageCapture::ResourceError,
tr("Failed to read captured image"));
emitReadyForCaptureChanged();
return;
}
const int quality = qualityToInt(m_imageSettings.quality());
QByteArray jpegBytes = encodeNativeImageToJpeg(image, quality);
OH_ImageNative_Release(image);
const int id = m_pendingCaptureId;
const QString fileName = m_pendingCaptureFileName;
const bool toBuffer = m_pendingCaptureToBuffer;
m_pendingCaptureFileName.clear();
m_pendingCaptureToBuffer = false;
m_pendingCaptureId = 0;
m_captureInProgress = false;
if (jpegBytes.isEmpty()) {
Q_EMIT imageCaptureError(id, QImageCapture::FormatError,
tr("Failed to encode captured image"));
emitReadyForCaptureChanged();
return;
}
QImage preview = QImage::fromData(jpegBytes, "JPEG");
Q_EMIT imageExposed(id);
Q_EMIT imageCaptured(id, preview);
if (toBuffer) {
QVideoFrame buffer = QVideoFramePrivate::createFrame(
std::make_unique<QMemoryVideoBuffer>(QByteArray(jpegBytes),
preview.bytesPerLine()),
QVideoFrameFormat(preview.size(), QVideoFrameFormat::Format_Jpeg));
Q_EMIT imageAvailable(id, buffer);
emitReadyForCaptureChanged();
return;
}
// captureToFile: persist to disk and emit imageSaved. Empty filename means
// "let the platform pick" — Qt apps default to PicturesLocation/IMG_<n>.jpg.
// Mirrors the Android backend.
const QImageCapture::FileFormat targetFormat = m_imageSettings.format();
const QString defaultExt = [&]() -> QString {
switch (targetFormat) {
case QImageCapture::PNG: return QStringLiteral("png");
case QImageCapture::WebP: return QStringLiteral("webp");
case QImageCapture::Tiff: return QStringLiteral("tiff");
case QImageCapture::JPEG:
case QImageCapture::UnspecifiedFormat:
default: return QStringLiteral("jpg");
}
}();
const char *qImageFormat = [&]() -> const char * {
switch (targetFormat) {
case QImageCapture::PNG: return "PNG";
case QImageCapture::WebP: return "WEBP";
case QImageCapture::Tiff: return "TIFF";
default: return nullptr; // JPEG: stream raw bytes
}
}();
const QString resolved = QMediaStorageLocation::generateFileName(
fileName, QStandardPaths::PicturesLocation, defaultExt);
bool saved = false;
if (qImageFormat) {
saved = preview.save(resolved, qImageFormat, quality);
} else {
QFile out(resolved);
if (out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
out.write(jpegBytes);
out.close();
saved = true;
}
}
if (saved) {
Q_EMIT imageSaved(id, resolved);
} else {
Q_EMIT imageCaptureError(id, QImageCapture::ResourceError,
tr("Could not save captured image to: %1").arg(resolved));
}
emitReadyForCaptureChanged();
}
void QOhosCameraSession::emitReadyForCaptureChanged()
{
const bool ready = isReadyForCapture();
if (m_lastReadyForCapture && *m_lastReadyForCapture == ready)
return;
m_lastReadyForCapture = ready;
Q_EMIT readyForCaptureChanged(ready);
}
bool QOhosCameraSession::ensureManager()
{
if (m_manager)
return true;
if (OH_Camera_GetCameraManager(&m_manager) != CAMERA_OK || !m_manager) {
qCWarning(qLcOhosMediaPlugin) << "OH_Camera_GetCameraManager failed";
return false;
}
if (OH_CameraManager_GetSupportedCameras(m_manager, &m_supportedDevices,
&m_supportedDeviceCount) != CAMERA_OK) {
qCWarning(qLcOhosMediaPlugin) << "GetSupportedCameras failed";
m_supportedDevices = nullptr;
m_supportedDeviceCount = 0;
}
return true;
}
Camera_Device *QOhosCameraSession::findDevice(const QByteArray &id)
{
if (!m_supportedDevices)
return nullptr;
for (uint32_t i = 0; i < m_supportedDeviceCount; ++i) {
if (m_supportedDevices[i].cameraId && id == m_supportedDevices[i].cameraId)
return &m_supportedDevices[i];
}
return nullptr;
}
bool QOhosCameraSession::createPhotoPath(Camera_OutputCapability *caps,
Camera_Profile *previewProfile)
{
if (!caps || caps->photoProfilesSize == 0)
return false;
Camera_Profile *photoProfile = caps->photoProfiles[0];
if (m_imageSettings.resolution().isValid()) {
const QSize wanted = m_imageSettings.resolution();
for (uint32_t i = 0; i < caps->photoProfilesSize; ++i) {
Camera_Profile *p = caps->photoProfiles[i];
if (!p)
continue;
if (int(p->size.width) == wanted.width()
&& int(p->size.height) == wanted.height()) {
photoProfile = p;
break;
}
}
} else if (previewProfile) {
for (uint32_t i = 0; i < caps->photoProfilesSize; ++i) {
Camera_Profile *p = caps->photoProfiles[i];
if (!p)
continue;
if (p->size.width == previewProfile->size.width
&& p->size.height == previewProfile->size.height) {
photoProfile = p;
break;
}
}
}
if (!photoProfile)
return false;
if (OH_ImageReceiverOptions_Create(&m_imageReceiverOptions) != IMAGE_SUCCESS
|| !m_imageReceiverOptions) {
return false;
}
Image_Size size{ uint32_t(photoProfile->size.width), uint32_t(photoProfile->size.height) };
OH_ImageReceiverOptions_SetSize(m_imageReceiverOptions, size);
OH_ImageReceiverOptions_SetCapacity(m_imageReceiverOptions, kImageReceiverCapacity);
if (OH_ImageReceiverNative_Create(m_imageReceiverOptions, &m_imageReceiver) != IMAGE_SUCCESS
|| !m_imageReceiver) {
return false;
}
OH_ImageReceiverNative_OnImageArrive(m_imageReceiver, imageArriveCallbackTrampoline, this);
uint64_t surfaceIdNum = 0;
if (OH_ImageReceiverNative_GetReceivingSurfaceId(m_imageReceiver, &surfaceIdNum) != IMAGE_SUCCESS
|| surfaceIdNum == 0) {
return false;
}
const QByteArray surfaceId = QByteArray::number(qulonglong(surfaceIdNum));
if (OH_CameraManager_CreatePhotoOutput(m_manager, photoProfile, surfaceId.constData(),
&m_photoOutput) != CAMERA_OK
|| !m_photoOutput) {
return false;
}
return true;
}
void QOhosCameraSession::destroyPhotoPath()
{
if (m_photoOutput) {
OH_PhotoOutput_Release(m_photoOutput);
m_photoOutput = nullptr;
}
if (m_imageReceiver) {
OH_ImageReceiverNative_OffImageArrive(m_imageReceiver, imageArriveCallbackTrampoline);
OH_ImageReceiverNative_Release(m_imageReceiver);
m_imageReceiver = nullptr;
}
if (m_imageReceiverOptions) {
OH_ImageReceiverOptions_Release(m_imageReceiverOptions);
m_imageReceiverOptions = nullptr;
}
}
bool QOhosCameraSession::startSession()
{
// Phones only let one Camera_Input be open at a time. Preempt any other
// live session in this process before attempting to open ours; otherwise
// OH_CameraInput_Open returns CAMERA_OPERATION_NOT_ALLOWED.
{
QMutexLocker lock{ &liveSessionMutex() };
const auto sessions = liveSessions();
for (auto *other : sessions) {
if (other != this)
other->stopSession();
}
}
// OHOS capture sessions require at least one preview output to commit, so
// we always produce a surface — backed by a real QVideoSink RHI when one is
// attached, or an internal offscreen GLES2 RHI otherwise. surfaceReady will
// re-attach later if a sink with a live RHI shows up.
if (!m_videoOutput) {
m_videoOutput = std::make_unique<QOhosVideoOutput>(
nullptr, QOhosVideoOutput::ContentSource::Camera, this);
connect(m_videoOutput.get(), &QOhosVideoOutput::surfaceReady, this,
&QOhosCameraSession::onSurfaceReady);
}
m_previewSurfaceId = m_videoOutput->surfaceId();
if (!ensureManager())
return false;
Camera_Device *device = findDevice(m_cameraDevice.id());
if (!device && m_supportedDeviceCount > 0)
device = &m_supportedDevices[0];
if (!device) {
qCWarning(qLcOhosMediaPlugin) << "No camera device available";
return false;
}
Camera_Profile *previewProfile = nullptr;
Camera_OutputCapability *caps = nullptr;
if (OH_CameraManager_GetSupportedCameraOutputCapability(m_manager, device, &caps) == CAMERA_OK
&& caps && caps->previewProfilesSize > 0) {
previewProfile = caps->previewProfiles[0];
const bool haveRequest = !m_cameraFormat.isNull();
const QSize requestedSize = haveRequest ? m_cameraFormat.resolution() : QSize{};
const QVideoFrameFormat::PixelFormat requestedPixel =
haveRequest ? m_cameraFormat.pixelFormat() : QVideoFrameFormat::Format_Invalid;
for (uint32_t i = 0; i < caps->previewProfilesSize; ++i) {
Camera_Profile *p = caps->previewProfiles[i];
if (!p)
continue;
const bool sizeMatches = !haveRequest
|| (int(p->size.width) == requestedSize.width()
&& int(p->size.height) == requestedSize.height());
const bool formatMatches = !haveRequest
|| requestedPixel == QVideoFrameFormat::Format_Invalid
|| requestedPixel == pixelFormatFor(p->format);
if (sizeMatches && formatMatches) {
previewProfile = p;
if (haveRequest)
break;
if (p->format == CAMERA_FORMAT_YUV_420_SP && p->size.width == 1280
&& p->size.height == 720)
break;
}
}
}
if (!previewProfile) {
qCWarning(qLcOhosMediaPlugin) << "No preview profile available";
if (caps)
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
return false;
}
if (OH_CameraManager_CreateCameraInput(m_manager, device, &m_cameraInput) != CAMERA_OK
|| !m_cameraInput) {
qCWarning(qLcOhosMediaPlugin) << "CreateCameraInput failed";
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
return false;
}
// OH_CameraInput_Open can briefly fail with CAMERA_CONFLICT_CAMERA right
// after a sibling Camera_Input was released — the camera service finishes
// tearing down its session asynchronously. Poll for up to ~1 s.
{
Camera_ErrorCode err = CAMERA_OK;
constexpr int kMaxAttempts = 20;
constexpr int kBackoffMs = 50;
for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
err = OH_CameraInput_Open(m_cameraInput);
if (err == CAMERA_OK)
break;
// CAMERA_OPERATION_NOT_ALLOWED is returned when another
// Camera_Input is still alive in this process. Force-stop any
// other live QOhosCameraSession and retry.
if (err == CAMERA_OPERATION_NOT_ALLOWED) {
QSet<QOhosCameraSession *> snapshot;
{
QMutexLocker lock{ &liveSessionMutex() };
snapshot = liveSessions();
}
for (auto *other : snapshot) {
if (other != this)
other->stopSession();
}
} else if (err != CAMERA_CONFLICT_CAMERA && err != CAMERA_DEVICE_PREEMPTED) {
break;
}
QThread::msleep(kBackoffMs);
}
if (err != CAMERA_OK) {
qCWarning(qLcOhosMediaPlugin) << "CameraInput_Open failed:" << err;
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
releaseSession();
return false;
}
QMutexLocker lock{ &liveSessionMutex() };
liveSessions().insert(this);
}
if (m_videoOutput && !m_previewSurfaceId.isEmpty()) {
m_videoOutput->setVideoSize(
QSize{ int(previewProfile->size.width), int(previewProfile->size.height) });
if (OH_CameraManager_CreatePreviewOutput(m_manager, previewProfile,
m_previewSurfaceId.constData(), &m_previewOutput)
!= CAMERA_OK
|| !m_previewOutput) {
qCWarning(qLcOhosMediaPlugin) << "CreatePreviewOutput failed";
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
releaseSession();
return false;
}
}
const bool hasPhoto = createPhotoPath(caps, previewProfile);
if (!hasPhoto)
qCWarning(qLcOhosMediaPlugin) << "Photo output unavailable; image capture disabled";
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
if (!m_previewOutput && !m_photoOutput) {
qCWarning(qLcOhosMediaPlugin) << "No capture outputs available";
releaseSession();
return false;
}
if (OH_CameraManager_CreateCaptureSession(m_manager, &m_captureSession) != CAMERA_OK
|| !m_captureSession) {
qCWarning(qLcOhosMediaPlugin) << "CreateCaptureSession failed";
releaseSession();
return false;
}
OH_CaptureSession_BeginConfig(m_captureSession);
OH_CaptureSession_AddInput(m_captureSession, m_cameraInput);
if (m_previewOutput)
OH_CaptureSession_AddPreviewOutput(m_captureSession, m_previewOutput);
if (m_photoOutput)
OH_CaptureSession_AddPhotoOutput(m_captureSession, m_photoOutput);
if (OH_CaptureSession_CommitConfig(m_captureSession) != CAMERA_OK) {
qCWarning(qLcOhosMediaPlugin) << "CommitConfig failed";
releaseSession();
return false;
}
if (OH_CaptureSession_Start(m_captureSession) != CAMERA_OK) {
qCWarning(qLcOhosMediaPlugin) << "CaptureSession_Start failed";
releaseSession();
return false;
}
return true;
}
void QOhosCameraSession::stopSession()
{
if (m_captureSession) {
OH_CaptureSession_Stop(m_captureSession);
}
releaseSession();
}
void QOhosCameraSession::releaseSession()
{
destroyRecorder();
detachVideoOutput();
destroyPhotoPath();
if (m_captureSession) {
OH_CaptureSession_Release(m_captureSession);
m_captureSession = nullptr;
}
if (m_previewOutput) {
OH_PreviewOutput_Release(m_previewOutput);
m_previewOutput = nullptr;
}
m_previewSurfaceId.clear();
if (m_cameraInput) {
OH_CameraInput_Close(m_cameraInput);
OH_CameraInput_Release(m_cameraInput);
m_cameraInput = nullptr;
}
QMutexLocker lock{ &liveSessionMutex() };
liveSessions().remove(this);
}
namespace {
OH_AVRecorder_CodecMimeType videoCodecToOhos(QMediaFormat::VideoCodec codec)
{
switch (codec) {
case QMediaFormat::VideoCodec::H265:
return AVRECORDER_VIDEO_HEVC;
case QMediaFormat::VideoCodec::MPEG4:
return AVRECORDER_VIDEO_MPEG4;
case QMediaFormat::VideoCodec::H264:
case QMediaFormat::VideoCodec::Unspecified:
default:
return AVRECORDER_VIDEO_AVC;
}
}
OH_AVRecorder_CodecMimeType audioCodecToOhos(QMediaFormat::AudioCodec codec)
{
switch (codec) {
case QMediaFormat::AudioCodec::MP3:
return AVRECORDER_AUDIO_MP3;
case QMediaFormat::AudioCodec::AAC:
case QMediaFormat::AudioCodec::Unspecified:
default:
return AVRECORDER_AUDIO_AAC;
}
}
OH_AVRecorder_ContainerFormatType containerToOhos(QMediaFormat::FileFormat fmt)
{
switch (fmt) {
case QMediaFormat::AAC:
return AVRECORDER_CFT_AAC;
case QMediaFormat::MP3:
return AVRECORDER_CFT_MP3;
case QMediaFormat::Wave:
return AVRECORDER_CFT_WAV;
case QMediaFormat::Mpeg4Audio:
return AVRECORDER_CFT_MPEG_4A;
case QMediaFormat::MPEG4:
case QMediaFormat::UnspecifiedFormat:
default:
return AVRECORDER_CFT_MPEG_4;
}
}
int qualityToVideoBitrate(QMediaRecorder::Quality q, const QSize &resolution)
{
const int pixels = qMax(1, resolution.width() * resolution.height());
const double bpp = [&]() {
switch (q) {
case QMediaRecorder::VeryLowQuality: return 0.05;
case QMediaRecorder::LowQuality: return 0.1;
case QMediaRecorder::HighQuality: return 0.25;
case QMediaRecorder::VeryHighQuality:return 0.4;
case QMediaRecorder::NormalQuality:
default: return 0.15;
}
}();
return int(pixels * 30 * bpp);
}
int qualityToAudioBitrate(QMediaRecorder::Quality q)
{
switch (q) {
case QMediaRecorder::VeryLowQuality: return 32000;
case QMediaRecorder::LowQuality: return 64000;
case QMediaRecorder::HighQuality: return 192000;
case QMediaRecorder::VeryHighQuality:return 256000;
case QMediaRecorder::NormalQuality:
default: return 128000;
}
}
} // namespace
bool QOhosCameraSession::findVideoProfile(const QMediaEncoderSettings &settings,
Camera_VideoProfile *out)
{
if (!m_manager || !out)
return false;
Camera_Device *device = findDevice(m_cameraDevice.id());
if (!device && m_supportedDeviceCount > 0)
device = &m_supportedDevices[0];
if (!device)
return false;
Camera_OutputCapability *caps = nullptr;
if (OH_CameraManager_GetSupportedCameraOutputCapability(m_manager, device, &caps) != CAMERA_OK
|| !caps || caps->videoProfilesSize == 0) {
if (caps)
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
return false;
}
Camera_VideoProfile *chosen = nullptr;
const QSize wanted = settings.videoResolution();
if (wanted.isValid()) {
for (uint32_t i = 0; i < caps->videoProfilesSize; ++i) {
Camera_VideoProfile *p = caps->videoProfiles[i];
if (!p)
continue;
if (int(p->size.width) == wanted.width()
&& int(p->size.height) == wanted.height()) {
chosen = p;
break;
}
}
}
if (!chosen) {
for (uint32_t i = 0; i < caps->videoProfilesSize; ++i) {
Camera_VideoProfile *p = caps->videoProfiles[i];
if (!p)
continue;
if (p->size.width == 1280 && p->size.height == 720
&& p->format == CAMERA_FORMAT_YUV_420_SP) {
chosen = p;
break;
}
}
}
if (!chosen)
chosen = caps->videoProfiles[0];
*out = *chosen;
OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
return true;
}
bool QOhosCameraSession::attachVideoOutput(const Camera_VideoProfile &profile,
const QByteArray &surfaceId)
{
if (!m_captureSession)
return false;
if (OH_CameraManager_CreateVideoOutput(m_manager, &profile, surfaceId.constData(),
&m_videoOutputCamera) != CAMERA_OK
|| !m_videoOutputCamera) {
return false;
}
OH_CaptureSession_Stop(m_captureSession);
OH_CaptureSession_BeginConfig(m_captureSession);
OH_CaptureSession_AddVideoOutput(m_captureSession, m_videoOutputCamera);
if (OH_CaptureSession_CommitConfig(m_captureSession) != CAMERA_OK) {
OH_VideoOutput_Release(m_videoOutputCamera);
m_videoOutputCamera = nullptr;
OH_CaptureSession_Start(m_captureSession);
return false;
}
if (OH_CaptureSession_Start(m_captureSession) != CAMERA_OK)
return false;
if (OH_VideoOutput_Start(m_videoOutputCamera) != CAMERA_OK)
return false;
return true;
}
void QOhosCameraSession::detachVideoOutput()
{
if (!m_videoOutputCamera)
return;
OH_VideoOutput_Stop(m_videoOutputCamera);
if (m_captureSession) {
OH_CaptureSession_Stop(m_captureSession);
OH_CaptureSession_BeginConfig(m_captureSession);
OH_CaptureSession_RemoveVideoOutput(m_captureSession, m_videoOutputCamera);
OH_CaptureSession_CommitConfig(m_captureSession);
OH_CaptureSession_Start(m_captureSession);
}
OH_VideoOutput_Release(m_videoOutputCamera);
m_videoOutputCamera = nullptr;
}
void QOhosCameraSession::recorderStateCallback(OH_AVRecorder * /*recorder*/,
OH_AVRecorder_State state,
OH_AVRecorder_StateChangeReason /*reason*/,
void *userData)
{
auto *self = static_cast<QOhosCameraSession *>(userData);
if (!self)
return;
QMetaObject::invokeMethod(self, "onRecorderStateNotification", Qt::QueuedConnection,
Q_ARG(int, int(state)));
}
void QOhosCameraSession::recorderErrorCallback(OH_AVRecorder * /*recorder*/, int32_t errorCode,
const char *errorMsg, void *userData)
{
auto *self = static_cast<QOhosCameraSession *>(userData);
if (!self)
return;
QMetaObject::invokeMethod(self, "onRecorderErrorNotification", Qt::QueuedConnection,
Q_ARG(int, errorCode),
Q_ARG(QString, QString::fromUtf8(errorMsg ? errorMsg : "")));
}
void QOhosCameraSession::onRecorderStateNotification(int state)
{
QMediaRecorder::RecorderState mapped = m_recorderState;
switch (state) {
case AVRECORDER_STARTED:
mapped = QMediaRecorder::RecordingState;
break;
case AVRECORDER_PAUSED:
mapped = QMediaRecorder::PausedState;
break;
case AVRECORDER_STOPPED:
case AVRECORDER_IDLE:
case AVRECORDER_RELEASED:
mapped = QMediaRecorder::StoppedState;
break;
case AVRECORDER_ERROR:
mapped = QMediaRecorder::StoppedState;
break;
default:
return;
}
if (mapped == m_recorderState)
return;
m_recorderState = mapped;
Q_EMIT recorderStateChanged(int(mapped));
}
void QOhosCameraSession::onRecorderErrorNotification(int code, const QString &message)
{
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
message.isEmpty()
? tr("Recorder error %1").arg(code)
: message);
}
qint64 QOhosCameraSession::recorderDuration() const
{
if (m_recorderState == QMediaRecorder::StoppedState)
return 0;
if (m_recorderState == QMediaRecorder::PausedState)
return m_recorderPausedMs;
if (!m_recorderTimer.isValid())
return m_recorderPausedMs;
return m_recorderPausedMs + (m_recorderTimer.elapsed() - m_recorderResumeStartMs);
}
bool QOhosCameraSession::startRecording(const QMediaEncoderSettings &settings,
const QString &location)
{
if (m_recorder) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("Recording already in progress"));
return false;
}
// Audio-only recording when no camera is attached: skip the camera
// pipeline and let OH_AVRecorder capture audio directly.
const bool videoEnabled = m_active && m_captureSession;
Camera_VideoProfile videoProfile{};
if (videoEnabled) {
if (!findVideoProfile(settings, &videoProfile)) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("No matching camera video profile"));
return false;
}
}
m_recorder = OH_AVRecorder_Create();
if (!m_recorder) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("OH_AVRecorder_Create failed"));
return false;
}
OH_AVRecorder_SetStateCallback(m_recorder, recorderStateCallback, this);
OH_AVRecorder_SetErrorCallback(m_recorder, recorderErrorCallback, this);
QString resolved = location;
if (QFileInfo(resolved).suffix().isEmpty()) {
const QString suffix = settings.preferredSuffix();
if (!suffix.isEmpty())
resolved.append(QLatin1Char('.')).append(suffix);
else
resolved.append(QStringLiteral(".mp4"));
}
QByteArray urlBytes = QStringLiteral("fd://").toUtf8();
int fd = ::open(QFile::encodeName(resolved).constData(),
O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("Could not open output file: %1").arg(resolved));
destroyRecorder();
return false;
}
urlBytes.append(QByteArray::number(fd));
OH_AVRecorder_Config config{};
config.audioSourceType = AVRECORDER_MIC;
config.profile.audioBitrate = settings.audioBitRate() > 0
? settings.audioBitRate() : qualityToAudioBitrate(settings.quality());
config.profile.audioChannels = settings.audioChannelCount() > 0
? settings.audioChannelCount() : 2;
config.profile.audioCodec = audioCodecToOhos(settings.audioCodec());
config.profile.audioSampleRate = settings.audioSampleRate() > 0
? settings.audioSampleRate() : 48000;
config.profile.fileFormat = containerToOhos(settings.fileFormat());
if (videoEnabled) {
const QSize videoSize{ int(videoProfile.size.width), int(videoProfile.size.height) };
config.videoSourceType = AVRECORDER_SURFACE_YUV;
config.profile.videoBitrate = settings.videoBitRate() > 0
? settings.videoBitRate() : qualityToVideoBitrate(settings.quality(), videoSize);
config.profile.videoCodec = videoCodecToOhos(settings.videoCodec());
config.profile.videoFrameWidth = videoSize.width();
config.profile.videoFrameHeight = videoSize.height();
config.profile.videoFrameRate = settings.videoFrameRate() > 0
? int(settings.videoFrameRate()) : 30;
}
config.profile.isHdr = false;
config.profile.enableTemporalScale = false;
config.url = const_cast<char *>(urlBytes.constData());
config.fileGenerationMode = AVRECORDER_APP_CREATE;
config.maxDuration = 0;
if (OH_AVRecorder_Prepare(m_recorder, &config) != AV_ERR_OK) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::FormatError),
tr("OH_AVRecorder_Prepare failed"));
::close(fd);
destroyRecorder();
return false;
}
if (videoEnabled) {
if (OH_AVRecorder_GetInputSurface(m_recorder, &m_recorderWindow) != AV_ERR_OK
|| !m_recorderWindow) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("OH_AVRecorder_GetInputSurface failed"));
destroyRecorder();
return false;
}
uint64_t surfaceIdNum = 0;
if (OH_NativeWindow_GetSurfaceId(m_recorderWindow, &surfaceIdNum) != 0
|| surfaceIdNum == 0) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("Failed to obtain recorder surface ID"));
destroyRecorder();
return false;
}
const QByteArray surfaceId = QByteArray::number(qulonglong(surfaceIdNum));
if (!attachVideoOutput(videoProfile, surfaceId)) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("Failed to attach video output to capture session"));
destroyRecorder();
return false;
}
}
if (OH_AVRecorder_Start(m_recorder) != AV_ERR_OK) {
Q_EMIT recorderErrorOccurred(int(QMediaRecorder::ResourceError),
tr("OH_AVRecorder_Start failed"));
detachVideoOutput();
destroyRecorder();
return false;
}
m_recorderActualLocation = QUrl::fromLocalFile(resolved);
Q_EMIT recorderActualLocationChanged(m_recorderActualLocation);
m_recorderPausedMs = 0;
m_recorderResumeStartMs = 0;
m_recorderTimer.restart();
return true;
}
void QOhosCameraSession::pauseRecording()
{
if (!m_recorder || m_recorderState != QMediaRecorder::RecordingState)
return;
if (OH_AVRecorder_Pause(m_recorder) == AV_ERR_OK) {
m_recorderPausedMs += (m_recorderTimer.elapsed() - m_recorderResumeStartMs);
}
}
void QOhosCameraSession::resumeRecording()
{
if (!m_recorder || m_recorderState != QMediaRecorder::PausedState)
return;
if (OH_AVRecorder_Resume(m_recorder) == AV_ERR_OK)
m_recorderResumeStartMs = m_recorderTimer.elapsed();
}
void QOhosCameraSession::stopRecording()
{
if (!m_recorder)
return;
OH_AVRecorder_Stop(m_recorder);
detachVideoOutput();
destroyRecorder();
}
void QOhosCameraSession::destroyRecorder()
{
if (m_recorder) {
OH_AVRecorder_Release(m_recorder);
m_recorder = nullptr;
}
m_recorderWindow = nullptr;
if (m_recorderState != QMediaRecorder::StoppedState) {
m_recorderState = QMediaRecorder::StoppedState;
Q_EMIT recorderStateChanged(int(QMediaRecorder::StoppedState));
}
m_recorderTimer.invalidate();
m_recorderPausedMs = 0;
m_recorderResumeStartMs = 0;
}
QT_END_NAMESPACE
#include "moc_qohoscamerasession_p.cpp"