Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qffmpegplaybackengine_p.h"
#include "qvideosink.h"
#include "qaudiooutput.h"
#include "private/qplatformaudiooutput_p.h"
#include "private/qplatformvideosink_p.h"
#include "private/qaudiobufferoutput_p.h"
#include "qiodevice.h"
#include "playbackengine/qffmpegdemuxer_p.h"
#include "playbackengine/qffmpegstreamdecoder_p.h"
#include "playbackengine/qffmpegsubtitlerenderer_p.h"
#include "playbackengine/qffmpegvideorenderer_p.h"
#include "playbackengine/qffmpegaudiorenderer_p.h"
#include <qloggingcategory.h>
QT_BEGIN_NAMESPACE
namespace ranges = QtMultimediaPrivate::ranges;
namespace QFFmpeg {
Q_STATIC_LOGGING_CATEGORY(qLcPlaybackEngine, "qt.multimedia.ffmpeg.playbackengine");
// The helper is needed since on some compilers std::unique_ptr
// doesn't have a default constructor in the case of sizeof(CustomDeleter) > 0
template <typename Array>
inline static Array defaultObjectsArray()
{
using T = typename Array::value_type;
return { T{ {}, {} }, T{ {}, {} }, T{ {}, {} } };
}
PlaybackEngine::PlaybackEngine(const QPlaybackOptions &options)
: m_demuxer({}, {}),
m_streams(defaultObjectsArray<decltype(m_streams)>()),
m_renderers(defaultObjectsArray<decltype(m_renderers)>()),
m_options{ options }
{
qCDebug(qLcPlaybackEngine) << "Create PlaybackEngine";
qRegisterMetaType<QFFmpeg::Packet>();
qRegisterMetaType<QFFmpeg::Frame>();
qRegisterMetaType<QFFmpeg::TrackPosition>();
qRegisterMetaType<QFFmpeg::PlaybackEngineObjectID>();
}
PlaybackEngine::~PlaybackEngine() {
qCDebug(qLcPlaybackEngine) << "Delete PlaybackEngine";
finalizeOutputs();
forEachExistingObject([](auto &object) { object.reset(); });
deleteFreeThreads();
}
void PlaybackEngine::onRendererFinished(const PlaybackEngineObjectID &id)
{
if (!hasRenderer(id))
return;
auto isAtEnd = [this](TrackType trackType) {
auto &renderer = m_renderers[trackType];
return !renderer || renderer->isAtEnd();
};
if (!isAtEnd(TrackType::VideoStream))
return;
if (!isAtEnd(TrackType::AudioStream))
return;
if (!isAtEnd(TrackType::SubtitleStream) && !hasMediaStream())
return;
if (std::exchange(m_state, QMediaPlayer::StoppedState) == QMediaPlayer::StoppedState)
return;
finilizeTime(duration().asTimePoint());
forceUpdate();
qCDebug(qLcPlaybackEngine) << "Playback engine end of stream";
Q_EMIT endOfStream();
}
void PlaybackEngine::onRendererLoopChanged(const PlaybackEngineObjectID &id, TrackPosition offset,
int loopIndex)
{
if (!hasRenderer(id))
return;
if (loopIndex > m_currentLoopOffset.loopIndex) {
m_currentLoopOffset = { offset, loopIndex };
Q_EMIT loopChanged();
} else if (loopIndex == m_currentLoopOffset.loopIndex && offset != m_currentLoopOffset.loopStartTimeUs) {
qWarning() << "Unexpected offset for loop" << loopIndex << ":" << offset.get() << "vs"
<< m_currentLoopOffset.loopStartTimeUs.get();
m_currentLoopOffset.loopStartTimeUs = offset;
}
}
void PlaybackEngine::onFirstPacketFound(const PlaybackEngineObjectID &id, TrackPosition absSeekPos)
{
if (!checkObjectID(m_demuxer, id))
return;
if (m_timeController.isStarted())
return;
const SteadyClock::time_point now = SteadyClock::now();
const SteadyClock::time_point expectedTimePoint = m_timeController.timeFromPosition(absSeekPos);
const auto delay =
std::chrono::round<std::chrono::microseconds>(now - expectedTimePoint);
qCDebug(qLcPlaybackEngine) << "Delay of demuxer initialization:" << delay;
m_timeController.sync(now, absSeekPos);
m_timeController.start();
forEachExistingObject<Renderer>(
[&](auto &renderer) { renderer->setTimeController(m_timeController); });
}
void PlaybackEngine::onRendererSynchronized(const PlaybackEngineObjectID &id,
SteadyClock::time_point tp, TrackPosition pos)
{
if (!hasRenderer(id))
return;
Q_ASSERT(checkObjectID(
m_renderers[TrackType::AudioStream], id));
forEachExistingObject<Renderer>([&](auto &renderer) {
if (id.objectID != renderer->objectID()) {
auto tc = m_timeController;
tc.syncSoft(tp, pos);
renderer->setTimeController(tc);
}
});
m_timeController.sync(tp, pos);
}
void PlaybackEngine::setState(QMediaPlayer::PlaybackState state) {
if (!m_media.avContext())
return;
if (state == m_state)
return;
const auto prevState = std::exchange(m_state, state);
if (m_state == QMediaPlayer::StoppedState) {
finalizeOutputs();
finilizeTime(TrackPosition(0));
}
if (prevState == QMediaPlayer::StoppedState || m_state == QMediaPlayer::StoppedState)
recreateObjects();
if (prevState == QMediaPlayer::StoppedState)
triggerStepIfNeeded();
updateObjectsPausedState();
}
void PlaybackEngine::updateObjectsPausedState()
{
const bool paused = m_state != QMediaPlayer::PlayingState;
m_timeController.setPaused(paused);
forEachExistingObject([&](auto &object) {
if constexpr (std::is_same_v<decltype(*object), Renderer &>)
object->setPaused(paused);
else
object->setPaused(false);
});
}
void PlaybackEngine::ObjectDeleter::operator()(PlaybackEngineObject *object) const
{
Q_ASSERT(engine);
if (!std::exchange(engine->m_threadsDirty, true))
QMetaObject::invokeMethod(engine, &PlaybackEngine::deleteFreeThreads, Qt::QueuedConnection);
object->kill();
}
void PlaybackEngine::registerObject(PlaybackEngineObject &object)
{
connect(&object, &PlaybackEngineObject::error, this, &PlaybackEngine::errorOccured);
auto threadName = objectThreadName(object);
auto &thread = m_threads[threadName];
if (!thread) {
thread = std::make_unique<QThread>();
thread->setObjectName(threadName);
thread->start();
}
Q_ASSERT(object.thread() != thread.get());
object.moveToThread(thread.get());
}
PlaybackEngine::RendererPtr
PlaybackEngine::createRenderer(TrackType trackType)
{
switch (trackType) {
case TrackType::VideoStream:
return m_videoSink ? createPlaybackEngineObject<VideoRenderer>(
m_timeController, m_videoSink, m_media.transformation())
: RendererPtr{ {}, {} };
case TrackType::AudioStream:
return m_audioOutput || m_audioBufferOutput
? createPlaybackEngineObject<AudioRenderer>(
m_timeController, m_audioOutput, m_audioBufferOutput, m_pitchCompensation)
: RendererPtr{ {}, {} };
case TrackType::SubtitleStream:
return m_videoSink
? createPlaybackEngineObject<SubtitleRenderer>(m_timeController, m_videoSink)
: RendererPtr{ {}, {} };
default:
return { {}, {} };
}
}
template<typename C, typename Action>
void PlaybackEngine::forEachExistingObject(Action &&action)
{
auto handleNotNullObject = [&](auto &object) {
if constexpr (std::is_base_of_v<C, std::remove_reference_t<decltype(*object)>>)
if (object)
action(object);
};
// The order Renderers => Demuxer is required for seek().
// For other cases, it doesn't make any difference.
std::for_each(m_renderers.begin(), m_renderers.end(), handleNotNullObject);
std::for_each(m_streams.begin(), m_streams.end(), handleNotNullObject);
handleNotNullObject(m_demuxer);
}
template<typename Action>
void PlaybackEngine::forEachExistingObject(Action &&action)
{
forEachExistingObject<PlaybackEngineObject>(std::forward<Action>(action));
}
void PlaybackEngine::seek(TrackPosition pos)
{
pos = boundPosition(pos);
m_timeController.sync(m_currentLoopOffset.loopStartTimeUs.asDuration() + pos);
if (!m_demuxer || !m_media.avContext()) {
m_seekPending = true;
return;
}
m_seekPending = false;
++m_currentID.sessionID;
m_timeController.deactivate();
m_timeController.setPaused(m_state != QMediaPlayer::PlayingState);
forEachExistingObject([&](auto &object) {
if constexpr (std::is_same_v<decltype(*object), Renderer &>)
object->seek(m_currentID.sessionID, m_timeController, m_currentLoopOffset);
else
object->seek(m_currentID.sessionID, pos, m_currentLoopOffset);
});
triggerStepIfNeeded();
}
void PlaybackEngine::setLoops(int loops)
{
if (!isSeekable()) {
qWarning() << "Cannot set loops for non-seekable source";
return;
}
if (std::exchange(m_loops, loops) == loops)
return;
qCDebug(qLcPlaybackEngine) << "set playback engine loops:" << loops << "prev loops:" << m_loops
<< "index:" << m_currentLoopOffset.loopIndex;
if (m_demuxer)
m_demuxer->setLoops(loops);
}
void PlaybackEngine::triggerStepIfNeeded()
{
if (m_state != QMediaPlayer::PausedState)
return;
auto &videoRenderer = m_renderers[TrackType::VideoStream];
if (videoRenderer)
videoRenderer->doForceStep();
// TODO: maybe trigger SubtitleStream.
// If trigger it, we have to make seeking for the current subtitle frame more stable.
// Or set some timeout for seeking.
}
QString PlaybackEngine::objectThreadName(const PlaybackEngineObject &object)
{
QString result = QString::fromLatin1(object.metaObject()->className());
if (auto stream = qobject_cast<const StreamDecoder *>(&object))
result += QString::number(qToUnderlying(stream->trackType()));
return result;
}
void PlaybackEngine::setPlaybackRate(float rate) {
if (rate == playbackRate())
return;
m_timeController.setPlaybackRate(rate);
forEachExistingObject<Renderer>([rate](auto &renderer) { renderer->setPlaybackRate(rate); });
}
float PlaybackEngine::playbackRate() const {
return m_timeController.playbackRate();
}
void PlaybackEngine::recreateObjects()
{
m_timeController.deactivate();
forEachExistingObject([](auto &object) { object.reset(); });
createObjectsIfNeeded();
}
void PlaybackEngine::createObjectsIfNeeded()
{
if (m_state == QMediaPlayer::StoppedState || !m_media.avContext())
return;
for (size_t i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i)
createStreamAndRenderer(static_cast<TrackType>(i));
createDemuxer();
// temporary, to comply with the test disablingAllTracks_doesNotStopPlayback
if (!m_demuxer)
m_timeController.start();
}
void PlaybackEngine::forceUpdate()
{
recreateObjects();
triggerStepIfNeeded();
updateObjectsPausedState();
}
void PlaybackEngine::createStreamAndRenderer(TrackType trackType)
{
auto codecContext = codecContextForTrack(trackType);
auto &renderer = m_renderers[trackType];
if (!codecContext)
return;
if (!renderer) {
renderer = createRenderer(trackType);
if (!renderer)
return;
connect(renderer.get(), &Renderer::synchronized, this,
&PlaybackEngine::onRendererSynchronized);
connect(renderer.get(), &Renderer::loopChanged, this,
&PlaybackEngine::onRendererLoopChanged);
connect(renderer.get(), &PlaybackEngineObject::atEnd, this,
&PlaybackEngine::onRendererFinished);
}
Q_ASSERT(MediaDataHolder::trackTypeFromMediaType(codecContext->context()->codec_type));
auto &stream = m_streams[trackType] =
createPlaybackEngineObject<StreamDecoder>(*codecContext, renderer->seekPosition());
Q_ASSERT(trackType == stream->trackType());
connect(stream.get(), &StreamDecoder::requestHandleFrame, renderer.get(), &Renderer::render);
connect(stream.get(), &PlaybackEngineObject::atEnd, renderer.get(),
&Renderer::onFinalFrameReceived);
connect(renderer.get(), &Renderer::frameProcessed, stream.get(),
&StreamDecoder::onFrameProcessed);
}
std::optional<CodecContext> PlaybackEngine::codecContextForTrack(TrackType trackType)
{
const auto streamIndex = m_media.currentStreamIndex(trackType);
if (streamIndex < 0)
return {};
auto &codecContext = m_codecContexts[trackType];
if (!codecContext) {
qCDebug(qLcPlaybackEngine)
<< "Create codec for stream:" << streamIndex << "trackType:" << trackType;
auto maybeCodecContext = CodecContext::create(m_media.avContext()->streams[streamIndex],
m_media.avContext(), m_options);
if (!maybeCodecContext) {
Q_EMIT errorOccured(QMediaPlayer::FormatError,
u"Cannot create codec," + maybeCodecContext.error());
return {};
}
codecContext = maybeCodecContext.value();
}
return codecContext;
}
bool PlaybackEngine::hasMediaStream() const
{
return m_renderers[TrackType::AudioStream]
|| m_renderers[TrackType::VideoStream];
}
void PlaybackEngine::createDemuxer()
{
StreamIndexes streamIndexes = { -1, -1, -1 };
bool hasStreams = false;
forEachExistingObject<StreamDecoder>([&](auto &stream) {
hasStreams = true;
const auto trackType = stream->trackType();
streamIndexes[trackType] = m_media.currentStreamIndex(trackType);
});
if (!hasStreams)
return;
const TrackPosition currentLoopPosUs = currentPosition(false);
m_demuxer = createPlaybackEngineObject<Demuxer>(m_media.avContext(), currentLoopPosUs,
m_seekPending, m_currentLoopOffset,
streamIndexes, m_loops);
m_seekPending = false;
connect(m_demuxer.get(), &Demuxer::packetsBuffered, this, &PlaybackEngine::buffered);
forEachExistingObject<StreamDecoder>([&](auto &stream) {
connect(m_demuxer.get(), Demuxer::signalByTrackType(stream->trackType()), stream.get(),
&StreamDecoder::decode);
connect(m_demuxer.get(), &PlaybackEngineObject::atEnd, stream.get(),
&StreamDecoder::onFinalPacketReceived);
connect(stream.get(), &StreamDecoder::packetProcessed, m_demuxer.get(),
&Demuxer::onPacketProcessed);
});
connect(m_demuxer.get(), &Demuxer::firstPacketFound, this, &PlaybackEngine::onFirstPacketFound);
}
void PlaybackEngine::deleteFreeThreads() {
m_threadsDirty = false;
auto freeThreads = std::move(m_threads);
forEachExistingObject([&](auto &object) {
m_threads.insert(freeThreads.extract(objectThreadName(*object)));
});
for (auto &[name, thr] : freeThreads)
thr->quit();
for (auto &[name, thr] : freeThreads)
thr->wait();
}
void PlaybackEngine::setMedia(MediaDataHolder media)
{
Q_ASSERT(!m_media.avContext()); // Playback engine does not support reloading media
Q_ASSERT(m_state == QMediaPlayer::StoppedState);
Q_ASSERT(m_threads.empty());
m_media = std::move(media);
updateVideoSinkSize();
}
void PlaybackEngine::setVideoSink(QVideoSink *sink)
{
auto prev = std::exchange(m_videoSink, sink);
if (prev == sink)
return;
updateVideoSinkSize(prev);
updateActiveVideoOutput(sink);
if (!sink || !prev) {
// might need some improvements
forceUpdate();
}
}
void PlaybackEngine::setAudioSink(QPlatformAudioOutput *output) {
setAudioSink(output ? output->q : nullptr);
}
void PlaybackEngine::setAudioSink(QAudioOutput *output)
{
QAudioOutput *prev = std::exchange(m_audioOutput, output);
if (prev == output)
return;
updateActiveAudioOutput(output);
if (!output || !prev) {
// might need some improvements
forceUpdate();
}
}
void PlaybackEngine::setAudioBufferOutput(QAudioBufferOutput *output)
{
QAudioBufferOutput *prev = std::exchange(m_audioBufferOutput, output);
if (prev == output)
return;
updateActiveAudioOutput(output);
}
TrackPosition PlaybackEngine::currentPosition(bool topPos) const
{
std::optional<TrackPosition> pos;
if (m_timeController.isStarted()) {
for (size_t i = 0; i < m_renderers.size(); ++i) {
const auto &renderer = m_renderers[i];
if (!renderer)
continue;
// skip subtitle stream for finding lower rendering position
if (!topPos && i == qToUnderlying(TrackType::SubtitleStream)
&& hasMediaStream())
continue;
const auto rendererPos = renderer->lastPosition();
pos = !pos ? rendererPos
: topPos ? std::max(*pos, rendererPos)
: std::min(*pos, rendererPos);
}
}
// else we cannot reliably (without RC) getthe current renderers position after seeking
if (!pos)
pos = m_timeController.currentPosition();
return boundPosition(*pos - m_currentLoopOffset.loopStartTimeUs.asDuration());
}
TrackDuration PlaybackEngine::duration() const
{
return m_media.duration();
}
bool PlaybackEngine::isSeekable() const { return m_media.isSeekable(); }
const QList<MediaDataHolder::StreamInfo> &
PlaybackEngine::streamInfo(TrackType trackType) const
{
return m_media.streamInfo(trackType);
}
const QMediaMetaData &PlaybackEngine::metaData() const
{
return m_media.metaData();
}
int PlaybackEngine::activeTrack(TrackType type) const
{
return m_media.activeTrack(type);
}
void PlaybackEngine::setPitchCompensation(bool enabled)
{
m_pitchCompensation = enabled;
if (AudioRenderer *renderer = getAudioRenderer())
renderer->setPitchCompensation(enabled);
}
void PlaybackEngine::setActiveTrack(TrackType trackType, int streamNumber)
{
if (!m_media.setActiveTrack(trackType, streamNumber))
return;
m_codecContexts[trackType] = {};
m_renderers[trackType].reset();
m_streams = defaultObjectsArray<decltype(m_streams)>();
m_demuxer.reset();
// Don't deactivate m_timeController:
//
// We strive to have a smooth playback if we change the active track. It means that
// we don't want to do any time shiftings. Instead, we rely on the fact that
// buffers in renderers are not empty to compensate the demuxer's lag.
updateVideoSinkSize();
createObjectsIfNeeded();
updateObjectsPausedState();
}
void PlaybackEngine::finilizeTime(TrackPosition pos)
{
Q_ASSERT(pos >= TrackPosition(0) && pos <= duration().asTimePoint());
m_timeController.deactivate();
m_timeController.sync(pos);
m_currentLoopOffset = {};
}
void PlaybackEngine::finalizeOutputs()
{
if (m_audioBufferOutput)
updateActiveAudioOutput(static_cast<QAudioBufferOutput *>(nullptr));
if (m_audioOutput)
updateActiveAudioOutput(static_cast<QAudioOutput *>(nullptr));
updateActiveVideoOutput(nullptr, true);
}
bool PlaybackEngine::hasRenderer(const PlaybackEngineObjectID &id) const
{
return ranges::any_of(m_renderers, [&](auto &renderer) {
return checkObjectID(renderer, id);
});
}
template <typename AudioOutput>
void PlaybackEngine::updateActiveAudioOutput(AudioOutput *output)
{
if (AudioRenderer *renderer = getAudioRenderer())
renderer->setOutput(output);
}
void PlaybackEngine::updateActiveVideoOutput(QVideoSink *sink, bool cleanOutput)
{
if (auto renderer = qobject_cast<SubtitleRenderer *>(
m_renderers[TrackType::SubtitleStream]
.get()))
renderer->setOutput(sink, cleanOutput);
if (auto renderer = qobject_cast<VideoRenderer *>(
m_renderers[TrackType::VideoStream].get()))
renderer->setOutput(sink, cleanOutput);
}
void PlaybackEngine::updateVideoSinkSize(QVideoSink *prevSink)
{
auto platformVideoSink = m_videoSink ? m_videoSink->platformVideoSink() : nullptr;
if (!platformVideoSink)
return;
if (prevSink && prevSink->platformVideoSink())
platformVideoSink->setNativeSize(prevSink->platformVideoSink()->nativeSize());
else {
const auto streamIndex =
m_media.currentStreamIndex(TrackType::VideoStream);
if (streamIndex >= 0) {
const auto context = m_media.avContext();
const auto stream = context->streams[streamIndex];
const AVRational pixelAspectRatio =
av_guess_sample_aspect_ratio(context, stream, nullptr);
// auto size = metaData().value(QMediaMetaData::Resolution)
const QSize size =
qCalculateFrameSize({ stream->codecpar->width, stream->codecpar->height },
{ pixelAspectRatio.num, pixelAspectRatio.den });
platformVideoSink->setNativeSize(
qRotatedFrameSize(size, m_media.transformation().rotation));
}
}
}
TrackPosition PlaybackEngine::boundPosition(TrackPosition position) const
{
position = qMax(position, TrackPosition(0));
return duration() > TrackDuration(0) ? qMin(position, duration().asTimePoint()) : position;
}
AudioRenderer *PlaybackEngine::getAudioRenderer()
{
return qobject_cast<AudioRenderer *>(
m_renderers[TrackType::AudioStream].get());
}
} // namespace QFFmpeg
QT_END_NAMESPACE
#include "moc_qffmpegplaybackengine_p.cpp"