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