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