Revision control

Copy as Markdown

// Copyright (C) 2016 Research In Motion
// Copyright (C) 2021 The Qt Company
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qqnxmediaplayer_p.h"
#include "qqnxvideosink_p.h"
#include "qqnxmediautil_p.h"
#include "qqnxmediaeventthread_p.h"
#include "qqnxwindowgrabber_p.h"
#include <private/qhwvideobuffer_p.h>
#include <private/qvideoframe_p.h>
#include <QtCore/qabstracteventdispatcher.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qdir.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/quuid.h>
#include <mm/renderer.h>
#include <qmediaplayer.h>
#include <qqnxaudiooutput_p.h>
#include <qaudiooutput.h>
#include <errno.h>
#include <sys/strm.h>
#include <sys/stat.h>
#include <algorithm>
#include <tuple>
static constexpr int rateToSpeed(qreal rate)
{
return std::floor(rate * 1000);
}
static constexpr qreal speedToRate(int speed)
{
return std::floor(speed / 1000.0);
}
static constexpr int normalizeVolume(float volume)
{
return std::clamp<int>(std::floor(volume * 100.0), 0, 100);
}
static std::tuple<int, int, bool> parseBufferLevel(const QString &value)
{
if (value.isEmpty())
return {};
const int slashPos = value.indexOf(QChar(u'/'));
if (slashPos <= 0)
return {};
bool ok = false;
const int level = value.left(slashPos).toInt(&ok);
if (!ok || level < 0)
return {};
const int capacity = value.mid(slashPos + 1).toInt(&ok);
if (!ok || capacity < 0)
return {};
return { level, capacity, true };
}
class QnxTextureBuffer : public QHwVideoBuffer
{
public:
QnxTextureBuffer(QQnxWindowGrabber *QQnxWindowGrabber)
{
m_windowGrabber = QQnxWindowGrabber;
m_handle = 0;
}
void unmap() override {}
MapData map(QVideoFrame::MapMode /*mode*/) override
{
return {};
}
quint64 textureHandle(QRhi &, int plane) override
{
if (plane != 0)
return 0;
if (!m_handle)
m_handle = m_windowGrabber->getNextTextureId();
return m_handle;
}
private:
QQnxWindowGrabber *m_windowGrabber;
quint64 m_handle;
};
class QnxRasterBuffer : public QAbstractVideoBuffer
{
public:
QnxRasterBuffer(QQnxWindowGrabber *windowGrabber) { m_windowGrabber = windowGrabber; }
MapData map(QVideoFrame::MapMode mode) override
{
if (mode != QVideoFrame::ReadOnly)
return {};
if (buffer.data) {
qWarning("QnxRasterBuffer: need to unmap before mapping");
return {};
}
buffer = m_windowGrabber->getNextBuffer();
return {
.planeCount = 1,
.bytesPerLine = { buffer.stride },
.data = { buffer.data },
.dataSize = { buffer.width * buffer.height * buffer.pixelSize }
};
}
void unmap() override
{
buffer = {};
}
QVideoFrameFormat format() const override { return {}; }
private:
QQnxWindowGrabber *m_windowGrabber;
QQnxWindowGrabber::BufferView buffer;
};
QT_BEGIN_NAMESPACE
QQnxMediaPlayer::QQnxMediaPlayer(QMediaPlayer *parent)
: QObject(parent)
, QPlatformMediaPlayer(parent)
, m_windowGrabber(new QQnxWindowGrabber(this))
{
m_flushPositionTimer.setSingleShot(true);
m_flushPositionTimer.setInterval(100);
connect(&m_flushPositionTimer, &QTimer::timeout, this, &QQnxMediaPlayer::flushPosition);
connect(m_windowGrabber, &QQnxWindowGrabber::updateScene, this, &QQnxMediaPlayer::updateScene);
openConnection();
}
QQnxMediaPlayer::~QQnxMediaPlayer()
{
stop();
detach();
closeConnection();
}
void QQnxMediaPlayer::openConnection()
{
static int idCounter = 0;
m_connection = mmr_connect(nullptr);
if (!m_connection) {
emitPError(QString::fromLatin1("Unable to connect to the multimedia renderer"));
return;
}
m_id = idCounter++;
m_contextName = QString::fromLatin1("QQnxMediaPlayer_%1_%2").arg(m_id)
.arg(QCoreApplication::applicationPid());
m_context = mmr_context_create(m_connection, m_contextName.toLatin1().constData(),
0, S_IRWXU|S_IRWXG|S_IRWXO);
if (!m_context) {
emitPError(QString::fromLatin1("Unable to create context"));
closeConnection();
return;
}
startMonitoring();
}
void QQnxMediaPlayer::handleMmEventState(const mmr_event_t *event)
{
if (!event || event->type != MMR_EVENT_STATE)
return;
switch (event->state) {
case MMR_STATE_DESTROYED:
break;
case MMR_STATE_IDLE:
mediaStatusChanged(QMediaPlayer::NoMedia);
stateChanged(QMediaPlayer::StoppedState);
detachVideoOutput();
detachInput();
break;
case MMR_STATE_STOPPED:
stateChanged(QMediaPlayer::StoppedState);
m_windowGrabber->stop();
if (m_platformVideoSink)
m_platformVideoSink->setVideoFrame({});
break;
case MMR_STATE_PLAYING:
if (event->speed == 0) {
stateChanged(QMediaPlayer::PausedState);
m_windowGrabber->pause();
} else if (state() == QMediaPlayer::PausedState) {
m_windowGrabber->resume();
stateChanged(QMediaPlayer::PlayingState);
} else {
m_windowGrabber->start();
stateChanged(QMediaPlayer::PlayingState);
}
if (event->speed != m_speed) {
m_speed = event->speed;
if (state() != QMediaPlayer::PausedState)
m_configuredSpeed = m_speed;
playbackRateChanged(::speedToRate(m_speed));
}
break;
}
}
void QQnxMediaPlayer::handleMmEventStatus(const mmr_event_t *event)
{
if (!event || event->type != MMR_EVENT_STATUS)
return;
if (event->data)
handleMmEventStatusData(event->data);
// update pos
if (!event->pos_str || isPendingPositionFlush())
return;
const QByteArray valueBa(event->pos_str);
bool ok;
const qint64 position = valueBa.toLongLong(&ok);
if (!ok)
qCritical("Could not parse position from '%s'", valueBa.constData());
else
handleMmPositionChanged(position);
}
void QQnxMediaPlayer::handleMmEventStatusData(const strm_dict_t *data)
{
if (!data)
return;
const auto getValue = [data](const char *key) -> QString {
const strm_string_t *value = strm_dict_find_rstr(data, key);
if (!value)
return {};
return QString::fromUtf8(strm_string_get(value));
};
// update bufferProgress
const QString bufferLevel = getValue("bufferlevel");
if (!bufferLevel.isEmpty()) {
const auto & [level, capacity, ok] = ::parseBufferLevel(bufferLevel);
if (ok)
updateBufferLevel(level, capacity);
else
qCritical("Could not parse buffer capacity from '%s'", qUtf8Printable(bufferLevel));
}
// update MediaStatus
const QString bufferStatus = getValue("bufferstatus");
const QString suspended = getValue("suspended");
if (suspended == QStringLiteral("yes"))
mediaStatusChanged(QMediaPlayer::StalledMedia);
else if (bufferStatus == QStringLiteral("buffering"))
mediaStatusChanged(QMediaPlayer::BufferingMedia);
else if (bufferStatus == QStringLiteral("playing"))
mediaStatusChanged(QMediaPlayer::BufferedMedia);
}
void QQnxMediaPlayer::handleMmEventError(const mmr_event_t *event)
{
if (!event)
return;
// When playback is explicitly stopped using mmr_stop(), mm-renderer
// generates a STATE event. When the end of media is reached, an ERROR
// event is generated and the error code contained in the event information
// is set to MMR_ERROR_NONE. When an error causes playback to stop,
// the error code is set to something else.
if (event->details.error.info.error_code == MMR_ERROR_NONE) {
mediaStatusChanged(QMediaPlayer::EndOfMedia);
stateChanged(QMediaPlayer::StoppedState);
}
}
void QQnxMediaPlayer::closeConnection()
{
stopMonitoring();
if (m_context) {
mmr_context_destroy(m_context);
m_context = nullptr;
m_contextName.clear();
}
if (m_connection) {
mmr_disconnect(m_connection);
m_connection = nullptr;
}
}
QByteArray QQnxMediaPlayer::resourcePathForUrl(const QUrl &url)
{
// If this is a local file, mmrenderer expects the file:// prefix and an absolute path.
// We treat URLs without scheme as local files, most likely someone just forgot to set the
// file:// prefix when constructing the URL.
if (url.isLocalFile() || url.scheme().isEmpty()) {
const QString relativeFilePath = url.scheme().isEmpty() ? url.path() : url.toLocalFile();
const QFileInfo fileInfo(relativeFilePath);
return QFile::encodeName(QStringLiteral("file://") + fileInfo.absoluteFilePath());
// HTTP or similar URL
} else {
return url.toEncoded();
}
}
void QQnxMediaPlayer::attach()
{
// Should only be called in detached state
if (isInputAttached())
return;
if (!m_media.isValid() || !m_context) {
mediaStatusChanged(QMediaPlayer::NoMedia);
return;
}
resetMonitoring();
if (!(attachVideoOutput() && attachAudioOutput() && attachInput())) {
detach();
return;
}
mediaStatusChanged(QMediaPlayer::LoadedMedia);
}
bool QQnxMediaPlayer::attachVideoOutput()
{
if (isVideoOutputAttached()) {
qWarning() << "QQnxMediaPlayer: Video output already attached!";
return true;
}
if (!m_context) {
qWarning() << "QQnxMediaPlayer: No media player context!";
return false;
}
const QByteArray windowGroupId = m_windowGrabber->windowGroupId();
if (windowGroupId.isEmpty()) {
qWarning() << "QQnxMediaPlayer: Unable to find window group";
return false;
}
static int winIdCounter = 0;
const QString windowName = QStringLiteral("QQnxVideoSink_%1_%2")
.arg(winIdCounter++)
.arg(QCoreApplication::applicationPid());
m_windowGrabber->setWindowId(windowName.toLatin1());
if (m_platformVideoSink)
m_windowGrabber->setRhi(m_platformVideoSink->rhi());
// Start with an invisible window, because we just want to grab the frames from it.
const QString videoDeviceUrl = QStringLiteral("screen:?winid=%1&wingrp=%2&initflags=invisible&nodstviewport=1")
.arg(windowName, QString::fromLatin1(windowGroupId));
m_videoId = mmr_output_attach(m_context, videoDeviceUrl.toLatin1().constData(), "video");
if (m_videoId == -1) {
qWarning() << "mmr_output_attach() for video failed";
return false;
}
return true;
}
bool QQnxMediaPlayer::attachAudioOutput()
{
if (isAudioOutputAttached()) {
qWarning() << "QQnxMediaPlayer: Audio output already attached!";
return true;
}
const QByteArray defaultAudioDevice = qgetenv("QQNX_RENDERER_DEFAULT_AUDIO_SINK");
// QNX 8.0+ serves audio through io-snd (ALSA/SALSA), reached as
// "alsa:default"; older releases use the QSA "snd:" device.
const char *fallbackDevice =
#if __QNX__ >= 800
"alsa:default";
#else
"snd:";
#endif
m_audioId = mmr_output_attach(m_context,
defaultAudioDevice.isEmpty() ? fallbackDevice : defaultAudioDevice.constData(), "audio");
if (m_audioId == -1) {
emitMmError("mmr_output_attach() for audio failed");
return false;
}
return true;
}
bool QQnxMediaPlayer::attachInput()
{
if (isInputAttached())
return true;
const QByteArray resourcePath = resourcePathForUrl(m_media);
if (resourcePath.isEmpty())
return false;
if (mmr_input_attach(m_context, resourcePath.constData(), "track") != 0) {
emitMmError(QStringLiteral("mmr_input_attach() failed for ")
+ QString::fromUtf8(resourcePath));
mediaStatusChanged(QMediaPlayer::InvalidMedia);
return false;
}
m_inputAttached = true;
return true;
}
void QQnxMediaPlayer::detach()
{
if (!m_context)
return;
if (isVideoOutputAttached())
detachVideoOutput();
if (isAudioOutputAttached())
detachAudioOutput();
if (isInputAttached())
detachInput();
resetMonitoring();
}
void QQnxMediaPlayer::detachVideoOutput()
{
m_windowGrabber->stop();
if (m_platformVideoSink)
m_platformVideoSink->setVideoFrame({});
if (isVideoOutputAttached())
mmr_output_detach(m_context, m_videoId);
m_videoId = -1;
}
void QQnxMediaPlayer::detachAudioOutput()
{
if (isAudioOutputAttached())
mmr_output_detach(m_context, m_audioId);
m_audioId = -1;
}
void QQnxMediaPlayer::detachInput()
{
if (isInputAttached())
mmr_input_detach(m_context);
m_inputAttached = false;
}
bool QQnxMediaPlayer::isVideoOutputAttached() const
{
return m_videoId != -1;
}
bool QQnxMediaPlayer::isAudioOutputAttached() const
{
return m_audioId != -1;
}
bool QQnxMediaPlayer::isInputAttached() const
{
return m_inputAttached;
}
void QQnxMediaPlayer::updateScene(const QSize &size)
{
if (!m_platformVideoSink)
return;
QVideoFrameFormat format(size, QVideoFrameFormat::Format_BGRX8888);
const QVideoFrame actualFrame = m_windowGrabber->isEglImageSupported()
? QVideoFramePrivate::createFrame(std::make_unique<QnxTextureBuffer>(m_windowGrabber),
std::move(format))
: QVideoFramePrivate::createFrame(std::make_unique<QnxRasterBuffer>(m_windowGrabber),
std::move(format));
m_platformVideoSink->setVideoFrame(actualFrame);
}
qint64 QQnxMediaPlayer::duration() const
{
return m_metaData.duration();
}
qint64 QQnxMediaPlayer::position() const
{
return m_position;
}
void QQnxMediaPlayer::setPosition(qint64 position)
{
if (m_position == position)
return;
m_pendingPosition = position;
m_flushPositionTimer.start();
}
void QQnxMediaPlayer::setPositionInternal(qint64 position)
{
if (!m_context || !m_metaData.isSeekable() || mediaStatus() == QMediaPlayer::NoMedia)
return;
if (mmr_seek(m_context, QString::number(position).toLatin1().constData()) != 0)
emitMmError("Seeking failed");
}
void QQnxMediaPlayer::flushPosition()
{
setPositionInternal(m_pendingPosition);
}
bool QQnxMediaPlayer::isPendingPositionFlush() const
{
return m_flushPositionTimer.isActive();
}
void QQnxMediaPlayer::setDeferredSpeedEnabled(bool enabled)
{
m_deferredSpeedEnabled = enabled;
}
bool QQnxMediaPlayer::isDeferredSpeedEnabled() const
{
return m_deferredSpeedEnabled;
}
void QQnxMediaPlayer::setVolume(float volume)
{
const int normalizedVolume = ::normalizeVolume(volume);
if (m_volume == normalizedVolume)
return;
m_volume = normalizedVolume;
if (!m_muted)
updateVolume();
}
void QQnxMediaPlayer::setMuted(bool muted)
{
if (m_muted == muted)
return;
m_muted = muted;
updateVolume();
}
void QQnxMediaPlayer::updateVolume()
{
if (!m_context || m_audioId == -1)
return;
const int volume = m_muted ? 0 : m_volume;
char buf[] = "100";
std::snprintf(buf, sizeof buf, "%d", volume);
strm_dict_t * dict = strm_dict_new();
dict = strm_dict_set(dict, "volume", buf);
if (mmr_output_parameters(m_context, m_audioId, dict) != 0)
emitMmError("mmr_output_parameters: Setting volume failed");
}
void QQnxMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
{
QAudioOutput *out = output ? output->q : nullptr;
if (m_audioOutput == out)
return;
if (m_audioOutput)
disconnect(m_audioOutput.get());
m_audioOutput = out;
if (m_audioOutput) {
connect(out, &QAudioOutput::volumeChanged, this, &QQnxMediaPlayer::setVolume);
connect(out, &QAudioOutput::mutedChanged, this, &QQnxMediaPlayer::setMuted);
}
setVolume(out ? out->volume() : 1.);
setMuted(out ? out->isMuted() : true);
}
float QQnxMediaPlayer::bufferProgress() const
{
// mm-renderer has buffer properties "status" and "level"
// QMediaPlayer's buffer status maps to mm-renderer's buffer level
return m_bufferLevel/100.0f;
}
bool QQnxMediaPlayer::isAudioAvailable() const
{
return m_metaData.hasAudio();
}
bool QQnxMediaPlayer::isVideoAvailable() const
{
return m_metaData.hasVideo();
}
bool QQnxMediaPlayer::isSeekable() const
{
return m_metaData.isSeekable();
}
QMediaTimeRange QQnxMediaPlayer::availablePlaybackRanges() const
{
// We can't get this information from the mmrenderer API yet, so pretend we can seek everywhere
return QMediaTimeRange(0, m_metaData.duration());
}
qreal QQnxMediaPlayer::playbackRate() const
{
return ::speedToRate(m_speed);
}
void QQnxMediaPlayer::setPlaybackRate(qreal rate)
{
if (!m_context)
return;
const int speed = ::rateToSpeed(rate);
if (m_speed == speed)
return;
// defer setting the playback speed for when play() is called to prevent
// mm-renderer from inadvertently transitioning into play state
if (isDeferredSpeedEnabled() && state() != QMediaPlayer::PlayingState) {
m_deferredSpeed = speed;
return;
}
if (mmr_speed_set(m_context, speed) != 0)
emitMmError("mmr_speed_set failed");
}
QUrl QQnxMediaPlayer::media() const
{
return m_media;
}
const QIODevice *QQnxMediaPlayer::mediaStream() const
{
// Always 0, we don't support QIODevice streams
return 0;
}
void QQnxMediaPlayer::setMedia(const QUrl &media, QIODevice *stream)
{
Q_UNUSED(stream); // not supported
stop();
detach();
stateChanged(QMediaPlayer::StoppedState);
mediaStatusChanged(QMediaPlayer::LoadingMedia);
m_media = media;
updateMetaData(nullptr);
attach();
}
void QQnxMediaPlayer::play()
{
if (!m_media.isValid() || !m_connection || !m_context || m_audioId == -1) {
stateChanged(QMediaPlayer::StoppedState);
return;
}
if (state() == QMediaPlayer::PlayingState)
return;
setDeferredSpeedEnabled(false);
if (m_deferredSpeed) {
setPlaybackRate(::speedToRate(*m_deferredSpeed));
m_deferredSpeed = {};
} else {
setPlaybackRate(::speedToRate(m_configuredSpeed));
}
setDeferredSpeedEnabled(true);
// Un-pause the state when it is paused
if (state() == QMediaPlayer::PausedState) {
return;
}
if (mediaStatus() == QMediaPlayer::EndOfMedia)
setPositionInternal(0);
resetMonitoring();
updateVolume();
if (mmr_play(m_context) != 0) {
stateChanged(QMediaPlayer::StoppedState);
emitMmError("mmr_play() failed");
return;
}
stateChanged(QMediaPlayer::PlayingState);
}
void QQnxMediaPlayer::pause()
{
if (state() != QMediaPlayer::PlayingState)
return;
setPlaybackRate(0);
}
void QQnxMediaPlayer::stop()
{
if (!m_context
|| state() == QMediaPlayer::StoppedState
|| mediaStatus() == QMediaPlayer::NoMedia)
return;
// mm-renderer does not rewind by default
setPositionInternal(0);
mmr_stop(m_context);
}
void QQnxMediaPlayer::setVideoSink(QVideoSink *videoSink)
{
m_platformVideoSink = videoSink
? static_cast<QQnxVideoSink *>(videoSink->platformVideoSink())
: nullptr;
}
void QQnxMediaPlayer::startMonitoring()
{
m_eventThread = new QQnxMediaEventThread(m_context);
connect(m_eventThread, &QQnxMediaEventThread::eventPending,
this, &QQnxMediaPlayer::readEvents);
m_eventThread->setObjectName(QStringLiteral("MmrEventThread-") + QString::number(m_id));
m_eventThread->start();
}
void QQnxMediaPlayer::stopMonitoring()
{
delete m_eventThread;
m_eventThread = nullptr;
}
void QQnxMediaPlayer::resetMonitoring()
{
m_bufferLevel = 0;
m_position = 0;
m_speed = 0;
}
void QQnxMediaPlayer::handleMmPositionChanged(qint64 newPosition)
{
m_position = newPosition;
if (state() == QMediaPlayer::PausedState)
m_windowGrabber->forceUpdate();
positionChanged(m_position);
}
void QQnxMediaPlayer::updateBufferLevel(int level, int capacity)
{
m_bufferLevel = capacity == 0 ? 0 : level / static_cast<float>(capacity) * 100.0f;
m_bufferLevel = qBound(0, m_bufferLevel, 100);
bufferProgressChanged(m_bufferLevel/100.0f);
}
void QQnxMediaPlayer::updateMetaData(const strm_dict *dict)
{
m_metaData.update(dict);
durationChanged(m_metaData.duration());
audioAvailableChanged(m_metaData.hasAudio());
videoAvailableChanged(m_metaData.hasVideo());
seekableChanged(m_metaData.isSeekable());
}
void QQnxMediaPlayer::emitMmError(const char *msg)
{
emitMmError(QString::fromUtf8(msg));
}
void QQnxMediaPlayer::emitMmError(const QString &msg)
{
int errorCode = MMR_ERROR_NONE;
const QString errorMessage = mmErrorMessage(msg, m_context, &errorCode);
Q_EMIT error(QMediaPlayer::ResourceError, errorMessage);
}
void QQnxMediaPlayer::emitPError(const QString &msg)
{
const QString errorMessage = QString::fromLatin1("%1: %2").arg(msg).arg(QString::fromUtf8(strerror(errno)));
Q_EMIT error(QMediaPlayer::ResourceError, errorMessage);
}
void QQnxMediaPlayer::readEvents()
{
while (const mmr_event_t *event = mmr_event_get(m_context)) {
if (event->type == MMR_EVENT_NONE)
break;
switch (event->type) {
case MMR_EVENT_STATUS:
handleMmEventStatus(event);
break;
case MMR_EVENT_STATE:
handleMmEventState(event);
break;
case MMR_EVENT_METADATA:
updateMetaData(event->data);
break;
case MMR_EVENT_ERROR:
handleMmEventError(event);
break;
case MMR_EVENT_NONE:
case MMR_EVENT_OVERFLOW:
case MMR_EVENT_WARNING:
case MMR_EVENT_PLAYLIST:
case MMR_EVENT_INPUT:
case MMR_EVENT_OUTPUT:
case MMR_EVENT_CTXTPAR:
case MMR_EVENT_TRKPAR:
case MMR_EVENT_OTHER:
break;
}
}
if (m_eventThread)
m_eventThread->signalRead();
}
QT_END_NAMESPACE
#include "moc_qqnxmediaplayer_p.cpp"