Revision control

Copy as Markdown

// 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 <mediacapture/qgstreamermediarecorder_p.h>
#include <qgstreamerformatinfo_p.h>
#include <common/qgstpipeline_p.h>
#include <common/qgstreamermessage_p.h>
#include <common/qgst_debug_p.h>
#include <qgstreamerintegration_p.h>
#include <QtMultimedia/private/qmediastoragelocation_p.h>
#include <QtMultimedia/private/qplatformcamera_p.h>
#include <QtMultimedia/qaudiodevice.h>
#include <QtCore/qdebug.h>
#include <QtCore/qeventloop.h>
#include <QtCore/qstandardpaths.h>
#include <QtCore/qloggingcategory.h>
#include <gst/gsttagsetter.h>
#include <gst/gstversion.h>
#include <gst/video/video.h>
#include <gst/pbutils/encoding-profile.h>
Q_STATIC_LOGGING_CATEGORY(qLcMediaRecorder, "qt.multimedia.recorder");
QT_BEGIN_NAMESPACE
QGstreamerMediaRecorder::QGstreamerMediaRecorder(QMediaRecorder *parent)
: QPlatformMediaRecorder(parent), audioPauseControl(*this), videoPauseControl(*this)
{
signalDurationChangedTimer.setInterval(100);
signalDurationChangedTimer.callOnTimeout(&signalDurationChangedTimer, [this]() {
durationChanged(duration());
});
}
QGstreamerMediaRecorder::~QGstreamerMediaRecorder()
{
if (m_session)
finalize();
}
bool QGstreamerMediaRecorder::isLocationWritable(const QUrl &) const
{
return true;
}
void QGstreamerMediaRecorder::handleSessionError(QMediaRecorder::Error code,
const QString &description)
{
updateError(code, description);
stop();
}
void QGstreamerMediaRecorder::processBusMessage(const QGstreamerMessage &msg)
{
constexpr bool traceStateChange = false;
constexpr bool traceAllEvents = false;
if constexpr (traceAllEvents)
qCDebug(qLcMediaRecorder) << "received event:" << msg;
switch (msg.type()) {
case GST_MESSAGE_ELEMENT: {
QGstStructureView s = msg.structure();
if (s.name() == "GstBinForwarded")
return processBusMessage(s.getMessage());
qCDebug(qLcMediaRecorder) << "received element message from" << msg.source().name()
<< s.name();
return;
}
case GST_MESSAGE_EOS: {
qCDebug(qLcMediaRecorder) << "received EOS from" << msg.source().name();
finalize();
return;
}
case GST_MESSAGE_ERROR: {
qCDebug(qLcMediaRecorder) << "received error:" << msg.source().name()
<< QCompactGstMessageAdaptor(msg);
QUniqueGErrorHandle err;
QGString debug;
gst_message_parse_error(msg.message(), &err, &debug);
updateError(QMediaRecorder::ResourceError, QString::fromUtf8(err.get()->message));
if (!m_finalizing)
stop();
finalize();
return;
}
case GST_MESSAGE_STATE_CHANGED: {
if constexpr (traceStateChange)
qCDebug(qLcMediaRecorder) << "received state change" << QCompactGstMessageAdaptor(msg);
return;
}
default:
return;
};
}
qint64 QGstreamerMediaRecorder::duration() const
{
return std::max(audioPauseControl.duration, videoPauseControl.duration);
}
static GstEncodingContainerProfile *createContainerProfile(const QMediaEncoderSettings &settings)
{
auto *formatInfo = QGstreamerIntegration::instance()->gstFormatsInfo();
auto caps = formatInfo->formatCaps(settings.fileFormat());
GstEncodingContainerProfile *profile =
(GstEncodingContainerProfile *)gst_encoding_container_profile_new(
"container_profile", (gchar *)"custom container profile",
const_cast<GstCaps *>(caps.caps()),
nullptr); // preset
return profile;
}
static GstEncodingProfile *createVideoProfile(const QMediaEncoderSettings &settings)
{
auto *formatInfo = QGstreamerIntegration::instance()->gstFormatsInfo();
QGstCaps caps = formatInfo->videoCaps(settings.mediaFormat());
if (!caps)
return nullptr;
QSize videoResolution = settings.videoResolution();
if (videoResolution.isValid())
caps.setResolution(videoResolution);
GstEncodingVideoProfile *profile =
gst_encoding_video_profile_new(const_cast<GstCaps *>(caps.caps()), nullptr,
nullptr, // restriction
0); // presence
gst_encoding_video_profile_set_pass(profile, 0);
gst_encoding_video_profile_set_variableframerate(profile, TRUE);
return (GstEncodingProfile *)profile;
}
static GstEncodingProfile *createAudioProfile(const QMediaEncoderSettings &settings)
{
auto *formatInfo = QGstreamerIntegration::instance()->gstFormatsInfo();
auto caps = formatInfo->audioCaps(settings.mediaFormat());
if (!caps)
return nullptr;
GstEncodingProfile *profile =
(GstEncodingProfile *)gst_encoding_audio_profile_new(const_cast<GstCaps *>(caps.caps()),
nullptr, // preset
nullptr, // restriction
0); // presence
return profile;
}
static GstEncodingContainerProfile *createEncodingProfile(const QMediaEncoderSettings &settings)
{
auto *containerProfile = createContainerProfile(settings);
if (!containerProfile) {
qWarning() << "QGstreamerMediaEncoder: failed to create container profile!";
return nullptr;
}
GstEncodingProfile *audioProfile = createAudioProfile(settings);
GstEncodingProfile *videoProfile = nullptr;
if (settings.videoCodec() != QMediaFormat::VideoCodec::Unspecified)
videoProfile = createVideoProfile(settings);
// qDebug() << "audio profile" << (audioProfile ? gst_caps_to_string(gst_encoding_profile_get_format(audioProfile)) : "(null)");
// qDebug() << "video profile" << (videoProfile ? gst_caps_to_string(gst_encoding_profile_get_format(videoProfile)) : "(null)");
// qDebug() << "conta profile" << gst_caps_to_string(gst_encoding_profile_get_format((GstEncodingProfile *)containerProfile));
if (videoProfile) {
if (!gst_encoding_container_profile_add_profile(containerProfile, videoProfile)) {
qWarning() << "QGstreamerMediaEncoder: failed to add video profile!";
gst_encoding_profile_unref(videoProfile);
}
}
if (audioProfile) {
if (!gst_encoding_container_profile_add_profile(containerProfile, audioProfile)) {
qWarning() << "QGstreamerMediaEncoder: failed to add audio profile!";
gst_encoding_profile_unref(audioProfile);
}
}
return containerProfile;
}
void QGstreamerMediaRecorder::PauseControl::reset()
{
pauseOffsetPts = 0;
pauseStartPts.reset();
duration = 0;
firstBufferPts.reset();
}
void QGstreamerMediaRecorder::PauseControl::installOn(QGstPad pad)
{
pad.addProbe<&QGstreamerMediaRecorder::PauseControl::processBuffer>(this,
GST_PAD_PROBE_TYPE_BUFFER);
}
GstPadProbeReturn QGstreamerMediaRecorder::PauseControl::processBuffer(const QGstPad &,
GstPadProbeInfo *info)
{
auto buffer = GST_PAD_PROBE_INFO_BUFFER(info);
if (!buffer)
return GST_PAD_PROBE_OK;
buffer = gst_buffer_make_writable(buffer);
if (!buffer)
return GST_PAD_PROBE_OK;
GST_PAD_PROBE_INFO_DATA(info) = buffer;
if (!GST_BUFFER_PTS_IS_VALID(buffer))
return GST_PAD_PROBE_OK;
if (!firstBufferPts)
firstBufferPts = GST_BUFFER_PTS(buffer);
if (encoder.state() == QMediaRecorder::PausedState) {
if (!pauseStartPts)
pauseStartPts = GST_BUFFER_PTS(buffer);
return GST_PAD_PROBE_DROP;
}
if (pauseStartPts) {
pauseOffsetPts += GST_BUFFER_PTS(buffer) - *pauseStartPts;
pauseStartPts.reset();
}
GST_BUFFER_PTS(buffer) -= pauseOffsetPts;
duration = (GST_BUFFER_PTS(buffer) - *firstBufferPts) / GST_MSECOND;
return GST_PAD_PROBE_OK;
}
void QGstreamerMediaRecorder::record(QMediaEncoderSettings &settings)
{
if (!m_session ||m_finalizing || state() != QMediaRecorder::StoppedState)
return;
const auto hasVideo = m_session->camera() && m_session->camera()->isActive();
const auto hasAudio = m_session->audioInput() != nullptr;
if (!hasVideo && !hasAudio) {
updateError(QMediaRecorder::ResourceError, QMediaRecorder::tr("No camera or audio input"));
return;
}
const auto audioOnly = settings.videoCodec() == QMediaFormat::VideoCodec::Unspecified;
auto primaryLocation = audioOnly ? QStandardPaths::MusicLocation : QStandardPaths::MoviesLocation;
auto container = settings.preferredSuffix();
auto location = QMediaStorageLocation::generateFileName(outputLocation().toLocalFile(), primaryLocation, container);
QUrl actualSink = QUrl::fromLocalFile(QDir::currentPath()).resolved(QUrl{location});
qCDebug(qLcMediaRecorder) << "recording new video to" << actualSink;
Q_ASSERT(!actualSink.isEmpty());
QGstBin gstEncodebin = QGstBin::createFromFactory("encodebin", "encodebin");
Q_ASSERT(gstEncodebin);
auto *encodingProfile = createEncodingProfile(settings);
g_object_set(gstEncodebin.object(), "profile", encodingProfile, nullptr);
gst_encoding_profile_unref(encodingProfile);
QGstElement gstFileSink = QGstElement::createFromFactory("filesink", "filesink");
Q_ASSERT(gstFileSink);
gstFileSink.set("location", QFile::encodeName(actualSink.toLocalFile()).constData());
QGstPad audioSink = {};
QGstPad videoSink = {};
audioPauseControl.reset();
videoPauseControl.reset();
if (hasAudio) {
audioSink = gstEncodebin.getRequestPad("audio_%u");
if (!audioSink)
qWarning() << "Unsupported audio codec";
else
audioPauseControl.installOn(audioSink);
}
if (hasVideo) {
videoSink = gstEncodebin.getRequestPad("video_%u");
if (!videoSink)
qWarning() << "Unsupported video codec";
else
videoPauseControl.installOn(videoSink);
}
QGstreamerMediaCaptureSession::RecorderElements recorder{
std::move(gstEncodebin),
std::move(gstFileSink),
std::move(audioSink),
std::move(videoSink),
};
m_session->linkAndStartEncoder(std::move(recorder), m_metaData);
signalDurationChangedTimer.start();
m_session->pipeline().dumpGraph("recording");
durationChanged(0);
actualLocationChanged(QUrl::fromLocalFile(location));
stateChanged(QMediaRecorder::RecordingState);
}
void QGstreamerMediaRecorder::pause()
{
if (!m_session || m_finalizing || state() != QMediaRecorder::RecordingState)
return;
signalDurationChangedTimer.stop();
durationChanged(duration());
m_session->pipeline().dumpGraph("before-pause");
stateChanged(QMediaRecorder::PausedState);
}
void QGstreamerMediaRecorder::resume()
{
m_session->pipeline().dumpGraph("before-resume");
if (!m_session || m_finalizing || state() != QMediaRecorder::PausedState)
return;
signalDurationChangedTimer.start();
stateChanged(QMediaRecorder::RecordingState);
}
void QGstreamerMediaRecorder::stop()
{
if (!m_session || m_finalizing || state() == QMediaRecorder::StoppedState)
return;
durationChanged(duration());
qCDebug(qLcMediaRecorder) << "stop";
m_finalizing = true;
m_session->unlinkRecorder();
signalDurationChangedTimer.stop();
}
void QGstreamerMediaRecorder::finalize()
{
if (!m_session || !m_finalizing)
return;
qCDebug(qLcMediaRecorder) << "finalize";
m_session->finalizeRecorder();
m_finalizing = false;
stateChanged(QMediaRecorder::StoppedState);
}
void QGstreamerMediaRecorder::setMetaData(const QMediaMetaData &metaData)
{
if (!m_session)
return;
m_metaData = metaData;
}
QMediaMetaData QGstreamerMediaRecorder::metaData() const
{
return m_metaData;
}
void QGstreamerMediaRecorder::setCaptureSession(QPlatformMediaCaptureSession *session)
{
QGstreamerMediaCaptureSession *captureSession =
static_cast<QGstreamerMediaCaptureSession *>(session);
if (m_session == captureSession)
return;
if (m_session) {
stop();
if (m_finalizing) {
QEventLoop loop;
QObject::connect(mediaRecorder(), &QMediaRecorder::recorderStateChanged, &loop,
&QEventLoop::quit);
loop.exec();
}
}
m_session = captureSession;
}
QT_END_NAMESPACE