| 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 <QtCore/qcoreapplication.h> |
| 5 | #include <QtCore/qloggingcategory.h> |
| 6 | |
| 7 | #include "qgstpipeline_p.h" |
| 8 | #include "qgst_bus_observer_p.h" |
| 9 | |
| 10 | #include <thread> |
| 11 | |
| 12 | QT_BEGIN_NAMESPACE |
| 13 | |
| 14 | static Q_LOGGING_CATEGORY(qLcGstPipeline, "qt.multimedia.gstpipeline" ); |
| 15 | |
| 16 | struct QGstPipelinePrivate |
| 17 | { |
| 18 | explicit QGstPipelinePrivate(QGstBusHandle); |
| 19 | ~QGstPipelinePrivate(); |
| 20 | |
| 21 | std::chrono::nanoseconds m_position{}; |
| 22 | double m_rate = 1.; |
| 23 | std::unique_ptr<QGstBusObserver> m_busObserver; |
| 24 | }; |
| 25 | |
| 26 | QGstPipelinePrivate::QGstPipelinePrivate(QGstBusHandle bus) |
| 27 | : m_busObserver{ |
| 28 | std::make_unique<QGstBusObserver>(args: std::move(bus)), |
| 29 | } |
| 30 | { |
| 31 | Q_ASSERT(QThread::isMainThread()); |
| 32 | } |
| 33 | |
| 34 | QGstPipelinePrivate::~QGstPipelinePrivate() |
| 35 | { |
| 36 | m_busObserver->close(); |
| 37 | |
| 38 | if (m_busObserver->currentThreadIsNotifierThread()) |
| 39 | return; |
| 40 | |
| 41 | // The QGstPipelinePrivate is owned the the GstPipeline and can be destroyed from a gstreamer |
| 42 | // thread. In this case we cannot destroy the object immediately, but need to marshall it |
| 43 | // through the event loop of the main thread |
| 44 | QMetaObject::invokeMethod(qApp, function: [bus = std::move(m_busObserver)] { |
| 45 | // nothing to do, we just extend the lifetime of the bus |
| 46 | }); |
| 47 | } |
| 48 | |
| 49 | // QGstPipeline |
| 50 | |
| 51 | QGstPipeline QGstPipeline::create(const char *name) |
| 52 | { |
| 53 | GstPipeline *pipeline = qGstCheckedCast<GstPipeline>(arg: gst_pipeline_new(name)); |
| 54 | return adopt(pipeline); |
| 55 | } |
| 56 | |
| 57 | QGstPipeline QGstPipeline::createFromFactory(const char *factory, const char *name) |
| 58 | { |
| 59 | QGstElement playbin3 = QGstElement::createFromFactory(factory, name); |
| 60 | GstPipeline *pipeline = qGstCheckedCast<GstPipeline>(arg: playbin3.element()); |
| 61 | |
| 62 | return QGstPipeline::adopt(pipeline); |
| 63 | } |
| 64 | |
| 65 | QGstPipeline QGstPipeline::adopt(GstPipeline *pipeline) |
| 66 | { |
| 67 | QGstPipeline wrappedObject{ |
| 68 | pipeline, |
| 69 | QGstPipeline::NeedsRef, |
| 70 | }; |
| 71 | |
| 72 | QGstBusHandle bus{ |
| 73 | gst_pipeline_get_bus(pipeline), |
| 74 | QGstBusHandle::HasRef, |
| 75 | }; |
| 76 | |
| 77 | auto d = std::make_unique<QGstPipelinePrivate>(args: std::move(bus)); |
| 78 | wrappedObject.set(property: "pipeline-private" , object: std::move(d)); |
| 79 | |
| 80 | return wrappedObject; |
| 81 | } |
| 82 | |
| 83 | QGstPipeline::QGstPipeline(GstPipeline *p, RefMode mode) : QGstBin(qGstCheckedCast<GstBin>(arg: p), mode) |
| 84 | { |
| 85 | } |
| 86 | |
| 87 | QGstPipeline::~QGstPipeline() = default; |
| 88 | |
| 89 | void QGstPipeline::installMessageFilter(QGstreamerBusMessageFilter *filter) |
| 90 | { |
| 91 | QGstPipelinePrivate *d = getPrivate(); |
| 92 | d->m_busObserver->installMessageFilter(filter); |
| 93 | } |
| 94 | |
| 95 | void QGstPipeline::removeMessageFilter(QGstreamerBusMessageFilter *filter) |
| 96 | { |
| 97 | QGstPipelinePrivate *d = getPrivate(); |
| 98 | d->m_busObserver->removeMessageFilter(filter); |
| 99 | } |
| 100 | |
| 101 | GstStateChangeReturn QGstPipeline::setState(GstState state) |
| 102 | { |
| 103 | return gst_element_set_state(element: element(), state); |
| 104 | } |
| 105 | |
| 106 | bool QGstPipeline::processNextPendingMessage(GstMessageType types, std::chrono::nanoseconds timeout) |
| 107 | { |
| 108 | QGstPipelinePrivate *d = getPrivate(); |
| 109 | return d->m_busObserver->processNextPendingMessage(type: types, timeout); |
| 110 | } |
| 111 | |
| 112 | bool QGstPipeline::processNextPendingMessage(std::chrono::nanoseconds timeout) |
| 113 | { |
| 114 | return processNextPendingMessage(types: GST_MESSAGE_ANY, timeout); |
| 115 | } |
| 116 | |
| 117 | void QGstPipeline::flush() |
| 118 | { |
| 119 | seek(pos: position(), /*flush=*/true); |
| 120 | } |
| 121 | |
| 122 | void QGstPipeline::seek(std::chrono::nanoseconds pos, double rate, bool flush) |
| 123 | { |
| 124 | using namespace std::chrono_literals; |
| 125 | |
| 126 | QGstPipelinePrivate *d = getPrivate(); |
| 127 | // always adjust the rate, so it can be set before playback starts |
| 128 | // setting position needs a loaded media file that's seekable |
| 129 | |
| 130 | qCDebug(qLcGstPipeline) << "QGstPipeline::seek to" << pos << "rate:" << rate |
| 131 | << (flush ? "flushing" : "not flushing" ); |
| 132 | |
| 133 | GstSeekFlags seekFlags = flush ? GST_SEEK_FLAG_FLUSH : GST_SEEK_FLAG_NONE; |
| 134 | seekFlags = GstSeekFlags(seekFlags | GST_SEEK_FLAG_SEGMENT | GST_SEEK_FLAG_ACCURATE); |
| 135 | |
| 136 | bool success = (rate > 0) |
| 137 | ? gst_element_seek(element: element(), rate, format: GST_FORMAT_TIME, flags: seekFlags, start_type: GST_SEEK_TYPE_SET, |
| 138 | start: pos.count(), stop_type: GST_SEEK_TYPE_END, stop: 0) |
| 139 | : gst_element_seek(element: element(), rate, format: GST_FORMAT_TIME, flags: seekFlags, start_type: GST_SEEK_TYPE_SET, start: 0, |
| 140 | stop_type: GST_SEEK_TYPE_SET, stop: pos.count()); |
| 141 | |
| 142 | if (!success) { |
| 143 | qDebug() << "seek: gst_element_seek failed" << pos; |
| 144 | dumpGraph(fileNamePrefix: "seekSeekFailed" ); |
| 145 | return; |
| 146 | } |
| 147 | |
| 148 | d->m_position = pos; |
| 149 | } |
| 150 | |
| 151 | void QGstPipeline::seek(std::chrono::nanoseconds pos, bool flush) |
| 152 | { |
| 153 | qCDebug(qLcGstPipeline) << "QGstPipeline::seek to" << pos; |
| 154 | seek(pos, rate: getPrivate()->m_rate, flush); |
| 155 | } |
| 156 | |
| 157 | void QGstPipeline::setPlaybackRate(double rate, bool forceFlushingSeek) |
| 158 | { |
| 159 | QGstPipelinePrivate *d = getPrivate(); |
| 160 | if (rate == d->m_rate) |
| 161 | return; |
| 162 | |
| 163 | d->m_rate = rate; |
| 164 | |
| 165 | qCDebug(qLcGstPipeline) << "QGstPipeline::setPlaybackRate to" << rate; |
| 166 | |
| 167 | applyPlaybackRate(forceFlushingSeek); |
| 168 | } |
| 169 | |
| 170 | double QGstPipeline::playbackRate() const |
| 171 | { |
| 172 | QGstPipelinePrivate *d = getPrivate(); |
| 173 | return d->m_rate; |
| 174 | } |
| 175 | |
| 176 | void QGstPipeline::applyPlaybackRate(bool forceFlushingSeek) |
| 177 | { |
| 178 | QGstPipelinePrivate *d = getPrivate(); |
| 179 | |
| 180 | // do not GST_SEEK_FLAG_FLUSH with GST_SEEK_TYPE_NONE |
| 181 | // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/3604 |
| 182 | if (!forceFlushingSeek) { |
| 183 | bool asyncChangeSuccess = waitForAsyncStateChangeComplete(); |
| 184 | if (!asyncChangeSuccess) { |
| 185 | qWarning() |
| 186 | << "QGstPipeline::seek: async pipeline change in progress. Seeking impossible" ; |
| 187 | return; |
| 188 | } |
| 189 | |
| 190 | qCDebug(qLcGstPipeline) << "QGstPipeline::applyPlaybackRate instantly" ; |
| 191 | bool success = gst_element_seek( |
| 192 | element: element(), rate: d->m_rate, format: GST_FORMAT_UNDEFINED, flags: GST_SEEK_FLAG_INSTANT_RATE_CHANGE, |
| 193 | start_type: GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE, stop_type: GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE); |
| 194 | if (!success) { |
| 195 | qDebug() << "setPlaybackRate: gst_element_seek failed" ; |
| 196 | dumpGraph(fileNamePrefix: "applyPlaybackRateSeekFailed" ); |
| 197 | } |
| 198 | } else { |
| 199 | seek(pos: position(), flush: d->m_rate); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | void QGstPipeline::setPosition(std::chrono::nanoseconds pos, bool flush) |
| 204 | { |
| 205 | seek(pos, flush); |
| 206 | } |
| 207 | |
| 208 | std::chrono::nanoseconds QGstPipeline::position() const |
| 209 | { |
| 210 | QGstPipelinePrivate *d = getPrivate(); |
| 211 | std::optional<std::chrono::nanoseconds> pos = QGstElement::position(); |
| 212 | if (pos) { |
| 213 | d->m_position = *pos; |
| 214 | qCDebug(qLcGstPipeline) << "QGstPipeline::position:" |
| 215 | << std::chrono::round<std::chrono::milliseconds>(d: *pos); |
| 216 | } else { |
| 217 | qDebug() << "QGstPipeline: failed to query position, using previous position" ; |
| 218 | dumpGraph(fileNamePrefix: "positionQueryFailed" ); |
| 219 | } |
| 220 | |
| 221 | return d->m_position; |
| 222 | } |
| 223 | |
| 224 | std::chrono::milliseconds QGstPipeline::positionInMs() const |
| 225 | { |
| 226 | using namespace std::chrono; |
| 227 | return round<milliseconds>(d: position()); |
| 228 | } |
| 229 | |
| 230 | void QGstPipeline::setPositionAndRate(std::chrono::nanoseconds pos, double rate) |
| 231 | { |
| 232 | QGstPipelinePrivate *d = getPrivate(); |
| 233 | d->m_rate = rate; |
| 234 | seek(pos, flush: rate); |
| 235 | } |
| 236 | |
| 237 | std::optional<std::chrono::nanoseconds> |
| 238 | QGstPipeline::queryPosition(std::chrono::nanoseconds timeout) const |
| 239 | { |
| 240 | using namespace std::chrono_literals; |
| 241 | using namespace std::chrono; |
| 242 | |
| 243 | std::chrono::nanoseconds totalSleepTime{}; |
| 244 | |
| 245 | for (;;) { |
| 246 | std::optional<nanoseconds> dur = QGstElement::duration(); |
| 247 | if (dur) |
| 248 | return dur; |
| 249 | |
| 250 | if (totalSleepTime >= timeout) |
| 251 | return std::nullopt; |
| 252 | std::this_thread::sleep_for(rtime: 20ms); |
| 253 | totalSleepTime += 20ms; |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | std::optional<std::chrono::nanoseconds> |
| 258 | QGstPipeline::queryDuration(std::chrono::nanoseconds timeout) const |
| 259 | { |
| 260 | using namespace std::chrono_literals; |
| 261 | using namespace std::chrono; |
| 262 | |
| 263 | std::chrono::nanoseconds totalSleepTime{}; |
| 264 | |
| 265 | for (;;) { |
| 266 | std::optional<nanoseconds> dur = QGstElement::duration(); |
| 267 | if (dur) |
| 268 | return dur; |
| 269 | |
| 270 | if (totalSleepTime >= timeout) |
| 271 | return std::nullopt; |
| 272 | |
| 273 | std::this_thread::sleep_for(rtime: 20ms); |
| 274 | totalSleepTime += 20ms; |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | std::optional<std::pair<std::chrono::nanoseconds, std::chrono::nanoseconds>> |
| 279 | QGstPipeline::queryPositionAndDuration(std::chrono::nanoseconds timeout) const |
| 280 | { |
| 281 | using namespace std::chrono_literals; |
| 282 | using namespace std::chrono; |
| 283 | |
| 284 | std::chrono::nanoseconds totalSleepTime{}; |
| 285 | |
| 286 | std::optional<nanoseconds> dur; |
| 287 | std::optional<nanoseconds> pos; |
| 288 | |
| 289 | for (;;) { |
| 290 | if (!dur) |
| 291 | dur = QGstElement::duration(); |
| 292 | if (!pos) |
| 293 | pos = QGstElement::position(); |
| 294 | |
| 295 | if (dur && pos) |
| 296 | return std::pair{ *dur, *pos }; |
| 297 | |
| 298 | if (totalSleepTime >= timeout) |
| 299 | return std::nullopt; |
| 300 | |
| 301 | std::this_thread::sleep_for(rtime: 20ms); |
| 302 | totalSleepTime += 20ms; |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | void QGstPipeline::seekToEndWithEOS() |
| 307 | { |
| 308 | QGstPipelinePrivate *d = getPrivate(); |
| 309 | |
| 310 | gst_element_seek(element: element(), rate: d->m_rate, format: GST_FORMAT_TIME, flags: GST_SEEK_FLAG_NONE, start_type: GST_SEEK_TYPE_END, |
| 311 | start: 0, stop_type: GST_SEEK_TYPE_END, stop: 0); |
| 312 | } |
| 313 | |
| 314 | QGstPipelinePrivate *QGstPipeline::getPrivate() const |
| 315 | { |
| 316 | QGstPipelinePrivate *ret = getObject<QGstPipelinePrivate>(property: "pipeline-private" ); |
| 317 | Q_ASSERT(ret); |
| 318 | return ret; |
| 319 | } |
| 320 | |
| 321 | QT_END_NAMESPACE |
| 322 | |