Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qohoscamerasession.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include "common/qohosvideooutput_p.h"
8
9#include <QtCore/qdir.h>
10#include <QtCore/qfile.h>
11#include <QtCore/qfileinfo.h>
12#include <QtCore/qstandardpaths.h>
13#include <QtCore/qloggingcategory.h>
14#include <QtCore/qmutex.h>
15#include <QtCore/qset.h>
16#include <QtCore/qthread.h>
17#include <QtCore/qthreadpool.h>
18#include <QtGui/qguiapplication.h>
19#include <QtGui/qimage.h>
20#include <QtMultimedia/qvideosink.h>
21
22#include <fcntl.h>
23#include <unistd.h>
24
25#include <private/qcameradevice_p.h>
26#include <private/qmediastoragelocation_p.h>
27#include <private/qmemoryvideobuffer_p.h>
28#include <private/qvideoframe_p.h>
29
30#include <multimedia/image_framework/image/image_native.h>
31#include <multimedia/image_framework/image/image_packer_native.h>
32#include <multimedia/image_framework/image/image_source_native.h>
33#include <native_buffer/native_buffer.h>
34#include <native_window/external_window.h>
35
36QT_BEGIN_NAMESPACE
37
38namespace {
39
40constexpr int32_t kImageReceiverCapacity = 4;
41constexpr const char *kJpegMimeType = "image/jpeg";
42
43// OHOS phones only allow one Camera_Input open at a time. Other platforms
44// auto-preempt the previously-open camera when a new one is opened, but on
45// OHOS OH_CameraInput_Open fails with CAMERA_OPERATION_NOT_ALLOWED if any
46// other input is still alive in this process. Track live sessions so a
47// starting session can stop the others first.
48//
49// The mutex is recursive because stopSession() -> releaseSession() removes
50// the session from liveSessions() under the same lock, and preemption iterates
51// while holding it (so a pointer can't be destroyed mid-iteration).
53{
54 static QRecursiveMutex m;
55 return m;
56}
58{
59 static QSet<QOhosCameraSession *> s;
60 return s;
61}
62
64{
65 switch (format) {
66 case CAMERA_FORMAT_RGBA_8888:
67 return QVideoFrameFormat::Format_RGBA8888;
68 case CAMERA_FORMAT_YUV_420_SP:
69 return QVideoFrameFormat::Format_NV12;
70 case CAMERA_FORMAT_JPEG:
71 return QVideoFrameFormat::Format_Jpeg;
72 default:
73 break;
74 }
75 return QVideoFrameFormat::Format_Invalid;
76}
77
78int qualityToInt(QImageCapture::Quality q)
79{
80 switch (q) {
81 case QImageCapture::VeryLowQuality:
82 return 25;
83 case QImageCapture::LowQuality:
84 return 50;
85 case QImageCapture::HighQuality:
86 return 90;
87 case QImageCapture::VeryHighQuality:
88 return 100;
89 case QImageCapture::NormalQuality:
90 default:
91 return 75;
92 }
93}
94
95QByteArray encodeNativeImageToJpeg(OH_ImageNative *image, int quality)
96{
97 uint32_t *components = nullptr;
98 size_t componentCount = 0;
99 if (OH_ImageNative_GetComponentTypes(image, nullptr, &componentCount) != IMAGE_SUCCESS
100 || componentCount == 0) {
101 return {};
102 }
103 std::vector<uint32_t> types(componentCount);
104 components = types.data();
105 if (OH_ImageNative_GetComponentTypes(image, &components, &componentCount) != IMAGE_SUCCESS)
106 return {};
107
108 OH_NativeBuffer *nativeBuffer = nullptr;
109 if (OH_ImageNative_GetByteBuffer(image, types[0], &nativeBuffer) != IMAGE_SUCCESS
110 || !nativeBuffer) {
111 return {};
112 }
113
114 OH_NativeBuffer_Config bufferConfig{};
115 OH_NativeBuffer_GetConfig(nativeBuffer, &bufferConfig);
116
117 void *mapped = nullptr;
118 if (OH_NativeBuffer_Map(nativeBuffer, &mapped) != 0 || !mapped)
119 return {};
120
121 size_t bufferSize = 0;
122 OH_ImageNative_GetBufferSize(image, types[0], &bufferSize);
123
124 QByteArray result;
125 OH_ImageSourceNative *source = nullptr;
126 if (OH_ImageSourceNative_CreateFromData(static_cast<uint8_t *>(mapped), bufferSize, &source)
127 == IMAGE_SUCCESS
128 && source) {
129 OH_ImagePackerNative *packer = nullptr;
130 if (OH_ImagePackerNative_Create(&packer) == IMAGE_SUCCESS && packer) {
131 OH_PackingOptions *options = nullptr;
132 if (OH_PackingOptions_Create(&options) == IMAGE_SUCCESS && options) {
133 Image_MimeType mime{ const_cast<char *>(kJpegMimeType),
134 std::strlen(kJpegMimeType) };
135 OH_PackingOptions_SetMimeType(options, &mime);
136 OH_PackingOptions_SetQuality(options, uint32_t(quality));
137
138 size_t outSize = bufferSize * 2 + 1024;
139 result.resize(int(outSize));
140 if (OH_ImagePackerNative_PackToDataFromImageSource(
141 packer, options, source,
142 reinterpret_cast<uint8_t *>(result.data()), &outSize)
143 == IMAGE_SUCCESS) {
144 result.resize(int(outSize));
145 } else {
146 result.clear();
147 }
148 OH_PackingOptions_Release(options);
149 }
150 OH_ImagePackerNative_Release(packer);
151 }
152 OH_ImageSourceNative_Release(source);
153 }
154
155 OH_NativeBuffer_Unmap(nativeBuffer);
156 return result;
157}
158
159void imageArriveCallbackTrampoline(OH_ImageReceiverNative * /*receiver*/, void *userData)
160{
161 auto *session = static_cast<QOhosCameraSession *>(userData);
162 if (!session)
163 return;
164 QMetaObject::invokeMethod(session, &QOhosCameraSession::onCapturedImageAvailable,
165 Qt::QueuedConnection);
166}
167
168} // namespace
169
170QOhosCameraSession::QOhosCameraSession(QObject *parent) : QObject(parent)
171{
172 if (qApp) {
173 connect(qApp, &QGuiApplication::applicationStateChanged, this,
174 &QOhosCameraSession::onApplicationStateChanged);
175 }
176}
177
178void QOhosCameraSession::onApplicationStateChanged()
179{
180 // HarmonyOS revokes camera access in the background and the session is left
181 // dead with no notification, so tear it down and build a new one on return.
182 if (QGuiApplication::applicationState() == Qt::ApplicationActive) {
183 if (m_restoreOnForeground) {
184 m_restoreOnForeground = false;
185 setActive(true);
186 }
187 } else if (m_active) {
188 setActive(false);
189 m_restoreOnForeground = true;
190 }
191}
192
194{
195 {
196 QMutexLocker lock{ &liveSessionMutex() };
197 liveSessions().remove(this);
198 }
199 releaseSession();
200 if (m_supportedDevices) {
201 OH_CameraManager_DeleteSupportedCameras(m_manager, m_supportedDevices,
202 m_supportedDeviceCount);
203 m_supportedDevices = nullptr;
204 }
205 if (m_manager) {
206 OH_Camera_DeleteCameraManager(m_manager);
207 m_manager = nullptr;
208 }
209}
210
211void QOhosCameraSession::setCamera(const QCameraDevice &camera)
212{
213 if (m_cameraDevice == camera)
214 return;
215 const bool wasActive = m_active;
216 if (wasActive)
217 setActive(false);
218 m_cameraDevice = camera;
219 if (wasActive)
220 setActive(true);
221}
222
223void QOhosCameraSession::setCameraFormat(const QCameraFormat &format)
224{
225 m_cameraFormat = format;
226}
227
228void QOhosCameraSession::setVideoSink(QVideoSink *sink)
229{
230 if (m_videoSink == sink)
231 return;
232 m_videoSink = sink;
233 if (m_videoOutput)
234 m_videoOutput.reset();
235 m_videoOutput = std::make_unique<QOhosVideoOutput>(
236 sink, QOhosVideoOutput::ContentSource::Camera, this);
237 connect(m_videoOutput.get(), &QOhosVideoOutput::surfaceReady, this,
238 &QOhosCameraSession::onSurfaceReady);
239}
240
242{
243 if (m_active == active)
244 return;
245
246 // Starting is refused while the application is in the background, so keep
247 // the request and act on it once the application is active again.
248 if (active && qApp && QGuiApplication::applicationState() != Qt::ApplicationActive) {
249 m_restoreOnForeground = true;
250 return;
251 }
252 m_restoreOnForeground = false;
253
254 if (active) {
255 if (!startSession()) {
256 m_pendingStart = true;
257 return;
258 }
259 } else {
260 m_pendingStart = false;
261 stopSession();
262 }
263 m_active = active;
264 emit activeChanged(active);
265 emitReadyForCaptureChanged();
266}
267
268void QOhosCameraSession::setImageSettings(const QImageEncoderSettings &settings)
269{
270 m_imageSettings = settings;
271}
272
273int QOhosCameraSession::capture(const QString &fileName, bool toBuffer)
274{
275 const int id = ++m_lastCaptureId;
276 if (!m_active || !m_photoOutput) {
277 emit imageCaptureError(id, QImageCapture::NotReadyError,
278 tr("Camera not ready for capture"));
279 return id;
280 }
281 if (m_captureInProgress) {
282 emit imageCaptureError(id, QImageCapture::NotReadyError,
283 tr("Capture already in progress"));
284 return id;
285 }
286
287 m_pendingCaptureId = id;
288 m_pendingCaptureFileName = fileName;
289 m_pendingCaptureToBuffer = toBuffer;
290 m_captureInProgress = true;
291 emitReadyForCaptureChanged();
292
293 if (OH_PhotoOutput_Capture(m_photoOutput) != CAMERA_OK) {
294 m_captureInProgress = false;
295 emit imageCaptureError(id, QImageCapture::ResourceError,
296 tr("OH_PhotoOutput_Capture failed"));
297 emitReadyForCaptureChanged();
298 }
299 return id;
300}
301
302void QOhosCameraSession::onSurfaceReady()
303{
304 if (m_pendingStart && !m_active) {
305 m_pendingStart = false;
306 if (startSession()) {
307 m_active = true;
308 emit activeChanged(true);
309 emitReadyForCaptureChanged();
310 }
311 return;
312 }
313
314 // Rebuild when the surface the session was started with has been replaced.
315 if (!m_active || !m_videoOutput)
316 return;
317 const QByteArray surfaceId = m_videoOutput->surfaceId();
318 if (surfaceId.isEmpty() || surfaceId == m_previewSurfaceId)
319 return;
320
321 m_active = false;
322 stopSession();
323 if (startSession()) {
324 m_active = true;
325 emit activeChanged(true);
326 emitReadyForCaptureChanged();
327 }
328}
329
330void QOhosCameraSession::onCapturedImageAvailable()
331{
332 if (!m_imageReceiver)
333 return;
334
335 OH_ImageNative *image = nullptr;
336 if (OH_ImageReceiverNative_ReadLatestImage(m_imageReceiver, &image) != IMAGE_SUCCESS
337 || !image) {
338 m_captureInProgress = false;
339 emit imageCaptureError(m_pendingCaptureId, QImageCapture::ResourceError,
340 tr("Failed to read captured image"));
341 emitReadyForCaptureChanged();
342 return;
343 }
344
345 const int quality = qualityToInt(m_imageSettings.quality());
346 QByteArray jpegBytes = encodeNativeImageToJpeg(image, quality);
347 OH_ImageNative_Release(image);
348
349 const int id = m_pendingCaptureId;
350 const QString fileName = m_pendingCaptureFileName;
351 const bool toBuffer = m_pendingCaptureToBuffer;
352 m_pendingCaptureFileName.clear();
353 m_pendingCaptureToBuffer = false;
354 m_pendingCaptureId = 0;
355 m_captureInProgress = false;
356
357 if (jpegBytes.isEmpty()) {
358 emit imageCaptureError(id, QImageCapture::FormatError,
359 tr("Failed to encode captured image"));
360 emitReadyForCaptureChanged();
361 return;
362 }
363
364 QImage preview = QImage::fromData(jpegBytes, "JPEG");
365 emit imageExposed(id);
366 emit imageCaptured(id, preview);
367
368 if (toBuffer) {
369 QVideoFrame buffer = QVideoFramePrivate::createFrame(
370 std::make_unique<QMemoryVideoBuffer>(QByteArray(jpegBytes),
371 preview.bytesPerLine()),
372 QVideoFrameFormat(preview.size(), QVideoFrameFormat::Format_Jpeg));
373 emit imageAvailable(id, buffer);
374 emitReadyForCaptureChanged();
375 return;
376 }
377
378 // captureToFile: persist to disk and emit imageSaved. Empty filename means
379 // "let the platform pick" — Qt apps default to PicturesLocation/IMG_<n>.jpg.
380 // Mirrors the Android backend.
381 const QImageCapture::FileFormat targetFormat = m_imageSettings.format();
382 const QString defaultExt = [&]() -> QString {
383 switch (targetFormat) {
384 case QImageCapture::PNG: return QStringLiteral("png");
385 case QImageCapture::WebP: return QStringLiteral("webp");
386 case QImageCapture::Tiff: return QStringLiteral("tiff");
387 case QImageCapture::JPEG:
388 case QImageCapture::UnspecifiedFormat:
389 default: return QStringLiteral("jpg");
390 }
391 }();
392 const char *qImageFormat = [&]() -> const char * {
393 switch (targetFormat) {
394 case QImageCapture::PNG: return "PNG";
395 case QImageCapture::WebP: return "WEBP";
396 case QImageCapture::Tiff: return "TIFF";
397 default: return nullptr; // JPEG: stream raw bytes
398 }
399 }();
400 const QString resolved = QMediaStorageLocation::generateFileName(
401 fileName, QStandardPaths::PicturesLocation, defaultExt);
402 bool saved = false;
403 if (qImageFormat) {
404 saved = preview.save(resolved, qImageFormat, quality);
405 } else {
406 QFile out(resolved);
407 if (out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
408 out.write(jpegBytes);
409 out.close();
410 saved = true;
411 }
412 }
413 if (saved) {
414 emit imageSaved(id, resolved);
415 } else {
416 emit imageCaptureError(id, QImageCapture::ResourceError,
417 tr("Could not save captured image to: %1").arg(resolved));
418 }
419
420 emitReadyForCaptureChanged();
421}
422
423void QOhosCameraSession::emitReadyForCaptureChanged()
424{
425 const bool ready = isReadyForCapture();
426 if (m_lastReadyForCapture && *m_lastReadyForCapture == ready)
427 return;
428 m_lastReadyForCapture = ready;
429 emit readyForCaptureChanged(ready);
430}
431
432bool QOhosCameraSession::ensureManager()
433{
434 if (m_manager)
435 return true;
436 if (OH_Camera_GetCameraManager(&m_manager) != CAMERA_OK || !m_manager) {
437 qCWarning(qLcOhosMediaPlugin) << "OH_Camera_GetCameraManager failed";
438 return false;
439 }
440 if (OH_CameraManager_GetSupportedCameras(m_manager, &m_supportedDevices,
441 &m_supportedDeviceCount) != CAMERA_OK) {
442 qCWarning(qLcOhosMediaPlugin) << "GetSupportedCameras failed";
443 m_supportedDevices = nullptr;
444 m_supportedDeviceCount = 0;
445 }
446 return true;
447}
448
449Camera_Device *QOhosCameraSession::findDevice(const QByteArray &id)
450{
451 if (!m_supportedDevices)
452 return nullptr;
453 for (uint32_t i = 0; i < m_supportedDeviceCount; ++i) {
454 if (m_supportedDevices[i].cameraId && id == m_supportedDevices[i].cameraId)
455 return &m_supportedDevices[i];
456 }
457 return nullptr;
458}
459
460bool QOhosCameraSession::createPhotoPath(Camera_OutputCapability *caps,
461 Camera_Profile *previewProfile)
462{
463 if (!caps || caps->photoProfilesSize == 0)
464 return false;
465
466 Camera_Profile *photoProfile = caps->photoProfiles[0];
467 if (m_imageSettings.resolution().isValid()) {
468 const QSize wanted = m_imageSettings.resolution();
469 for (uint32_t i = 0; i < caps->photoProfilesSize; ++i) {
470 Camera_Profile *p = caps->photoProfiles[i];
471 if (!p)
472 continue;
473 if (int(p->size.width) == wanted.width()
474 && int(p->size.height) == wanted.height()) {
475 photoProfile = p;
476 break;
477 }
478 }
479 } else if (previewProfile) {
480 for (uint32_t i = 0; i < caps->photoProfilesSize; ++i) {
481 Camera_Profile *p = caps->photoProfiles[i];
482 if (!p)
483 continue;
484 if (p->size.width == previewProfile->size.width
485 && p->size.height == previewProfile->size.height) {
486 photoProfile = p;
487 break;
488 }
489 }
490 }
491 if (!photoProfile)
492 return false;
493
494 if (OH_ImageReceiverOptions_Create(&m_imageReceiverOptions) != IMAGE_SUCCESS
495 || !m_imageReceiverOptions) {
496 return false;
497 }
498 Image_Size size{ uint32_t(photoProfile->size.width), uint32_t(photoProfile->size.height) };
499 OH_ImageReceiverOptions_SetSize(m_imageReceiverOptions, size);
500 OH_ImageReceiverOptions_SetCapacity(m_imageReceiverOptions, kImageReceiverCapacity);
501
502 if (OH_ImageReceiverNative_Create(m_imageReceiverOptions, &m_imageReceiver) != IMAGE_SUCCESS
503 || !m_imageReceiver) {
504 return false;
505 }
506
507 OH_ImageReceiverNative_OnImageArrive(m_imageReceiver, imageArriveCallbackTrampoline, this);
508
509 uint64_t surfaceIdNum = 0;
510 if (OH_ImageReceiverNative_GetReceivingSurfaceId(m_imageReceiver, &surfaceIdNum) != IMAGE_SUCCESS
511 || surfaceIdNum == 0) {
512 return false;
513 }
514 const QByteArray surfaceId = QByteArray::number(qulonglong(surfaceIdNum));
515
516 if (OH_CameraManager_CreatePhotoOutput(m_manager, photoProfile, surfaceId.constData(),
517 &m_photoOutput) != CAMERA_OK
518 || !m_photoOutput) {
519 return false;
520 }
521 return true;
522}
523
524void QOhosCameraSession::destroyPhotoPath()
525{
526 if (m_photoOutput) {
527 OH_PhotoOutput_Release(m_photoOutput);
528 m_photoOutput = nullptr;
529 }
530 if (m_imageReceiver) {
531 OH_ImageReceiverNative_OffImageArrive(m_imageReceiver, imageArriveCallbackTrampoline);
532 OH_ImageReceiverNative_Release(m_imageReceiver);
533 m_imageReceiver = nullptr;
534 }
535 if (m_imageReceiverOptions) {
536 OH_ImageReceiverOptions_Release(m_imageReceiverOptions);
537 m_imageReceiverOptions = nullptr;
538 }
539}
540
541bool QOhosCameraSession::startSession()
542{
543 // Phones only let one Camera_Input be open at a time. Preempt any other
544 // live session in this process before attempting to open ours; otherwise
545 // OH_CameraInput_Open returns CAMERA_OPERATION_NOT_ALLOWED.
546 {
547 QMutexLocker lock{ &liveSessionMutex() };
548 const auto sessions = liveSessions();
549 for (auto *other : sessions) {
550 if (other != this)
551 other->stopSession();
552 }
553 }
554
555 // OHOS capture sessions require at least one preview output to commit, so
556 // we always produce a surface — backed by a real QVideoSink RHI when one is
557 // attached, or an internal offscreen GLES2 RHI otherwise. surfaceReady will
558 // re-attach later if a sink with a live RHI shows up.
559 if (!m_videoOutput) {
560 m_videoOutput = std::make_unique<QOhosVideoOutput>(
561 nullptr, QOhosVideoOutput::ContentSource::Camera, this);
562 connect(m_videoOutput.get(), &QOhosVideoOutput::surfaceReady, this,
563 &QOhosCameraSession::onSurfaceReady);
564 }
565 m_previewSurfaceId = m_videoOutput->surfaceId();
566
567 if (!ensureManager())
568 return false;
569
570 Camera_Device *device = findDevice(m_cameraDevice.id());
571 if (!device && m_supportedDeviceCount > 0)
572 device = &m_supportedDevices[0];
573 if (!device) {
574 qCWarning(qLcOhosMediaPlugin) << "No camera device available";
575 return false;
576 }
577
578 Camera_Profile *previewProfile = nullptr;
579 Camera_OutputCapability *caps = nullptr;
580 if (OH_CameraManager_GetSupportedCameraOutputCapability(m_manager, device, &caps) == CAMERA_OK
581 && caps && caps->previewProfilesSize > 0) {
582 previewProfile = caps->previewProfiles[0];
583 const bool haveRequest = !m_cameraFormat.isNull();
584 const QSize requestedSize = haveRequest ? m_cameraFormat.resolution() : QSize{};
585 const QVideoFrameFormat::PixelFormat requestedPixel =
586 haveRequest ? m_cameraFormat.pixelFormat() : QVideoFrameFormat::Format_Invalid;
587 for (uint32_t i = 0; i < caps->previewProfilesSize; ++i) {
588 Camera_Profile *p = caps->previewProfiles[i];
589 if (!p)
590 continue;
591 const bool sizeMatches = !haveRequest
592 || (int(p->size.width) == requestedSize.width()
593 && int(p->size.height) == requestedSize.height());
594 const bool formatMatches = !haveRequest
595 || requestedPixel == QVideoFrameFormat::Format_Invalid
596 || requestedPixel == pixelFormatFor(p->format);
597 if (sizeMatches && formatMatches) {
598 previewProfile = p;
599 if (haveRequest)
600 break;
601 if (p->format == CAMERA_FORMAT_YUV_420_SP && p->size.width == 1280
602 && p->size.height == 720)
603 break;
604 }
605 }
606 }
607
608 if (!previewProfile) {
609 qCWarning(qLcOhosMediaPlugin) << "No preview profile available";
610 if (caps)
611 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
612 return false;
613 }
614
615 if (OH_CameraManager_CreateCameraInput(m_manager, device, &m_cameraInput) != CAMERA_OK
616 || !m_cameraInput) {
617 qCWarning(qLcOhosMediaPlugin) << "CreateCameraInput failed";
618 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
619 return false;
620 }
621
622 // OH_CameraInput_Open can briefly fail with CAMERA_CONFLICT_CAMERA right
623 // after a sibling Camera_Input was released — the camera service finishes
624 // tearing down its session asynchronously. Poll for up to ~1 s.
625 {
626 Camera_ErrorCode err = CAMERA_OK;
627 constexpr int kMaxAttempts = 20;
628 constexpr int kBackoffMs = 50;
629 for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
630 err = OH_CameraInput_Open(m_cameraInput);
631 if (err == CAMERA_OK)
632 break;
633 // CAMERA_OPERATION_NOT_ALLOWED is returned when another
634 // Camera_Input is still alive in this process. Force-stop any
635 // other live QOhosCameraSession and retry.
636 if (err == CAMERA_OPERATION_NOT_ALLOWED) {
637 QSet<QOhosCameraSession *> snapshot;
638 {
639 QMutexLocker lock{ &liveSessionMutex() };
640 snapshot = liveSessions();
641 }
642 for (auto *other : snapshot) {
643 if (other != this)
644 other->stopSession();
645 }
646 } else if (err != CAMERA_CONFLICT_CAMERA && err != CAMERA_DEVICE_PREEMPTED) {
647 break;
648 }
649 QThread::msleep(kBackoffMs);
650 }
651 if (err != CAMERA_OK) {
652 qCWarning(qLcOhosMediaPlugin) << "CameraInput_Open failed:" << err;
653 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
654 releaseSession();
655 return false;
656 }
657 QMutexLocker lock{ &liveSessionMutex() };
658 liveSessions().insert(this);
659 }
660
661 if (m_videoOutput && !m_previewSurfaceId.isEmpty()) {
662 m_videoOutput->setVideoSize(
663 QSize{ int(previewProfile->size.width), int(previewProfile->size.height) });
664 if (OH_CameraManager_CreatePreviewOutput(m_manager, previewProfile,
665 m_previewSurfaceId.constData(), &m_previewOutput)
666 != CAMERA_OK
667 || !m_previewOutput) {
668 qCWarning(qLcOhosMediaPlugin) << "CreatePreviewOutput failed";
669 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
670 releaseSession();
671 return false;
672 }
673 }
674
675 const bool hasPhoto = createPhotoPath(caps, previewProfile);
676 if (!hasPhoto)
677 qCWarning(qLcOhosMediaPlugin) << "Photo output unavailable; image capture disabled";
678
679 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
680
681 if (!m_previewOutput && !m_photoOutput) {
682 qCWarning(qLcOhosMediaPlugin) << "No capture outputs available";
683 releaseSession();
684 return false;
685 }
686
687 if (OH_CameraManager_CreateCaptureSession(m_manager, &m_captureSession) != CAMERA_OK
688 || !m_captureSession) {
689 qCWarning(qLcOhosMediaPlugin) << "CreateCaptureSession failed";
690 releaseSession();
691 return false;
692 }
693
694 OH_CaptureSession_BeginConfig(m_captureSession);
695 OH_CaptureSession_AddInput(m_captureSession, m_cameraInput);
696 if (m_previewOutput)
697 OH_CaptureSession_AddPreviewOutput(m_captureSession, m_previewOutput);
698 if (m_photoOutput)
699 OH_CaptureSession_AddPhotoOutput(m_captureSession, m_photoOutput);
700 if (OH_CaptureSession_CommitConfig(m_captureSession) != CAMERA_OK) {
701 qCWarning(qLcOhosMediaPlugin) << "CommitConfig failed";
702 releaseSession();
703 return false;
704 }
705
706 if (OH_CaptureSession_Start(m_captureSession) != CAMERA_OK) {
707 qCWarning(qLcOhosMediaPlugin) << "CaptureSession_Start failed";
708 releaseSession();
709 return false;
710 }
711
712 return true;
713}
714
715void QOhosCameraSession::stopSession()
716{
717 if (m_captureSession) {
718 OH_CaptureSession_Stop(m_captureSession);
719 }
720 releaseSession();
721}
722
723void QOhosCameraSession::releaseSession()
724{
725 destroyRecorder();
726 detachVideoOutput();
727 destroyPhotoPath();
728 if (m_captureSession) {
729 OH_CaptureSession_Release(m_captureSession);
730 m_captureSession = nullptr;
731 }
732 if (m_previewOutput) {
733 OH_PreviewOutput_Release(m_previewOutput);
734 m_previewOutput = nullptr;
735 }
736 m_previewSurfaceId.clear();
737 if (m_cameraInput) {
738 OH_CameraInput_Close(m_cameraInput);
739 OH_CameraInput_Release(m_cameraInput);
740 m_cameraInput = nullptr;
741 }
742 QMutexLocker lock{ &liveSessionMutex() };
743 liveSessions().remove(this);
744}
745
746namespace {
747
748OH_AVRecorder_CodecMimeType videoCodecToOhos(QMediaFormat::VideoCodec codec)
749{
750 switch (codec) {
751 case QMediaFormat::VideoCodec::H265:
752 return AVRECORDER_VIDEO_HEVC;
753 case QMediaFormat::VideoCodec::MPEG4:
754 return AVRECORDER_VIDEO_MPEG4;
755 case QMediaFormat::VideoCodec::H264:
756 case QMediaFormat::VideoCodec::Unspecified:
757 default:
758 return AVRECORDER_VIDEO_AVC;
759 }
760}
761
762OH_AVRecorder_CodecMimeType audioCodecToOhos(QMediaFormat::AudioCodec codec)
763{
764 switch (codec) {
765 case QMediaFormat::AudioCodec::MP3:
766 return AVRECORDER_AUDIO_MP3;
767 case QMediaFormat::AudioCodec::AAC:
768 case QMediaFormat::AudioCodec::Unspecified:
769 default:
770 return AVRECORDER_AUDIO_AAC;
771 }
772}
773
774OH_AVRecorder_ContainerFormatType containerToOhos(QMediaFormat::FileFormat fmt)
775{
776 switch (fmt) {
777 case QMediaFormat::AAC:
778 return AVRECORDER_CFT_AAC;
779 case QMediaFormat::MP3:
780 return AVRECORDER_CFT_MP3;
781 case QMediaFormat::Wave:
782 return AVRECORDER_CFT_WAV;
783 case QMediaFormat::Mpeg4Audio:
784 return AVRECORDER_CFT_MPEG_4A;
785 case QMediaFormat::MPEG4:
786 case QMediaFormat::UnspecifiedFormat:
787 default:
788 return AVRECORDER_CFT_MPEG_4;
789 }
790}
791
792int qualityToVideoBitrate(QMediaRecorder::Quality q, const QSize &resolution)
793{
794 const int pixels = qMax(1, resolution.width() * resolution.height());
795 const double bpp = [&]() {
796 switch (q) {
797 case QMediaRecorder::VeryLowQuality: return 0.05;
798 case QMediaRecorder::LowQuality: return 0.1;
799 case QMediaRecorder::HighQuality: return 0.25;
800 case QMediaRecorder::VeryHighQuality:return 0.4;
801 case QMediaRecorder::NormalQuality:
802 default: return 0.15;
803 }
804 }();
805 return int(pixels * 30 * bpp);
806}
807
808int qualityToAudioBitrate(QMediaRecorder::Quality q)
809{
810 switch (q) {
811 case QMediaRecorder::VeryLowQuality: return 32000;
812 case QMediaRecorder::LowQuality: return 64000;
813 case QMediaRecorder::HighQuality: return 192000;
814 case QMediaRecorder::VeryHighQuality:return 256000;
815 case QMediaRecorder::NormalQuality:
816 default: return 128000;
817 }
818}
819
820} // namespace
821
822bool QOhosCameraSession::findVideoProfile(const QMediaEncoderSettings &settings,
823 Camera_VideoProfile *out)
824{
825 if (!m_manager || !out)
826 return false;
827 Camera_Device *device = findDevice(m_cameraDevice.id());
828 if (!device && m_supportedDeviceCount > 0)
829 device = &m_supportedDevices[0];
830 if (!device)
831 return false;
832
833 Camera_OutputCapability *caps = nullptr;
834 if (OH_CameraManager_GetSupportedCameraOutputCapability(m_manager, device, &caps) != CAMERA_OK
835 || !caps || caps->videoProfilesSize == 0) {
836 if (caps)
837 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
838 return false;
839 }
840
841 Camera_VideoProfile *chosen = nullptr;
842 const QSize wanted = settings.videoResolution();
843 if (wanted.isValid()) {
844 for (uint32_t i = 0; i < caps->videoProfilesSize; ++i) {
845 Camera_VideoProfile *p = caps->videoProfiles[i];
846 if (!p)
847 continue;
848 if (int(p->size.width) == wanted.width()
849 && int(p->size.height) == wanted.height()) {
850 chosen = p;
851 break;
852 }
853 }
854 }
855 if (!chosen) {
856 for (uint32_t i = 0; i < caps->videoProfilesSize; ++i) {
857 Camera_VideoProfile *p = caps->videoProfiles[i];
858 if (!p)
859 continue;
860 if (p->size.width == 1280 && p->size.height == 720
861 && p->format == CAMERA_FORMAT_YUV_420_SP) {
862 chosen = p;
863 break;
864 }
865 }
866 }
867 if (!chosen)
868 chosen = caps->videoProfiles[0];
869 *out = *chosen;
870 OH_CameraManager_DeleteSupportedCameraOutputCapability(m_manager, caps);
871 return true;
872}
873
874bool QOhosCameraSession::attachVideoOutput(const Camera_VideoProfile &profile,
875 const QByteArray &surfaceId)
876{
877 if (!m_captureSession)
878 return false;
879 if (OH_CameraManager_CreateVideoOutput(m_manager, &profile, surfaceId.constData(),
880 &m_videoOutputCamera) != CAMERA_OK
881 || !m_videoOutputCamera) {
882 return false;
883 }
884
885 OH_CaptureSession_Stop(m_captureSession);
886 OH_CaptureSession_BeginConfig(m_captureSession);
887 OH_CaptureSession_AddVideoOutput(m_captureSession, m_videoOutputCamera);
888 if (OH_CaptureSession_CommitConfig(m_captureSession) != CAMERA_OK) {
889 OH_VideoOutput_Release(m_videoOutputCamera);
890 m_videoOutputCamera = nullptr;
891 OH_CaptureSession_Start(m_captureSession);
892 return false;
893 }
894 if (OH_CaptureSession_Start(m_captureSession) != CAMERA_OK)
895 return false;
896 if (OH_VideoOutput_Start(m_videoOutputCamera) != CAMERA_OK)
897 return false;
898 return true;
899}
900
901void QOhosCameraSession::detachVideoOutput()
902{
903 if (!m_videoOutputCamera)
904 return;
905 OH_VideoOutput_Stop(m_videoOutputCamera);
906 if (m_captureSession) {
907 OH_CaptureSession_Stop(m_captureSession);
908 OH_CaptureSession_BeginConfig(m_captureSession);
909 OH_CaptureSession_RemoveVideoOutput(m_captureSession, m_videoOutputCamera);
910 OH_CaptureSession_CommitConfig(m_captureSession);
911 OH_CaptureSession_Start(m_captureSession);
912 }
913 OH_VideoOutput_Release(m_videoOutputCamera);
914 m_videoOutputCamera = nullptr;
915}
916
917void QOhosCameraSession::recorderStateCallback(OH_AVRecorder * /*recorder*/,
918 OH_AVRecorder_State state,
919 OH_AVRecorder_StateChangeReason /*reason*/,
920 void *userData)
921{
922 auto *self = static_cast<QOhosCameraSession *>(userData);
923 if (!self)
924 return;
925 QMetaObject::invokeMethod(self, "onRecorderStateNotification", Qt::QueuedConnection,
926 Q_ARG(int, int(state)));
927}
928
929void QOhosCameraSession::recorderErrorCallback(OH_AVRecorder * /*recorder*/, int32_t errorCode,
930 const char *errorMsg, void *userData)
931{
932 auto *self = static_cast<QOhosCameraSession *>(userData);
933 if (!self)
934 return;
935 QMetaObject::invokeMethod(self, "onRecorderErrorNotification", Qt::QueuedConnection,
936 Q_ARG(int, errorCode),
937 Q_ARG(QString, QString::fromUtf8(errorMsg ? errorMsg : "")));
938}
939
941{
942 QMediaRecorder::RecorderState mapped = m_recorderState;
943 switch (state) {
944 case AVRECORDER_STARTED:
945 mapped = QMediaRecorder::RecordingState;
946 break;
947 case AVRECORDER_PAUSED:
948 mapped = QMediaRecorder::PausedState;
949 break;
950 case AVRECORDER_STOPPED:
951 case AVRECORDER_IDLE:
952 case AVRECORDER_RELEASED:
953 mapped = QMediaRecorder::StoppedState;
954 break;
955 case AVRECORDER_ERROR:
956 mapped = QMediaRecorder::StoppedState;
957 break;
958 default:
959 return;
960 }
961 if (mapped == m_recorderState)
962 return;
963 m_recorderState = mapped;
964 emit recorderStateChanged(int(mapped));
965}
966
967void QOhosCameraSession::onRecorderErrorNotification(int code, const QString &message)
968{
969 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
970 message.isEmpty()
971 ? tr("Recorder error %1").arg(code)
972 : message);
973}
974
976{
977 if (m_recorderState == QMediaRecorder::StoppedState)
978 return 0;
979 if (m_recorderState == QMediaRecorder::PausedState)
980 return m_recorderPausedMs;
981 if (!m_recorderTimer.isValid())
982 return m_recorderPausedMs;
983 return m_recorderPausedMs + (m_recorderTimer.elapsed() - m_recorderResumeStartMs);
984}
985
986bool QOhosCameraSession::startRecording(const QMediaEncoderSettings &settings,
987 const QString &location)
988{
989 if (m_recorder) {
990 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
991 tr("Recording already in progress"));
992 return false;
993 }
994
995 // Audio-only recording when no camera is attached: skip the camera
996 // pipeline and let OH_AVRecorder capture audio directly.
997 const bool videoEnabled = m_active && m_captureSession;
998 Camera_VideoProfile videoProfile{};
999 if (videoEnabled) {
1000 if (!findVideoProfile(settings, &videoProfile)) {
1001 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1002 tr("No matching camera video profile"));
1003 return false;
1004 }
1005 }
1006
1007 m_recorder = OH_AVRecorder_Create();
1008 if (!m_recorder) {
1009 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1010 tr("OH_AVRecorder_Create failed"));
1011 return false;
1012 }
1013
1014 OH_AVRecorder_SetStateCallback(m_recorder, recorderStateCallback, this);
1015 OH_AVRecorder_SetErrorCallback(m_recorder, recorderErrorCallback, this);
1016
1017 QString resolved = location;
1018 if (QFileInfo(resolved).suffix().isEmpty()) {
1019 const QString suffix = settings.preferredSuffix();
1020 if (!suffix.isEmpty())
1021 resolved.append(QLatin1Char('.')).append(suffix);
1022 else
1023 resolved.append(QStringLiteral(".mp4"));
1024 }
1025
1026 QByteArray urlBytes = QStringLiteral("fd://").toUtf8();
1027 int fd = ::open(QFile::encodeName(resolved).constData(),
1028 O_RDWR | O_CREAT | O_TRUNC, 0644);
1029 if (fd < 0) {
1030 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1031 tr("Could not open output file: %1").arg(resolved));
1032 destroyRecorder();
1033 return false;
1034 }
1035 urlBytes.append(QByteArray::number(fd));
1036
1037 OH_AVRecorder_Config config{};
1038 config.audioSourceType = AVRECORDER_MIC;
1039 config.profile.audioBitrate = settings.audioBitRate() > 0
1040 ? settings.audioBitRate() : qualityToAudioBitrate(settings.quality());
1041 config.profile.audioChannels = settings.audioChannelCount() > 0
1042 ? settings.audioChannelCount() : 2;
1043 config.profile.audioCodec = audioCodecToOhos(settings.audioCodec());
1044 config.profile.audioSampleRate = settings.audioSampleRate() > 0
1045 ? settings.audioSampleRate() : 48000;
1046 config.profile.fileFormat = containerToOhos(settings.fileFormat());
1047 if (videoEnabled) {
1048 const QSize videoSize{ int(videoProfile.size.width), int(videoProfile.size.height) };
1049 config.videoSourceType = AVRECORDER_SURFACE_YUV;
1050 config.profile.videoBitrate = settings.videoBitRate() > 0
1051 ? settings.videoBitRate() : qualityToVideoBitrate(settings.quality(), videoSize);
1052 config.profile.videoCodec = videoCodecToOhos(settings.videoCodec());
1053 config.profile.videoFrameWidth = videoSize.width();
1054 config.profile.videoFrameHeight = videoSize.height();
1055 config.profile.videoFrameRate = settings.videoFrameRate() > 0
1056 ? int(settings.videoFrameRate()) : 30;
1057 }
1058 config.profile.isHdr = false;
1059 config.profile.enableTemporalScale = false;
1060 config.url = const_cast<char *>(urlBytes.constData());
1061 config.fileGenerationMode = AVRECORDER_APP_CREATE;
1062 config.maxDuration = 0;
1063
1064 if (OH_AVRecorder_Prepare(m_recorder, &config) != AV_ERR_OK) {
1065 emit recorderErrorOccurred(int(QMediaRecorder::FormatError),
1066 tr("OH_AVRecorder_Prepare failed"));
1067 ::close(fd);
1068 destroyRecorder();
1069 return false;
1070 }
1071
1072 if (videoEnabled) {
1073 if (OH_AVRecorder_GetInputSurface(m_recorder, &m_recorderWindow) != AV_ERR_OK
1074 || !m_recorderWindow) {
1075 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1076 tr("OH_AVRecorder_GetInputSurface failed"));
1077 destroyRecorder();
1078 return false;
1079 }
1080
1081 uint64_t surfaceIdNum = 0;
1082 if (OH_NativeWindow_GetSurfaceId(m_recorderWindow, &surfaceIdNum) != 0
1083 || surfaceIdNum == 0) {
1084 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1085 tr("Failed to obtain recorder surface ID"));
1086 destroyRecorder();
1087 return false;
1088 }
1089 const QByteArray surfaceId = QByteArray::number(qulonglong(surfaceIdNum));
1090
1091 if (!attachVideoOutput(videoProfile, surfaceId)) {
1092 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1093 tr("Failed to attach video output to capture session"));
1094 destroyRecorder();
1095 return false;
1096 }
1097 }
1098
1099 if (OH_AVRecorder_Start(m_recorder) != AV_ERR_OK) {
1100 emit recorderErrorOccurred(int(QMediaRecorder::ResourceError),
1101 tr("OH_AVRecorder_Start failed"));
1102 detachVideoOutput();
1103 destroyRecorder();
1104 return false;
1105 }
1106
1107 m_recorderActualLocation = QUrl::fromLocalFile(resolved);
1108 emit recorderActualLocationChanged(m_recorderActualLocation);
1109 m_recorderPausedMs = 0;
1110 m_recorderResumeStartMs = 0;
1111 m_recorderTimer.restart();
1112 return true;
1113}
1114
1116{
1117 if (!m_recorder || m_recorderState != QMediaRecorder::RecordingState)
1118 return;
1119 if (OH_AVRecorder_Pause(m_recorder) == AV_ERR_OK) {
1120 m_recorderPausedMs += (m_recorderTimer.elapsed() - m_recorderResumeStartMs);
1121 }
1122}
1123
1125{
1126 if (!m_recorder || m_recorderState != QMediaRecorder::PausedState)
1127 return;
1128 if (OH_AVRecorder_Resume(m_recorder) == AV_ERR_OK)
1129 m_recorderResumeStartMs = m_recorderTimer.elapsed();
1130}
1131
1133{
1134 if (!m_recorder)
1135 return;
1136 OH_AVRecorder_Stop(m_recorder);
1137 detachVideoOutput();
1138 destroyRecorder();
1139}
1140
1141void QOhosCameraSession::destroyRecorder()
1142{
1143 if (m_recorder) {
1144 OH_AVRecorder_Release(m_recorder);
1145 m_recorder = nullptr;
1146 }
1147 m_recorderWindow = nullptr;
1148 if (m_recorderState != QMediaRecorder::StoppedState) {
1149 m_recorderState = QMediaRecorder::StoppedState;
1150 emit recorderStateChanged(int(QMediaRecorder::StoppedState));
1151 }
1152 m_recorderTimer.invalidate();
1153 m_recorderPausedMs = 0;
1154 m_recorderResumeStartMs = 0;
1155}
1156
1157QT_END_NAMESPACE
1158
1159#include "moc_qohoscamerasession_p.cpp"
int capture(const QString &fileName, bool toBuffer=false)
void setCamera(const QCameraDevice &camera)
qint64 recorderDuration() const
void setActive(bool active)
void onRecorderStateNotification(int state)
void setCameraFormat(const QCameraFormat &format)
void onRecorderErrorNotification(int code, const QString &message)
bool startRecording(const QMediaEncoderSettings &settings, const QString &location)
void setImageSettings(const QImageEncoderSettings &settings)
QVideoFrameFormat::PixelFormat pixelFormatFor(Camera_Format format)
int qualityToInt(QImageCapture::Quality q)
QByteArray encodeNativeImageToJpeg(OH_ImageNative *image, int quality)
constexpr int32_t kImageReceiverCapacity
void imageArriveCallbackTrampoline(OH_ImageReceiverNative *, void *userData)
constexpr const char * kJpegMimeType
QSet< QOhosCameraSession * > & liveSessions()
QRecursiveMutex & liveSessionMutex()