Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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"
11#include "playbackengine/qffmpegdemuxer_p.h"
12#include "playbackengine/qffmpegstreamdecoder_p.h"
13#include "playbackengine/qffmpegsubtitlerenderer_p.h"
14#include "playbackengine/qffmpegvideorenderer_p.h"
15#include "playbackengine/qffmpegaudiorenderer_p.h"
16
17#include <qloggingcategory.h>
18
20namespace ranges = QtMultimediaPrivate::ranges;
21
22namespace QFFmpeg {
23
24Q_STATIC_LOGGING_CATEGORY(qLcPlaybackEngine, "qt.multimedia.ffmpeg.playbackengine");
25
26// The helper is needed since on some compilers std::unique_ptr
27// doesn't have a default constructor in the case of sizeof(CustomDeleter) > 0
28template <typename Array>
29inline static Array defaultObjectsArray()
30{
31 using T = typename Array::value_type;
32 return { T{ {}, {} }, T{ {}, {} }, T{ {}, {} } };
33}
34
35PlaybackEngine::PlaybackEngine(const QPlaybackOptions &options)
36 : m_demuxer({}, {}),
37 m_streams(defaultObjectsArray<decltype(m_streams)>()),
38 m_renderers(defaultObjectsArray<decltype(m_renderers)>()),
39 m_options{ options }
40{
41 qCDebug(qLcPlaybackEngine) << "Create PlaybackEngine";
42 qRegisterMetaType<QFFmpeg::Packet>();
43 qRegisterMetaType<QFFmpeg::Frame>();
44 qRegisterMetaType<QFFmpeg::TrackPosition>();
45 qRegisterMetaType<QFFmpeg::PlaybackEngineObjectID>();
46}
47
49 qCDebug(qLcPlaybackEngine) << "Delete PlaybackEngine";
50
51 finalizeOutputs();
52 forEachExistingObject([](auto &object) { object.reset(); });
53 deleteFreeThreads();
54}
55
56void PlaybackEngine::onRendererFinished(const PlaybackEngineObjectID &id)
57{
58 if (!hasRenderer(id))
59 return;
60
61 auto isAtEnd = [this](auto trackType) {
62 return !m_renderers[trackType] || m_renderers[trackType]->isAtEnd();
63 };
64
65 if (!isAtEnd(QPlatformMediaPlayer::VideoStream))
66 return;
67
68 if (!isAtEnd(QPlatformMediaPlayer::AudioStream))
69 return;
70
71 if (!isAtEnd(QPlatformMediaPlayer::SubtitleStream) && !hasMediaStream())
72 return;
73
74 if (std::exchange(m_state, QMediaPlayer::StoppedState) == QMediaPlayer::StoppedState)
75 return;
76
77 finilizeTime(duration().asTimePoint());
78
79 forceUpdate();
80
81 qCDebug(qLcPlaybackEngine) << "Playback engine end of stream";
82
83 emit endOfStream();
84}
85
86void PlaybackEngine::onRendererLoopChanged(const PlaybackEngineObjectID &id, TrackPosition offset,
87 int loopIndex)
88{
89 if (!hasRenderer(id))
90 return;
91
92 if (loopIndex > m_currentLoopOffset.loopIndex) {
93 m_currentLoopOffset = { offset, loopIndex };
94 emit loopChanged();
95 } else if (loopIndex == m_currentLoopOffset.loopIndex && offset != m_currentLoopOffset.loopStartTimeUs) {
96 qWarning() << "Unexpected offset for loop" << loopIndex << ":" << offset.get() << "vs"
97 << m_currentLoopOffset.loopStartTimeUs.get();
98 m_currentLoopOffset.loopStartTimeUs = offset;
99 }
100}
101
102void PlaybackEngine::onFirstPacketFound(const PlaybackEngineObjectID &id, TrackPosition absSeekPos)
103{
104 if (!checkObjectID(m_demuxer, id))
105 return;
106
107 if (m_timeController.isStarted())
108 return;
109
110 const SteadyClock::time_point now = SteadyClock::now();
111 const SteadyClock::time_point expectedTimePoint = m_timeController.timeFromPosition(absSeekPos);
112 const auto delay =
113 std::chrono::round<std::chrono::microseconds>(now - expectedTimePoint);
114 qCDebug(qLcPlaybackEngine) << "Delay of demuxer initialization:" << delay;
115 m_timeController.sync(now, absSeekPos);
116 m_timeController.start();
117
118 forEachExistingObject<Renderer>(
119 [&](auto &renderer) { renderer->setTimeController(m_timeController); });
120}
121
122void PlaybackEngine::onRendererSynchronized(const PlaybackEngineObjectID &id,
123 SteadyClock::time_point tp, TrackPosition pos)
124{
125 if (!hasRenderer(id))
126 return;
127
128 Q_ASSERT(checkObjectID(m_renderers[QPlatformMediaPlayer::AudioStream], id));
129
130 forEachExistingObject<Renderer>([&](auto &renderer) {
131 if (id.objectID != renderer->objectID()) {
132 auto tc = m_timeController;
133 tc.syncSoft(tp, pos);
134 renderer->setTimeController(tc);
135 }
136 });
137
138 m_timeController.sync(tp, pos);
139}
140
141void PlaybackEngine::setState(QMediaPlayer::PlaybackState state) {
142 if (!m_media.avContext())
143 return;
144
145 if (state == m_state)
146 return;
147
148 const auto prevState = std::exchange(m_state, state);
149
150 if (m_state == QMediaPlayer::StoppedState) {
151 finalizeOutputs();
152 finilizeTime(TrackPosition(0));
153 }
154
155 if (prevState == QMediaPlayer::StoppedState || m_state == QMediaPlayer::StoppedState)
156 recreateObjects();
157
158 if (prevState == QMediaPlayer::StoppedState)
159 triggerStepIfNeeded();
160
161 updateObjectsPausedState();
162}
163
164void PlaybackEngine::updateObjectsPausedState()
165{
166 const bool paused = m_state != QMediaPlayer::PlayingState;
167 m_timeController.setPaused(paused);
168
169 forEachExistingObject([&](auto &object) {
170 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
171 object->setPaused(paused);
172 else
173 object->setPaused(false);
174 });
175}
176
177void PlaybackEngine::ObjectDeleter::operator()(PlaybackEngineObject *object) const
178{
179 Q_ASSERT(engine);
180 if (!std::exchange(engine->m_threadsDirty, true))
181 QMetaObject::invokeMethod(engine, &PlaybackEngine::deleteFreeThreads, Qt::QueuedConnection);
182
183 object->kill();
184}
185
186void PlaybackEngine::registerObject(PlaybackEngineObject &object)
187{
188 connect(&object, &PlaybackEngineObject::error, this, &PlaybackEngine::errorOccured);
189
190 auto threadName = objectThreadName(object);
191 auto &thread = m_threads[threadName];
192 if (!thread) {
193 thread = std::make_unique<QThread>();
194 thread->setObjectName(threadName);
195 thread->start();
196 }
197
198 Q_ASSERT(object.thread() != thread.get());
199 object.moveToThread(thread.get());
200}
201
203PlaybackEngine::createRenderer(QPlatformMediaPlayer::TrackType trackType)
204{
205 switch (trackType) {
206 case QPlatformMediaPlayer::VideoStream:
207 return m_videoSink ? createPlaybackEngineObject<VideoRenderer>(
208 m_timeController, m_videoSink, m_media.transformation())
209 : RendererPtr{ {}, {} };
210 case QPlatformMediaPlayer::AudioStream:
211 return m_audioOutput || m_audioBufferOutput
212 ? createPlaybackEngineObject<AudioRenderer>(
213 m_timeController, m_audioOutput, m_audioBufferOutput, m_pitchCompensation)
214 : RendererPtr{ {}, {} };
215 case QPlatformMediaPlayer::SubtitleStream:
216 return m_videoSink
217 ? createPlaybackEngineObject<SubtitleRenderer>(m_timeController, m_videoSink)
218 : RendererPtr{ {}, {} };
219 default:
220 return { {}, {} };
221 }
222}
223
224template<typename C, typename Action>
225void PlaybackEngine::forEachExistingObject(Action &&action)
226{
227 auto handleNotNullObject = [&](auto &object) {
228 if constexpr (std::is_base_of_v<C, std::remove_reference_t<decltype(*object)>>)
229 if (object)
230 action(object);
231 };
232
233 // The order Renderers => Demuxer is required for seek().
234 // For other cases, it doesn't make any difference.
235 std::for_each(m_renderers.begin(), m_renderers.end(), handleNotNullObject);
236 std::for_each(m_streams.begin(), m_streams.end(), handleNotNullObject);
237 handleNotNullObject(m_demuxer);
238}
239
240template<typename Action>
241void PlaybackEngine::forEachExistingObject(Action &&action)
242{
243 forEachExistingObject<PlaybackEngineObject>(std::forward<Action>(action));
244}
245
246void PlaybackEngine::seek(TrackPosition pos)
247{
248 pos = boundPosition(pos);
249
250 m_timeController.sync(m_currentLoopOffset.loopStartTimeUs.asDuration() + pos);
251 if (!m_demuxer || !m_media.avContext()) {
252 m_seekPending = true;
253 return;
254 }
255
256 m_seekPending = false;
257 ++m_currentID.sessionID;
258
259 m_timeController.deactivate();
260 m_timeController.setPaused(m_state != QMediaPlayer::PlayingState);
261
262 forEachExistingObject([&](auto &object) {
263 if constexpr (std::is_same_v<decltype(*object), Renderer &>)
264 object->seek(m_currentID.sessionID, m_timeController, m_currentLoopOffset);
265 else
266 object->seek(m_currentID.sessionID, pos, m_currentLoopOffset);
267 });
268
269 triggerStepIfNeeded();
270}
271
272void PlaybackEngine::setLoops(int loops)
273{
274 if (!isSeekable()) {
275 qWarning() << "Cannot set loops for non-seekable source";
276 return;
277 }
278
279 if (std::exchange(m_loops, loops) == loops)
280 return;
281
282 qCDebug(qLcPlaybackEngine) << "set playback engine loops:" << loops << "prev loops:" << m_loops
283 << "index:" << m_currentLoopOffset.loopIndex;
284
285 if (m_demuxer)
286 m_demuxer->setLoops(loops);
287}
288
289void PlaybackEngine::triggerStepIfNeeded()
290{
291 if (m_state != QMediaPlayer::PausedState)
292 return;
293
294 if (m_renderers[QPlatformMediaPlayer::VideoStream])
295 m_renderers[QPlatformMediaPlayer::VideoStream]->doForceStep();
296
297 // TODO: maybe trigger SubtitleStream.
298 // If trigger it, we have to make seeking for the current subtitle frame more stable.
299 // Or set some timeout for seeking.
300}
301
302QString PlaybackEngine::objectThreadName(const PlaybackEngineObject &object)
303{
304 QString result = QString::fromLatin1(object.metaObject()->className());
305 if (auto stream = qobject_cast<const StreamDecoder *>(&object))
306 result += QString::number(stream->trackType());
307
308 return result;
309}
310
312 if (rate == playbackRate())
313 return;
314
315 m_timeController.setPlaybackRate(rate);
316 forEachExistingObject<Renderer>([rate](auto &renderer) { renderer->setPlaybackRate(rate); });
317}
318
320 return m_timeController.playbackRate();
321}
322
323void PlaybackEngine::recreateObjects()
324{
325 m_timeController.deactivate();
326
327 forEachExistingObject([](auto &object) { object.reset(); });
328
329 createObjectsIfNeeded();
330}
331
332void PlaybackEngine::createObjectsIfNeeded()
333{
334 if (m_state == QMediaPlayer::StoppedState || !m_media.avContext())
335 return;
336
337 for (int i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i)
338 createStreamAndRenderer(static_cast<QPlatformMediaPlayer::TrackType>(i));
339
340 createDemuxer();
341
342 // temporary, to comply with the test disablingAllTracks_doesNotStopPlayback
343 if (!m_demuxer)
344 m_timeController.start();
345}
346
347void PlaybackEngine::forceUpdate()
348{
349 recreateObjects();
350 triggerStepIfNeeded();
351 updateObjectsPausedState();
352}
353
354void PlaybackEngine::createStreamAndRenderer(QPlatformMediaPlayer::TrackType trackType)
355{
356 auto codecContext = codecContextForTrack(trackType);
357
358 auto &renderer = m_renderers[trackType];
359
360 if (!codecContext)
361 return;
362
363 if (!renderer) {
364 renderer = createRenderer(trackType);
365
366 if (!renderer)
367 return;
368
369 connect(renderer.get(), &Renderer::synchronized, this,
370 &PlaybackEngine::onRendererSynchronized);
371
372 connect(renderer.get(), &Renderer::loopChanged, this,
373 &PlaybackEngine::onRendererLoopChanged);
374
375 connect(renderer.get(), &PlaybackEngineObject::atEnd, this,
376 &PlaybackEngine::onRendererFinished);
377 }
378
379 auto &stream = m_streams[trackType] =
380 createPlaybackEngineObject<StreamDecoder>(*codecContext, renderer->seekPosition());
381
382 Q_ASSERT(trackType == stream->trackType());
383
384 connect(stream.get(), &StreamDecoder::requestHandleFrame, renderer.get(), &Renderer::render);
385 connect(stream.get(), &PlaybackEngineObject::atEnd, renderer.get(),
386 &Renderer::onFinalFrameReceived);
387 connect(renderer.get(), &Renderer::frameProcessed, stream.get(),
388 &StreamDecoder::onFrameProcessed);
389}
390
391std::optional<CodecContext> PlaybackEngine::codecContextForTrack(QPlatformMediaPlayer::TrackType trackType)
392{
393 const auto streamIndex = m_media.currentStreamIndex(trackType);
394 if (streamIndex < 0)
395 return {};
396
397 auto &codecContext = m_codecContexts[trackType];
398
399 if (!codecContext) {
400 qCDebug(qLcPlaybackEngine)
401 << "Create codec for stream:" << streamIndex << "trackType:" << trackType;
402 auto maybeCodecContext = CodecContext::create(m_media.avContext()->streams[streamIndex],
403 m_media.avContext(), m_options);
404
405 if (!maybeCodecContext) {
406 emit errorOccured(QMediaPlayer::FormatError,
407 u"Cannot create codec," + maybeCodecContext.error());
408 return {};
409 }
410
411 codecContext = maybeCodecContext.value();
412 }
413
414 return codecContext;
415}
416
417bool PlaybackEngine::hasMediaStream() const
418{
419 return m_renderers[QPlatformMediaPlayer::AudioStream]
420 || m_renderers[QPlatformMediaPlayer::VideoStream];
421}
422
423void PlaybackEngine::createDemuxer()
424{
425 std::array<int, QPlatformMediaPlayer::NTrackTypes> streamIndexes = { -1, -1, -1 };
426
427 bool hasStreams = false;
428 forEachExistingObject<StreamDecoder>([&](auto &stream) {
429 hasStreams = true;
430 const auto trackType = stream->trackType();
431 streamIndexes[trackType] = m_media.currentStreamIndex(trackType);
432 });
433
434 if (!hasStreams)
435 return;
436
437 const TrackPosition currentLoopPosUs = currentPosition(false);
438
439 m_demuxer = createPlaybackEngineObject<Demuxer>(m_media.avContext(), currentLoopPosUs,
440 m_seekPending, m_currentLoopOffset,
441 streamIndexes, m_loops);
442
443 m_seekPending = false;
444
445 connect(m_demuxer.get(), &Demuxer::packetsBuffered, this, &PlaybackEngine::buffered);
446
447 forEachExistingObject<StreamDecoder>([&](auto &stream) {
448 connect(m_demuxer.get(), Demuxer::signalByTrackType(stream->trackType()), stream.get(),
449 &StreamDecoder::decode);
450 connect(m_demuxer.get(), &PlaybackEngineObject::atEnd, stream.get(),
451 &StreamDecoder::onFinalPacketReceived);
452 connect(stream.get(), &StreamDecoder::packetProcessed, m_demuxer.get(),
453 &Demuxer::onPacketProcessed);
454 });
455
456 connect(m_demuxer.get(), &Demuxer::firstPacketFound, this, &PlaybackEngine::onFirstPacketFound);
457}
458
459void PlaybackEngine::deleteFreeThreads() {
460 m_threadsDirty = false;
461 auto freeThreads = std::move(m_threads);
462
463 forEachExistingObject([&](auto &object) {
464 m_threads.insert(freeThreads.extract(objectThreadName(*object)));
465 });
466
467 for (auto &[name, thr] : freeThreads)
468 thr->quit();
469
470 for (auto &[name, thr] : freeThreads)
471 thr->wait();
472}
473
474void PlaybackEngine::setMedia(MediaDataHolder media)
475{
476 Q_ASSERT(!m_media.avContext()); // Playback engine does not support reloading media
477 Q_ASSERT(m_state == QMediaPlayer::StoppedState);
478 Q_ASSERT(m_threads.empty());
479
480 m_media = std::move(media);
481 updateVideoSinkSize();
482}
483
484void PlaybackEngine::setVideoSink(QVideoSink *sink)
485{
486 auto prev = std::exchange(m_videoSink, sink);
487 if (prev == sink)
488 return;
489
490 updateVideoSinkSize(prev);
492
493 if (!sink || !prev) {
494 // might need some improvements
495 forceUpdate();
496 }
497}
498
499void PlaybackEngine::setAudioSink(QPlatformAudioOutput *output) {
500 setAudioSink(output ? output->q : nullptr);
501}
502
503void PlaybackEngine::setAudioSink(QAudioOutput *output)
504{
505 QAudioOutput *prev = std::exchange(m_audioOutput, output);
506 if (prev == output)
507 return;
508
509 updateActiveAudioOutput(output);
510
511 if (!output || !prev) {
512 // might need some improvements
513 forceUpdate();
514 }
515}
516
517void PlaybackEngine::setAudioBufferOutput(QAudioBufferOutput *output)
518{
519 QAudioBufferOutput *prev = std::exchange(m_audioBufferOutput, output);
520 if (prev == output)
521 return;
522 updateActiveAudioOutput(output);
523}
524
526{
527 std::optional<TrackPosition> pos;
528
529 if (m_timeController.isStarted()) {
530 for (size_t i = 0; i < m_renderers.size(); ++i) {
531 const auto &renderer = m_renderers[i];
532 if (!renderer)
533 continue;
534
535 // skip subtitle stream for finding lower rendering position
536 if (!topPos && i == QPlatformMediaPlayer::SubtitleStream && hasMediaStream())
537 continue;
538
539 const auto rendererPos = renderer->lastPosition();
540 pos = !pos ? rendererPos
541 : topPos ? std::max(*pos, rendererPos)
542 : std::min(*pos, rendererPos);
543 }
544 }
545 // else we cannot reliably (without RC) getthe current renderers position after seeking
546
547 if (!pos)
548 pos = m_timeController.currentPosition();
549
550 return boundPosition(*pos - m_currentLoopOffset.loopStartTimeUs.asDuration());
551}
552
554{
555 return m_media.duration();
556}
557
558bool PlaybackEngine::isSeekable() const { return m_media.isSeekable(); }
559
561PlaybackEngine::streamInfo(QPlatformMediaPlayer::TrackType trackType) const
562{
563 return m_media.streamInfo(trackType);
564}
565
567{
568 return m_media.metaData();
569}
570
572{
573 return m_media.activeTrack(type);
574}
575
577{
578 m_pitchCompensation = enabled;
579 if (AudioRenderer *renderer = getAudioRenderer())
580 renderer->setPitchCompensation(enabled);
581}
582
583void PlaybackEngine::setActiveTrack(QPlatformMediaPlayer::TrackType trackType, int streamNumber)
584{
585 if (!m_media.setActiveTrack(trackType, streamNumber))
586 return;
587
588 m_codecContexts[trackType] = {};
589
590 m_renderers[trackType].reset();
591 m_streams = defaultObjectsArray<decltype(m_streams)>();
592 m_demuxer.reset();
593
594 // Don't deactivate m_timeController:
595 //
596 // We strive to have a smooth playback if we change the active track. It means that
597 // we don't want to do any time shiftings. Instead, we rely on the fact that
598 // buffers in renderers are not empty to compensate the demuxer's lag.
599
600 updateVideoSinkSize();
601 createObjectsIfNeeded();
602 updateObjectsPausedState();
603}
604
605void PlaybackEngine::finilizeTime(TrackPosition pos)
606{
607 Q_ASSERT(pos >= TrackPosition(0) && pos <= duration().asTimePoint());
608
609 m_timeController.deactivate();
610 m_timeController.sync(pos);
611 m_currentLoopOffset = {};
612}
613
614void PlaybackEngine::finalizeOutputs()
615{
616 if (m_audioBufferOutput)
617 updateActiveAudioOutput(static_cast<QAudioBufferOutput *>(nullptr));
618 if (m_audioOutput)
619 updateActiveAudioOutput(static_cast<QAudioOutput *>(nullptr));
620 updateActiveVideoOutput(nullptr, true);
621}
622
623bool PlaybackEngine::hasRenderer(const PlaybackEngineObjectID &id) const
624{
625 return ranges::any_of(m_renderers, [&](auto &renderer) {
626 return checkObjectID(renderer, id);
627 });
628}
629
630template <typename AudioOutput>
631void PlaybackEngine::updateActiveAudioOutput(AudioOutput *output)
632{
633 if (AudioRenderer *renderer = getAudioRenderer())
634 renderer->setOutput(output);
635}
636
637void PlaybackEngine::updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput)
638{
639 if (auto renderer = qobject_cast<SubtitleRenderer *>(
640 m_renderers[QPlatformMediaPlayer::SubtitleStream].get()))
641 renderer->setOutput(sink, cleanOutput);
642 if (auto renderer =
643 qobject_cast<VideoRenderer *>(m_renderers[QPlatformMediaPlayer::VideoStream].get()))
644 renderer->setOutput(sink, cleanOutput);
645}
646
647void PlaybackEngine::updateVideoSinkSize(QVideoSink *prevSink)
648{
649 auto platformVideoSink = m_videoSink ? m_videoSink->platformVideoSink() : nullptr;
650 if (!platformVideoSink)
651 return;
652
653 if (prevSink && prevSink->platformVideoSink())
654 platformVideoSink->setNativeSize(prevSink->platformVideoSink()->nativeSize());
655 else {
656 const auto streamIndex = m_media.currentStreamIndex(QPlatformMediaPlayer::VideoStream);
657 if (streamIndex >= 0) {
658 const auto context = m_media.avContext();
659 const auto stream = context->streams[streamIndex];
660 const AVRational pixelAspectRatio =
661 av_guess_sample_aspect_ratio(context, stream, nullptr);
662 // auto size = metaData().value(QMediaMetaData::Resolution)
663 const QSize size =
664 qCalculateFrameSize({ stream->codecpar->width, stream->codecpar->height },
665 { pixelAspectRatio.num, pixelAspectRatio.den });
666
667 platformVideoSink->setNativeSize(
668 qRotatedFrameSize(size, m_media.transformation().rotation));
669 }
670 }
671}
672
673TrackPosition PlaybackEngine::boundPosition(TrackPosition position) const
674{
675 position = qMax(position, TrackPosition(0));
676 return duration() > TrackDuration(0) ? qMin(position, duration().asTimePoint()) : position;
677}
678
679AudioRenderer *PlaybackEngine::getAudioRenderer()
680{
681 return qobject_cast<AudioRenderer *>(m_renderers[QPlatformMediaPlayer::AudioStream].get());
682}
683
684} // namespace QFFmpeg
685
686QT_END_NAMESPACE
687
688#include "moc_qffmpegplaybackengine_p.cpp"
void setPitchCompensation(bool enabled)
void packetsBuffered()
void seek(TrackPosition pos)
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)
void setPitchCompensation(bool enabled)
const QList< MediaDataHolder::StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
void setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
void setMedia(MediaDataHolder media)
TrackPosition currentPosition(bool topPos=true) const
const QMediaMetaData & metaData() const
void updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput=false)
The QPlaybackOptions class enables low-level control of media playback options.
static Array defaultObjectsArray()
std::conditional_t< QT_FFMPEG_AVIO_WRITE_CONST, const uint8_t *, uint8_t * > AvioWriteBufferType
Combined button and popup list for selecting options.
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)
void operator()(PlaybackEngineObject *) const