| 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 | |
| 4 | #include <mediacapture/qgstreamermediarecorder_p.h> |
| 5 | #include <qgstreamerformatinfo_p.h> |
| 6 | #include <common/qgstpipeline_p.h> |
| 7 | #include <common/qgstreamermessage_p.h> |
| 8 | #include <common/qgst_debug_p.h> |
| 9 | #include <qgstreamerintegration_p.h> |
| 10 | |
| 11 | #include <QtMultimedia/private/qmediastoragelocation_p.h> |
| 12 | #include <QtMultimedia/private/qplatformcamera_p.h> |
| 13 | #include <QtMultimedia/qaudiodevice.h> |
| 14 | |
| 15 | #include <QtCore/qdebug.h> |
| 16 | #include <QtCore/qeventloop.h> |
| 17 | #include <QtCore/qstandardpaths.h> |
| 18 | #include <QtCore/qloggingcategory.h> |
| 19 | |
| 20 | #include <gst/gsttagsetter.h> |
| 21 | #include <gst/gstversion.h> |
| 22 | #include <gst/video/video.h> |
| 23 | #include <gst/pbutils/encoding-profile.h> |
| 24 | |
| 25 | Q_STATIC_LOGGING_CATEGORY(qLcMediaRecorder, "qt.multimedia.recorder" ); |
| 26 | |
| 27 | QT_BEGIN_NAMESPACE |
| 28 | |
| 29 | QGstreamerMediaRecorder::QGstreamerMediaRecorder(QMediaRecorder *parent) |
| 30 | : QPlatformMediaRecorder(parent), audioPauseControl(*this), videoPauseControl(*this) |
| 31 | { |
| 32 | signalDurationChangedTimer.setInterval(100); |
| 33 | signalDurationChangedTimer.callOnTimeout(args: &signalDurationChangedTimer, args: [this]() { |
| 34 | durationChanged(position: duration()); |
| 35 | }); |
| 36 | } |
| 37 | |
| 38 | QGstreamerMediaRecorder::~QGstreamerMediaRecorder() |
| 39 | { |
| 40 | if (m_session) |
| 41 | finalize(); |
| 42 | } |
| 43 | |
| 44 | bool QGstreamerMediaRecorder::isLocationWritable(const QUrl &) const |
| 45 | { |
| 46 | return true; |
| 47 | } |
| 48 | |
| 49 | void QGstreamerMediaRecorder::handleSessionError(QMediaRecorder::Error code, |
| 50 | const QString &description) |
| 51 | { |
| 52 | updateError(error: code, errorString: description); |
| 53 | stop(); |
| 54 | } |
| 55 | |
| 56 | void QGstreamerMediaRecorder::processBusMessage(const QGstreamerMessage &msg) |
| 57 | { |
| 58 | constexpr bool traceStateChange = false; |
| 59 | constexpr bool traceAllEvents = false; |
| 60 | |
| 61 | if constexpr (traceAllEvents) |
| 62 | qCDebug(qLcMediaRecorder) << "received event:" << msg; |
| 63 | |
| 64 | switch (msg.type()) { |
| 65 | case GST_MESSAGE_ELEMENT: { |
| 66 | QGstStructureView s = msg.structure(); |
| 67 | if (s.name() == "GstBinForwarded" ) |
| 68 | return processBusMessage(msg: s.getMessage()); |
| 69 | |
| 70 | qCDebug(qLcMediaRecorder) << "received element message from" << msg.source().name() |
| 71 | << s.name(); |
| 72 | return; |
| 73 | } |
| 74 | |
| 75 | case GST_MESSAGE_EOS: { |
| 76 | qCDebug(qLcMediaRecorder) << "received EOS from" << msg.source().name(); |
| 77 | finalize(); |
| 78 | return; |
| 79 | } |
| 80 | |
| 81 | case GST_MESSAGE_ERROR: { |
| 82 | qCDebug(qLcMediaRecorder) << "received error:" << msg.source().name() |
| 83 | << QCompactGstMessageAdaptor(msg); |
| 84 | |
| 85 | QUniqueGErrorHandle err; |
| 86 | QGString debug; |
| 87 | gst_message_parse_error(message: msg.message(), gerror: &err, debug: &debug); |
| 88 | updateError(error: QMediaRecorder::ResourceError, errorString: QString::fromUtf8(utf8: err.get()->message)); |
| 89 | if (!m_finalizing) |
| 90 | stop(); |
| 91 | finalize(); |
| 92 | return; |
| 93 | } |
| 94 | |
| 95 | case GST_MESSAGE_STATE_CHANGED: { |
| 96 | if constexpr (traceStateChange) |
| 97 | qCDebug(qLcMediaRecorder) << "received state change" << QCompactGstMessageAdaptor(msg); |
| 98 | |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | default: |
| 103 | return; |
| 104 | }; |
| 105 | } |
| 106 | |
| 107 | qint64 QGstreamerMediaRecorder::duration() const |
| 108 | { |
| 109 | return std::max(a: audioPauseControl.duration, b: videoPauseControl.duration); |
| 110 | } |
| 111 | |
| 112 | |
| 113 | static GstEncodingContainerProfile *createContainerProfile(const QMediaEncoderSettings &settings) |
| 114 | { |
| 115 | auto *formatInfo = QGstreamerIntegration::instance()->gstFormatsInfo(); |
| 116 | |
| 117 | auto caps = formatInfo->formatCaps(f: settings.fileFormat()); |
| 118 | |
| 119 | GstEncodingContainerProfile *profile = |
| 120 | (GstEncodingContainerProfile *)gst_encoding_container_profile_new( |
| 121 | name: "container_profile" , description: (gchar *)"custom container profile" , |
| 122 | format: const_cast<GstCaps *>(caps.caps()), |
| 123 | preset: nullptr); // preset |
| 124 | return profile; |
| 125 | } |
| 126 | |
| 127 | static GstEncodingProfile *createVideoProfile(const QMediaEncoderSettings &settings) |
| 128 | { |
| 129 | auto *formatInfo = QGstreamerIntegration::instance()->gstFormatsInfo(); |
| 130 | |
| 131 | QGstCaps caps = formatInfo->videoCaps(f: settings.mediaFormat()); |
| 132 | if (!caps) |
| 133 | return nullptr; |
| 134 | |
| 135 | QSize videoResolution = settings.videoResolution(); |
| 136 | if (videoResolution.isValid()) |
| 137 | caps.setResolution(videoResolution); |
| 138 | |
| 139 | GstEncodingVideoProfile *profile = |
| 140 | gst_encoding_video_profile_new(format: const_cast<GstCaps *>(caps.caps()), preset: nullptr, |
| 141 | restriction: nullptr, // restriction |
| 142 | presence: 0); // presence |
| 143 | |
| 144 | gst_encoding_video_profile_set_pass(prof: profile, pass: 0); |
| 145 | gst_encoding_video_profile_set_variableframerate(prof: profile, TRUE); |
| 146 | |
| 147 | return (GstEncodingProfile *)profile; |
| 148 | } |
| 149 | |
| 150 | static GstEncodingProfile *createAudioProfile(const QMediaEncoderSettings &settings) |
| 151 | { |
| 152 | auto *formatInfo = QGstreamerIntegration::instance()->gstFormatsInfo(); |
| 153 | |
| 154 | auto caps = formatInfo->audioCaps(f: settings.mediaFormat()); |
| 155 | if (!caps) |
| 156 | return nullptr; |
| 157 | |
| 158 | GstEncodingProfile *profile = |
| 159 | (GstEncodingProfile *)gst_encoding_audio_profile_new(format: const_cast<GstCaps *>(caps.caps()), |
| 160 | preset: nullptr, // preset |
| 161 | restriction: nullptr, // restriction |
| 162 | presence: 0); // presence |
| 163 | |
| 164 | return profile; |
| 165 | } |
| 166 | |
| 167 | |
| 168 | static GstEncodingContainerProfile *createEncodingProfile(const QMediaEncoderSettings &settings) |
| 169 | { |
| 170 | auto *containerProfile = createContainerProfile(settings); |
| 171 | if (!containerProfile) { |
| 172 | qWarning() << "QGstreamerMediaEncoder: failed to create container profile!" ; |
| 173 | return nullptr; |
| 174 | } |
| 175 | |
| 176 | GstEncodingProfile *audioProfile = createAudioProfile(settings); |
| 177 | GstEncodingProfile *videoProfile = nullptr; |
| 178 | if (settings.videoCodec() != QMediaFormat::VideoCodec::Unspecified) |
| 179 | videoProfile = createVideoProfile(settings); |
| 180 | // qDebug() << "audio profile" << (audioProfile ? gst_caps_to_string(gst_encoding_profile_get_format(audioProfile)) : "(null)"); |
| 181 | // qDebug() << "video profile" << (videoProfile ? gst_caps_to_string(gst_encoding_profile_get_format(videoProfile)) : "(null)"); |
| 182 | // qDebug() << "conta profile" << gst_caps_to_string(gst_encoding_profile_get_format((GstEncodingProfile *)containerProfile)); |
| 183 | |
| 184 | if (videoProfile) { |
| 185 | if (!gst_encoding_container_profile_add_profile(container: containerProfile, profile: videoProfile)) { |
| 186 | qWarning() << "QGstreamerMediaEncoder: failed to add video profile!" ; |
| 187 | gst_encoding_profile_unref(videoProfile); |
| 188 | } |
| 189 | } |
| 190 | if (audioProfile) { |
| 191 | if (!gst_encoding_container_profile_add_profile(container: containerProfile, profile: audioProfile)) { |
| 192 | qWarning() << "QGstreamerMediaEncoder: failed to add audio profile!" ; |
| 193 | gst_encoding_profile_unref(audioProfile); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | return containerProfile; |
| 198 | } |
| 199 | |
| 200 | void QGstreamerMediaRecorder::PauseControl::reset() |
| 201 | { |
| 202 | pauseOffsetPts = 0; |
| 203 | pauseStartPts.reset(); |
| 204 | duration = 0; |
| 205 | firstBufferPts.reset(); |
| 206 | } |
| 207 | |
| 208 | void QGstreamerMediaRecorder::PauseControl::installOn(QGstPad pad) |
| 209 | { |
| 210 | pad.addProbe<&QGstreamerMediaRecorder::PauseControl::processBuffer>(instance: this, |
| 211 | type: GST_PAD_PROBE_TYPE_BUFFER); |
| 212 | } |
| 213 | |
| 214 | GstPadProbeReturn QGstreamerMediaRecorder::PauseControl::processBuffer(QGstPad, |
| 215 | GstPadProbeInfo *info) |
| 216 | { |
| 217 | auto buffer = GST_PAD_PROBE_INFO_BUFFER(info); |
| 218 | if (!buffer) |
| 219 | return GST_PAD_PROBE_OK; |
| 220 | |
| 221 | buffer = gst_buffer_make_writable(buffer); |
| 222 | |
| 223 | if (!buffer) |
| 224 | return GST_PAD_PROBE_OK; |
| 225 | |
| 226 | GST_PAD_PROBE_INFO_DATA(info) = buffer; |
| 227 | |
| 228 | if (!GST_BUFFER_PTS_IS_VALID(buffer)) |
| 229 | return GST_PAD_PROBE_OK; |
| 230 | |
| 231 | if (!firstBufferPts) |
| 232 | firstBufferPts = GST_BUFFER_PTS(buffer); |
| 233 | |
| 234 | if (encoder.state() == QMediaRecorder::PausedState) { |
| 235 | if (!pauseStartPts) |
| 236 | pauseStartPts = GST_BUFFER_PTS(buffer); |
| 237 | |
| 238 | return GST_PAD_PROBE_DROP; |
| 239 | } |
| 240 | |
| 241 | if (pauseStartPts) { |
| 242 | pauseOffsetPts += GST_BUFFER_PTS(buffer) - *pauseStartPts; |
| 243 | pauseStartPts.reset(); |
| 244 | } |
| 245 | GST_BUFFER_PTS(buffer) -= pauseOffsetPts; |
| 246 | |
| 247 | duration = (GST_BUFFER_PTS(buffer) - *firstBufferPts) / GST_MSECOND; |
| 248 | |
| 249 | return GST_PAD_PROBE_OK; |
| 250 | } |
| 251 | |
| 252 | void QGstreamerMediaRecorder::record(QMediaEncoderSettings &settings) |
| 253 | { |
| 254 | if (!m_session ||m_finalizing || state() != QMediaRecorder::StoppedState) |
| 255 | return; |
| 256 | |
| 257 | const auto hasVideo = m_session->camera() && m_session->camera()->isActive(); |
| 258 | const auto hasAudio = m_session->audioInput() != nullptr; |
| 259 | |
| 260 | if (!hasVideo && !hasAudio) { |
| 261 | updateError(error: QMediaRecorder::ResourceError, errorString: QMediaRecorder::tr(s: "No camera or audio input" )); |
| 262 | return; |
| 263 | } |
| 264 | |
| 265 | const auto audioOnly = settings.videoCodec() == QMediaFormat::VideoCodec::Unspecified; |
| 266 | |
| 267 | auto primaryLocation = audioOnly ? QStandardPaths::MusicLocation : QStandardPaths::MoviesLocation; |
| 268 | auto container = settings.preferredSuffix(); |
| 269 | auto location = QMediaStorageLocation::generateFileName(requestedName: outputLocation().toLocalFile(), type: primaryLocation, extension: container); |
| 270 | |
| 271 | QUrl actualSink = QUrl::fromLocalFile(localfile: QDir::currentPath()).resolved(relative: location); |
| 272 | qCDebug(qLcMediaRecorder) << "recording new video to" << actualSink; |
| 273 | |
| 274 | Q_ASSERT(!actualSink.isEmpty()); |
| 275 | |
| 276 | QGstBin gstEncodebin = QGstBin::createFromFactory(factory: "encodebin" , name: "encodebin" ); |
| 277 | Q_ASSERT(gstEncodebin); |
| 278 | auto *encodingProfile = createEncodingProfile(settings); |
| 279 | g_object_set(object: gstEncodebin.object(), first_property_name: "profile" , encodingProfile, nullptr); |
| 280 | gst_encoding_profile_unref(encodingProfile); |
| 281 | |
| 282 | QGstElement gstFileSink = QGstElement::createFromFactory(factory: "filesink" , name: "filesink" ); |
| 283 | Q_ASSERT(gstFileSink); |
| 284 | gstFileSink.set(property: "location" , str: QFile::encodeName(fileName: actualSink.toLocalFile()).constData()); |
| 285 | |
| 286 | QGstPad audioSink = {}; |
| 287 | QGstPad videoSink = {}; |
| 288 | |
| 289 | audioPauseControl.reset(); |
| 290 | videoPauseControl.reset(); |
| 291 | |
| 292 | if (hasAudio) { |
| 293 | audioSink = gstEncodebin.getRequestPad(name: "audio_%u" ); |
| 294 | if (!audioSink) |
| 295 | qWarning() << "Unsupported audio codec" ; |
| 296 | else |
| 297 | audioPauseControl.installOn(pad: audioSink); |
| 298 | } |
| 299 | |
| 300 | if (hasVideo) { |
| 301 | videoSink = gstEncodebin.getRequestPad(name: "video_%u" ); |
| 302 | if (!videoSink) |
| 303 | qWarning() << "Unsupported video codec" ; |
| 304 | else |
| 305 | videoPauseControl.installOn(pad: videoSink); |
| 306 | } |
| 307 | |
| 308 | QGstreamerMediaCaptureSession::RecorderElements recorder{ |
| 309 | .encodeBin: std::move(gstEncodebin), |
| 310 | .fileSink: std::move(gstFileSink), |
| 311 | .audioSink: std::move(audioSink), |
| 312 | .videoSink: std::move(videoSink), |
| 313 | }; |
| 314 | |
| 315 | m_session->linkAndStartEncoder(std::move(recorder), m_metaData); |
| 316 | |
| 317 | signalDurationChangedTimer.start(); |
| 318 | |
| 319 | m_session->pipeline().dumpGraph(fileNamePrefix: "recording" ); |
| 320 | |
| 321 | durationChanged(position: 0); |
| 322 | actualLocationChanged(location: QUrl::fromLocalFile(localfile: location)); |
| 323 | stateChanged(state: QMediaRecorder::RecordingState); |
| 324 | } |
| 325 | |
| 326 | void QGstreamerMediaRecorder::pause() |
| 327 | { |
| 328 | if (!m_session || m_finalizing || state() != QMediaRecorder::RecordingState) |
| 329 | return; |
| 330 | signalDurationChangedTimer.stop(); |
| 331 | durationChanged(position: duration()); |
| 332 | m_session->pipeline().dumpGraph(fileNamePrefix: "before-pause" ); |
| 333 | stateChanged(state: QMediaRecorder::PausedState); |
| 334 | } |
| 335 | |
| 336 | void QGstreamerMediaRecorder::resume() |
| 337 | { |
| 338 | m_session->pipeline().dumpGraph(fileNamePrefix: "before-resume" ); |
| 339 | if (!m_session || m_finalizing || state() != QMediaRecorder::PausedState) |
| 340 | return; |
| 341 | signalDurationChangedTimer.start(); |
| 342 | stateChanged(state: QMediaRecorder::RecordingState); |
| 343 | } |
| 344 | |
| 345 | void QGstreamerMediaRecorder::stop() |
| 346 | { |
| 347 | if (!m_session || m_finalizing || state() == QMediaRecorder::StoppedState) |
| 348 | return; |
| 349 | durationChanged(position: duration()); |
| 350 | qCDebug(qLcMediaRecorder) << "stop" ; |
| 351 | m_finalizing = true; |
| 352 | m_session->unlinkRecorder(); |
| 353 | signalDurationChangedTimer.stop(); |
| 354 | } |
| 355 | |
| 356 | void QGstreamerMediaRecorder::finalize() |
| 357 | { |
| 358 | if (!m_session || !m_finalizing) |
| 359 | return; |
| 360 | |
| 361 | qCDebug(qLcMediaRecorder) << "finalize" ; |
| 362 | |
| 363 | m_session->finalizeRecorder(); |
| 364 | m_finalizing = false; |
| 365 | stateChanged(state: QMediaRecorder::StoppedState); |
| 366 | } |
| 367 | |
| 368 | void QGstreamerMediaRecorder::setMetaData(const QMediaMetaData &metaData) |
| 369 | { |
| 370 | if (!m_session) |
| 371 | return; |
| 372 | m_metaData = metaData; |
| 373 | } |
| 374 | |
| 375 | QMediaMetaData QGstreamerMediaRecorder::metaData() const |
| 376 | { |
| 377 | return m_metaData; |
| 378 | } |
| 379 | |
| 380 | void QGstreamerMediaRecorder::setCaptureSession(QPlatformMediaCaptureSession *session) |
| 381 | { |
| 382 | QGstreamerMediaCaptureSession *captureSession = |
| 383 | static_cast<QGstreamerMediaCaptureSession *>(session); |
| 384 | if (m_session == captureSession) |
| 385 | return; |
| 386 | |
| 387 | if (m_session) { |
| 388 | stop(); |
| 389 | if (m_finalizing) { |
| 390 | QEventLoop loop; |
| 391 | QObject::connect(sender: mediaRecorder(), signal: &QMediaRecorder::recorderStateChanged, context: &loop, |
| 392 | slot: &QEventLoop::quit); |
| 393 | loop.exec(); |
| 394 | } |
| 395 | } |
| 396 | |
| 397 | m_session = captureSession; |
| 398 | } |
| 399 | |
| 400 | QT_END_NAMESPACE |
| 401 | |