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