blob: 62197e9007a01044d32ee83434b82bef97dc3109 [file] [log] [blame]
/****************************************************************************
**
** 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"