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
qohosmediarecorder.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
8
9#include <QtCore/qfile.h>
10#include <QtCore/qfileinfo.h>
11#include <QtCore/qloggingcategory.h>
12#include <QtMultimedia/qaudiodevice.h>
13#include <QtMultimedia/qmediadevices.h>
14
15#include <private/qplatformaudioinput_p.h>
16
17#include <fcntl.h>
18#include <unistd.h>
19
20#include <cstring>
21
23
24Q_STATIC_LOGGING_CATEGORY(qLcOhosMediaRecorder, "qt.multimedia.ohos.mediarecorder")
25
26namespace {
27
28OH_AVRecorder_CodecMimeType audioCodecToOhos(QMediaFormat::AudioCodec codec)
29{
30 switch (codec) {
31 case QMediaFormat::AudioCodec::MP3:
32 return AVRECORDER_AUDIO_MP3;
33 case QMediaFormat::AudioCodec::AAC:
34 case QMediaFormat::AudioCodec::Unspecified:
35 default:
36 return AVRECORDER_AUDIO_AAC;
37 }
38}
39
41{
42 switch (fmt) {
43 case QMediaFormat::AAC:
44 return AVRECORDER_CFT_AAC;
45 case QMediaFormat::MP3:
46 return AVRECORDER_CFT_MP3;
47 case QMediaFormat::Wave:
48 return AVRECORDER_CFT_WAV;
49 case QMediaFormat::Mpeg4Audio:
50 return AVRECORDER_CFT_MPEG_4A;
51 case QMediaFormat::MPEG4:
52 case QMediaFormat::UnspecifiedFormat:
53 default:
54 return AVRECORDER_CFT_MPEG_4;
55 }
56}
57
58int qualityToAudioBitrate(QMediaRecorder::Quality q)
59{
60 switch (q) {
61 case QMediaRecorder::VeryLowQuality: return 32000;
62 case QMediaRecorder::LowQuality: return 64000;
63 case QMediaRecorder::HighQuality: return 192000;
64 case QMediaRecorder::VeryHighQuality:return 256000;
65 case QMediaRecorder::NormalQuality:
66 default: return 128000;
67 }
68}
69
70} // namespace
71
72QOhosMediaRecorder::QOhosMediaRecorder(QMediaRecorder *parent)
73 : QObject(parent), QPlatformMediaRecorder(parent)
74{
75 m_audioOnlyDurationTimer.setInterval(100);
76 connect(&m_audioOnlyDurationTimer, &QTimer::timeout, this, [this] {
77 if (state() == QMediaRecorder::RecordingState)
78 durationChanged(duration());
79 });
80}
81
83{
84 destroyWaveRecorder();
85 destroyAudioOnlyRecorder();
86}
87
88bool QOhosMediaRecorder::isLocationWritable(const QUrl &location) const
89{
90 return location.isValid() && (location.isLocalFile() || location.isRelative());
91}
92
94{
95 if (m_waveSource)
96 return m_waveState;
97 if (m_audioOnlyRecorder)
98 return m_audioOnlyState;
99 return m_session ? m_session->recorderState() : QMediaRecorder::StoppedState;
100}
101
103{
104 if (m_waveSource) {
105 if (m_waveState == QMediaRecorder::StoppedState)
106 return 0;
107 if (m_waveState == QMediaRecorder::PausedState)
108 return m_wavePausedMs;
109 if (!m_waveTimer.isValid())
110 return m_wavePausedMs;
111 return m_wavePausedMs + (m_waveTimer.elapsed() - m_waveResumeStartMs);
112 }
113 if (m_audioOnlyRecorder) {
114 if (m_audioOnlyState == QMediaRecorder::StoppedState)
115 return 0;
116 if (m_audioOnlyState == QMediaRecorder::PausedState)
117 return m_audioOnlyPausedMs;
118 if (!m_audioOnlyTimer.isValid())
119 return m_audioOnlyPausedMs;
120 return m_audioOnlyPausedMs
121 + (m_audioOnlyTimer.elapsed() - m_audioOnlyResumeStartMs);
122 }
123 return m_session ? m_session->recorderDuration() : 0;
124}
125
126void QOhosMediaRecorder::record(QMediaEncoderSettings &settings)
127{
128 if (!m_service) {
129 updateError(QMediaRecorder::ResourceError,
130 tr("Recorder has no capture session attached"));
131 return;
132 }
133 settings.resolveFormat();
134 const QString location = findActualLocation(settings);
135 if (location.isEmpty()) {
136 updateError(QMediaRecorder::ResourceError, tr("No writable output location"));
137 return;
138 }
139
140 if (m_session && m_session->isActive()) {
141 m_session->startRecording(settings, location);
142 m_audioOnlyDurationTimer.start();
143 return;
144 }
145
146 // No camera available — audio-only requires an audio input on the session.
147 if (!m_service->audioInput()) {
148 updateError(QMediaRecorder::ResourceError,
149 tr("No audio or video input is attached to the capture session"));
150 return;
151 }
152
153 // OH_AVRecorder has no PCM codec, so Wave output is implemented by piping
154 // OH_AudioCapturer frames into a hand-written RIFF/WAVE file.
155 if (settings.audioCodec() == QMediaFormat::AudioCodec::Wave
156 || settings.fileFormat() == QMediaFormat::Wave) {
157 if (recordWave(settings, location))
158 m_audioOnlyDurationTimer.start();
159 else
160 destroyWaveRecorder();
161 return;
162 }
163
164 if (recordAudioOnly(settings, location))
165 m_audioOnlyDurationTimer.start();
166 else
167 destroyAudioOnlyRecorder();
168}
169
171{
172 if (m_waveSource) {
173 pauseWave();
174 return;
175 }
176 if (m_audioOnlyRecorder) {
177 pauseAudioOnly();
178 return;
179 }
180 if (m_session)
181 m_session->pauseRecording();
182}
183
185{
186 if (m_waveSource) {
187 resumeWave();
188 return;
189 }
190 if (m_audioOnlyRecorder) {
191 resumeAudioOnly();
192 return;
193 }
194 if (m_session)
195 m_session->resumeRecording();
196}
197
199{
200 m_audioOnlyDurationTimer.stop();
201 if (m_waveSource) {
202 stopWave();
203 return;
204 }
205 if (m_audioOnlyRecorder) {
206 stopAudioOnly();
207 return;
208 }
209 if (m_session)
210 m_session->stopRecording();
211}
212
213void QOhosMediaRecorder::setMetaData(const QMediaMetaData &metaData)
214{
215 if (m_metaData == metaData)
216 return;
217 m_metaData = metaData;
218 metaDataChanged();
219}
220
221void QOhosMediaRecorder::setCaptureSession(QPlatformMediaCaptureSession *session)
222{
223 auto *ohosSession = static_cast<QOhosMediaCaptureSession *>(session);
224 QOhosCameraSession *newCameraSession = ohosSession ? ohosSession->cameraSession() : nullptr;
225 if (m_service == ohosSession && m_session == newCameraSession)
226 return;
227
228 // Detach from any running recording — losing the session means we have to stop.
229 if (m_waveSource)
230 stopWave();
231 if (m_audioOnlyRecorder)
232 stopAudioOnly();
233 else if (m_session
234 && m_session->recorderState() != QMediaRecorder::StoppedState
235 && m_session != newCameraSession)
236 m_session->stopRecording();
237
238 disconnectFromSession();
239 m_service = ohosSession;
240 m_session = newCameraSession;
241 if (m_session)
242 connectToSession();
243}
244
245void QOhosMediaRecorder::onRecorderStateChanged(int state)
246{
247 stateChanged(static_cast<QMediaRecorder::RecorderState>(state));
248}
249
250void QOhosMediaRecorder::onRecorderError(int code, const QString &message)
251{
252 updateError(static_cast<QMediaRecorder::Error>(code), message);
253}
254
255void QOhosMediaRecorder::onRecorderDuration(qint64 ms)
256{
257 durationChanged(ms);
258}
259
260void QOhosMediaRecorder::onRecorderActualLocation(const QUrl &url)
261{
262 actualLocationChanged(url);
263}
264
265void QOhosMediaRecorder::connectToSession()
266{
267 if (!m_session)
268 return;
269 connect(m_session, &QOhosCameraSession::recorderStateChanged, this,
270 &QOhosMediaRecorder::onRecorderStateChanged);
271 connect(m_session, &QOhosCameraSession::recorderErrorOccurred, this,
272 &QOhosMediaRecorder::onRecorderError);
273 connect(m_session, &QOhosCameraSession::recorderDurationChanged, this,
274 &QOhosMediaRecorder::onRecorderDuration);
275 connect(m_session, &QOhosCameraSession::recorderActualLocationChanged, this,
276 &QOhosMediaRecorder::onRecorderActualLocation);
277}
278
279void QOhosMediaRecorder::disconnectFromSession()
280{
281 if (m_session)
282 disconnect(m_session, nullptr, this, nullptr);
283}
284
285bool QOhosMediaRecorder::recordAudioOnly(const QMediaEncoderSettings &settings,
286 const QString &location)
287{
288 m_audioOnlyRecorder = OH_AVRecorder_Create();
289 if (!m_audioOnlyRecorder) {
290 updateError(QMediaRecorder::ResourceError, tr("OH_AVRecorder_Create failed"));
291 return false;
292 }
293
294 OH_AVRecorder_SetStateCallback(m_audioOnlyRecorder, audioOnlyStateCallback, this);
295 OH_AVRecorder_SetErrorCallback(m_audioOnlyRecorder, audioOnlyErrorCallback, this);
296
297 QString resolved = location;
298 if (QFileInfo(resolved).suffix().isEmpty()) {
299 const QString suffix = settings.preferredSuffix();
300 if (!suffix.isEmpty())
301 resolved.append(QLatin1Char('.')).append(suffix);
302 else
303 resolved.append(QStringLiteral(".m4a"));
304 }
305
306 m_audioOnlyFd = ::open(QFile::encodeName(resolved).constData(),
307 O_RDWR | O_CREAT | O_TRUNC, 0644);
308 if (m_audioOnlyFd < 0) {
309 updateError(QMediaRecorder::ResourceError,
310 tr("Could not open output file: %1").arg(resolved));
311 return false;
312 }
313 QByteArray urlBytes = QStringLiteral("fd://").toUtf8();
314 urlBytes.append(QByteArray::number(m_audioOnlyFd));
315
316 OH_AVRecorder_Config config{};
317 config.audioSourceType = AVRECORDER_MIC;
318 config.profile.audioBitrate = settings.audioBitRate() > 0
319 ? settings.audioBitRate() : qualityToAudioBitrate(settings.quality());
320 config.profile.audioChannels = settings.audioChannelCount() > 0
321 ? settings.audioChannelCount() : 2;
322 config.profile.audioCodec = audioCodecToOhos(settings.audioCodec());
323 config.profile.audioSampleRate = settings.audioSampleRate() > 0
324 ? settings.audioSampleRate() : 48000;
325 config.profile.fileFormat = containerToOhos(settings.fileFormat());
326 config.profile.isHdr = false;
327 config.profile.enableTemporalScale = false;
328 config.url = const_cast<char *>(urlBytes.constData());
329 config.fileGenerationMode = AVRECORDER_APP_CREATE;
330 config.maxDuration = 0;
331
332 if (auto prepResult = OH_AVRecorder_Prepare(m_audioOnlyRecorder, &config);
333 prepResult != AV_ERR_OK) {
334 qCWarning(qLcOhosMediaRecorder) << "OH_AVRecorder_Prepare failed code:" << prepResult;
335 updateError(QMediaRecorder::FormatError, tr("OH_AVRecorder_Prepare failed"));
336 return false;
337 }
338
339 if (auto startResult = OH_AVRecorder_Start(m_audioOnlyRecorder); startResult != AV_ERR_OK) {
340 qCWarning(qLcOhosMediaRecorder) << "OH_AVRecorder_Start failed code:" << startResult;
341 updateError(QMediaRecorder::ResourceError, tr("OH_AVRecorder_Start failed"));
342 return false;
343 }
344
345 m_audioOnlyActualLocation = QUrl::fromLocalFile(resolved);
346 actualLocationChanged(m_audioOnlyActualLocation);
347 m_audioOnlyPausedMs = 0;
348 m_audioOnlyResumeStartMs = 0;
349 m_audioOnlyTimer.restart();
350 return true;
351}
352
353void QOhosMediaRecorder::stopAudioOnly()
354{
355 if (!m_audioOnlyRecorder)
356 return;
357 OH_AVRecorder_Stop(m_audioOnlyRecorder);
358 destroyAudioOnlyRecorder();
359}
360
361void QOhosMediaRecorder::pauseAudioOnly()
362{
363 if (!m_audioOnlyRecorder || m_audioOnlyState != QMediaRecorder::RecordingState)
364 return;
365 if (OH_AVRecorder_Pause(m_audioOnlyRecorder) == AV_ERR_OK)
366 m_audioOnlyPausedMs += (m_audioOnlyTimer.elapsed() - m_audioOnlyResumeStartMs);
367}
368
369void QOhosMediaRecorder::resumeAudioOnly()
370{
371 if (!m_audioOnlyRecorder || m_audioOnlyState != QMediaRecorder::PausedState)
372 return;
373 if (OH_AVRecorder_Resume(m_audioOnlyRecorder) == AV_ERR_OK)
374 m_audioOnlyResumeStartMs = m_audioOnlyTimer.elapsed();
375}
376
377void QOhosMediaRecorder::destroyAudioOnlyRecorder()
378{
379 m_audioOnlyDurationTimer.stop();
380 if (m_audioOnlyRecorder) {
381 OH_AVRecorder_Release(m_audioOnlyRecorder);
382 m_audioOnlyRecorder = nullptr;
383 }
384 if (m_audioOnlyFd >= 0) {
385 ::close(m_audioOnlyFd);
386 m_audioOnlyFd = -1;
387 }
388 if (m_audioOnlyState != QMediaRecorder::StoppedState) {
389 m_audioOnlyState = QMediaRecorder::StoppedState;
390 stateChanged(QMediaRecorder::StoppedState);
391 }
392 m_audioOnlyTimer.invalidate();
393 m_audioOnlyPausedMs = 0;
394 m_audioOnlyResumeStartMs = 0;
395}
396
397void QOhosMediaRecorder::audioOnlyStateCallback(OH_AVRecorder * /*recorder*/,
398 OH_AVRecorder_State state,
399 OH_AVRecorder_StateChangeReason /*reason*/,
400 void *userData)
401{
402 auto *self = static_cast<QOhosMediaRecorder *>(userData);
403 if (!self)
404 return;
405 QMetaObject::invokeMethod(
406 self, [self, state] { self->onAudioOnlyStateNotification(int(state)); },
407 Qt::QueuedConnection);
408}
409
410void QOhosMediaRecorder::audioOnlyErrorCallback(OH_AVRecorder * /*recorder*/, int32_t errorCode,
411 const char *errorMsg, void *userData)
412{
413 auto *self = static_cast<QOhosMediaRecorder *>(userData);
414 if (!self)
415 return;
416 const QString message = QString::fromUtf8(errorMsg ? errorMsg : "");
417 QMetaObject::invokeMethod(
418 self, [self, errorCode, message] {
419 self->onAudioOnlyErrorNotification(errorCode, message);
420 },
421 Qt::QueuedConnection);
422}
423
424void QOhosMediaRecorder::onAudioOnlyStateNotification(int state)
425{
426 QMediaRecorder::RecorderState mapped = m_audioOnlyState;
427 switch (state) {
428 case AVRECORDER_STARTED:
429 mapped = QMediaRecorder::RecordingState;
430 break;
431 case AVRECORDER_PAUSED:
432 mapped = QMediaRecorder::PausedState;
433 break;
434 case AVRECORDER_STOPPED:
435 case AVRECORDER_IDLE:
436 case AVRECORDER_RELEASED:
437 case AVRECORDER_ERROR:
438 mapped = QMediaRecorder::StoppedState;
439 break;
440 default:
441 return;
442 }
443 if (mapped == m_audioOnlyState)
444 return;
445 m_audioOnlyState = mapped;
446 stateChanged(mapped);
447 durationChanged(duration());
448}
449
450void QOhosMediaRecorder::onAudioOnlyErrorNotification(int code, const QString &message)
451{
452 updateError(QMediaRecorder::ResourceError,
453 message.isEmpty() ? tr("Recorder error %1").arg(code) : message);
454}
455
456namespace {
457
458// 44-byte RIFF/WAVE header for linear PCM. Sizes use placeholders that we
459// rewrite once the data chunk is finalised.
460void writeWavHeader(QFile &out, int channels, int sampleRate, int bitsPerSample)
461{
462 const auto write = [&](const char *s) { out.write(s, 4); };
463 const auto writeU32 = [&](quint32 v) {
464 char b[4]{
465 char(v & 0xff), char((v >> 8) & 0xff),
466 char((v >> 16) & 0xff), char((v >> 24) & 0xff),
467 };
468 out.write(b, 4);
469 };
470 const auto writeU16 = [&](quint16 v) {
471 char b[2]{ char(v & 0xff), char((v >> 8) & 0xff) };
472 out.write(b, 2);
473 };
474 write("RIFF");
475 writeU32(0); // chunkSize, patched on stop
476 write("WAVE");
477 write("fmt ");
478 writeU32(16); // subChunk1Size
479 writeU16(1); // AudioFormat = PCM
480 writeU16(quint16(channels));
481 writeU32(quint32(sampleRate));
482 writeU32(quint32(sampleRate * channels * (bitsPerSample / 8)));
483 writeU16(quint16(channels * (bitsPerSample / 8)));
484 writeU16(quint16(bitsPerSample));
485 write("data");
486 writeU32(0); // subChunk2Size, patched on stop
487}
488
489void patchWavSizes(QFile &file, qint64 dataBytes)
490{
491 file.seek(4);
492 quint32 chunkSize = quint32(36 + dataBytes);
493 char b[4]{
494 char(chunkSize & 0xff), char((chunkSize >> 8) & 0xff),
495 char((chunkSize >> 16) & 0xff), char((chunkSize >> 24) & 0xff),
496 };
497 file.write(b, 4);
498 file.seek(40);
499 quint32 dataSize = quint32(dataBytes);
500 char d[4]{
501 char(dataSize & 0xff), char((dataSize >> 8) & 0xff),
502 char((dataSize >> 16) & 0xff), char((dataSize >> 24) & 0xff),
503 };
504 file.write(d, 4);
505}
506
507int wavBitsForSampleFormat(QAudioFormat::SampleFormat f)
508{
509 switch (f) {
510 case QAudioFormat::UInt8: return 8;
511 case QAudioFormat::Int16: return 16;
512 case QAudioFormat::Int32: return 32;
513 case QAudioFormat::Float: return 32;
514 case QAudioFormat::Unknown:
515 case QAudioFormat::NSampleFormats: break;
516 }
517 return 16;
518}
519
520} // namespace
521
522bool QOhosMediaRecorder::recordWave(const QMediaEncoderSettings &settings,
523 const QString &location)
524{
525 QString resolved = location;
526 if (QFileInfo(resolved).suffix().isEmpty())
527 resolved.append(QStringLiteral(".wav"));
528
529 m_waveFormat = QAudioFormat{};
530 m_waveFormat.setSampleRate(settings.audioSampleRate() > 0 ? settings.audioSampleRate()
531 : 48000);
532 m_waveFormat.setChannelCount(settings.audioChannelCount() > 0 ? settings.audioChannelCount()
533 : 2);
534 m_waveFormat.setSampleFormat(QAudioFormat::Int16);
535 m_waveFormat.setChannelConfig(
536 QAudioFormat::defaultChannelConfigForChannelCount(m_waveFormat.channelCount()));
537
538 const QAudioDevice inputDevice = m_service && m_service->audioInput()
539 ? m_service->audioInput()->device
540 : QMediaDevices::defaultAudioInput();
541 if (inputDevice.isNull()) {
542 updateError(QMediaRecorder::ResourceError, tr("No audio input device available"));
543 return false;
544 }
545
546 auto file = std::make_unique<QFile>(resolved);
547 if (!file->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
548 updateError(QMediaRecorder::ResourceError,
549 tr("Could not open output file: %1").arg(resolved));
550 return false;
551 }
552 writeWavHeader(*file, m_waveFormat.channelCount(), m_waveFormat.sampleRate(),
553 wavBitsForSampleFormat(m_waveFormat.sampleFormat()));
554
555 auto source = std::make_unique<QAudioSource>(inputDevice, m_waveFormat);
556 source->start(file.get());
557 if (source->error() != QAudio::NoError) {
558 updateError(QMediaRecorder::ResourceError, tr("Audio capture start failed"));
559 return false;
560 }
561
562 m_waveFile = std::move(file);
563 m_waveSource = std::move(source);
564 m_wavePausedMs = 0;
565 m_waveResumeStartMs = 0;
566 m_waveTimer.restart();
567 m_waveActualLocation = QUrl::fromLocalFile(resolved);
568 actualLocationChanged(m_waveActualLocation);
569 m_waveState = QMediaRecorder::RecordingState;
570 stateChanged(QMediaRecorder::RecordingState);
571 return true;
572}
573
574void QOhosMediaRecorder::stopWave()
575{
576 if (!m_waveSource)
577 return;
578 destroyWaveRecorder();
579}
580
581void QOhosMediaRecorder::pauseWave()
582{
583 if (!m_waveSource || m_waveState != QMediaRecorder::RecordingState)
584 return;
585 m_waveSource->suspend();
586 m_wavePausedMs += (m_waveTimer.elapsed() - m_waveResumeStartMs);
587 m_waveState = QMediaRecorder::PausedState;
588 stateChanged(QMediaRecorder::PausedState);
589}
590
591void QOhosMediaRecorder::resumeWave()
592{
593 if (!m_waveSource || m_waveState != QMediaRecorder::PausedState)
594 return;
595 m_waveSource->resume();
596 m_waveResumeStartMs = m_waveTimer.elapsed();
597 m_waveState = QMediaRecorder::RecordingState;
598 stateChanged(QMediaRecorder::RecordingState);
599}
600
601void QOhosMediaRecorder::destroyWaveRecorder()
602{
603 if (m_waveSource) {
604 m_waveSource->stop();
605 m_waveSource.reset();
606 }
607 if (m_waveFile) {
608 m_waveFile->flush();
609 const qint64 dataBytes = qMax<qint64>(0, m_waveFile->size() - 44);
610 patchWavSizes(*m_waveFile, dataBytes);
611 m_waveFile->close();
612 m_waveFile.reset();
613 }
614 if (m_waveState != QMediaRecorder::StoppedState) {
615 m_waveState = QMediaRecorder::StoppedState;
616 stateChanged(QMediaRecorder::StoppedState);
617 }
618 m_waveTimer.invalidate();
619 m_wavePausedMs = 0;
620 m_waveResumeStartMs = 0;
621}
622
623QT_END_NAMESPACE
624
625#include "moc_qohosmediarecorder_p.cpp"
void recorderStateChanged(int state)
void setMetaData(const QMediaMetaData &metaData) override
QMediaRecorder::RecorderState state() const override
qint64 duration() const override
bool isLocationWritable(const QUrl &location) const override
void record(QMediaEncoderSettings &settings) override
void setCaptureSession(QPlatformMediaCaptureSession *session)
int qualityToAudioBitrate(QMediaRecorder::Quality q)
OH_AVRecorder_ContainerFormatType containerToOhos(QMediaFormat::FileFormat fmt)
OH_AVRecorder_CodecMimeType audioCodecToOhos(QMediaFormat::AudioCodec codec)
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)