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
qandroidcapturesession.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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#include "qaudioinput.h"
9#include "qaudiooutput.h"
15#include <private/qplatformaudioinput_p.h>
16#include <private/qplatformaudiooutput_p.h>
17#include <private/qmediarecorder_p.h>
18#include <private/qmediastoragelocation_p.h>
19
20#include <algorithm>
21
22QT_BEGIN_NAMESPACE
23
24QAndroidCaptureSession::QAndroidCaptureSession()
25 : QObject()
26 , m_mediaRecorder(0)
27 , m_cameraSession(0)
28 , m_duration(0)
29 , m_state(QMediaRecorder::StoppedState)
30 , m_outputFormat(AndroidMediaRecorder::DefaultOutputFormat)
31 , m_audioEncoder(AndroidMediaRecorder::DefaultAudioEncoder)
32 , m_videoEncoder(AndroidMediaRecorder::DefaultVideoEncoder)
33{
34 m_notifyTimer.setInterval(1000);
35 connect(&m_notifyTimer, &QTimer::timeout, this, &QAndroidCaptureSession::updateDuration);
36}
37
38QAndroidCaptureSession::~QAndroidCaptureSession()
39{
40 stop();
41 m_mediaRecorder = nullptr;
42 if (m_audioInput && m_audioOutput)
43 AndroidMediaPlayer::stopSoundStreaming();
44}
45
46void QAndroidCaptureSession::setCameraSession(QAndroidCameraSession *cameraSession)
47{
48 if (m_cameraSession) {
49 disconnect(m_connOpenCamera);
50 disconnect(m_connActiveChangedCamera);
51 }
52
53 m_cameraSession = cameraSession;
54 if (m_cameraSession) {
55 m_connOpenCamera = connect(cameraSession, &QAndroidCameraSession::opened,
56 this, &QAndroidCaptureSession::onCameraOpened);
57 m_connActiveChangedCamera = connect(cameraSession, &QAndroidCameraSession::activeChanged,
58 this, [this](bool isActive) {
59 if (!isActive)
60 stop();
61 });
62 }
63}
64
65void QAndroidCaptureSession::setAudioInput(QPlatformAudioInput *input)
66{
67 if (m_audioInput == input)
68 return;
69
70 if (m_audioInput) {
71 disconnect(m_audioInputChanged);
72 }
73
74 m_audioInput = input;
75
76 if (m_audioInput) {
77 m_audioInputChanged = connect(m_audioInput->q, &QAudioInput::deviceChanged, this, [this]() {
78 if (m_state == QMediaRecorder::RecordingState)
79 m_mediaRecorder->setAudioInput(m_audioInput->device.id());
80 updateStreamingState();
81 });
82 }
83 updateStreamingState();
84}
85
86void QAndroidCaptureSession::setAudioOutput(QPlatformAudioOutput *output)
87{
88 if (m_audioOutput == output)
89 return;
90
91 if (m_audioOutput)
92 disconnect(m_audioOutputChanged);
93
94 m_audioOutput = output;
95
96 if (m_audioOutput) {
97 m_audioOutputChanged = connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this,
98 [this] () {
99 AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id());
100 updateStreamingState();
101 });
102 AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id());
103 }
104 updateStreamingState();
105}
106
107void QAndroidCaptureSession::updateStreamingState()
108{
109 if (m_audioInput && m_audioOutput) {
110 AndroidMediaPlayer::startSoundStreaming(m_audioInput->device.id().toInt(),
111 m_audioOutput->device.id().toInt());
112 } else {
113 AndroidMediaPlayer::stopSoundStreaming();
114 }
115}
116
117QMediaRecorder::RecorderState QAndroidCaptureSession::state() const
118{
119 return m_state;
120}
121
122void QAndroidCaptureSession::setKeepAlive(bool keepAlive)
123{
124 if (m_cameraSession)
125 m_cameraSession->setKeepAlive(keepAlive);
126}
127
128
129void QAndroidCaptureSession::start(QMediaEncoderSettings &settings, const QUrl &outputLocation)
130{
131 if (m_state == QMediaRecorder::RecordingState)
132 return;
133
134 if (!m_cameraSession && !m_audioInput) {
135 updateError(QMediaRecorder::ResourceError, QLatin1String("No devices are set"));
136 return;
137 }
138
139 setKeepAlive(true);
140
141 const bool validCameraSession = m_cameraSession && m_cameraSession->camera();
142
143 if (validCameraSession && !qt_androidCheckCameraPermission()) {
144 updateError(QMediaRecorder::ResourceError, QLatin1String("Camera permission denied."));
145 setKeepAlive(false);
146 return;
147 }
148
149 if (m_audioInput && !qt_androidCheckMicrophonePermission()) {
150 updateError(QMediaRecorder::ResourceError, QLatin1String("Microphone permission denied."));
151 setKeepAlive(false);
152 return;
153 }
154
155 m_mediaRecorder = std::make_shared<AndroidMediaRecorder>();
156 connect(m_mediaRecorder.get(), &AndroidMediaRecorder::error, this,
157 &QAndroidCaptureSession::onError);
158 connect(m_mediaRecorder.get(), &AndroidMediaRecorder::info, this,
159 &QAndroidCaptureSession::onInfo);
160
161 applySettings(settings);
162
163 // Set audio/video sources
164 if (validCameraSession) {
165 m_cameraSession->camera()->stopPreviewSynchronous();
166 m_cameraSession->camera()->unlock();
167
168 m_mediaRecorder->setCamera(m_cameraSession->camera());
169 m_mediaRecorder->setVideoSource(AndroidMediaRecorder::Camera);
170 }
171
172 if (m_audioInput) {
173 m_mediaRecorder->setAudioInput(m_audioInput->device.id());
174 if (!m_mediaRecorder->isAudioSourceSet())
175 m_mediaRecorder->setAudioSource(AndroidMediaRecorder::DefaultAudioSource);
176 }
177
178 // Set output format
179 m_mediaRecorder->setOutputFormat(m_outputFormat);
180
181 // Set video encoder settings
182 if (validCameraSession) {
183 m_mediaRecorder->setVideoSize(settings.videoResolution());
184 m_mediaRecorder->setVideoFrameRate(qRound(settings.videoFrameRate()));
185 m_mediaRecorder->setVideoEncodingBitRate(settings.videoBitRate());
186 m_mediaRecorder->setVideoEncoder(m_videoEncoder);
187
188 // media recorder is also compensanting the mirror on front camera
189 auto rotation = m_cameraSession->currentCameraRotation();
190 if (m_cameraSession->camera()->getFacing() == AndroidCamera::CameraFacingFront)
191 rotation = (360 - rotation) % 360; // remove mirror compensation
192
193 m_mediaRecorder->setOrientationHint(rotation);
194 }
195
196 // Set audio encoder settings
197 if (m_audioInput) {
198 m_mediaRecorder->setAudioChannels(settings.audioChannelCount());
199 m_mediaRecorder->setAudioEncodingBitRate(settings.audioBitRate());
200 m_mediaRecorder->setAudioSamplingRate(settings.audioSampleRate());
201 m_mediaRecorder->setAudioEncoder(m_audioEncoder);
202 }
203
204 QString extension = settings.preferredSuffix();
205 // Set output file
206 auto location = outputLocation.toString(QUrl::PreferLocalFile);
207 QString filePath = location;
208 if (QUrl(filePath).scheme() != QLatin1String("content")) {
209 filePath = QMediaStorageLocation::generateFileName(
210 location, m_cameraSession ? QStandardPaths::MoviesLocation
211 : QStandardPaths::MusicLocation, extension);
212 }
213
214 m_usedOutputLocation = QUrl::fromLocalFile(filePath);
215 m_outputLocationIsStandard = location.isEmpty() || QFileInfo(location).isRelative();
216 m_mediaRecorder->setOutputFile(filePath);
217
218 if (validCameraSession) {
219 m_cameraSession->disableRotation();
220 }
221
222 if (!m_mediaRecorder->prepare()) {
223 updateError(QMediaRecorder::FormatError,
224 QLatin1String("Unable to prepare the media recorder."));
225 restartViewfinder();
226
227 return;
228 }
229
230 if (!m_mediaRecorder->start()) {
231 updateError(QMediaRecorder::FormatError, QMediaRecorderPrivate::msgFailedStartRecording());
232 restartViewfinder();
233
234 return;
235 }
236
237 m_elapsedTime.start();
238 m_notifyTimer.start();
239 updateDuration();
240
241 if (validCameraSession) {
242 m_cameraSession->setReadyForCapture(false);
243
244 // Preview frame callback is cleared when setting up the camera with the media recorder.
245 // We need to reset it.
246 m_cameraSession->camera()->setupPreviewFrameCallback();
247 }
248
249 m_state = QMediaRecorder::RecordingState;
250 emit stateChanged(m_state);
251}
252
253void QAndroidCaptureSession::stop(bool error)
254{
255 if (m_state == QMediaRecorder::StoppedState || m_mediaRecorder == nullptr)
256 return;
257
258 m_mediaRecorder->stop();
259 m_notifyTimer.stop();
260 updateDuration();
261 m_elapsedTime.invalidate();
262
263 m_mediaRecorder = nullptr;
264
265 if (m_cameraSession && m_cameraSession->isActive()) {
266 // Viewport needs to be restarted after recording
267 restartViewfinder();
268 }
269
270 if (!error) {
271 // if the media is saved into the standard media location, register it
272 // with the Android media scanner so it appears immediately in apps
273 // such as the gallery.
274 if (m_outputLocationIsStandard)
275 AndroidMultimediaUtils::registerMediaFile(m_usedOutputLocation.toLocalFile());
276
277 emit actualLocationChanged(m_usedOutputLocation);
278 }
279
280 m_state = QMediaRecorder::StoppedState;
281 emit stateChanged(m_state);
282}
283
284qint64 QAndroidCaptureSession::duration() const
285{
286 return m_duration;
287}
288
289void QAndroidCaptureSession::applySettings(QMediaEncoderSettings &settings)
290{
291 // container settings
292 auto fileFormat = settings.mediaFormat().fileFormat();
293 if (!m_cameraSession && fileFormat == QMediaFormat::AAC) {
294 m_outputFormat = AndroidMediaRecorder::AAC_ADTS;
295 } else if (fileFormat == QMediaFormat::Ogg) {
296 m_outputFormat = AndroidMediaRecorder::OGG;
297 } else if (fileFormat == QMediaFormat::WebM) {
298 m_outputFormat = AndroidMediaRecorder::WEBM;
299// } else if (fileFormat == QLatin1String("3gp")) {
300// m_outputFormat = AndroidMediaRecorder::THREE_GPP;
301 } else {
302 // fallback to MP4
303 m_outputFormat = AndroidMediaRecorder::MPEG_4;
304 }
305
306 // audio settings
307 if (settings.audioChannelCount() <= 0)
308 settings.setAudioChannelCount(m_defaultSettings.audioChannels);
309 if (settings.audioBitRate() <= 0)
310 settings.setAudioBitRate(m_defaultSettings.audioBitRate);
311 if (settings.audioSampleRate() <= 0)
312 settings.setAudioSampleRate(m_defaultSettings.audioSampleRate);
313
314 if (settings.audioCodec() == QMediaFormat::AudioCodec::AAC)
315 m_audioEncoder = AndroidMediaRecorder::AAC;
316 else if (settings.audioCodec() == QMediaFormat::AudioCodec::Opus)
317 m_audioEncoder = AndroidMediaRecorder::OPUS;
318 else if (settings.audioCodec() == QMediaFormat::AudioCodec::Vorbis)
319 m_audioEncoder = AndroidMediaRecorder::VORBIS;
320 else
321 m_audioEncoder = m_defaultSettings.audioEncoder;
322
323
324 // video settings
325 if (m_cameraSession && m_cameraSession->camera()) {
326 if (settings.videoResolution().isEmpty()) {
327 settings.setVideoResolution(m_defaultSettings.videoResolution);
328 } else if (!m_supportedResolutions.contains(settings.videoResolution())) {
329 // if the requested resolution is not supported, find the closest one
330 QSize reqSize = settings.videoResolution();
331 int reqPixelCount = reqSize.width() * reqSize.height();
332 QList<int> supportedPixelCounts;
333 for (int i = 0; i < m_supportedResolutions.size(); ++i) {
334 const QSize &s = m_supportedResolutions.at(i);
335 supportedPixelCounts.append(s.width() * s.height());
336 }
337 int closestIndex = qt_findClosestValue(supportedPixelCounts, reqPixelCount);
338 settings.setVideoResolution(m_supportedResolutions.at(closestIndex));
339 }
340
341 if (settings.videoFrameRate() <= 0)
342 settings.setVideoFrameRate(m_defaultSettings.videoFrameRate);
343 if (settings.videoBitRate() <= 0)
344 settings.setVideoBitRate(m_defaultSettings.videoBitRate);
345
346 if (settings.videoCodec() == QMediaFormat::VideoCodec::H264)
347 m_videoEncoder = AndroidMediaRecorder::H264;
348 else if (settings.videoCodec() == QMediaFormat::VideoCodec::H265)
349 m_videoEncoder = AndroidMediaRecorder::HEVC;
350 else if (settings.videoCodec() == QMediaFormat::VideoCodec::MPEG4)
351 m_videoEncoder = AndroidMediaRecorder::MPEG_4_SP;
352 else
353 m_videoEncoder = m_defaultSettings.videoEncoder;
354
355 }
356}
357
358void QAndroidCaptureSession::restartViewfinder()
359{
360
361 setKeepAlive(false);
362
363 if (!m_cameraSession)
364 return;
365
366 if (m_cameraSession && m_cameraSession->camera()) {
367 m_cameraSession->camera()->reconnect();
368 m_cameraSession->camera()->stopPreviewSynchronous();
369 m_cameraSession->camera()->startPreview();
370 m_cameraSession->setReadyForCapture(true);
371 m_cameraSession->enableRotation();
372 }
373
374 m_mediaRecorder = nullptr;
375}
376
377void QAndroidCaptureSession::updateDuration()
378{
379 if (m_elapsedTime.isValid())
380 m_duration = m_elapsedTime.elapsed();
381
382 emit durationChanged(m_duration);
383}
384
385void QAndroidCaptureSession::onCameraOpened()
386{
387 m_supportedResolutions.clear();
388 m_supportedFramerates.clear();
389
390 // get supported resolutions from predefined profiles
391 for (int i = 0; i < 8; ++i) {
392 CaptureProfile profile = getProfile(i);
393 if (!profile.isNull) {
394 if (i == AndroidCamcorderProfile::QUALITY_HIGH)
395 m_defaultSettings = profile;
396
397 if (!m_supportedResolutions.contains(profile.videoResolution))
398 m_supportedResolutions.append(profile.videoResolution);
399 if (!m_supportedFramerates.contains(profile.videoFrameRate))
400 m_supportedFramerates.append(profile.videoFrameRate);
401 }
402 }
403
404 std::sort(m_supportedResolutions.begin(), m_supportedResolutions.end(), qt_sizeLessThan);
405 std::sort(m_supportedFramerates.begin(), m_supportedFramerates.end());
406
407 QMediaEncoderSettings defaultSettings;
408 applySettings(defaultSettings);
409 m_cameraSession->applyResolution(defaultSettings.videoResolution());
410}
411
412QAndroidCaptureSession::CaptureProfile QAndroidCaptureSession::getProfile(int id)
413{
414 CaptureProfile profile;
415 const bool hasProfile = AndroidCamcorderProfile::hasProfile(m_cameraSession->camera()->cameraId(),
416 AndroidCamcorderProfile::Quality(id));
417
418 if (hasProfile) {
419 AndroidCamcorderProfile camProfile = AndroidCamcorderProfile::get(m_cameraSession->camera()->cameraId(),
420 AndroidCamcorderProfile::Quality(id));
421
422 profile.outputFormat = AndroidMediaRecorder::OutputFormat(camProfile.getValue(AndroidCamcorderProfile::fileFormat));
423 profile.audioEncoder = AndroidMediaRecorder::AudioEncoder(camProfile.getValue(AndroidCamcorderProfile::audioCodec));
424 profile.audioBitRate = camProfile.getValue(AndroidCamcorderProfile::audioBitRate);
425 profile.audioChannels = camProfile.getValue(AndroidCamcorderProfile::audioChannels);
426 profile.audioSampleRate = camProfile.getValue(AndroidCamcorderProfile::audioSampleRate);
427 profile.videoEncoder = AndroidMediaRecorder::VideoEncoder(camProfile.getValue(AndroidCamcorderProfile::videoCodec));
428 profile.videoBitRate = camProfile.getValue(AndroidCamcorderProfile::videoBitRate);
429 profile.videoFrameRate = camProfile.getValue(AndroidCamcorderProfile::videoFrameRate);
430 profile.videoResolution = QSize(camProfile.getValue(AndroidCamcorderProfile::videoFrameWidth),
431 camProfile.getValue(AndroidCamcorderProfile::videoFrameHeight));
432
433 if (profile.outputFormat == AndroidMediaRecorder::MPEG_4)
434 profile.outputFileExtension = QStringLiteral("mp4");
435 else if (profile.outputFormat == AndroidMediaRecorder::THREE_GPP)
436 profile.outputFileExtension = QStringLiteral("3gp");
437 else if (profile.outputFormat == AndroidMediaRecorder::AMR_NB_Format)
438 profile.outputFileExtension = QStringLiteral("amr");
439 else if (profile.outputFormat == AndroidMediaRecorder::AMR_WB_Format)
440 profile.outputFileExtension = QStringLiteral("awb");
441
442 profile.isNull = false;
443 }
444
445 return profile;
446}
447
448void QAndroidCaptureSession::onError(int what, int extra)
449{
450 Q_UNUSED(what);
451 Q_UNUSED(extra);
452 stop(true);
453 updateError(QMediaRecorder::ResourceError, QLatin1String("Unknown error."));
454}
455
456void QAndroidCaptureSession::onInfo(int what, int extra)
457{
458 Q_UNUSED(extra);
459 if (what == 800) {
460 // MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
461 stop();
462 updateError(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum duration reached."));
463 } else if (what == 801) {
464 // MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
465 stop();
466 updateError(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum file size reached."));
467 }
468}
469
470QT_END_NAMESPACE
471
472#include "moc_qandroidcapturesession_p.cpp"