Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qffmpegplaybackengine.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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
4
5#include "qvideosink.h"
6#include "qaudiooutput.h"
7#include "private/qplatformaudiooutput_p.h"
8#include "private/qplatformvideosink_p.h"
9#include "private/qaudiobufferoutput_p.h"
10#include "qiodevice.h"
16
17#include <qloggingcategory.h>
18
20
21namespace QFFmpeg {
22
23Q_STATIC_LOGGING_CATEGORY(qLcPlaybackEngine, "qt.multimedia.ffmpeg.playbackengine");
24
25// The helper is needed since on some compilers std::unique_ptr
26// doesn't have a default constructor in the case of sizeof(CustomDeleter) > 0
27template<typename Array>
28inline Array defaultObjectsArray()
29{
30 using T = typename Array::value_type;
31 return { T{ {}, {} }, T{ {}, {} }, T{ {}, {} } };
32}
33
34// TODO: investigate what's better: profile and try network case
35// Most likely, shouldPauseStreams = false is better because of:
36// - packet and frame buffers are not big, the saturration of the is pretty fast.
37// - after any pause a user has some preloaded buffers, so the playback is
38// supposed to be more stable in cases with a weak processor or bad internet.
39// - the code is simplier, usage is more convenient.
40//
41static constexpr bool shouldPauseStreams = false;
42
44 : m_demuxer({}, {}),
45 m_streams(defaultObjectsArray<decltype(m_streams)>()),
46 m_renderers(defaultObjectsArray<decltype(m_renderers)>())
47{
48 qCDebug(qLcPlaybackEngine) << "Create PlaybackEngine";
49 qRegisterMetaType<QFFmpeg::Packet>();
50 qRegisterMetaType<QFFmpeg::Frame>();
51}
52
54 qCDebug(qLcPlaybackEngine) << "Delete PlaybackEngine";
55
56 finalizeOutputs();
57 forEachExistingObject([](auto &object) { object.reset(); });
58 deleteFreeThreads();
59}
60
61void PlaybackEngine::onRendererFinished()
62{
63 auto isAtEnd = [this](auto trackType) {
64 return !m_renderers[trackType] || m_renderers[trackType]->isAtEnd();
65 };
66
68 return;
69
71 return;
72
73 if (!isAtEnd(QPlatformMediaPlayer::SubtitleStream) && !hasMediaStream())
74 return;
75
76 if (std::exchange(m_state, QMediaPlayer::StoppedState) == QMediaPlayer::StoppedState)
77 return;
78
79 finilizeTime(duration());
80
81 forceUpdate();
82
83 qCDebug(qLcPlaybackEngine) << "Playback engine end of stream";
84
86}
87
88void PlaybackEngine::onRendererLoopChanged(quint64 id, qint64 offset, int loopIndex)
89{
90 if (!hasRenderer(id))
91 return;
92
93 if (loopIndex > m_currentLoopOffset.index) {
94 m_currentLoopOffset = { offset, loopIndex };
96 } else if (loopIndex == m_currentLoopOffset.index && offset != m_currentLoopOffset.pos) {
97 qWarning() << "Unexpected offset for loop" << loopIndex << ":" << offset << "vs"
98 << m_currentLoopOffset.pos;
99 m_currentLoopOffset.pos = offset;
100 }
101}
102
103void PlaybackEngine::onRendererSynchronized(quint64 id, std::chrono::steady_clock::time_point tp,
104 qint64 pos)
105{
106 if (!hasRenderer(id))
107 return;
108
110 && m_renderers[QPlatformMediaPlayer::AudioStream]->id() == id);
111
112 m_timeController.sync(tp, pos);
113
114 forEachExistingObject<Renderer>([&](auto &renderer) {
115 if (id != renderer->id())
116 renderer->syncSoft(tp, pos);
117 });
118}
119
121 if (!m_media.avContext())
122 return;
123
124 if (state == m_state)
125 return;
126
127 const auto prevState = std::exchange(m_state, state);
128
129 if (m_state == QMediaPlayer::StoppedState) {
130 finalizeOutputs();
131 finilizeTime(0);
132 }
133
134 if (prevState == QMediaPlayer::StoppedState || m_state == QMediaPlayer::StoppedState)
135 recreateObjects();
136
137 if (prevState == QMediaPlayer::StoppedState)
138 triggerStepIfNeeded();
139
140 updateObjectsPausedState();
141}
142
143void PlaybackEngine::updateObjectsPausedState()
144{
145 const auto paused = m_state != QMediaPlayer::PlayingState;
146 m_timeController.setPaused(paused);
147
148 forEachExistingObject([&](auto &object) {
149 bool objectPaused = false;
150
151 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
152 objectPaused = paused;
153 else if constexpr (shouldPauseStreams) {
154 auto streamPaused = [](bool p, auto &r) {
155 const auto needMoreFrames = r && r->stepInProgress();
156 return p && !needMoreFrames;
157 };
158
159 if constexpr (std::is_same_v<decltype(*object), StreamDecoder &>)
160 objectPaused = streamPaused(paused, renderer(object->trackType()));
161 else
162 objectPaused = std::accumulate(m_renderers.begin(), m_renderers.end(), paused,
163 streamPaused);
164 }
165
166 object->setPaused(objectPaused);
167 });
168}
169
171{
173 if (!std::exchange(engine->m_threadsDirty, true))
174 QMetaObject::invokeMethod(engine, &PlaybackEngine::deleteFreeThreads, Qt::QueuedConnection);
175
176 object->kill();
177}
178
179void PlaybackEngine::registerObject(PlaybackEngineObject &object)
180{
182
183 auto threadName = objectThreadName(object);
184 auto &thread = m_threads[threadName];
185 if (!thread) {
186 thread = std::make_unique<QThread>();
187 thread->setObjectName(threadName);
188 thread->start();
189 }
190
191 Q_ASSERT(object.thread() != thread.get());
192 object.moveToThread(thread.get());
193}
194
197{
198 switch (trackType) {
200 return m_videoSink
201 ? createPlaybackEngineObject<VideoRenderer>(m_timeController, m_videoSink, m_media.rotation())
202 : RendererPtr{ {}, {} };
204 return m_audioOutput || m_audioBufferOutput
205 ? createPlaybackEngineObject<AudioRenderer>(m_timeController, m_audioOutput, m_audioBufferOutput)
206 : RendererPtr{ {}, {} };
208 return m_videoSink
209 ? createPlaybackEngineObject<SubtitleRenderer>(m_timeController, m_videoSink)
210 : RendererPtr{ {}, {} };
211 default:
212 return { {}, {} };
213 }
214}
215
216template<typename C, typename Action>
217void PlaybackEngine::forEachExistingObject(Action &&action)
218{
219 auto handleNotNullObject = [&](auto &object) {
220 if constexpr (std::is_base_of_v<C, std::remove_reference_t<decltype(*object)>>)
221 if (object)
222 action(object);
223 };
224
225 handleNotNullObject(m_demuxer);
226 std::for_each(m_streams.begin(), m_streams.end(), handleNotNullObject);
227 std::for_each(m_renderers.begin(), m_renderers.end(), handleNotNullObject);
228}
229
230template<typename Action>
231void PlaybackEngine::forEachExistingObject(Action &&action)
232{
233 forEachExistingObject<PlaybackEngineObject>(std::forward<Action>(action));
234}
235
237{
238 pos = boundPosition(pos);
239
240 m_timeController.setPaused(true);
241 m_timeController.sync(m_currentLoopOffset.pos + pos);
242
243 forceUpdate();
244}
245
247{
248 if (!isSeekable()) {
249 qWarning() << "Cannot set loops for non-seekable source";
250 return;
251 }
252
253 if (std::exchange(m_loops, loops) == loops)
254 return;
255
256 qCDebug(qLcPlaybackEngine) << "set playback engine loops:" << loops << "prev loops:" << m_loops
257 << "index:" << m_currentLoopOffset.index;
258
259 if (m_demuxer)
260 m_demuxer->setLoops(loops);
261}
262
263void PlaybackEngine::triggerStepIfNeeded()
264{
265 if (m_state != QMediaPlayer::PausedState)
266 return;
267
268 if (m_renderers[QPlatformMediaPlayer::VideoStream])
269 m_renderers[QPlatformMediaPlayer::VideoStream]->doForceStep();
270
271 // TODO: maybe trigger SubtitleStream.
272 // If trigger it, we have to make seeking for the current subtitle frame more stable.
273 // Or set some timeout for seeking.
274}
275
276QString PlaybackEngine::objectThreadName(const PlaybackEngineObject &object)
277{
278 QString result = object.metaObject()->className();
279 if (auto stream = qobject_cast<const StreamDecoder *>(&object))
280 result += QString::number(stream->trackType());
281
282 return result;
283}
284
286 if (rate == playbackRate())
287 return;
288
289 m_timeController.setPlaybackRate(rate);
290 forEachExistingObject<Renderer>([rate](auto &renderer) { renderer->setPlaybackRate(rate); });
291}
292
294 return m_timeController.playbackRate();
295}
296
297void PlaybackEngine::recreateObjects()
298{
299 m_timeController.setPaused(true);
300
301 forEachExistingObject([](auto &object) { object.reset(); });
302
303 createObjectsIfNeeded();
304}
305
306void PlaybackEngine::createObjectsIfNeeded()
307{
308 if (m_state == QMediaPlayer::StoppedState || !m_media.avContext())
309 return;
310
311 for (int i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i)
312 createStreamAndRenderer(static_cast<QPlatformMediaPlayer::TrackType>(i));
313
314 createDemuxer();
315}
316
317void PlaybackEngine::forceUpdate()
318{
319 recreateObjects();
320 triggerStepIfNeeded();
321 updateObjectsPausedState();
322}
323
324void PlaybackEngine::createStreamAndRenderer(QPlatformMediaPlayer::TrackType trackType)
325{
326 auto codec = codecForTrack(trackType);
327
328 auto &renderer = m_renderers[trackType];
329
330 if (!codec)
331 return;
332
333 if (!renderer) {
334 renderer = createRenderer(trackType);
335
336 if (!renderer)
337 return;
338
340 &PlaybackEngine::onRendererSynchronized);
341
343 &PlaybackEngine::onRendererLoopChanged);
344
345 if constexpr (shouldPauseStreams)
347 &PlaybackEngine::updateObjectsPausedState);
348
350 &PlaybackEngine::onRendererFinished);
351 }
352
353 auto &stream = m_streams[trackType] =
354 createPlaybackEngineObject<StreamDecoder>(*codec, renderer->seekPosition());
355
356 Q_ASSERT(trackType == stream->trackType());
357
363}
364
365std::optional<Codec> PlaybackEngine::codecForTrack(QPlatformMediaPlayer::TrackType trackType)
366{
367 const auto streamIndex = m_media.currentStreamIndex(trackType);
368 if (streamIndex < 0)
369 return {};
370
371 auto &result = m_codecs[trackType];
372
373 if (!result) {
374 qCDebug(qLcPlaybackEngine)
375 << "Create codec for stream:" << streamIndex << "trackType:" << trackType;
376 auto maybeCodec =
377 Codec::create(m_media.avContext()->streams[streamIndex], m_media.avContext());
378
379 if (!maybeCodec) {
381 "Cannot create codec," + maybeCodec.error());
382 return {};
383 }
384
385 result = maybeCodec.value();
386 }
387
388 return result;
389}
390
391bool PlaybackEngine::hasMediaStream() const
392{
393 return m_renderers[QPlatformMediaPlayer::AudioStream]
394 || m_renderers[QPlatformMediaPlayer::VideoStream];
395}
396
397void PlaybackEngine::createDemuxer()
398{
399 std::array<int, QPlatformMediaPlayer::NTrackTypes> streamIndexes = { -1, -1, -1 };
400
401 bool hasStreams = false;
402 forEachExistingObject<StreamDecoder>([&](auto &stream) {
403 hasStreams = true;
404 const auto trackType = stream->trackType();
405 streamIndexes[trackType] = m_media.currentStreamIndex(trackType);
406 });
407
408 if (!hasStreams)
409 return;
410
411 const PositionWithOffset positionWithOffset{ currentPosition(false), m_currentLoopOffset };
412
413 m_demuxer = createPlaybackEngineObject<Demuxer>(m_media.avContext(), positionWithOffset,
414 streamIndexes, m_loops);
415
417
418 forEachExistingObject<StreamDecoder>([&](auto &stream) {
419 connect(m_demuxer.get(), Demuxer::signalByTrackType(stream->trackType()), stream.get(),
421 connect(m_demuxer.get(), &PlaybackEngineObject::atEnd, stream.get(),
423 connect(stream.get(), &StreamDecoder::packetProcessed, m_demuxer.get(),
425 });
426
427 if (!isSeekable() || duration() <= 0) {
428 // We need initial synchronization for such streams
429 forEachExistingObject([&](auto &object) {
430 using Type = std::remove_reference_t<decltype(*object)>;
431 if constexpr (!std::is_same_v<Type, Demuxer>)
432 connect(m_demuxer.get(), &Demuxer::firstPacketFound, object.get(),
433 &Type::setInitialPosition);
434 });
435
436 auto updateTimeController = [this](TimeController::TimePoint tp, qint64 pos) {
437 m_timeController.sync(tp, pos);
438 };
439
440 connect(m_demuxer.get(), &Demuxer::firstPacketFound, this, updateTimeController);
441 }
442}
443
444void PlaybackEngine::deleteFreeThreads() {
445 m_threadsDirty = false;
446 auto freeThreads = std::move(m_threads);
447
448 forEachExistingObject([&](auto &object) {
449 m_threads.insert(freeThreads.extract(objectThreadName(*object)));
450 });
451
452 for (auto &[name, thr] : freeThreads)
453 thr->quit();
454
455 for (auto &[name, thr] : freeThreads)
456 thr->wait();
457}
458
460{
461 Q_ASSERT(!m_media.avContext()); // Playback engine does not support reloading media
463 Q_ASSERT(m_threads.empty());
464
465 m_media = std::move(media);
466 updateVideoSinkSize();
467}
468
470{
471 auto prev = std::exchange(m_videoSink, sink);
472 if (prev == sink)
473 return;
474
475 updateVideoSinkSize(prev);
477
478 if (!sink || !prev) {
479 // might need some improvements
480 forceUpdate();
481 }
482}
483
487
489{
490 QAudioOutput *prev = std::exchange(m_audioOutput, output);
491 if (prev == output)
492 return;
493
495
496 if (!output || !prev) {
497 // might need some improvements
498 forceUpdate();
499 }
500}
501
503{
504 QAudioBufferOutput *prev = std::exchange(m_audioBufferOutput, output);
505 if (prev == output)
506 return;
508}
509
511 std::optional<qint64> pos;
512
513 for (size_t i = 0; i < m_renderers.size(); ++i) {
514 const auto &renderer = m_renderers[i];
515 if (!renderer)
516 continue;
517
518 // skip subtitle stream for finding lower rendering position
519 if (!topPos && i == QPlatformMediaPlayer::SubtitleStream && hasMediaStream())
520 continue;
521
522 const auto rendererPos = renderer->lastPosition();
523 pos = !pos ? rendererPos
524 : topPos ? std::max(*pos, rendererPos)
525 : std::min(*pos, rendererPos);
526 }
527
528 if (!pos)
529 pos = m_timeController.currentPosition();
530
531 return boundPosition(*pos - m_currentLoopOffset.pos);
532}
533
535{
536 return m_media.duration();
537}
538
539bool PlaybackEngine::isSeekable() const { return m_media.isSeekable(); }
540
541const QList<MediaDataHolder::StreamInfo> &
543{
544 return m_media.streamInfo(trackType);
545}
546
548{
549 return m_media.metaData();
550}
551
556
558{
559 if (!m_media.setActiveTrack(trackType, streamNumber))
560 return;
561
562 m_codecs[trackType] = {};
563
564 m_renderers[trackType].reset();
565 m_streams = defaultObjectsArray<decltype(m_streams)>();
566 m_demuxer.reset();
567
568 updateVideoSinkSize();
569 createObjectsIfNeeded();
570 updateObjectsPausedState();
571}
572
573void PlaybackEngine::finilizeTime(qint64 pos)
574{
575 Q_ASSERT(pos >= 0 && pos <= duration());
576
577 m_timeController.setPaused(true);
578 m_timeController.sync(pos);
579 m_currentLoopOffset = {};
580}
581
582void PlaybackEngine::finalizeOutputs()
583{
584 if (m_audioBufferOutput)
585 updateActiveAudioOutput(static_cast<QAudioBufferOutput *>(nullptr));
586 if (m_audioOutput)
587 updateActiveAudioOutput(static_cast<QAudioOutput *>(nullptr));
588 updateActiveVideoOutput(nullptr, true);
589}
590
591bool PlaybackEngine::hasRenderer(quint64 id) const
592{
593 return std::any_of(m_renderers.begin(), m_renderers.end(),
594 [id](auto &renderer) { return renderer && renderer->id() == id; });
595}
596
597template <typename AudioOutput>
599{
600 if (auto renderer =
601 qobject_cast<AudioRenderer *>(m_renderers[QPlatformMediaPlayer::AudioStream].get()))
602 renderer->setOutput(output);
603}
604
606{
607 if (auto renderer = qobject_cast<SubtitleRenderer *>(
609 renderer->setOutput(sink, cleanOutput);
610 if (auto renderer =
611 qobject_cast<VideoRenderer *>(m_renderers[QPlatformMediaPlayer::VideoStream].get()))
612 renderer->setOutput(sink, cleanOutput);
613}
614
615void PlaybackEngine::updateVideoSinkSize(QVideoSink *prevSink)
616{
617 auto platformVideoSink = m_videoSink ? m_videoSink->platformVideoSink() : nullptr;
618 if (!platformVideoSink)
619 return;
620
621 if (prevSink && prevSink->platformVideoSink())
622 platformVideoSink->setNativeSize(prevSink->platformVideoSink()->nativeSize());
623 else {
624 const auto streamIndex = m_media.currentStreamIndex(QPlatformMediaPlayer::VideoStream);
625 if (streamIndex >= 0) {
626 const auto context = m_media.avContext();
627 const auto stream = context->streams[streamIndex];
628 const AVRational pixelAspectRatio =
629 av_guess_sample_aspect_ratio(context, stream, nullptr);
630 // auto size = metaData().value(QMediaMetaData::Resolution)
631 const QSize size =
632 qCalculateFrameSize({ stream->codecpar->width, stream->codecpar->height },
633 { pixelAspectRatio.num, pixelAspectRatio.den });
634
635 platformVideoSink->setNativeSize(qRotatedFrameSize(size, m_media.rotation()));
636 }
637 }
638}
639
640qint64 PlaybackEngine::boundPosition(qint64 position) const
641{
642 position = qMax(position, 0);
643 return duration() > 0 ? qMin(position, duration()) : position;
644}
645}
646
648
649#include "moc_qffmpegplaybackengine_p.cpp"
\inmodule QtMultimedia
\qmltype AudioOutput \instantiates QAudioOutput
static QMaybe< Codec > create(AVStream *stream, AVFormatContext *formatContext)
void packetsBuffered()
void firstPacketFound(TimePoint tp, qint64 trackPos)
void onPacketProcessed(Packet)
static RequestingSignal signalByTrackType(QPlatformMediaPlayer::TrackType trackType)
bool setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
QtVideo::Rotation rotation() const
int currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
const QMediaMetaData & metaData() const
const QList< StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
int activeTrack(QPlatformMediaPlayer::TrackType type) const
void error(int code, const QString &errorString)
int activeTrack(QPlatformMediaPlayer::TrackType type) const
void setVideoSink(QVideoSink *sink)
void setState(QMediaPlayer::PlaybackState state)
void updateActiveAudioOutput(AudioOutput *output)
virtual RendererPtr createRenderer(QPlatformMediaPlayer::TrackType trackType)
void setAudioBufferOutput(QAudioBufferOutput *output)
void setAudioSink(QAudioOutput *output)
qint64 currentPosition(bool topPos=true) const
void errorOccured(int, const QString &)
ObjectPtr< Renderer > RendererPtr
const QList< MediaDataHolder::StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
void setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
void setMedia(MediaDataHolder media)
const QMediaMetaData & metaData() const
void updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput=false)
void synchronized(Id id, TimePoint tp, qint64 pos)
void frameProcessed(Frame)
void loopChanged(Id id, qint64 offset, int index)
void onFrameProcessed(Frame frame)
void packetProcessed(Packet)
void requestHandleFrame(Frame frame)
PlaybackRate playbackRate() const
void sync(qint64 trackPos=0)
void setPlaybackRate(PlaybackRate playbackRate)
qint64 currentPosition(const Clock::duration &offset=Clock::duration{ 0 }) const
\inmodule QtMultimedia
PlaybackState
Defines the current state of a media player.
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
QThread * thread() const
Returns the thread in which the object lives.
Definition qobject.cpp:1598
Q_WEAK_OVERLOAD void setObjectName(const QString &name)
Sets the object's name to name.
Definition qobject.h:127
\inmodule QtCore
Definition qsize.h:25
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
static QString number(int, int base=10)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:8095
void start(Priority=InheritPriority)
Definition qthread.cpp:996
The QVideoSink class represents a generic sink for video data.
Definition qvideosink.h:22
QPlatformVideoSink * platformVideoSink() const
else opt state
[0]
Array defaultObjectsArray()
static constexpr bool shouldPauseStreams
Combined button and popup list for selecting options.
@ QueuedConnection
static QDBusError::ErrorType get(const char *name)
EGLStreamKHR stream
QMediaFormat::VideoCodec codec
#define qWarning
Definition qlogging.h:167
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)
QT_BEGIN_NAMESPACE constexpr const T & qMin(const T &a, const T &b)
Definition qminmax.h:19
constexpr const T & qMax(const T &a, const T &b)
Definition qminmax.h:21
QSize qRotatedFrameSize(QSize size, int rotation)
QSize qCalculateFrameSize(QSize resolution, Fraction par)
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLboolean r
[2]
GLuint object
[3]
GLenum type
GLenum GLuint GLintptr offset
GLuint name
GLuint GLenum * rate
GLsizei GLenum GLboolean sink
GLuint64EXT * result
[6]
GLfloat GLfloat p
[1]
static qreal position(const QQuickItem *item, QQuickAnchors::Anchor anchorLine)
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
#define emit
unsigned long long quint64
Definition qtypes.h:61
long long qint64
Definition qtypes.h:60
QT_BEGIN_NAMESPACE typedef uchar * output
QSvgRenderer * renderer
[0]
void operator()(PlaybackEngineObject *) const
static bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType, QGenericReturnArgument ret, QGenericArgument val0=QGenericArgument(nullptr), QGenericArgument val1=QGenericArgument(), QGenericArgument val2=QGenericArgument(), QGenericArgument val3=QGenericArgument(), QGenericArgument val4=QGenericArgument(), QGenericArgument val5=QGenericArgument(), QGenericArgument val6=QGenericArgument(), QGenericArgument val7=QGenericArgument(), QGenericArgument val8=QGenericArgument(), QGenericArgument val9=QGenericArgument())
\threadsafe This is an overloaded member function, provided for convenience. It differs from the abov...
Definition moc.h:23