Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qandroidcapturesession_p.h"
++
++ #include "androidcamera_p.h"
++ #include "qandroidcamerasession_p.h"
++ #include "qaudioinput.h"
++ #include "qaudiooutput.h"
++ #include "androidmediaplayer_p.h"
++ #include "androidmultimediautils_p.h"
++ #include "qandroidmultimediautils_p.h"
++ #include "qandroidvideooutput_p.h"
++ #include "qandroidglobal_p.h"
++ #include <private/qplatformaudioinput_p.h>
++ #include <private/qplatformaudiooutput_p.h>
++ #include <private/qmediarecorder_p.h>
++ #include <private/qmediastoragelocation_p.h>
++ #include <QtMultimedia/private/qmultimedia_ranges_p.h>
++
++ #include <algorithm>
++
++ QT_BEGIN_NAMESPACE
++ namespace ranges = QtMultimediaPrivate::ranges;
++
++ QAndroidCaptureSession::QAndroidCaptureSession()
++ : QObject()
++ , m_mediaRecorder(0)
++ , m_cameraSession(0)
++ , m_duration(0)
++ , m_state(QMediaRecorder::StoppedState)
++ , m_outputFormat(AndroidMediaRecorder::DefaultOutputFormat)
++ , m_audioEncoder(AndroidMediaRecorder::DefaultAudioEncoder)
++ , m_videoEncoder(AndroidMediaRecorder::DefaultVideoEncoder)
++ {
++ m_notifyTimer.setInterval(1000);
++ connect(&m_notifyTimer, &QTimer::timeout, this, &QAndroidCaptureSession::updateDuration);
++ }
++
++ QAndroidCaptureSession::~QAndroidCaptureSession()
++ {
++ stop();
++ m_mediaRecorder = nullptr;
++ if (m_audioInput && m_audioOutput)
++ AndroidMediaPlayer::stopSoundStreaming();
++ }
++
++ void QAndroidCaptureSession::setCameraSession(QAndroidCameraSession *cameraSession)
++ {
++ if (m_cameraSession) {
++ disconnect(m_connOpenCamera);
++ disconnect(m_connActiveChangedCamera);
++ }
++
++ m_cameraSession = cameraSession;
++ if (m_cameraSession) {
++ m_connOpenCamera = connect(cameraSession, &QAndroidCameraSession::opened,
++ this, &QAndroidCaptureSession::onCameraOpened);
++ m_connActiveChangedCamera = connect(cameraSession, &QAndroidCameraSession::activeChanged,
++ this, [this](bool isActive) {
++ if (!isActive)
++ stop();
++ });
++ }
++ }
++
++ void QAndroidCaptureSession::setAudioInput(QPlatformAudioInput *input)
++ {
++ if (m_audioInput == input)
++ return;
++
++ if (m_audioInput) {
++ disconnect(m_audioInputChanged);
++ }
++
++ m_audioInput = input;
++
++ if (m_audioInput) {
++ m_audioInputChanged = connect(m_audioInput->q, &QAudioInput::deviceChanged, this, [this]() {
++ if (m_state == QMediaRecorder::RecordingState)
++ m_mediaRecorder->setAudioInput(m_audioInput->device.id());
++ updateStreamingState();
++ });
++ }
++ updateStreamingState();
++ }
++
++ void QAndroidCaptureSession::setAudioOutput(QPlatformAudioOutput *output)
++ {
++ if (m_audioOutput == output)
++ return;
++
++ if (m_audioOutput)
++ disconnect(m_audioOutputChanged);
++
++ m_audioOutput = output;
++
++ if (m_audioOutput) {
++ m_audioOutputChanged = connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this,
++ [this] () {
++ AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id());
++ updateStreamingState();
++ });
++ AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id());
++ }
++ updateStreamingState();
++ }
++
++ void QAndroidCaptureSession::updateStreamingState()
++ {
++ if (m_audioInput && m_audioOutput) {
++ AndroidMediaPlayer::startSoundStreaming(m_audioInput->device.id().toInt(),
++ m_audioOutput->device.id().toInt());
++ } else {
++ AndroidMediaPlayer::stopSoundStreaming();
++ }
++ }
++
++ QMediaRecorder::RecorderState QAndroidCaptureSession::state() const
++ {
++ return m_state;
++ }
++
++ void QAndroidCaptureSession::setKeepAlive(bool keepAlive)
++ {
++ if (m_cameraSession)
++ m_cameraSession->setKeepAlive(keepAlive);
++ }
++
++
++ void QAndroidCaptureSession::start(QMediaEncoderSettings &settings, const QUrl &outputLocation)
++ {
++ if (m_state == QMediaRecorder::RecordingState)
++ return;
++
++ if (!m_cameraSession && !m_audioInput) {
++ updateError(QMediaRecorder::ResourceError, QLatin1String("No devices are set"));
++ return;
++ }
++
++ setKeepAlive(true);
++
++ const bool validCameraSession = m_cameraSession && m_cameraSession->camera();
++
++ if (validCameraSession && !qt_androidCheckCameraPermission()) {
++ updateError(QMediaRecorder::ResourceError, QLatin1String("Camera permission denied."));
++ setKeepAlive(false);
++ return;
++ }
++
++ if (m_audioInput && !qt_androidCheckMicrophonePermission()) {
++ updateError(QMediaRecorder::ResourceError, QLatin1String("Microphone permission denied."));
++ setKeepAlive(false);
++ return;
++ }
++
++ m_mediaRecorder = std::make_shared<AndroidMediaRecorder>();
++ connect(m_mediaRecorder.get(), &AndroidMediaRecorder::error, this,
++ &QAndroidCaptureSession::onError);
++ connect(m_mediaRecorder.get(), &AndroidMediaRecorder::info, this,
++ &QAndroidCaptureSession::onInfo);
++
++ applySettings(settings);
++
++ // Set audio/video sources
++ if (validCameraSession) {
++ m_cameraSession->camera()->stopPreviewSynchronous();
++ m_cameraSession->camera()->unlock();
++
++ m_mediaRecorder->setCamera(m_cameraSession->camera());
++ m_mediaRecorder->setVideoSource(AndroidMediaRecorder::Camera);
++ }
++
++ if (m_audioInput) {
++ m_mediaRecorder->setAudioInput(m_audioInput->device.id());
++ if (!m_mediaRecorder->isAudioSourceSet())
++ m_mediaRecorder->setAudioSource(AndroidMediaRecorder::DefaultAudioSource);
++ }
++
++ // Set output format
++ m_mediaRecorder->setOutputFormat(m_outputFormat);
++
++ // Set video encoder settings
++ if (validCameraSession) {
++ m_mediaRecorder->setVideoSize(settings.videoResolution());
++ m_mediaRecorder->setVideoFrameRate(qRound(settings.videoFrameRate()));
++ m_mediaRecorder->setVideoEncodingBitRate(settings.videoBitRate());
++ m_mediaRecorder->setVideoEncoder(m_videoEncoder);
++
++ // media recorder is also compensanting the mirror on front camera
++ auto rotation = m_cameraSession->currentCameraRotation();
++ if (m_cameraSession->camera()->getFacing() == AndroidCamera::CameraFacingFront)
++ rotation = (360 - rotation) % 360; // remove mirror compensation
++
++ m_mediaRecorder->setOrientationHint(rotation);
++ }
++
++ // Set audio encoder settings
++ if (m_audioInput) {
++ m_mediaRecorder->setAudioChannels(settings.audioChannelCount());
++ m_mediaRecorder->setAudioEncodingBitRate(settings.audioBitRate());
++ m_mediaRecorder->setAudioSamplingRate(settings.audioSampleRate());
++ m_mediaRecorder->setAudioEncoder(m_audioEncoder);
++ }
++
++ QString extension = settings.preferredSuffix();
++ // Set output file
++ auto location = outputLocation.toString(QUrl::PreferLocalFile);
++ QString filePath = location;
++ if (QUrl(filePath).scheme() != QLatin1String("content")) {
++ filePath = QMediaStorageLocation::generateFileName(
++ location, m_cameraSession ? QStandardPaths::MoviesLocation
++ : QStandardPaths::MusicLocation, extension);
++ }
++
++ m_usedOutputLocation = QUrl::fromLocalFile(filePath);
++ m_outputLocationIsStandard = location.isEmpty() || QFileInfo(location).isRelative();
++ m_mediaRecorder->setOutputFile(filePath);
++
++ if (validCameraSession) {
++ m_cameraSession->disableRotation();
++ }
++
++ if (!m_mediaRecorder->prepare()) {
++ updateError(QMediaRecorder::FormatError,
++ QLatin1String("Unable to prepare the media recorder."));
++ restartViewfinder();
++
++ return;
++ }
++
++ if (!m_mediaRecorder->start()) {
++ updateError(QMediaRecorder::FormatError, QMediaRecorderPrivate::msgFailedStartRecording());
++ restartViewfinder();
++
++ return;
++ }
++
++ m_elapsedTime.start();
++ m_notifyTimer.start();
++ updateDuration();
++
++ if (validCameraSession) {
++ m_cameraSession->setReadyForCapture(false);
++
++ // Preview frame callback is cleared when setting up the camera with the media recorder.
++ // We need to reset it.
++ m_cameraSession->camera()->setupPreviewFrameCallback();
++ }
++
++ m_state = QMediaRecorder::RecordingState;
++ Q_EMIT stateChanged(m_state);
++ }
++
++ void QAndroidCaptureSession::stop(bool error)
++ {
++ if (m_state == QMediaRecorder::StoppedState || m_mediaRecorder == nullptr)
++ return;
++
++ m_mediaRecorder->stop();
++ m_notifyTimer.stop();
++ updateDuration();
++ m_elapsedTime.invalidate();
++
++ m_mediaRecorder = nullptr;
++
++ if (m_cameraSession && m_cameraSession->isActive()) {
++ // Viewport needs to be restarted after recording
++ restartViewfinder();
++ }
++
++ if (!error) {
++ // if the media is saved into the standard media location, register it
++ // with the Android media scanner so it appears immediately in apps
++ // such as the gallery.
++ if (m_outputLocationIsStandard)
++ AndroidMultimediaUtils::registerMediaFile(m_usedOutputLocation.toLocalFile());
++
++ Q_EMIT actualLocationChanged(m_usedOutputLocation);
++ }
++
++ m_state = QMediaRecorder::StoppedState;
++ Q_EMIT stateChanged(m_state);
++ }
++
++ qint64 QAndroidCaptureSession::duration() const
++ {
++ return m_duration;
++ }
++
++ void QAndroidCaptureSession::applySettings(QMediaEncoderSettings &settings)
++ {
++ // container settings
++ auto fileFormat = settings.mediaFormat().fileFormat();
++ if (!m_cameraSession && fileFormat == QMediaFormat::AAC) {
++ m_outputFormat = AndroidMediaRecorder::AAC_ADTS;
++ } else if (fileFormat == QMediaFormat::Ogg) {
++ m_outputFormat = AndroidMediaRecorder::OGG;
++ } else if (fileFormat == QMediaFormat::WebM) {
++ m_outputFormat = AndroidMediaRecorder::WEBM;
++ // } else if (fileFormat == QLatin1String("3gp")) {
++ // m_outputFormat = AndroidMediaRecorder::THREE_GPP;
++ } else {
++ // fallback to MP4
++ m_outputFormat = AndroidMediaRecorder::MPEG_4;
++ }
++
++ // audio settings
++ if (settings.audioChannelCount() <= 0)
++ settings.setAudioChannelCount(m_defaultSettings.audioChannels);
++ if (settings.audioBitRate() <= 0)
++ settings.setAudioBitRate(m_defaultSettings.audioBitRate);
++ if (settings.audioSampleRate() <= 0)
++ settings.setAudioSampleRate(m_defaultSettings.audioSampleRate);
++
++ if (settings.audioCodec() == QMediaFormat::AudioCodec::AAC)
++ m_audioEncoder = AndroidMediaRecorder::AAC;
++ else if (settings.audioCodec() == QMediaFormat::AudioCodec::Opus)
++ m_audioEncoder = AndroidMediaRecorder::OPUS;
++ else if (settings.audioCodec() == QMediaFormat::AudioCodec::Vorbis)
++ m_audioEncoder = AndroidMediaRecorder::VORBIS;
++ else
++ m_audioEncoder = m_defaultSettings.audioEncoder;
++
++
++ // video settings
++ if (m_cameraSession && m_cameraSession->camera()) {
++ if (settings.videoResolution().isEmpty()) {
++ settings.setVideoResolution(m_defaultSettings.videoResolution);
++ } else if (!m_supportedResolutions.contains(settings.videoResolution())) {
++ // if the requested resolution is not supported, find the closest one
++ QSize reqSize = settings.videoResolution();
++ int reqPixelCount = reqSize.width() * reqSize.height();
++ QList<int> supportedPixelCounts;
++ for (int i = 0; i < m_supportedResolutions.size(); ++i) {
++ const QSize &s = m_supportedResolutions.at(i);
++ supportedPixelCounts.append(s.width() * s.height());
++ }
++ int closestIndex = qt_findClosestValue(supportedPixelCounts, reqPixelCount);
++ settings.setVideoResolution(m_supportedResolutions.at(closestIndex));
++ }
++
++ if (settings.videoFrameRate() <= 0)
++ settings.setVideoFrameRate(m_defaultSettings.videoFrameRate);
++ if (settings.videoBitRate() <= 0)
++ settings.setVideoBitRate(m_defaultSettings.videoBitRate);
++
++ if (settings.videoCodec() == QMediaFormat::VideoCodec::H264)
++ m_videoEncoder = AndroidMediaRecorder::H264;
++ else if (settings.videoCodec() == QMediaFormat::VideoCodec::H265)
++ m_videoEncoder = AndroidMediaRecorder::HEVC;
++ else if (settings.videoCodec() == QMediaFormat::VideoCodec::MPEG4)
++ m_videoEncoder = AndroidMediaRecorder::MPEG_4_SP;
++ else
++ m_videoEncoder = m_defaultSettings.videoEncoder;
++
++ }
++ }
++
++ void QAndroidCaptureSession::restartViewfinder()
++ {
++
++ setKeepAlive(false);
++
++ if (!m_cameraSession)
++ return;
++
++ if (m_cameraSession && m_cameraSession->camera()) {
++ m_cameraSession->camera()->reconnect();
++ m_cameraSession->camera()->stopPreviewSynchronous();
++ m_cameraSession->camera()->startPreview();
++ m_cameraSession->setReadyForCapture(true);
++ m_cameraSession->enableRotation();
++ }
++
++ m_mediaRecorder = nullptr;
++ }
++
++ void QAndroidCaptureSession::updateDuration()
++ {
++ if (m_elapsedTime.isValid())
++ m_duration = m_elapsedTime.elapsed();
++
++ Q_EMIT durationChanged(m_duration);
++ }
++
++ void QAndroidCaptureSession::onCameraOpened()
++ {
++ m_supportedResolutions.clear();
++ m_supportedFramerates.clear();
++
++ // get supported resolutions from predefined profiles
++ for (int i = 0; i < 8; ++i) {
++ CaptureProfile profile = getProfile(i);
++ if (!profile.isNull) {
++ if (i == AndroidCamcorderProfile::QUALITY_HIGH)
++ m_defaultSettings = profile;
++
++ if (!m_supportedResolutions.contains(profile.videoResolution))
++ m_supportedResolutions.append(profile.videoResolution);
++ if (!m_supportedFramerates.contains(profile.videoFrameRate))
++ m_supportedFramerates.append(profile.videoFrameRate);
++ }
++ }
++
++ ranges::sort(m_supportedResolutions, qt_sizeLessThan);
++ ranges::sort(m_supportedFramerates);
++
++ QMediaEncoderSettings defaultSettings;
++ applySettings(defaultSettings);
++ m_cameraSession->applyResolution(defaultSettings.videoResolution());
++ }
++
++ QAndroidCaptureSession::CaptureProfile QAndroidCaptureSession::getProfile(int id)
++ {
++ CaptureProfile profile;
++ const bool hasProfile = AndroidCamcorderProfile::hasProfile(m_cameraSession->camera()->cameraId(),
++ AndroidCamcorderProfile::Quality(id));
++
++ if (hasProfile) {
++ AndroidCamcorderProfile camProfile = AndroidCamcorderProfile::get(m_cameraSession->camera()->cameraId(),
++ AndroidCamcorderProfile::Quality(id));
++
++ profile.outputFormat = AndroidMediaRecorder::OutputFormat(camProfile.getValue(AndroidCamcorderProfile::fileFormat));
++ profile.audioEncoder = AndroidMediaRecorder::AudioEncoder(camProfile.getValue(AndroidCamcorderProfile::audioCodec));
++ profile.audioBitRate = camProfile.getValue(AndroidCamcorderProfile::audioBitRate);
++ profile.audioChannels = camProfile.getValue(AndroidCamcorderProfile::audioChannels);
++ profile.audioSampleRate = camProfile.getValue(AndroidCamcorderProfile::audioSampleRate);
++ profile.videoEncoder = AndroidMediaRecorder::VideoEncoder(camProfile.getValue(AndroidCamcorderProfile::videoCodec));
++ profile.videoBitRate = camProfile.getValue(AndroidCamcorderProfile::videoBitRate);
++ profile.videoFrameRate = camProfile.getValue(AndroidCamcorderProfile::videoFrameRate);
++ profile.videoResolution = QSize(camProfile.getValue(AndroidCamcorderProfile::videoFrameWidth),
++ camProfile.getValue(AndroidCamcorderProfile::videoFrameHeight));
++
++ if (profile.outputFormat == AndroidMediaRecorder::MPEG_4)
++ profile.outputFileExtension = QStringLiteral("mp4");
++ else if (profile.outputFormat == AndroidMediaRecorder::THREE_GPP)
++ profile.outputFileExtension = QStringLiteral("3gp");
++ else if (profile.outputFormat == AndroidMediaRecorder::AMR_NB_Format)
++ profile.outputFileExtension = QStringLiteral("amr");
++ else if (profile.outputFormat == AndroidMediaRecorder::AMR_WB_Format)
++ profile.outputFileExtension = QStringLiteral("awb");
++
++ profile.isNull = false;
++ }
++
++ return profile;
++ }
++
++ void QAndroidCaptureSession::onError(int what, int extra)
++ {
++ Q_UNUSED(what);
++ Q_UNUSED(extra);
++ stop(true);
++ updateError(QMediaRecorder::ResourceError, QLatin1String("Unknown error."));
++ }
++
++ void QAndroidCaptureSession::onInfo(int what, int extra)
++ {
++ Q_UNUSED(extra);
++ if (what == 800) {
++ // MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
++ stop();
++ updateError(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum duration reached."));
++ } else if (what == 801) {
++ // MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
++ stop();
++ updateError(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum file size reached."));
++ }
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qandroidcapturesession_p.cpp"