| 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 | static Q_LOGGING_CATEGORY(qLcMediaRecorder, "qt.multimedia.encoder" ) | 
| 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 |  |