Qt 6.x
The Qt SDK
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
6#include "androidcamera_p.h"
8#include "qaudioinput.h"
9#include "qaudiooutput.h"
14#include "qandroidglobal_p.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#include <QtCore/qmimetype.h>
20
21#include <algorithm>
22
24
26 : QObject()
27 , m_mediaRecorder(0)
28 , m_cameraSession(0)
29 , m_duration(0)
30 , m_state(QMediaRecorder::StoppedState)
31 , m_outputFormat(AndroidMediaRecorder::DefaultOutputFormat)
32 , m_audioEncoder(AndroidMediaRecorder::DefaultAudioEncoder)
33 , m_videoEncoder(AndroidMediaRecorder::DefaultVideoEncoder)
34{
35 m_notifyTimer.setInterval(1000);
36 connect(&m_notifyTimer, &QTimer::timeout, this, &QAndroidCaptureSession::updateDuration);
37}
38
40{
41 stop();
42 m_mediaRecorder = nullptr;
43 if (m_audioInput && m_audioOutput)
45}
46
48{
49 if (m_cameraSession) {
50 disconnect(m_connOpenCamera);
51 disconnect(m_connActiveChangedCamera);
52 }
53
54 m_cameraSession = cameraSession;
55 if (m_cameraSession) {
56 m_connOpenCamera = connect(cameraSession, &QAndroidCameraSession::opened,
57 this, &QAndroidCaptureSession::onCameraOpened);
58 m_connActiveChangedCamera = connect(cameraSession, &QAndroidCameraSession::activeChanged,
59 this, [this](bool isActive) {
60 if (!isActive)
61 stop();
62 });
63 }
64}
65
67{
68 if (m_audioInput == input)
69 return;
70
71 if (m_audioInput) {
72 disconnect(m_audioInputChanged);
73 }
74
75 m_audioInput = input;
76
77 if (m_audioInput) {
78 m_audioInputChanged = connect(m_audioInput->q, &QAudioInput::deviceChanged, this, [this]() {
79 if (m_state == QMediaRecorder::RecordingState)
80 m_mediaRecorder->setAudioInput(m_audioInput->device.id());
81 updateStreamingState();
82 });
83 }
84 updateStreamingState();
85}
86
88{
89 if (m_audioOutput == output)
90 return;
91
92 if (m_audioOutput)
93 disconnect(m_audioOutputChanged);
94
95 m_audioOutput = output;
96
97 if (m_audioOutput) {
98 m_audioOutputChanged = connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this,
99 [this] () {
100 AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id());
101 updateStreamingState();
102 });
104 }
105 updateStreamingState();
106}
107
108void QAndroidCaptureSession::updateStreamingState()
109{
110 if (m_audioInput && m_audioOutput) {
112 m_audioOutput->device.id().toInt());
113 } else {
115 }
116}
117
119{
120 return m_state;
121}
122
123void QAndroidCaptureSession::setKeepAlive(bool keepAlive)
124{
125 if (m_cameraSession)
126 m_cameraSession->setKeepAlive(keepAlive);
127}
128
129
131{
132 if (m_state == QMediaRecorder::RecordingState)
133 return;
134
135 if (!m_cameraSession && !m_audioInput) {
137 return;
138 }
139
140 setKeepAlive(true);
141
142 const bool validCameraSession = m_cameraSession && m_cameraSession->camera();
143
144 if (validCameraSession && !qt_androidCheckCameraPermission()) {
145 emit error(QMediaRecorder::ResourceError, QLatin1String("Camera permission denied."));
146 setKeepAlive(false);
147 return;
148 }
149
150 if (m_audioInput && !qt_androidCheckMicrophonePermission()) {
151 emit error(QMediaRecorder::ResourceError, QLatin1String("Microphone permission denied."));
152 setKeepAlive(false);
153 return;
154 }
155
156 m_mediaRecorder = std::make_shared<AndroidMediaRecorder>();
157 connect(m_mediaRecorder.get(), &AndroidMediaRecorder::error, this,
158 &QAndroidCaptureSession::onError);
159 connect(m_mediaRecorder.get(), &AndroidMediaRecorder::info, this,
160 &QAndroidCaptureSession::onInfo);
161
162 applySettings(settings);
163
164 // Set audio/video sources
165 if (validCameraSession) {
166 m_cameraSession->camera()->stopPreviewSynchronous();
167 m_cameraSession->camera()->unlock();
168
169 m_mediaRecorder->setCamera(m_cameraSession->camera());
170 m_mediaRecorder->setVideoSource(AndroidMediaRecorder::Camera);
171 }
172
173 if (m_audioInput) {
174 m_mediaRecorder->setAudioSource(AndroidMediaRecorder::Camcorder);
175 m_mediaRecorder->setAudioInput(m_audioInput->device.id());
176 if (!m_mediaRecorder->isAudioSourceSet())
177 m_mediaRecorder->setAudioSource(AndroidMediaRecorder::DefaultAudioSource);
178 }
179
180 // Set output format
181 m_mediaRecorder->setOutputFormat(m_outputFormat);
182
183 // Set video encoder settings
184 if (validCameraSession) {
185 m_mediaRecorder->setVideoSize(settings.videoResolution());
186 m_mediaRecorder->setVideoFrameRate(qRound(settings.videoFrameRate()));
187 m_mediaRecorder->setVideoEncodingBitRate(settings.videoBitRate());
188 m_mediaRecorder->setVideoEncoder(m_videoEncoder);
189
190 // media recorder is also compensanting the mirror on front camera
191 auto rotation = m_cameraSession->currentCameraRotation();
192 if (m_cameraSession->camera()->getFacing() == AndroidCamera::CameraFacingFront)
193 rotation = (360 - rotation) % 360; // remove mirror compensation
194
195 m_mediaRecorder->setOrientationHint(rotation);
196 }
197
198 // Set audio encoder settings
199 if (m_audioInput) {
200 m_mediaRecorder->setAudioChannels(settings.audioChannelCount());
201 m_mediaRecorder->setAudioEncodingBitRate(settings.audioBitRate());
202 m_mediaRecorder->setAudioSamplingRate(settings.audioSampleRate());
203 m_mediaRecorder->setAudioEncoder(m_audioEncoder);
204 }
205
206 QString extension = settings.mimeType().preferredSuffix();
207 // Set output file
208 auto location = outputLocation.toString(QUrl::PreferLocalFile);
209 QString filePath = location;
210 if (QUrl(filePath).scheme() != QLatin1String("content")) {
214 }
215
216 m_usedOutputLocation = QUrl::fromLocalFile(filePath);
217 m_outputLocationIsStandard = location.isEmpty() || QFileInfo(location).isRelative();
218 m_mediaRecorder->setOutputFile(filePath);
219
220 // Even though the Android doc explicitly says that calling MediaRecorder.setPreviewDisplay()
221 // is not necessary when the Camera already has a Surface, it doesn't actually work on some
222 // devices. For example on the Samsung Galaxy Tab 2, the camera server dies after prepare()
223 // and start() if MediaRecorder.setPreviewDisplay() is not called.
224 if (validCameraSession) {
225
226 if (m_cameraSession->videoOutput()) {
227 // When using a SurfaceTexture, we need to pass a new one to the MediaRecorder, not the
228 // same one that is set on the Camera or it will crash, hence the reset().
229 m_cameraSession->videoOutput()->reset();
230 if (m_cameraSession->videoOutput()->surfaceTexture())
231 m_mediaRecorder->setSurfaceTexture(
232 m_cameraSession->videoOutput()->surfaceTexture());
233 else if (m_cameraSession->videoOutput()->surfaceHolder())
234 m_mediaRecorder->setSurfaceHolder(m_cameraSession->videoOutput()->surfaceHolder());
235 }
236
237 m_cameraSession->disableRotation();
238 }
239
240 if (!m_mediaRecorder->prepare()) {
241 emit error(QMediaRecorder::FormatError, QLatin1String("Unable to prepare the media recorder."));
242 restartViewfinder();
243
244 return;
245 }
246
247 if (!m_mediaRecorder->start()) {
250 restartViewfinder();
251
252 return;
253 }
254
255 m_elapsedTime.start();
256 m_notifyTimer.start();
257 updateDuration();
258
259 if (validCameraSession) {
260 m_cameraSession->setReadyForCapture(false);
261
262 // Preview frame callback is cleared when setting up the camera with the media recorder.
263 // We need to reset it.
264 m_cameraSession->camera()->setupPreviewFrameCallback();
265 }
266
268 emit stateChanged(m_state);
269}
270
272{
273 if (m_state == QMediaRecorder::StoppedState || m_mediaRecorder == nullptr)
274 return;
275
276 m_mediaRecorder->stop();
277 m_notifyTimer.stop();
278 updateDuration();
279 m_elapsedTime.invalidate();
280
281 m_mediaRecorder = nullptr;
282
283 if (m_cameraSession && m_cameraSession->isActive()) {
284 // Viewport needs to be restarted after recording
285 restartViewfinder();
286 }
287
288 if (!error) {
289 // if the media is saved into the standard media location, register it
290 // with the Android media scanner so it appears immediately in apps
291 // such as the gallery.
292 if (m_outputLocationIsStandard)
294
295 emit actualLocationChanged(m_usedOutputLocation);
296 }
297
299 emit stateChanged(m_state);
300}
301
303{
304 return m_duration;
305}
306
307void QAndroidCaptureSession::applySettings(QMediaEncoderSettings &settings)
308{
309 // container settings
310 auto fileFormat = settings.mediaFormat().fileFormat();
311 if (!m_cameraSession && fileFormat == QMediaFormat::AAC) {
312 m_outputFormat = AndroidMediaRecorder::AAC_ADTS;
313 } else if (fileFormat == QMediaFormat::Ogg) {
314 m_outputFormat = AndroidMediaRecorder::OGG;
315 } else if (fileFormat == QMediaFormat::WebM) {
316 m_outputFormat = AndroidMediaRecorder::WEBM;
317// } else if (fileFormat == QLatin1String("3gp")) {
318// m_outputFormat = AndroidMediaRecorder::THREE_GPP;
319 } else {
320 // fallback to MP4
321 m_outputFormat = AndroidMediaRecorder::MPEG_4;
322 }
323
324 // audio settings
325 if (settings.audioChannelCount() <= 0)
326 settings.setAudioChannelCount(m_defaultSettings.audioChannels);
327 if (settings.audioBitRate() <= 0)
328 settings.setAudioBitRate(m_defaultSettings.audioBitRate);
329 if (settings.audioSampleRate() <= 0)
330 settings.setAudioSampleRate(m_defaultSettings.audioSampleRate);
331
332 if (settings.audioCodec() == QMediaFormat::AudioCodec::AAC)
333 m_audioEncoder = AndroidMediaRecorder::AAC;
334 else if (settings.audioCodec() == QMediaFormat::AudioCodec::Opus)
335 m_audioEncoder = AndroidMediaRecorder::OPUS;
336 else if (settings.audioCodec() == QMediaFormat::AudioCodec::Vorbis)
337 m_audioEncoder = AndroidMediaRecorder::VORBIS;
338 else
339 m_audioEncoder = m_defaultSettings.audioEncoder;
340
341
342 // video settings
343 if (m_cameraSession && m_cameraSession->camera()) {
344 if (settings.videoResolution().isEmpty()) {
345 settings.setVideoResolution(m_defaultSettings.videoResolution);
346 } else if (!m_supportedResolutions.contains(settings.videoResolution())) {
347 // if the requested resolution is not supported, find the closest one
348 QSize reqSize = settings.videoResolution();
349 int reqPixelCount = reqSize.width() * reqSize.height();
350 QList<int> supportedPixelCounts;
351 for (int i = 0; i < m_supportedResolutions.size(); ++i) {
352 const QSize &s = m_supportedResolutions.at(i);
353 supportedPixelCounts.append(s.width() * s.height());
354 }
355 int closestIndex = qt_findClosestValue(supportedPixelCounts, reqPixelCount);
356 settings.setVideoResolution(m_supportedResolutions.at(closestIndex));
357 }
358
359 if (settings.videoFrameRate() <= 0)
360 settings.setVideoFrameRate(m_defaultSettings.videoFrameRate);
361 if (settings.videoBitRate() <= 0)
362 settings.setVideoBitRate(m_defaultSettings.videoBitRate);
363
364 if (settings.videoCodec() == QMediaFormat::VideoCodec::H264)
365 m_videoEncoder = AndroidMediaRecorder::H264;
366 else if (settings.videoCodec() == QMediaFormat::VideoCodec::H265)
367 m_videoEncoder = AndroidMediaRecorder::HEVC;
368 else if (settings.videoCodec() == QMediaFormat::VideoCodec::MPEG4)
369 m_videoEncoder = AndroidMediaRecorder::MPEG_4_SP;
370 else
371 m_videoEncoder = m_defaultSettings.videoEncoder;
372
373 }
374}
375
376void QAndroidCaptureSession::restartViewfinder()
377{
378
379 setKeepAlive(false);
380
381 if (!m_cameraSession)
382 return;
383
384 if (m_cameraSession && m_cameraSession->camera()) {
385 m_cameraSession->camera()->reconnect();
386
387 // This is not necessary on most devices, but it crashes on some if we don't stop the
388 // preview and reset the preview display on the camera when recording is over.
389 m_cameraSession->camera()->stopPreviewSynchronous();
390
391 if (m_cameraSession->videoOutput()) {
392 // When using a SurfaceTexture, we need to pass a new one to the MediaRecorder, not the
393 // same one that is set on the Camera or it will crash, hence the reset().
394 m_cameraSession->videoOutput()->reset();
395 if (m_cameraSession->videoOutput()->surfaceTexture())
396 m_cameraSession->camera()->setPreviewTexture(
397 m_cameraSession->videoOutput()->surfaceTexture());
398 else if (m_cameraSession->videoOutput()->surfaceHolder())
399 m_cameraSession->camera()->setPreviewDisplay(
400 m_cameraSession->videoOutput()->surfaceHolder());
401 }
402
403 m_cameraSession->camera()->startPreview();
404 m_cameraSession->setReadyForCapture(true);
405 m_cameraSession->enableRotation();
406 }
407
408 m_mediaRecorder = nullptr;
409}
410
411void QAndroidCaptureSession::updateDuration()
412{
413 if (m_elapsedTime.isValid())
414 m_duration = m_elapsedTime.elapsed();
415
416 emit durationChanged(m_duration);
417}
418
419void QAndroidCaptureSession::onCameraOpened()
420{
421 m_supportedResolutions.clear();
422 m_supportedFramerates.clear();
423
424 // get supported resolutions from predefined profiles
425 for (int i = 0; i < 8; ++i) {
426 CaptureProfile profile = getProfile(i);
427 if (!profile.isNull) {
429 m_defaultSettings = profile;
430
431 if (!m_supportedResolutions.contains(profile.videoResolution))
432 m_supportedResolutions.append(profile.videoResolution);
433 if (!m_supportedFramerates.contains(profile.videoFrameRate))
434 m_supportedFramerates.append(profile.videoFrameRate);
435 }
436 }
437
438 std::sort(m_supportedResolutions.begin(), m_supportedResolutions.end(), qt_sizeLessThan);
439 std::sort(m_supportedFramerates.begin(), m_supportedFramerates.end());
440
441 QMediaEncoderSettings defaultSettings;
442 applySettings(defaultSettings);
443 m_cameraSession->applyResolution(defaultSettings.videoResolution());
444}
445
446QAndroidCaptureSession::CaptureProfile QAndroidCaptureSession::getProfile(int id)
447{
448 CaptureProfile profile;
449 const bool hasProfile = AndroidCamcorderProfile::hasProfile(m_cameraSession->camera()->cameraId(),
451
452 if (hasProfile) {
453 AndroidCamcorderProfile camProfile = AndroidCamcorderProfile::get(m_cameraSession->camera()->cameraId(),
455
458 profile.audioBitRate = camProfile.getValue(AndroidCamcorderProfile::audioBitRate);
459 profile.audioChannels = camProfile.getValue(AndroidCamcorderProfile::audioChannels);
460 profile.audioSampleRate = camProfile.getValue(AndroidCamcorderProfile::audioSampleRate);
462 profile.videoBitRate = camProfile.getValue(AndroidCamcorderProfile::videoBitRate);
463 profile.videoFrameRate = camProfile.getValue(AndroidCamcorderProfile::videoFrameRate);
464 profile.videoResolution = QSize(camProfile.getValue(AndroidCamcorderProfile::videoFrameWidth),
466
467 if (profile.outputFormat == AndroidMediaRecorder::MPEG_4)
468 profile.outputFileExtension = QStringLiteral("mp4");
469 else if (profile.outputFormat == AndroidMediaRecorder::THREE_GPP)
470 profile.outputFileExtension = QStringLiteral("3gp");
471 else if (profile.outputFormat == AndroidMediaRecorder::AMR_NB_Format)
472 profile.outputFileExtension = QStringLiteral("amr");
473 else if (profile.outputFormat == AndroidMediaRecorder::AMR_WB_Format)
474 profile.outputFileExtension = QStringLiteral("awb");
475
476 profile.isNull = false;
477 }
478
479 return profile;
480}
481
482void QAndroidCaptureSession::onError(int what, int extra)
483{
484 Q_UNUSED(what);
485 Q_UNUSED(extra);
486 stop(true);
488}
489
490void QAndroidCaptureSession::onInfo(int what, int extra)
491{
492 Q_UNUSED(extra);
493 if (what == 800) {
494 // MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
495 stop();
496 emit error(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum duration reached."));
497 } else if (what == 801) {
498 // MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
499 stop();
500 emit error(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum file size reached."));
501 }
502}
503
505
506#include "moc_qandroidcapturesession_p.cpp"
bool isActive
int getValue(Field field) const
static bool hasProfile(jint cameraId, Quality quality)
static AndroidCamcorderProfile get(jint cameraId, Quality quality)
void setupPreviewFrameCallback()
void stopPreviewSynchronous()
bool setPreviewDisplay(AndroidSurfaceHolder *surfaceHolder)
bool setPreviewTexture(AndroidSurfaceTexture *surfaceTexture)
CameraFacing getFacing()
int cameraId() const
static void stopSoundStreaming()
static bool setAudioOutput(const QByteArray &deviceId)
static void startSoundStreaming(const int inputId, const int outputId)
void error(int what, int extra)
void info(int what, int extra)
static void registerMediaFile(const QString &file)
QAndroidVideoOutput * videoOutput() const
void activeChanged(bool)
void applyResolution(const QSize &captureSize=QSize(), bool restartPreview=true)
AndroidCamera * camera() const
void setKeepAlive(bool keepAlive)
void stateChanged(QMediaRecorder::RecorderState state)
QMediaRecorder::RecorderState state() const
void start(QMediaEncoderSettings &settings, const QUrl &outputLocation)
void actualLocationChanged(const QUrl &location)
void setAudioInput(QPlatformAudioInput *input)
void setAudioOutput(QPlatformAudioOutput *output)
void durationChanged(qint64 position)
void setCameraSession(QAndroidCameraSession *cameraSession=0)
virtual AndroidSurfaceHolder * surfaceHolder()
virtual AndroidSurfaceTexture * surfaceTexture()
QByteArray id
\qmlproperty string QtMultimedia::audioDevice::id
void deviceChanged()
void deviceChanged()
int toInt(bool *ok=nullptr, int base=10) const
Returns the byte array converted to an int using base base, which is ten by default.
void invalidate() noexcept
Marks this QElapsedTimer object as invalid.
qint64 elapsed() const noexcept
Returns the number of milliseconds since this QElapsedTimer was last started.
void start() noexcept
Starts this timer.
bool isValid() const noexcept
Returns false if the timer has never been started or invalidated by a call to invalidate().
\inmodule QtCore \reentrant
Definition qfileinfo.h:22
bool isRelative() const
Returns true if the file path is relative, otherwise returns false (i.e.
Definition qlist.h:74
qsizetype size() const noexcept
Definition qlist.h:386
iterator end()
Definition qlist.h:609
const_reference at(qsizetype i) const noexcept
Definition qlist.h:429
iterator begin()
Definition qlist.h:608
void append(parameter_type t)
Definition qlist.h:441
void clear()
Definition qlist.h:417
static QString msgFailedStartRecording()
\inmodule QtMultimedia
RecorderState
\qmlproperty enumeration QtMultimedia::MediaRecorder::recorderState
\inmodule QtCore
Definition qobject.h:90
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2823
\inmodule QtCore
Definition qsize.h:25
constexpr int height() const noexcept
Returns the height.
Definition qsize.h:132
constexpr int width() const noexcept
Returns the width.
Definition qsize.h:129
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
void start(int msec)
Starts or restarts the timer with a timeout interval of msec milliseconds.
Definition qtimer.cpp:208
void setInterval(int msec)
Definition qtimer.cpp:607
void stop()
Stops the timer.
Definition qtimer.cpp:226
void timeout(QPrivateSignal)
This signal is emitted when the timer times out.
\inmodule QtCore
Definition qurl.h:94
static QUrl fromLocalFile(const QString &localfile)
Returns a QUrl representation of localFile, interpreted as a local file.
Definition qurl.cpp:3354
QString scheme() const
Returns the scheme of the URL.
Definition qurl.cpp:1983
@ PreferLocalFile
Definition qurl.h:114
QString toString(FormattingOptions options=FormattingOptions(PrettyDecoded)) const
Returns a string representation of the URL.
Definition qurl.cpp:2828
QString toLocalFile() const
Returns the path of this URL formatted as a local file path.
Definition qurl.cpp:3411
void extension()
[6]
Definition dialogs.cpp:230
Q_MULTIMEDIA_EXPORT QString generateFileName(const QString &requestedName, QStandardPaths::StandardLocation type, const QString &extension)
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE int qt_findClosestValue(const QList< int > &list, int value)
bool qt_androidCheckMicrophonePermission()
bool qt_androidCheckCameraPermission()
bool qt_sizeLessThan(const QSize &s1, const QSize &s2)
DBusConnection const char DBusError * error
QMediaFormat::FileFormat fileFormat
int qRound(qfloat16 d) noexcept
Definition qfloat16.h:281
GLint location
GLdouble s
[6]
Definition qopenglext.h:235
GLenum GLenum GLenum input
QLatin1StringView QLatin1String
Definition qstringfwd.h:31
#define QStringLiteral(str)
#define emit
#define Q_UNUSED(x)
long long qint64
Definition qtypes.h:55
QT_BEGIN_NAMESPACE typedef uchar * output
QSettings settings("MySoft", "Star Runner")
[0]
myObject disconnect()
[26]
bool contains(const AT &t) const noexcept
Definition qlist.h:44