| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| |
| #include "avfmediarecordercontrol_ios.h" |
| #include "avfcamerarenderercontrol.h" |
| #include "avfcamerasession.h" |
| #include "avfcameracontrol.h" |
| #include "avfcameraservice.h" |
| #include "avfcameradebug.h" |
| #include "avfaudioencodersettingscontrol.h" |
| #include "avfvideoencodersettingscontrol.h" |
| #include "avfmediacontainercontrol.h" |
| #include "avfcamerautility.h" |
| |
| #include <QtCore/qmath.h> |
| #include <QtCore/qdebug.h> |
| |
| QT_USE_NAMESPACE |
| |
| namespace { |
| |
| bool qt_is_writable_file_URL(NSURL *fileURL) |
| { |
| Q_ASSERT(fileURL); |
| |
| if (![fileURL isFileURL]) |
| return false; |
| |
| if (NSString *path = [[fileURL path] stringByExpandingTildeInPath]) { |
| return [[NSFileManager defaultManager] |
| isWritableFileAtPath:[path stringByDeletingLastPathComponent]]; |
| } |
| |
| return false; |
| } |
| |
| bool qt_file_exists(NSURL *fileURL) |
| { |
| Q_ASSERT(fileURL); |
| |
| if (NSString *path = [[fileURL path] stringByExpandingTildeInPath]) |
| return [[NSFileManager defaultManager] fileExistsAtPath:path]; |
| |
| return false; |
| } |
| |
| } |
| |
| AVFMediaRecorderControlIOS::AVFMediaRecorderControlIOS(AVFCameraService *service, QObject *parent) |
| : QMediaRecorderControl(parent) |
| , m_service(service) |
| , m_state(QMediaRecorder::StoppedState) |
| , m_lastStatus(QMediaRecorder::UnloadedStatus) |
| , m_audioSettings(nil) |
| , m_videoSettings(nil) |
| { |
| Q_ASSERT(service); |
| |
| m_writer.reset([[QT_MANGLE_NAMESPACE(AVFMediaAssetWriter) alloc] initWithDelegate:this]); |
| if (!m_writer) { |
| qDebugCamera() << Q_FUNC_INFO << "failed to create an asset writer"; |
| return; |
| } |
| |
| AVFCameraControl *cameraControl = m_service->cameraControl(); |
| if (!cameraControl) { |
| qDebugCamera() << Q_FUNC_INFO << "camera control is nil"; |
| return; |
| } |
| |
| connect(cameraControl, SIGNAL(captureModeChanged(QCamera::CaptureModes)), |
| SLOT(captureModeChanged(QCamera::CaptureModes))); |
| connect(cameraControl, SIGNAL(statusChanged(QCamera::Status)), |
| SLOT(cameraStatusChanged(QCamera::Status))); |
| } |
| |
| AVFMediaRecorderControlIOS::~AVFMediaRecorderControlIOS() |
| { |
| [m_writer abort]; |
| |
| if (m_audioSettings) |
| [m_audioSettings release]; |
| if (m_videoSettings) |
| [m_videoSettings release]; |
| } |
| |
| QUrl AVFMediaRecorderControlIOS::outputLocation() const |
| { |
| return m_outputLocation; |
| } |
| |
| bool AVFMediaRecorderControlIOS::setOutputLocation(const QUrl &location) |
| { |
| m_outputLocation = location; |
| return location.scheme() == QLatin1String("file") || location.scheme().isEmpty(); |
| } |
| |
| QMediaRecorder::State AVFMediaRecorderControlIOS::state() const |
| { |
| return m_state; |
| } |
| |
| QMediaRecorder::Status AVFMediaRecorderControlIOS::status() const |
| { |
| return m_lastStatus; |
| } |
| |
| qint64 AVFMediaRecorderControlIOS::duration() const |
| { |
| return m_writer.data().durationInMs; |
| } |
| |
| bool AVFMediaRecorderControlIOS::isMuted() const |
| { |
| return false; |
| } |
| |
| qreal AVFMediaRecorderControlIOS::volume() const |
| { |
| return 1.; |
| } |
| |
| void AVFMediaRecorderControlIOS::applySettings() |
| { |
| AVFCameraSession *session = m_service->session(); |
| if (!session) |
| return; |
| |
| if (m_state != QMediaRecorder::StoppedState |
| || (session->state() != QCamera::ActiveState && session->state() != QCamera::LoadedState) |
| || !m_service->cameraControl()->captureMode().testFlag(QCamera::CaptureVideo)) { |
| return; |
| } |
| |
| // audio settings |
| m_audioSettings = m_service->audioEncoderSettingsControl()->applySettings(); |
| if (m_audioSettings) |
| [m_audioSettings retain]; |
| |
| // video settings |
| AVCaptureConnection *conn = [m_service->videoOutput()->videoDataOutput() connectionWithMediaType:AVMediaTypeVideo]; |
| m_videoSettings = m_service->videoEncoderSettingsControl()->applySettings(conn); |
| if (m_videoSettings) |
| [m_videoSettings retain]; |
| } |
| |
| void AVFMediaRecorderControlIOS::unapplySettings() |
| { |
| m_service->audioEncoderSettingsControl()->unapplySettings(); |
| |
| AVCaptureConnection *conn = [m_service->videoOutput()->videoDataOutput() connectionWithMediaType:AVMediaTypeVideo]; |
| m_service->videoEncoderSettingsControl()->unapplySettings(conn); |
| |
| if (m_audioSettings) { |
| [m_audioSettings release]; |
| m_audioSettings = nil; |
| } |
| if (m_videoSettings) { |
| [m_videoSettings release]; |
| m_videoSettings = nil; |
| } |
| } |
| |
| void AVFMediaRecorderControlIOS::setState(QMediaRecorder::State state) |
| { |
| Q_ASSERT(m_service->session() |
| && m_service->session()->captureSession()); |
| |
| if (!m_writer) { |
| qDebugCamera() << Q_FUNC_INFO << "Invalid recorder"; |
| return; |
| } |
| |
| if (state == m_state) |
| return; |
| |
| switch (state) { |
| case QMediaRecorder::RecordingState: |
| { |
| AVFCameraControl *cameraControl = m_service->cameraControl(); |
| Q_ASSERT(cameraControl); |
| |
| if (!(cameraControl->captureMode() & QCamera::CaptureVideo)) { |
| qDebugCamera() << Q_FUNC_INFO << "wrong capture mode, CaptureVideo expected"; |
| Q_EMIT error(QMediaRecorder::ResourceError, tr("Failed to start recording")); |
| return; |
| } |
| |
| if (cameraControl->status() != QCamera::ActiveStatus) { |
| qDebugCamera() << Q_FUNC_INFO << "can not start record while camera is not active"; |
| Q_EMIT error(QMediaRecorder::ResourceError, tr("Failed to start recording")); |
| return; |
| } |
| |
| const QString path(m_outputLocation.scheme() == QLatin1String("file") ? |
| m_outputLocation.path() : m_outputLocation.toString()); |
| const QUrl fileURL(QUrl::fromLocalFile(m_storageLocation.generateFileName(path, QCamera::CaptureVideo, |
| QLatin1String("clip_"), |
| m_service->mediaContainerControl()->containerFormat()))); |
| |
| NSURL *nsFileURL = fileURL.toNSURL(); |
| if (!nsFileURL) { |
| qWarning() << Q_FUNC_INFO << "invalid output URL:" << fileURL; |
| Q_EMIT error(QMediaRecorder::ResourceError, tr("Invalid output file URL")); |
| return; |
| } |
| if (!qt_is_writable_file_URL(nsFileURL)) { |
| qWarning() << Q_FUNC_INFO << "invalid output URL:" << fileURL |
| << "(the location is not writable)"; |
| Q_EMIT error(QMediaRecorder::ResourceError, tr("Non-writeable file location")); |
| return; |
| } |
| if (qt_file_exists(nsFileURL)) { |
| // We test for/handle this error here since AWAssetWriter will raise an |
| // Objective-C exception, which is not good at all. |
| qWarning() << Q_FUNC_INFO << "invalid output URL:" << fileURL |
| << "(file already exists)"; |
| Q_EMIT error(QMediaRecorder::ResourceError, tr("File already exists")); |
| return; |
| } |
| |
| AVCaptureSession *session = m_service->session()->captureSession(); |
| // We stop session now so that no more frames for renderer's queue |
| // generated, will restart in assetWriterStarted. |
| [session stopRunning]; |
| |
| applySettings(); |
| |
| // Make sure the video is recorded in device orientation. |
| // The top of the video will match the side of the device which is on top |
| // when recording starts (regardless of the UI orientation). |
| AVFCameraInfo cameraInfo = m_service->session()->activeCameraInfo(); |
| int screenOrientation = 360 - m_orientationHandler.currentOrientation(); |
| float rotation = 0; |
| if (cameraInfo.position == QCamera::FrontFace) |
| rotation = (screenOrientation + cameraInfo.orientation) % 360; |
| else |
| rotation = (screenOrientation + (360 - cameraInfo.orientation)) % 360; |
| |
| if ([m_writer setupWithFileURL:nsFileURL |
| cameraService:m_service |
| audioSettings:m_audioSettings |
| videoSettings:m_videoSettings |
| transform:CGAffineTransformMakeRotation(qDegreesToRadians(rotation))]) { |
| |
| m_state = QMediaRecorder::RecordingState; |
| m_lastStatus = QMediaRecorder::StartingStatus; |
| |
| Q_EMIT actualLocationChanged(fileURL); |
| Q_EMIT stateChanged(m_state); |
| Q_EMIT statusChanged(m_lastStatus); |
| |
| // Apple recommends to call startRunning and do all |
| // setup on a special queue, and that's what we had |
| // initially (dispatch_async to writerQueue). Unfortunately, |
| // writer's queue is not the only queue/thread that can |
| // access/modify the session, and as a result we have |
| // all possible data/race-conditions with Obj-C exceptions |
| // at best and something worse in general. |
| // Now we try to only modify session on the same thread. |
| [m_writer start]; |
| } else { |
| [session startRunning]; |
| Q_EMIT error(QMediaRecorder::FormatError, tr("Failed to start recording")); |
| } |
| } break; |
| case QMediaRecorder::PausedState: |
| { |
| Q_EMIT error(QMediaRecorder::FormatError, tr("Recording pause not supported")); |
| return; |
| } break; |
| case QMediaRecorder::StoppedState: |
| { |
| // Do not check the camera status, we can stop if we started. |
| stopWriter(); |
| } |
| } |
| } |
| |
| void AVFMediaRecorderControlIOS::setMuted(bool muted) |
| { |
| Q_UNUSED(muted) |
| qDebugCamera() << Q_FUNC_INFO << "not implemented"; |
| } |
| |
| void AVFMediaRecorderControlIOS::setVolume(qreal volume) |
| { |
| Q_UNUSED(volume) |
| qDebugCamera() << Q_FUNC_INFO << "not implemented"; |
| } |
| |
| void AVFMediaRecorderControlIOS::assetWriterStarted() |
| { |
| m_lastStatus = QMediaRecorder::RecordingStatus; |
| Q_EMIT statusChanged(QMediaRecorder::RecordingStatus); |
| } |
| |
| void AVFMediaRecorderControlIOS::assetWriterFinished() |
| { |
| AVFCameraControl *cameraControl = m_service->cameraControl(); |
| Q_ASSERT(cameraControl); |
| |
| const QMediaRecorder::Status lastStatus = m_lastStatus; |
| const QMediaRecorder::State lastState = m_state; |
| if (cameraControl->captureMode() & QCamera::CaptureVideo) |
| m_lastStatus = QMediaRecorder::LoadedStatus; |
| else |
| m_lastStatus = QMediaRecorder::UnloadedStatus; |
| |
| unapplySettings(); |
| |
| m_service->videoOutput()->resetCaptureDelegate(); |
| [m_service->session()->captureSession() startRunning]; |
| m_state = QMediaRecorder::StoppedState; |
| if (m_lastStatus != lastStatus) |
| Q_EMIT statusChanged(m_lastStatus); |
| if (m_state != lastState) |
| Q_EMIT stateChanged(m_state); |
| } |
| |
| void AVFMediaRecorderControlIOS::captureModeChanged(QCamera::CaptureModes newMode) |
| { |
| AVFCameraControl *cameraControl = m_service->cameraControl(); |
| Q_ASSERT(cameraControl); |
| |
| const QMediaRecorder::Status lastStatus = m_lastStatus; |
| |
| if (newMode & QCamera::CaptureVideo) { |
| if (cameraControl->status() == QCamera::ActiveStatus) |
| m_lastStatus = QMediaRecorder::LoadedStatus; |
| } else { |
| if (m_lastStatus == QMediaRecorder::RecordingStatus) |
| return stopWriter(); |
| else |
| m_lastStatus = QMediaRecorder::UnloadedStatus; |
| } |
| |
| if (m_lastStatus != lastStatus) |
| Q_EMIT statusChanged(m_lastStatus); |
| } |
| |
| void AVFMediaRecorderControlIOS::cameraStatusChanged(QCamera::Status newStatus) |
| { |
| AVFCameraControl *cameraControl = m_service->cameraControl(); |
| Q_ASSERT(cameraControl); |
| |
| const QMediaRecorder::Status lastStatus = m_lastStatus; |
| const bool isCapture = cameraControl->captureMode() & QCamera::CaptureVideo; |
| if (newStatus == QCamera::StartingStatus) { |
| if (isCapture && m_lastStatus == QMediaRecorder::UnloadedStatus) |
| m_lastStatus = QMediaRecorder::LoadingStatus; |
| } else if (newStatus == QCamera::ActiveStatus) { |
| if (isCapture && m_lastStatus == QMediaRecorder::LoadingStatus) |
| m_lastStatus = QMediaRecorder::LoadedStatus; |
| } else { |
| if (m_lastStatus == QMediaRecorder::RecordingStatus) |
| return stopWriter(); |
| if (newStatus == QCamera::UnloadedStatus) |
| m_lastStatus = QMediaRecorder::UnloadedStatus; |
| } |
| |
| if (lastStatus != m_lastStatus) |
| Q_EMIT statusChanged(m_lastStatus); |
| } |
| |
| void AVFMediaRecorderControlIOS::stopWriter() |
| { |
| if (m_lastStatus == QMediaRecorder::RecordingStatus) { |
| m_lastStatus = QMediaRecorder::FinalizingStatus; |
| |
| Q_EMIT statusChanged(m_lastStatus); |
| |
| [m_writer stop]; |
| } |
| } |
| |
| #include "moc_avfmediarecordercontrol_ios.cpp" |