1 /*
2 * Kaidan - A user-friendly XMPP client for every device!
3 *
4 * Copyright (C) 2016-2021 Kaidan developers and contributors
5 * (see the LICENSE file for a full list of copyright authors)
6 *
7 * Kaidan is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * In addition, as a special exception, the author of Kaidan gives
13 * permission to link the code of its release with the OpenSSL
14 * project's "OpenSSL" library (or with modified versions of it that
15 * use the same license as the "OpenSSL" library), and distribute the
16 * linked executables. You must obey the GNU General Public License in
17 * all respects for all of the code used other than "OpenSSL". If you
18 * modify this file, you may extend this exception to your version of
19 * the file, but you are not obligated to do so. If you do not wish to
20 * do so, delete this exception statement from your version.
21 *
22 * Kaidan is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * You should have received a copy of the GNU General Public License
28 * along with Kaidan. If not, see <http://www.gnu.org/licenses/>.
29 */
30
31 #include "MediaRecorder.h"
32 #include "Kaidan.h"
33
34 #include <QUrl>
35 #include <QFile>
36 #include <QSettings>
37
38 /*
39 * NOTES: Codecs and containers supported list are available as soon as the object is created.
40 * Resolutions, frame rates etc are populated once the objects become *ready*.
41 */
42
43 #define ENABLE_DEBUG false
44
45 #define SETTING_USER_DEFAULT QStringLiteral("User Default")
46 #define SETTING_DEFAULT_CAMERA_DEVICE_NAME QStringLiteral("Camera Device Name")
47 #define SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME QStringLiteral("Audio Input Device Name")
48
connectCamera(QCamera * camera,MediaRecorder * receiver)49 static void connectCamera(QCamera *camera, MediaRecorder *receiver) {
50 QObject::connect(camera, &QCamera::statusChanged, receiver, &MediaRecorder::readyChanged);
51 }
52
connectImageCapturer(CameraImageCapture * capturer,MediaRecorder * receiver)53 static void connectImageCapturer(CameraImageCapture *capturer, MediaRecorder *receiver) {
54 QObject::connect(capturer, &CameraImageCapture::availabilityChanged, receiver, &MediaRecorder::availabilityStatusChanged);
55 QObject::connect(capturer, QOverload<int, QCameraImageCapture::Error, const QString&>::of(&CameraImageCapture::error), receiver, &MediaRecorder::errorChanged);
56 QObject::connect(capturer, &CameraImageCapture::actualLocationChanged, receiver, &MediaRecorder::actualLocationChanged);
57 QObject::connect(capturer, &CameraImageCapture::readyForCaptureChanged, receiver, &MediaRecorder::readyChanged);
58 }
59
60 template <typename T>
connectMediaRecorder(T * recorder,MediaRecorder * receiver)61 static void connectMediaRecorder(T *recorder, MediaRecorder *receiver) {
62 QObject::connect(recorder, QOverload<QMultimedia::AvailabilityStatus>::of(&T::availabilityChanged), receiver, &MediaRecorder::availabilityStatusChanged);
63 QObject::connect(recorder, &T::stateChanged, receiver, &MediaRecorder::stateChanged);
64 QObject::connect(recorder, &T::statusChanged, receiver, &MediaRecorder::statusChanged);
65 QObject::connect(recorder, QOverload<QMediaRecorder::Error>::of(&T::error), receiver, &MediaRecorder::errorChanged);
66 QObject::connect(recorder, &T::actualLocationChanged, receiver, &MediaRecorder::actualLocationChanged);
67 QObject::connect(recorder, &T::durationChanged, receiver, &MediaRecorder::durationChanged);
68 QObject::connect(recorder, &T::mutedChanged, receiver, &MediaRecorder::mutedChanged);
69 QObject::connect(recorder, &T::volumeChanged, receiver, &MediaRecorder::volumeChanged);
70 QObject::connect(recorder, QOverload<QMultimedia::AvailabilityStatus>::of(&T::availabilityChanged), receiver, &MediaRecorder::readyChanged);
71 QObject::connect(recorder, &T::statusChanged, receiver, &MediaRecorder::readyChanged);
72 }
73
74 template <>
connectMediaRecorder(QAudioRecorder * recorder,MediaRecorder * receiver)75 void connectMediaRecorder<QAudioRecorder>(QAudioRecorder *recorder, MediaRecorder *receiver) {
76 connectMediaRecorder(qobject_cast<QMediaRecorder *>(recorder), receiver);
77 }
78
79 template <typename F>
buildCodecList(const QStringList & codecs,F toString,MediaRecorder * recorder,const QString & startsWith=QString ())80 static QStringList buildCodecList(const QStringList &codecs, F toString,
81 MediaRecorder *recorder, const QString &startsWith = QString()) {
82 QStringList entries(codecs);
83
84 if (!startsWith.isEmpty()) {
85 entries.erase(std::remove_if(entries.begin(), entries.end(), [&startsWith](const QString &entry) {
86 return !entry.startsWith(startsWith, Qt::CaseInsensitive);
87 }), entries.end());
88 }
89
90 std::sort(entries.begin(), entries.end(), [&toString, recorder](const QString &left, const QString &right) {
91 return toString(left, recorder).compare(toString(right, recorder), Qt::CaseInsensitive) < 0;
92 });
93
94 entries.prepend(QString());
95
96 return entries;
97 }
98
buildResolutionList(const QList<QSize> & resolutions)99 static QList<QSize> buildResolutionList(const QList<QSize> &resolutions) {
100 QList<QSize> entries(resolutions);
101
102 std::sort(entries.begin(), entries.end(), [](const QSize &left, const QSize &right) {
103 return left.width() == right.width()
104 ? left.height() < right.height()
105 : left.width() < right.width();
106 });
107
108 entries.prepend(QSize());
109
110 return entries;
111 }
112
buildQualityList()113 static QList<CommonEncoderSettings::EncodingQuality> buildQualityList() {
114 const QList<CommonEncoderSettings::EncodingQuality> entries {
115 CommonEncoderSettings::EncodingQuality::VeryLowQuality,
116 CommonEncoderSettings::EncodingQuality::LowQuality,
117 CommonEncoderSettings::EncodingQuality::NormalQuality,
118 CommonEncoderSettings::EncodingQuality::HighQuality,
119 CommonEncoderSettings::EncodingQuality::VeryHighQuality
120 };
121
122 return entries;
123 }
124
buildSampleRateList(const QList<int> & sampleRates)125 static QList<int> buildSampleRateList(const QList<int> &sampleRates) {
126 QList<int> entries(sampleRates);
127
128 if (entries.isEmpty()) {
129 entries = {
130 8000,
131 16000,
132 22050,
133 32000,
134 37800,
135 44100,
136 48000,
137 96000,
138 192000
139 };
140 }
141
142 std::sort(entries.begin(), entries.end(), [](const int left, const int right) {
143 return left < right;
144 });
145
146 entries.prepend(-1);
147
148 return entries;
149 }
150
buildFrameRateList(const QList<qreal> & frameRates)151 static QList<qreal> buildFrameRateList(const QList<qreal> &frameRates) {
152 QList<qreal> entries(frameRates);
153
154 std::sort(entries.begin(), entries.end(), [](const qreal left, const qreal right) {
155 return left < right;
156 });
157
158 entries.prepend(0.0);
159
160 return entries;
161 }
162
MediaRecorder(QObject * parent)163 MediaRecorder::MediaRecorder(QObject *parent)
164 : QObject(parent)
165 , m_cameraModel(new CameraModel(this))
166 , m_audioDeviceModel(new AudioDeviceModel(this))
167 , m_containerModel(new MediaSettingsContainerModel(this, this))
168 , m_imageCodecModel(new MediaSettingsImageCodecModel(this, this))
169 , m_imageResolutionModel(new MediaSettingsResolutionModel(this, this))
170 , m_imageQualityModel(new MediaSettingsQualityModel(this, this))
171 , m_audioCodecModel(new MediaSettingsAudioCodecModel(this, this))
172 , m_audioSampleRateModel(new MediaSettingsAudioSampleRateModel(this, this))
173 , m_audioQualityModel(new MediaSettingsQualityModel(this, this))
174 , m_videoCodecModel(new MediaSettingsVideoCodecModel(this, this))
175 , m_videoResolutionModel(new MediaSettingsResolutionModel(this, this))
176 , m_videoFrameRateModel(new MediaSettingsVideoFrameRateModel(this, this))
177 , m_videoQualityModel(new MediaSettingsQualityModel(this, this))
178 {
179 connect(this, &MediaRecorder::readyChanged, this, [this]() {
180 if (!isReady()) {
181 if (m_type == MediaRecorder::Type::Invalid) {
182 m_cameraModel->setCurrentIndex(-1);
183 m_audioDeviceModel->setCurrentIndex(-1);
184 m_containerModel->clear();
185
186 m_imageCodecModel->clear();
187 m_imageResolutionModel->clear();
188 m_imageQualityModel->clear();
189
190 m_audioCodecModel->clear();
191 m_audioSampleRateModel->clear();
192 m_audioQualityModel->clear();
193
194 m_videoCodecModel->clear();
195 m_videoResolutionModel->clear();
196 m_videoFrameRateModel->clear();
197 m_videoQualityModel->clear();
198 }
199
200 return;
201 }
202
203 #if ENABLE_DEBUG
204 qDebug("syncProperties Begin");
205 #endif
206
207 switch (m_type) {
208 case MediaRecorder::Type::Invalid: {
209 Q_UNREACHABLE();
210 break;
211 }
212 case MediaRecorder::Type::Image: {
213 m_cameraModel->setCurrentCamera(m_mediaSettings.camera);
214 m_imageCodecModel->setValuesAndCurrentValue(buildCodecList(m_imageCapturer->supportedImageCodecs(), imageEncoderCodec, this),
215 m_imageEncoderSettings.codec);
216 m_imageResolutionModel->setValuesAndCurrentValue(buildResolutionList(m_imageCapturer->supportedResolutions()),
217 m_imageEncoderSettings.resolution);
218 m_imageQualityModel->setValuesAndCurrentValue(buildQualityList(),
219 m_imageEncoderSettings.quality);
220 break;
221 }
222 case MediaRecorder::Type::Audio: {
223 m_audioDeviceModel->setCurrentAudioDevice(m_mediaSettings.audioInputDevice);
224 m_containerModel->setValuesAndCurrentValue(buildCodecList(m_audioRecorder->supportedContainers(), encoderContainer, this, QStringLiteral("audio")),
225 m_mediaSettings.container);
226 m_audioCodecModel->setValuesAndCurrentValue(buildCodecList(m_audioRecorder->supportedAudioCodecs(), audioEncoderCodec, this),
227 m_audioEncoderSettings.codec);
228 m_audioSampleRateModel->setValuesAndCurrentValue(buildSampleRateList(m_audioRecorder->supportedAudioSampleRates()),
229 m_audioEncoderSettings.sampleRate);
230 m_audioQualityModel->setValuesAndCurrentValue(buildQualityList(),
231 m_audioEncoderSettings.quality);
232 break;
233 }
234 case MediaRecorder::Type::Video: {
235 m_cameraModel->setCurrentCamera(m_mediaSettings.camera);
236 m_containerModel->setValuesAndCurrentValue(buildCodecList(m_videoRecorder->supportedContainers(), encoderContainer, this, QStringLiteral("video")),
237 m_mediaSettings.container);
238 m_audioCodecModel->setValuesAndCurrentValue(buildCodecList(m_videoRecorder->supportedAudioCodecs(), audioEncoderCodec, this),
239 m_audioEncoderSettings.codec);
240 m_audioSampleRateModel->setValuesAndCurrentValue(buildSampleRateList(m_videoRecorder->supportedAudioSampleRates()),
241 m_audioEncoderSettings.sampleRate);
242 m_audioQualityModel->setValuesAndCurrentValue(buildQualityList(),
243 m_audioEncoderSettings.quality);
244 m_videoCodecModel->setValuesAndCurrentValue(buildCodecList(m_videoRecorder->supportedVideoCodecs(), videoEncoderCodec, this),
245 m_videoEncoderSettings.codec);
246 m_videoResolutionModel->setValuesAndCurrentValue(buildResolutionList(m_videoRecorder->supportedResolutions()),
247 m_videoEncoderSettings.resolution);
248 m_videoFrameRateModel->setValuesAndCurrentValue(buildFrameRateList(m_videoRecorder->supportedFrameRates()),
249 m_videoEncoderSettings.frameRate);
250 m_videoQualityModel->setValuesAndCurrentValue(buildQualityList(),
251 m_videoEncoderSettings.quality);
252 break;
253 }
254 }
255
256 #if ENABLE_DEBUG
257 qDebug("syncProperties End");
258 #endif
259 });
260 }
261
~MediaRecorder()262 MediaRecorder::~MediaRecorder()
263 {
264 if (isAvailable() && !isReady()) {
265 cancel();
266 }
267 }
268
type() const269 MediaRecorder::Type MediaRecorder::type() const
270 {
271 return m_type;
272 }
273
setType(MediaRecorder::Type type)274 void MediaRecorder::setType(MediaRecorder::Type type)
275 {
276 if (m_type == type) {
277 return;
278 }
279
280 setupRecorder(type);
281 }
282
mediaObject() const283 QMediaObject *MediaRecorder::mediaObject() const
284 {
285 switch (m_type) {
286 case MediaRecorder::Type::Image:
287 case MediaRecorder::Type::Video:
288 return m_camera;
289 case MediaRecorder::Type::Invalid:
290 case MediaRecorder::Type::Audio:
291 break;
292 }
293
294 return nullptr;
295 }
296
currentSettingsKey() const297 QString MediaRecorder::currentSettingsKey() const
298 {
299 switch (m_type) {
300 case MediaRecorder::Type::Invalid:
301 break;
302 case MediaRecorder::Type::Image:
303 Q_ASSERT(!m_mediaSettings.camera.isNull());
304 return settingsKey(m_type, m_mediaSettings.camera.deviceName());
305 case MediaRecorder::Type::Audio:
306 Q_ASSERT(!m_mediaSettings.audioInputDevice.isNull());
307 return settingsKey(m_type, m_mediaSettings.audioInputDevice.deviceName());
308 case MediaRecorder::Type::Video:
309 Q_ASSERT(!m_mediaSettings.camera.isNull());
310 return settingsKey(m_type, m_mediaSettings.camera.deviceName());
311 }
312
313 return { };
314 }
315
availabilityStatus() const316 MediaRecorder::AvailabilityStatus MediaRecorder::availabilityStatus() const
317 {
318 switch (m_type) {
319 case MediaRecorder::Type::Invalid:
320 break;
321 case MediaRecorder::Type::Image:
322 return static_cast<MediaRecorder::AvailabilityStatus>(m_imageCapturer->availability());
323 case MediaRecorder::Type::Audio:
324 return static_cast<MediaRecorder::AvailabilityStatus>(m_audioRecorder->availability());
325 case MediaRecorder::Type::Video:
326 return static_cast<MediaRecorder::AvailabilityStatus>(m_videoRecorder->availability());
327 }
328
329 return MediaRecorder::AvailabilityStatus::ServiceMissing;
330 }
331
state() const332 MediaRecorder::State MediaRecorder::state() const
333 {
334 switch (m_type) {
335 case MediaRecorder::Type::Invalid:
336 case MediaRecorder::Type::Image:
337 break;
338 case MediaRecorder::Type::Audio:
339 return static_cast<MediaRecorder::State>(m_audioRecorder->state());
340 case MediaRecorder::Type::Video:
341 return static_cast<MediaRecorder::State>(m_videoRecorder->state());
342 }
343
344 return MediaRecorder::State::StoppedState;
345 }
346
status() const347 MediaRecorder::Status MediaRecorder::status() const
348 {
349 switch (m_type) {
350 case MediaRecorder::Type::Invalid:
351 case MediaRecorder::Type::Image:
352 break;
353 case MediaRecorder::Type::Audio:
354 return static_cast<MediaRecorder::Status>(m_audioRecorder->status());
355 case MediaRecorder::Type::Video:
356 return static_cast<MediaRecorder::Status>(m_videoRecorder->status());
357 }
358
359 return MediaRecorder::Status::UnavailableStatus;
360 }
361
isAvailable() const362 bool MediaRecorder::isAvailable() const
363 {
364 switch (m_type) {
365 case MediaRecorder::Type::Invalid:
366 break;
367 case MediaRecorder::Type::Image:
368 return m_imageCapturer->isAvailable();
369 case MediaRecorder::Type::Audio:
370 return m_audioRecorder->isAvailable();
371 case MediaRecorder::Type::Video:
372 return m_videoRecorder->isAvailable();
373 }
374
375 return false;
376 }
377
isReady() const378 bool MediaRecorder::isReady() const
379 {
380 switch (m_type) {
381 case MediaRecorder::Type::Invalid:
382 break;
383 case MediaRecorder::Type::Image:
384 return m_imageCapturer->isReadyForCapture();
385 case MediaRecorder::Type::Audio:
386 return isAvailable()
387 && status() >= MediaRecorder::Status::UnloadedStatus
388 && status() <= MediaRecorder::Status::LoadedStatus;
389 case MediaRecorder::Type::Video:
390 return isAvailable()
391 && m_camera->status() == QCamera::Status::ActiveStatus
392 && status() >= MediaRecorder::Status::UnloadedStatus
393 && status() <= MediaRecorder::Status::LoadedStatus;
394 }
395
396 return false;
397 }
398
error() const399 MediaRecorder::Error MediaRecorder::error() const
400 {
401 static const QMap<int, MediaRecorder::Error> capturerMapping = {
402 { QCameraImageCapture::Error::NoError, MediaRecorder::Error::NoError },
403 { QCameraImageCapture::Error::NotReadyError, MediaRecorder::Error::NotReadyError },
404 { QCameraImageCapture::Error::ResourceError, MediaRecorder::Error::ResourceError },
405 { QCameraImageCapture::Error::OutOfSpaceError, MediaRecorder::Error::OutOfSpaceError },
406 { QCameraImageCapture::Error::NotSupportedFeatureError, MediaRecorder::Error::NotSupportedFeatureError },
407 { QCameraImageCapture::Error::FormatError, MediaRecorder::Error::FormatError }
408 };
409 static const QMap<int, MediaRecorder::Error> recorderMapping = {
410 { QMediaRecorder::Error::NoError, MediaRecorder::Error::NoError },
411 { QMediaRecorder::Error::ResourceError, MediaRecorder::Error::ResourceError },
412 { QMediaRecorder::Error::FormatError, MediaRecorder::Error::FormatError },
413 { QMediaRecorder::Error::OutOfSpaceError, MediaRecorder::Error::OutOfSpaceError }
414 };
415
416 switch (m_type) {
417 case MediaRecorder::Type::Invalid:
418 break;
419 case MediaRecorder::Type::Image: {
420 const auto it = capturerMapping.constFind(m_imageCapturer->error());
421 Q_ASSERT(it != capturerMapping.constEnd());
422 return it.value();
423 }
424 case MediaRecorder::Type::Audio: {
425 const auto it = recorderMapping.constFind(m_audioRecorder->error());
426 Q_ASSERT(it != recorderMapping.constEnd());
427 return it.value();
428 }
429 case MediaRecorder::Type::Video: {
430 const auto it = recorderMapping.constFind(m_videoRecorder->error());
431 Q_ASSERT(it != recorderMapping.constEnd());
432 return it.value();
433 }
434 }
435
436 return MediaRecorder::Error::NoError;
437 }
438
errorString() const439 QString MediaRecorder::errorString() const
440 {
441 switch (m_type) {
442 case MediaRecorder::Type::Invalid:
443 break;
444 case MediaRecorder::Type::Image:
445 return m_imageCapturer->errorString();
446 case MediaRecorder::Type::Audio:
447 return m_audioRecorder->errorString();
448 case MediaRecorder::Type::Video:
449 return m_videoRecorder->errorString();
450 }
451
452 return { };
453 }
454
actualLocation() const455 QUrl MediaRecorder::actualLocation() const
456 {
457 static const auto urlExists = [](const QUrl &url) {
458 if (url.isEmpty() || (!url.scheme().isEmpty() && !url.isLocalFile())) {
459 return url;
460 }
461
462 const QUrl u(url.isLocalFile() ? url : QUrl::fromLocalFile(url.toString()));
463 return QFile::exists(u.toLocalFile()) ? u : QUrl();
464 };
465
466 switch (m_type) {
467 case MediaRecorder::Type::Invalid:
468 break;
469 case MediaRecorder::Type::Image:
470 return urlExists(m_imageCapturer->actualLocation());
471 case MediaRecorder::Type::Audio:
472 return urlExists(m_audioRecorder->actualLocation());
473 case MediaRecorder::Type::Video:
474 return urlExists(m_videoRecorder->actualLocation());
475 }
476
477 return { };
478 }
479
duration() const480 qint64 MediaRecorder::duration() const
481 {
482 switch (m_type) {
483 case MediaRecorder::Type::Invalid:
484 case MediaRecorder::Type::Image:
485 break;
486 case MediaRecorder::Type::Audio:
487 return m_audioRecorder->duration();
488 case MediaRecorder::Type::Video:
489 return m_videoRecorder->duration();
490 }
491
492 return 0;
493 }
494
isMuted() const495 bool MediaRecorder::isMuted() const
496 {
497 switch (m_type) {
498 case MediaRecorder::Type::Invalid:
499 case MediaRecorder::Type::Image:
500 break;
501 case MediaRecorder::Type::Audio:
502 return m_audioRecorder->isMuted();
503 case MediaRecorder::Type::Video:
504 return m_videoRecorder->isMuted();
505 }
506
507 return false;
508 }
509
setMuted(bool muted)510 void MediaRecorder::setMuted(bool muted)
511 {
512 switch (m_type) {
513 case MediaRecorder::Type::Invalid:
514 case MediaRecorder::Type::Image:
515 Q_UNREACHABLE();
516 break;
517 case MediaRecorder::Type::Audio:
518 m_audioRecorder->setMuted(muted);
519 break;
520 case MediaRecorder::Type::Video:
521 m_videoRecorder->setMuted(muted);
522 break;
523 }
524 }
525
volume() const526 qreal MediaRecorder::volume() const
527 {
528 switch (m_type) {
529 case MediaRecorder::Type::Invalid:
530 case MediaRecorder::Type::Image:
531 break;
532 case MediaRecorder::Type::Audio:
533 return m_audioRecorder->volume();
534 case MediaRecorder::Type::Video:
535 return m_videoRecorder->volume();
536 }
537
538 return false;
539 }
540
setVolume(qreal volume)541 void MediaRecorder::setVolume(qreal volume)
542 {
543 switch (m_type) {
544 case MediaRecorder::Type::Invalid:
545 case MediaRecorder::Type::Image:
546 Q_UNREACHABLE();
547 break;
548 case MediaRecorder::Type::Audio:
549 m_audioRecorder->setVolume(volume);
550 break;
551 case MediaRecorder::Type::Video:
552 m_videoRecorder->setVolume(volume);
553 break;
554 }
555 }
556
mediaSettings() const557 MediaSettings MediaRecorder::mediaSettings() const
558 {
559 return m_mediaSettings;
560 }
561
setMediaSettings(const MediaSettings & settings)562 void MediaRecorder::setMediaSettings(const MediaSettings &settings)
563 {
564 #if ENABLE_DEBUG
565 qDebug(Q_FUNC_INFO);
566 #endif
567
568 if (settings == m_mediaSettings) {
569 return;
570 }
571
572 const auto oldSettings = m_mediaSettings;
573
574 #if ENABLE_DEBUG
575 settings.dumpProperties(QStringLiteral("New"));
576 oldSettings.dumpProperties(QStringLiteral("Old"));
577 #endif
578
579 m_mediaSettings = settings;
580
581 if (!m_initializing) {
582 switch (m_type) {
583 case MediaRecorder::Type::Invalid:
584 Q_UNREACHABLE();
585 break;
586 case MediaRecorder::Type::Image:
587 if (oldSettings.camera != settings.camera) {
588 setupRecorder(m_type);
589 }
590
591 break;
592 case MediaRecorder::Type::Audio:
593 if (oldSettings.audioInputDevice != settings.audioInputDevice) {
594 setupRecorder(m_type);
595 } else {
596 m_audioRecorder->setContainerFormat(m_mediaSettings.container);
597 }
598
599 break;
600 case MediaRecorder::Type::Video:
601 if (oldSettings.camera != settings.camera) {
602 setupRecorder(m_type);
603 } else {
604 m_videoRecorder->setContainerFormat(m_mediaSettings.container);
605 }
606
607 break;
608 }
609
610 emit mediaSettingsChanged();
611 }
612 }
613
imageEncoderSettings() const614 ImageEncoderSettings MediaRecorder::imageEncoderSettings() const
615 {
616 return m_imageEncoderSettings;
617 }
618
setImageEncoderSettings(const ImageEncoderSettings & settings)619 void MediaRecorder::setImageEncoderSettings(const ImageEncoderSettings &settings)
620 {
621 #if ENABLE_DEBUG
622 qDebug(Q_FUNC_INFO);
623 #endif
624
625 switch (m_type) {
626 case MediaRecorder::Type::Image: {
627 if (settings != m_imageEncoderSettings) {
628 #if ENABLE_DEBUG
629 settings.dumpProperties(QStringLiteral("New"));
630 m_imageEncoderSettings.dumpProperties(QStringLiteral("Old"));
631 #endif
632
633 m_imageEncoderSettings = settings;
634
635 if (!m_initializing) {
636 m_imageCapturer->setEncodingSettings(m_imageEncoderSettings.toQImageEncoderSettings());
637 emit imageEncoderSettingsChanged();
638 }
639 }
640
641 break;
642 }
643 case MediaRecorder::Type::Invalid:
644 case MediaRecorder::Type::Audio:
645 case MediaRecorder::Type::Video:
646 Q_UNREACHABLE();
647 break;
648 }
649 }
650
audioEncoderSettings() const651 AudioEncoderSettings MediaRecorder::audioEncoderSettings() const
652 {
653 return m_audioEncoderSettings;
654 }
655
setAudioEncoderSettings(const AudioEncoderSettings & settings)656 void MediaRecorder::setAudioEncoderSettings(const AudioEncoderSettings &settings)
657 {
658 #if ENABLE_DEBUG
659 qDebug(Q_FUNC_INFO);
660 #endif
661
662 switch (m_type) {
663 case MediaRecorder::Type::Audio:
664 case MediaRecorder::Type::Video: {
665 if (settings != m_audioEncoderSettings) {
666 #if ENABLE_DEBUG
667 settings.dumpProperties(QStringLiteral("New"));
668 m_audioEncoderSettings.dumpProperties(QStringLiteral("Old"));
669 #endif
670
671 m_audioEncoderSettings = settings;
672
673 if (!m_initializing) {
674 if (m_audioRecorder) {
675 m_audioRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings());
676 } else if (m_videoRecorder) {
677 m_videoRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings());
678 }
679
680 emit audioEncoderSettingsChanged();
681
682 // Changing audio settings does not trigger ready changed.
683 if (m_type == MediaRecorder::Type::Audio) {
684 emit readyChanged();
685 }
686 }
687 }
688
689 break;
690 }
691 case MediaRecorder::Type::Image:
692 case MediaRecorder::Type::Invalid:
693 Q_UNREACHABLE();
694 break;
695 }
696 }
697
videoEncoderSettings() const698 VideoEncoderSettings MediaRecorder::videoEncoderSettings() const
699 {
700 return m_videoEncoderSettings;
701 }
702
setVideoEncoderSettings(const VideoEncoderSettings & settings)703 void MediaRecorder::setVideoEncoderSettings(const VideoEncoderSettings &settings)
704 {
705 #if ENABLE_DEBUG
706 qDebug(Q_FUNC_INFO);
707 #endif
708
709 switch (m_type) {
710 case MediaRecorder::Type::Video: {
711 if (settings != m_videoEncoderSettings) {
712 #if ENABLE_DEBUG
713 settings.dumpProperties(QStringLiteral("New"));
714 m_videoEncoderSettings.dumpProperties(QStringLiteral("Old"));
715 #endif
716
717 m_videoEncoderSettings = settings;
718
719 if (!m_initializing) {
720 m_videoRecorder->setVideoSettings(m_videoEncoderSettings.toQVideoEncoderSettings());
721 emit videoEncoderSettingsChanged();
722 }
723 }
724
725 break;
726 }
727 case MediaRecorder::Type::Image:
728 case MediaRecorder::Type::Audio:
729 case MediaRecorder::Type::Invalid:
730 Q_UNREACHABLE();
731 break;
732 }
733 }
734
resetSettingsToDefaults()735 void MediaRecorder::resetSettingsToDefaults()
736 {
737 #if ENABLE_DEBUG
738 qDebug(Q_FUNC_INFO);
739 #endif
740
741 switch (m_type) {
742 case MediaRecorder::Type::Invalid:
743 Q_UNREACHABLE();
744 break;
745 case MediaRecorder::Type::Image:
746 setMediaSettings(MediaSettings());
747 setImageEncoderSettings(ImageEncoderSettings());
748 break;
749 case MediaRecorder::Type::Audio:
750 setMediaSettings(MediaSettings());
751 setAudioEncoderSettings(AudioEncoderSettings());
752 break;
753 case MediaRecorder::Type::Video:
754 setMediaSettings(MediaSettings());
755 setAudioEncoderSettings(AudioEncoderSettings());
756 setVideoEncoderSettings(VideoEncoderSettings());
757 break;
758 }
759 }
760
resetUserSettings()761 void MediaRecorder::resetUserSettings()
762 {
763 resetSettings(userDefaultCamera(), userDefaultAudioInput());
764 }
765
saveUserSettings()766 void MediaRecorder::saveUserSettings()
767 {
768 #if ENABLE_DEBUG
769 qDebug(Q_FUNC_INFO);
770 #endif
771
772 switch (m_type) {
773 case MediaRecorder::Type::Invalid:
774 Q_UNREACHABLE();
775 break;
776 case MediaRecorder::Type::Image: {
777 QSettings &settings(*Kaidan::instance()->settings());
778
779 settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT));
780 settings.setValue(SETTING_DEFAULT_CAMERA_DEVICE_NAME, m_mediaSettings.camera.deviceName());
781 settings.endGroup();
782
783 if (!m_mediaSettings.camera.isNull() || !m_mediaSettings.audioInputDevice.isNull()) {
784 settings.beginGroup(currentSettingsKey());
785 m_mediaSettings.saveSettings(&settings);
786 m_imageEncoderSettings.saveSettings(&settings);
787 settings.endGroup();
788 }
789 break;
790 }
791 case MediaRecorder::Type::Audio: {
792 QSettings &settings(*Kaidan::instance()->settings());
793
794 settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT));
795 settings.setValue(SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME, m_mediaSettings.audioInputDevice.deviceName());
796 settings.endGroup();
797
798 settings.beginGroup(currentSettingsKey());
799 m_mediaSettings.saveSettings(&settings);
800 m_audioEncoderSettings.saveSettings(&settings);
801 settings.endGroup();
802 break;
803 }
804 case MediaRecorder::Type::Video: {
805 QSettings &settings(*Kaidan::instance()->settings());
806
807 settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT));
808 settings.setValue(SETTING_DEFAULT_CAMERA_DEVICE_NAME, m_mediaSettings.camera.deviceName());
809 settings.endGroup();
810
811 settings.beginGroup(currentSettingsKey());
812 m_mediaSettings.saveSettings(&settings);
813 m_audioEncoderSettings.saveSettings(&settings);
814 m_videoEncoderSettings.saveSettings(&settings);
815 settings.endGroup();
816 break;
817 }
818 }
819 }
820
record()821 void MediaRecorder::record()
822 {
823 switch (m_type) {
824 case MediaRecorder::Type::Invalid:
825 Q_UNREACHABLE();
826 break;
827 case MediaRecorder::Type::Image:
828 #if ENABLE_DEBUG
829 m_mediaSettings.dumpProperties("Capture");
830 m_imageEncoderSettings.dumpProperties("Capture");
831 #endif
832 m_imageCapturer->capture();
833 break;
834 case MediaRecorder::Type::Audio:
835 #if ENABLE_DEBUG
836 m_mediaSettings.dumpProperties("Capture");
837 m_audioEncoderSettings.dumpProperties("Capture");
838 #endif
839 m_audioRecorder->record();
840 break;
841 case MediaRecorder::Type::Video:
842 #if ENABLE_DEBUG
843 m_mediaSettings.dumpProperties("Capture");
844 m_audioEncoderSettings.dumpProperties("Capture");
845 m_videoEncoderSettings.dumpProperties("Capture");
846 #endif
847 m_videoRecorder->record();
848 break;
849 }
850 }
851
pause()852 void MediaRecorder::pause()
853 {
854 switch (m_type) {
855 case MediaRecorder::Type::Invalid:
856 case MediaRecorder::Type::Image:
857 Q_UNREACHABLE();
858 break;
859 case MediaRecorder::Type::Audio:
860 m_audioRecorder->pause();
861 break;
862 case MediaRecorder::Type::Video:
863 m_videoRecorder->pause();
864 break;
865 }
866 }
867
stop()868 void MediaRecorder::stop()
869 {
870 switch (m_type) {
871 case MediaRecorder::Type::Invalid:
872 case MediaRecorder::Type::Image:
873 Q_UNREACHABLE();
874 break;
875 case MediaRecorder::Type::Audio:
876 m_audioRecorder->stop();
877 break;
878 case MediaRecorder::Type::Video:
879 m_videoRecorder->stop();
880 break;
881 }
882 }
883
cancel()884 void MediaRecorder::cancel()
885 {
886 switch (m_type) {
887 case MediaRecorder::Type::Invalid:
888 Q_UNREACHABLE();
889 break;
890 case MediaRecorder::Type::Image:
891 m_imageCapturer->cancelCapture();
892 deleteActualLocation();
893 break;
894 case MediaRecorder::Type::Audio:
895 case MediaRecorder::Type::Video: {
896 if (state() == MediaRecorder::State::RecordingState) {
897 stop();
898 }
899
900 deleteActualLocation();
901 break;
902 }
903 }
904 }
905
setMediaObject(QMediaObject * object)906 bool MediaRecorder::setMediaObject(QMediaObject *object)
907 {
908 Q_UNUSED(object);
909 return false;
910 }
911
settingsKey(MediaRecorder::Type type,const QString & deviceName) const912 QString MediaRecorder::settingsKey(MediaRecorder::Type type, const QString &deviceName) const
913 {
914 Q_ASSERT(type != MediaRecorder::Type::Invalid);
915
916 const QString deviceKey = QString(deviceName)
917 .replace(QLatin1Char('/'), QLatin1Char(' '))
918 .replace(QLatin1Char('\\'), QLatin1Char(' '))
919 .replace(QLatin1Char('('), QLatin1Char(' '))
920 .replace(QLatin1Char(')'), QLatin1Char(' '))
921 .replace(QLatin1Char('='), QLatin1Char(' '))
922 .replace(QLatin1Char(':'), QLatin1Char(' '))
923 .replace(QLatin1Char('\n'), QLatin1Char(' '))
924 .simplified();
925 return QString::fromLatin1("Multimedia/%1/%2").arg(Enums::toString(type), deviceKey);
926 }
927
userDefaultCamera() const928 CameraInfo MediaRecorder::userDefaultCamera() const
929 {
930 if (m_type == MediaRecorder::Type::Invalid) {
931 return CameraInfo();
932 }
933
934 QSettings &settings(*Kaidan::instance()->settings());
935 CameraInfo cameraInfo = m_cameraModel->defaultCamera();
936
937 settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT));
938
939 if (settings.contains(SETTING_DEFAULT_CAMERA_DEVICE_NAME)) {
940 const CameraInfo userCamera(settings.value(SETTING_DEFAULT_CAMERA_DEVICE_NAME).toString());
941
942 if (!userCamera.isNull()) {
943 cameraInfo = userCamera;
944 }
945 }
946
947 settings.endGroup();
948
949 return cameraInfo;
950 }
951
userDefaultAudioInput() const952 AudioDeviceInfo MediaRecorder::userDefaultAudioInput() const
953 {
954 if (m_type == MediaRecorder::Type::Invalid) {
955 return AudioDeviceInfo();
956 }
957
958 QSettings &settings(*Kaidan::instance()->settings());
959 AudioDeviceInfo audioInput = m_audioDeviceModel->defaultAudioInputDevice();
960
961 settings.beginGroup(settingsKey(m_type, SETTING_USER_DEFAULT));
962
963 if (settings.contains(SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME)) {
964 const AudioDeviceInfo userAudioInput(AudioDeviceModel::audioInputDevice(settings.value(SETTING_DEFAULT_AUDIO_INPUT_DEVICE_NAME).toString()));
965
966 if (!userAudioInput.isNull()) {
967 audioInput = userAudioInput;
968 }
969 }
970
971 settings.endGroup();
972
973 return audioInput;
974 }
975
resetSettings(const CameraInfo & camera,const AudioDeviceInfo & audioInput)976 void MediaRecorder::resetSettings(const CameraInfo &camera, const AudioDeviceInfo &audioInput)
977 {
978 #if ENABLE_DEBUG
979 qDebug(Q_FUNC_INFO);
980 #endif
981
982 switch (m_type) {
983 case MediaRecorder::Type::Invalid:
984 m_mediaSettings = MediaSettings();
985 m_imageEncoderSettings = ImageEncoderSettings();
986 m_audioEncoderSettings = AudioEncoderSettings();
987 m_videoEncoderSettings = VideoEncoderSettings();
988 break;
989 case MediaRecorder::Type::Image: {
990 QSettings &settings(*Kaidan::instance()->settings());
991 MediaSettings mediaSettings(camera, AudioDeviceInfo());
992 ImageEncoderSettings imageSettings;
993
994 settings.beginGroup(settingsKey(m_type, mediaSettings.camera.deviceName()));
995 mediaSettings.loadSettings(&settings);
996 imageSettings.loadSettings(&settings);
997 settings.endGroup();
998
999 setMediaSettings(mediaSettings);
1000 setImageEncoderSettings(imageSettings);
1001 break;
1002 }
1003 case MediaRecorder::Type::Audio: {
1004 QSettings &settings(*Kaidan::instance()->settings());
1005 MediaSettings mediaSettings(CameraInfo(), audioInput);
1006 AudioEncoderSettings audioSettings;
1007
1008 settings.beginGroup(settingsKey(m_type, mediaSettings.audioInputDevice.deviceName()));
1009 mediaSettings.loadSettings(&settings);
1010 audioSettings.loadSettings(&settings);
1011 settings.endGroup();
1012
1013 setMediaSettings(mediaSettings);
1014 setAudioEncoderSettings(audioSettings);
1015 break;
1016 }
1017 case MediaRecorder::Type::Video: {
1018 QSettings &settings(*Kaidan::instance()->settings());
1019 MediaSettings mediaSettings(camera, AudioDeviceInfo());
1020 AudioEncoderSettings audioSettings;
1021 VideoEncoderSettings videoSettings;
1022
1023 settings.beginGroup(settingsKey(m_type, mediaSettings.camera.deviceName()));
1024 mediaSettings.loadSettings(&settings);
1025 audioSettings.loadSettings(&settings);
1026 videoSettings.loadSettings(&settings);
1027 settings.endGroup();
1028
1029 setMediaSettings(mediaSettings);
1030 setAudioEncoderSettings(audioSettings);
1031 setVideoEncoderSettings(videoSettings);
1032 break;
1033 }
1034 }
1035 }
1036
setupCamera()1037 void MediaRecorder::setupCamera()
1038 {
1039 // If there is no camera, there is no need to work any further
1040 if (m_mediaSettings.camera.isNull())
1041 return;
1042
1043 m_camera = new QCamera(m_mediaSettings.camera, this);
1044
1045 switch (m_type) {
1046 case MediaRecorder::Type::Invalid:
1047 case MediaRecorder::Type::Audio:
1048 Q_UNREACHABLE();
1049 break;
1050 case MediaRecorder::Type::Image:
1051 m_camera->setCaptureMode(QCamera::CaptureMode::CaptureStillImage);
1052 m_camera->imageProcessing()->setWhiteBalanceMode(QCameraImageProcessing::WhiteBalanceMode::WhiteBalanceFlash);
1053 m_camera->exposure()->setExposureCompensation(-1.0);
1054 m_camera->exposure()->setExposureMode(QCameraExposure::ExposureMode::ExposurePortrait);
1055 m_camera->exposure()->setFlashMode(QCameraExposure::FlashMode::FlashRedEyeReduction);
1056 break;
1057 case MediaRecorder::Type::Video:
1058 m_camera->setCaptureMode(QCamera::CaptureMode::CaptureVideo);
1059 m_camera->imageProcessing()->setWhiteBalanceMode(QCameraImageProcessing::WhiteBalanceMode::WhiteBalanceAuto);
1060 m_camera->exposure()->setExposureCompensation(0);
1061 m_camera->exposure()->setExposureMode(QCameraExposure::ExposureMode::ExposureAuto);
1062 m_camera->exposure()->setFlashMode(QCameraExposure::FlashMode::FlashOff);
1063 break;
1064 }
1065
1066 connectCamera(m_camera, this);
1067 }
1068
setupCapturer()1069 void MediaRecorder::setupCapturer()
1070 {
1071 if (m_camera)
1072 m_imageCapturer = new CameraImageCapture(m_camera, this);
1073 else
1074 m_imageCapturer = new CameraImageCapture({});
1075
1076 m_imageCapturer->setEncodingSettings(m_imageEncoderSettings.toQImageEncoderSettings());
1077
1078 connectImageCapturer(m_imageCapturer, this);
1079 }
1080
setupAudioRecorder()1081 void MediaRecorder::setupAudioRecorder()
1082 {
1083 m_audioRecorder = new QAudioRecorder(this);
1084 m_audioRecorder->setAudioInput(m_mediaSettings.audioInputDevice.deviceName());
1085 m_audioRecorder->setContainerFormat(m_mediaSettings.container);
1086 m_audioRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings());
1087
1088 connectMediaRecorder(m_audioRecorder, this);
1089 }
1090
setupVideoRecorder()1091 void MediaRecorder::setupVideoRecorder()
1092 {
1093 if (m_camera)
1094 m_videoRecorder = new QMediaRecorder(m_camera, this);
1095 else
1096 m_videoRecorder = new QMediaRecorder({}, this);
1097
1098 m_videoRecorder->setContainerFormat(m_mediaSettings.container);
1099 m_videoRecorder->setAudioSettings(m_audioEncoderSettings.toQAudioEncoderSettings());
1100 m_videoRecorder->setVideoSettings(m_videoEncoderSettings.toQVideoEncoderSettings());
1101
1102 connectMediaRecorder(m_videoRecorder, this);
1103 }
1104
setupRecorder(MediaRecorder::Type type)1105 void MediaRecorder::setupRecorder(MediaRecorder::Type type)
1106 {
1107 if (isAvailable() && !isReady()) {
1108 cancel();
1109 }
1110
1111 delete m_imageCapturer; m_imageCapturer = nullptr;
1112 delete m_audioRecorder; m_audioRecorder = nullptr;
1113 delete m_videoRecorder; m_videoRecorder = nullptr;
1114 delete m_camera; m_camera = nullptr;
1115
1116 if (m_type != type) {
1117 m_type = type;
1118 m_initializing = true;
1119 resetUserSettings();
1120 m_initializing = false;
1121 } else {
1122 m_initializing = true;
1123 resetSettings(m_mediaSettings.camera, m_mediaSettings.audioInputDevice);
1124 m_initializing = false;
1125 }
1126
1127 switch (m_type) {
1128 case MediaRecorder::Type::Invalid:
1129 emit imageEncoderSettingsChanged();
1130 emit audioEncoderSettingsChanged();
1131 emit videoEncoderSettingsChanged();
1132 break;
1133 case MediaRecorder::Type::Image:
1134 setupCamera();
1135 setupCapturer();
1136 break;
1137 case MediaRecorder::Type::Audio:
1138 setupAudioRecorder();
1139 break;
1140 case MediaRecorder::Type::Video:
1141 setupCamera();
1142 setupVideoRecorder();
1143 break;
1144 }
1145
1146 emit typeChanged();
1147 emit availabilityStatusChanged();
1148 emit stateChanged();
1149 emit statusChanged();
1150 emit readyChanged();
1151 emit errorChanged();
1152 emit actualLocationChanged();
1153 emit durationChanged();
1154 emit mutedChanged();
1155 emit volumeChanged();
1156
1157 if (m_camera && m_camera->state() != QCamera::State::ActiveState) {
1158 m_camera->start();
1159 }
1160 }
1161
deleteActualLocation()1162 void MediaRecorder::deleteActualLocation()
1163 {
1164 const QUrl url(actualLocation());
1165
1166 if (!url.isEmpty() && url.isLocalFile()) {
1167 const QString filePath(url.toLocalFile());
1168 QFile file(filePath);
1169
1170 if (file.exists()) {
1171 if (file.remove()) {
1172 emit actualLocationChanged();
1173 } else {
1174 qWarning("Can not delete record '%s'", qUtf8Printable(filePath));
1175 }
1176 }
1177 }
1178 }
1179
encoderContainer(const QString & container,const void * userData)1180 QString MediaRecorder::encoderContainer(const QString &container, const void *userData)
1181 {
1182 const auto *recorder = static_cast<const MediaRecorder *>(userData);
1183 const auto *mediaRecorder = recorder->m_audioRecorder ? recorder->m_audioRecorder : recorder->m_videoRecorder;
1184 return container.isEmpty()
1185 ? tr("Default")
1186 : QString::fromLatin1("%1 (%2)").arg(mediaRecorder->containerDescription(container), container);
1187 }
1188
encoderResolution(const QSize & size,const void * userData)1189 QString MediaRecorder::encoderResolution(const QSize &size, const void *userData)
1190 {
1191 Q_UNUSED(userData);
1192 return size.isEmpty()
1193 ? tr("Default")
1194 : QString::fromLatin1("%1x%2").arg(QString::number(size.width()), QString::number(size.height()));
1195 }
1196
encoderQuality(const CommonEncoderSettings::EncodingQuality quality,const void * userData)1197 QString MediaRecorder::encoderQuality(const CommonEncoderSettings::EncodingQuality quality, const void *userData)
1198 {
1199 Q_UNUSED(userData);
1200 return CommonEncoderSettings::toString(quality);
1201 }
1202
imageEncoderCodec(const QString & codec,const void * userData)1203 QString MediaRecorder::imageEncoderCodec(const QString &codec, const void *userData)
1204 {
1205 const auto *recorder = static_cast<const MediaRecorder *>(userData);
1206 const auto *capturer = recorder->m_imageCapturer;
1207 return codec.isEmpty()
1208 ? tr("Default")
1209 : QString::fromLatin1("%1 (%2)").arg(capturer->imageCodecDescription(codec), codec);
1210 }
1211
audioEncoderCodec(const QString & codec,const void * userData)1212 QString MediaRecorder::audioEncoderCodec(const QString &codec, const void *userData)
1213 {
1214 const auto *recorder = static_cast<const MediaRecorder *>(userData);
1215 const auto *mediaRecorder = recorder->m_audioRecorder ? recorder->m_audioRecorder : recorder->m_videoRecorder;
1216 return codec.isEmpty()
1217 ? tr("Default")
1218 : QString::fromLatin1("%1 (%2)").arg(mediaRecorder->audioCodecDescription(codec), codec);
1219 }
1220
audioEncoderSampleRate(const int sampleRate,const void * userData)1221 QString MediaRecorder::audioEncoderSampleRate(const int sampleRate, const void *userData)
1222 {
1223 Q_UNUSED(userData);
1224 return sampleRate == -1
1225 ? tr("Default")
1226 : QString::fromLatin1("%1 Hz").arg(sampleRate);
1227 }
1228
videoEncoderCodec(const QString & codec,const void * userData)1229 QString MediaRecorder::videoEncoderCodec(const QString &codec, const void *userData)
1230 {
1231 const auto *recorder = static_cast<const MediaRecorder *>(userData);
1232 const auto *videoRecorder = recorder->m_videoRecorder;
1233 return codec.isEmpty()
1234 ? tr("Default")
1235 : QString::fromLatin1("%1 (%2)").arg(videoRecorder->videoCodecDescription(codec), codec);
1236 }
1237
videoEncoderFrameRate(const qreal frameRate,const void * userData)1238 QString MediaRecorder::videoEncoderFrameRate(const qreal frameRate, const void *userData)
1239 {
1240 Q_UNUSED(userData);
1241 return qIsNull(frameRate)
1242 ? tr("Default")
1243 : QString::fromLatin1("%1 FPS").arg(frameRate);
1244 }
1245