Revision control
++ // Copyright (C) 2016 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 "qandroidmediaplayer_p.h"
++ #include "androidmediaplayer_p.h"
++ #include "qandroidvideooutput_p.h"
++ #include "qandroidmetadata_p.h"
++ #include "qandroidaudiooutput_p.h"
++ #include "qaudiooutput.h"
++
++ #include <private/qplatformvideosink_p.h>
++ #include <qloggingcategory.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using TrackType = QPlatformMediaPlayer::TrackType;
++
++ Q_STATIC_LOGGING_CATEGORY(lcMediaPlayer, "qt.multimedia.mediaplayer.android");
++
++ class StateChangeNotifier
++ {
++ public:
++ StateChangeNotifier(QAndroidMediaPlayer *mp)
++ : mControl(mp)
++ , mPreviousState(mp->state())
++ , mPreviousMediaStatus(mp->mediaStatus())
++ {
++ ++mControl->mActiveStateChangeNotifiers;
++ }
++
++ ~StateChangeNotifier()
++ {
++ if (--mControl->mActiveStateChangeNotifiers)
++ return;
++
++ if (mPreviousMediaStatus != mControl->mediaStatus())
++ Q_EMIT mControl->mediaStatusChanged(mControl->mediaStatus());
++
++ if (mPreviousState != mControl->state())
++ Q_EMIT mControl->stateChanged(mControl->state());
++ }
++
++ private:
++ QAndroidMediaPlayer *mControl;
++ QMediaPlayer::PlaybackState mPreviousState;
++ QMediaPlayer::MediaStatus mPreviousMediaStatus;
++ };
++
++ QAndroidMediaPlayer::QAndroidMediaPlayer(QMediaPlayer *parent)
++ : QPlatformMediaPlayer(parent),
++ mMediaPlayer(new AndroidMediaPlayer),
++ mState(AndroidMediaPlayer::Uninitialized)
++ {
++ // Set seekable to True by default. It changes if MEDIA_INFO_NOT_SEEKABLE is received
++ seekableChanged(true);
++ connect(mMediaPlayer, &AndroidMediaPlayer::bufferingChanged, this,
++ &QAndroidMediaPlayer::onBufferingChanged);
++ connect(mMediaPlayer, &AndroidMediaPlayer::info, this, &QAndroidMediaPlayer::onInfo);
++ connect(mMediaPlayer, &AndroidMediaPlayer::error, this, &QAndroidMediaPlayer::onError);
++ connect(mMediaPlayer, &AndroidMediaPlayer::stateChanged, this,
++ &QAndroidMediaPlayer::onStateChanged);
++ connect(mMediaPlayer, &AndroidMediaPlayer::videoSizeChanged, this,
++ &QAndroidMediaPlayer::onVideoSizeChanged);
++ connect(mMediaPlayer, &AndroidMediaPlayer::progressChanged, this,
++ &QAndroidMediaPlayer::positionChanged);
++ connect(mMediaPlayer, &AndroidMediaPlayer::durationChanged, this,
++ &QAndroidMediaPlayer::durationChanged);
++ connect(mMediaPlayer, &AndroidMediaPlayer::tracksInfoChanged, this,
++ &QAndroidMediaPlayer::updateTrackInfo);
++ }
++
++ QAndroidMediaPlayer::~QAndroidMediaPlayer()
++ {
++ if (m_videoSink)
++ disconnect(m_videoSink->platformVideoSink(), nullptr, this, nullptr);
++
++ mMediaPlayer->disconnect();
++ mMediaPlayer->release();
++ delete mMediaPlayer;
++ }
++
++ qint64 QAndroidMediaPlayer::duration() const
++ {
++ if (mediaStatus() == QMediaPlayer::NoMedia)
++ return 0;
++
++ if ((mState & (AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::Stopped
++ | AndroidMediaPlayer::PlaybackCompleted)) == 0) {
++ return 0;
++ }
++
++ return mMediaPlayer->getDuration();
++ }
++
++ qint64 QAndroidMediaPlayer::position() const
++ {
++ if (mediaStatus() == QMediaPlayer::EndOfMedia)
++ return duration();
++
++ if ((mState & (AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted))) {
++ return mMediaPlayer->getCurrentPosition();
++ }
++
++ return (mPendingPosition == -1) ? 0 : mPendingPosition;
++ }
++
++ void QAndroidMediaPlayer::setPosition(qint64 position)
++ {
++ if (!isSeekable())
++ return;
++
++ const int seekPosition = (position > INT_MAX) ? INT_MAX : position;
++
++ qint64 currentPosition = mMediaPlayer->getCurrentPosition();
++ if (seekPosition == currentPosition) {
++ // update position - will send a new frame of this position
++ // for consistency with other platforms
++ mMediaPlayer->seekTo(seekPosition);
++ return;
++ }
++ StateChangeNotifier notifier(this);
++
++ if (mediaStatus() == QMediaPlayer::EndOfMedia)
++ setMediaStatus(QMediaPlayer::LoadedMedia);
++
++ if ((mState & (AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted)) == 0) {
++ mPendingPosition = seekPosition;
++ } else {
++ mMediaPlayer->seekTo(seekPosition);
++
++ if (mPendingPosition != -1) {
++ mPendingPosition = -1;
++ }
++ }
++
++ Q_EMIT positionChanged(seekPosition);
++ }
++
++ void QAndroidMediaPlayer::setVolume(float volume)
++ {
++ if ((mState & (AndroidMediaPlayer::Idle
++ | AndroidMediaPlayer::Initialized
++ | AndroidMediaPlayer::Stopped
++ | AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted)) == 0) {
++ mPendingVolume = volume;
++ return;
++ }
++
++ mMediaPlayer->setVolume(qRound(volume*100.));
++ mPendingVolume = -1;
++ }
++
++ void QAndroidMediaPlayer::setMuted(bool muted)
++ {
++ if ((mState & (AndroidMediaPlayer::Idle
++ | AndroidMediaPlayer::Initialized
++ | AndroidMediaPlayer::Stopped
++ | AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted)) == 0) {
++ mPendingMute = muted;
++ return;
++ }
++
++ mMediaPlayer->setMuted(muted);
++ mPendingMute = -1;
++ }
++
++ QMediaMetaData QAndroidMediaPlayer::metaData() const
++ {
++ return QAndroidMetaData::extractMetadata(mMediaContent);
++ }
++
++ float QAndroidMediaPlayer::bufferProgress() const
++ {
++ return mBufferFilled ? 1. : mBufferPercent;
++ }
++
++ bool QAndroidMediaPlayer::isAudioAvailable() const
++ {
++ return mAudioAvailable;
++ }
++
++ bool QAndroidMediaPlayer::isVideoAvailable() const
++ {
++ return mVideoAvailable;
++ }
++
++ QMediaTimeRange QAndroidMediaPlayer::availablePlaybackRanges() const
++ {
++ return mAvailablePlaybackRange;
++ }
++
++ void QAndroidMediaPlayer::updateAvailablePlaybackRanges()
++ {
++ if (mBuffering) {
++ const qint64 pos = position();
++ const qint64 end = (duration() / 100) * mBufferPercent;
++ mAvailablePlaybackRange.addInterval(pos, end);
++ } else if (isSeekable()) {
++ mAvailablePlaybackRange = QMediaTimeRange(0, duration());
++ } else {
++ mAvailablePlaybackRange = QMediaTimeRange();
++ }
++
++ // #### Q_EMIT availablePlaybackRangesChanged(mAvailablePlaybackRange);
++ }
++
++ qreal QAndroidMediaPlayer::playbackRate() const
++ {
++ return mCurrentPlaybackRate;
++ }
++
++ void QAndroidMediaPlayer::setPlaybackRate(qreal rate)
++ {
++ if (mState != AndroidMediaPlayer::Started) {
++ // If video isn't playing, changing speed rate may start it automatically
++ // It need to be postponed
++ if (mCurrentPlaybackRate != rate) {
++ mCurrentPlaybackRate = rate;
++ mHasPendingPlaybackRate = true;
++ Q_EMIT playbackRateChanged(rate);
++ }
++ return;
++ }
++
++ if (mMediaPlayer->setPlaybackRate(rate)) {
++ mCurrentPlaybackRate = rate;
++ Q_EMIT playbackRateChanged(rate);
++ }
++ }
++
++ QUrl QAndroidMediaPlayer::media() const
++ {
++ return mMediaContent;
++ }
++
++ const QIODevice *QAndroidMediaPlayer::mediaStream() const
++ {
++ return mMediaStream;
++ }
++
++ void QAndroidMediaPlayer::setMedia(const QUrl &mediaContent,
++ QIODevice *stream)
++ {
++ StateChangeNotifier notifier(this);
++
++ mReloadingMedia = (mMediaContent == mediaContent) && !mPendingSetMedia;
++
++ if (!mReloadingMedia) {
++ mMediaContent = mediaContent;
++ mMediaStream = stream;
++ }
++
++ if (mediaContent.isEmpty()) {
++ setMediaStatus(QMediaPlayer::NoMedia);
++ } else {
++ if (mVideoOutput && !mVideoOutput->isReady()) {
++ // if a video output is set but the video texture is not ready, delay loading the media
++ // since it can cause problems on some hardware
++ mPendingSetMedia = true;
++ return;
++ }
++
++ if (mVideoSize.isValid() && mVideoOutput)
++ mVideoOutput->setVideoSize(mVideoSize);
++
++ if (mVideoOutput &&
++ (mMediaPlayer->display() == 0 || mVideoOutput->shouldTextureBeUpdated()))
++ mMediaPlayer->setDisplay(mVideoOutput->surfaceTexture());
++ mMediaPlayer->setDataSource(QNetworkRequest(mediaContent));
++ mMediaPlayer->prepareAsync();
++
++ if (!mReloadingMedia)
++ setMediaStatus(QMediaPlayer::LoadingMedia);
++ }
++
++ resetBufferingProgress();
++
++ mReloadingMedia = false;
++ }
++
++ void QAndroidMediaPlayer::setVideoSink(QVideoSink *sink)
++ {
++ if (m_videoSink == sink)
++ return;
++
++ if (m_videoSink)
++ disconnect(m_videoSink->platformVideoSink(), nullptr, this, nullptr);
++
++ m_videoSink = sink;
++
++ if (!m_videoSink) {
++ return;
++ }
++
++ if (mVideoOutput) {
++ delete mVideoOutput;
++ mVideoOutput = nullptr;
++ mMediaPlayer->setDisplay(nullptr);
++ }
++
++ mVideoOutput = new QAndroidTextureVideoOutput(sink, this);
++ connect(mVideoOutput, &QAndroidTextureVideoOutput::readyChanged, this,
++ &QAndroidMediaPlayer::onVideoOutputReady);
++ connect(mMediaPlayer, &AndroidMediaPlayer::timedTextChanged, mVideoOutput,
++ &QAndroidTextureVideoOutput::setSubtitle);
++
++ if (mVideoOutput->isReady())
++ mMediaPlayer->setDisplay(mVideoOutput->surfaceTexture());
++
++ connect(m_videoSink->platformVideoSink(), &QPlatformVideoSink::rhiChanged, this, [&]()
++ { mMediaPlayer->setDisplay(mVideoOutput->surfaceTexture()); });
++ }
++
++ void QAndroidMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
++ {
++ if (m_audioOutput == output)
++ return;
++ if (m_audioOutput)
++ m_audioOutput->q->disconnect(this);
++ m_audioOutput = static_cast<QAndroidAudioOutput *>(output);
++ if (m_audioOutput) {
++ connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this, &QAndroidMediaPlayer::updateAudioDevice);
++ connect(m_audioOutput->q, &QAudioOutput::volumeChanged, this, &QAndroidMediaPlayer::setVolume);
++ connect(m_audioOutput->q, &QAudioOutput::mutedChanged, this, &QAndroidMediaPlayer::setMuted);
++ updateAudioDevice();
++ }
++ }
++
++ void QAndroidMediaPlayer::updateAudioDevice()
++ {
++ if (m_audioOutput)
++ mMediaPlayer->setAudioOutput(m_audioOutput->device.id());
++ }
++
++ void QAndroidMediaPlayer::play()
++ {
++ StateChangeNotifier notifier(this);
++
++ resetCurrentLoop();
++
++ // We need to prepare the mediaplayer again.
++ if ((mState & AndroidMediaPlayer::Stopped) && !mMediaContent.isEmpty()) {
++ setMedia(mMediaContent, mMediaStream);
++ }
++
++ if (!mMediaContent.isEmpty())
++ stateChanged(QMediaPlayer::PlayingState);
++
++ if ((mState & (AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted)) == 0) {
++ mPendingState = QMediaPlayer::PlayingState;
++ return;
++ }
++
++ if (mVideoOutput)
++ mVideoOutput->start();
++
++ updateAudioDevice();
++
++ if (mHasPendingPlaybackRate) {
++ mHasPendingPlaybackRate = false;
++ if (mMediaPlayer->setPlaybackRate(mCurrentPlaybackRate))
++ return;
++ mCurrentPlaybackRate = mMediaPlayer->playbackRate();
++ Q_EMIT playbackRateChanged(mCurrentPlaybackRate);
++ }
++
++ mMediaPlayer->play();
++ }
++
++ void QAndroidMediaPlayer::pause()
++ {
++ // cannot pause without media
++ if (mediaStatus() == QMediaPlayer::NoMedia)
++ return;
++
++ StateChangeNotifier notifier(this);
++
++ stateChanged(QMediaPlayer::PausedState);
++
++ if ((mState & (AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted
++ | AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Stopped)) == 0) {
++ mPendingState = QMediaPlayer::PausedState;
++ return;
++ }
++
++ const qint64 currentPosition = mMediaPlayer->getCurrentPosition();
++ setPosition(currentPosition);
++
++ mMediaPlayer->pause();
++ }
++
++ void QAndroidMediaPlayer::stop()
++ {
++ StateChangeNotifier notifier(this);
++
++ stateChanged(QMediaPlayer::StoppedState);
++
++ if ((mState & (AndroidMediaPlayer::Prepared
++ | AndroidMediaPlayer::Started
++ | AndroidMediaPlayer::Stopped
++ | AndroidMediaPlayer::Paused
++ | AndroidMediaPlayer::PlaybackCompleted)) == 0) {
++ if ((mState & (AndroidMediaPlayer::Idle | AndroidMediaPlayer::Uninitialized | AndroidMediaPlayer::Error)) == 0)
++ mPendingState = QMediaPlayer::StoppedState;
++ return;
++ }
++
++ if (mCurrentPlaybackRate != 1.)
++ // Playback rate need to by reapplied
++ mHasPendingPlaybackRate = true;
++
++ if (mVideoOutput)
++ mVideoOutput->stop();
++
++ mMediaPlayer->stop();
++ }
++
++ void QAndroidMediaPlayer::onInfo(qint32 what, qint32 extra)
++ {
++ StateChangeNotifier notifier(this);
++
++ Q_UNUSED(extra);
++ switch (what) {
++ case AndroidMediaPlayer::MEDIA_INFO_UNKNOWN:
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_VIDEO_TRACK_LAGGING:
++ // IGNORE
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_VIDEO_RENDERING_START:
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_BUFFERING_START:
++ mPendingState = state();
++ stateChanged(QMediaPlayer::PausedState);
++ setMediaStatus(QMediaPlayer::StalledMedia);
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_BUFFERING_END:
++ if (state() != QMediaPlayer::StoppedState)
++ flushPendingStates();
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_BAD_INTERLEAVING:
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_NOT_SEEKABLE:
++ seekableChanged(false);
++ break;
++ case AndroidMediaPlayer::MEDIA_INFO_METADATA_UPDATE:
++ Q_EMIT metaDataChanged();
++ break;
++ }
++ }
++
++ void QAndroidMediaPlayer::onError(qint32 what, qint32 extra)
++ {
++ StateChangeNotifier notifier(this);
++
++ QString errorString;
++ QMediaPlayer::Error error = QMediaPlayer::ResourceError;
++
++ switch (what) {
++ case AndroidMediaPlayer::MEDIA_ERROR_UNKNOWN:
++ errorString = QLatin1String("Error:");
++ break;
++ case AndroidMediaPlayer::MEDIA_ERROR_SERVER_DIED:
++ errorString = QLatin1String("Error: Server died");
++ error = QMediaPlayer::ResourceError;
++ break;
++ case AndroidMediaPlayer::MEDIA_ERROR_INVALID_STATE:
++ errorString = QLatin1String("Error: Invalid state");
++ error = QMediaPlayer::ResourceError;
++ break;
++ }
++
++ switch (extra) {
++ case AndroidMediaPlayer::MEDIA_ERROR_IO: // Network OR file error
++ errorString += QLatin1String(" (I/O operation failed)");
++ setInvalidMediaWithError(QMediaPlayer::NetworkError, errorString);
++ return;
++ case AndroidMediaPlayer::MEDIA_ERROR_MALFORMED:
++ errorString += QLatin1String(" (Malformed bitstream)");
++ setInvalidMediaWithError(QMediaPlayer::FormatError, errorString);
++ return;
++ case AndroidMediaPlayer::MEDIA_ERROR_UNSUPPORTED:
++ errorString += QLatin1String(" (Unsupported media)");
++ setInvalidMediaWithError(QMediaPlayer::FormatError, errorString);
++ return;
++ case AndroidMediaPlayer::MEDIA_ERROR_TIMED_OUT:
++ errorString += QLatin1String(" (Timed out)");
++ break;
++ case AndroidMediaPlayer::MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK:
++ errorString += QLatin1String(" (Unable to start progressive playback')");
++ setInvalidMediaWithError(QMediaPlayer::FormatError, errorString);
++ return;
++ case AndroidMediaPlayer::MEDIA_ERROR_BAD_THINGS_ARE_GOING_TO_HAPPEN:
++ errorString += mMediaContent.scheme() == QLatin1String("rtsp")
++ ? QLatin1String(" (Unknown error/Insufficient resources or RTSP may not be supported)")
++ : QLatin1String(" (Unknown error/Insufficient resources)");
++ error = QMediaPlayer::ResourceError;
++ break;
++ }
++
++ Q_EMIT QPlatformMediaPlayer::error(error, errorString);
++ }
++
++ void QAndroidMediaPlayer::onBufferingChanged(qint32 percent)
++ {
++ StateChangeNotifier notifier(this);
++
++ mBuffering = percent != 100;
++ mBufferPercent = percent;
++
++ updateAvailablePlaybackRanges();
++
++ if (state() != QMediaPlayer::StoppedState)
++ setMediaStatus(mBuffering ? QMediaPlayer::BufferingMedia : QMediaPlayer::BufferedMedia);
++
++ updateBufferStatus();
++ }
++
++ void QAndroidMediaPlayer::onVideoSizeChanged(qint32 width, qint32 height)
++ {
++ QSize newSize(width, height);
++
++ if (width == 0 || height == 0 || newSize == mVideoSize)
++ return;
++
++ setVideoAvailable(true);
++ mVideoSize = newSize;
++
++ if (mVideoOutput)
++ mVideoOutput->setVideoSize(mVideoSize);
++ }
++
++ void QAndroidMediaPlayer::onStateChanged(qint32 state)
++ {
++ // If reloading, don't report state changes unless the new state is Prepared or Error.
++ if ((mState & AndroidMediaPlayer::Stopped)
++ && (state & (AndroidMediaPlayer::Prepared | AndroidMediaPlayer::Error | AndroidMediaPlayer::Uninitialized)) == 0) {
++ return;
++ }
++
++ StateChangeNotifier notifier(this);
++
++ mState = state;
++ switch (mState) {
++ case AndroidMediaPlayer::Idle:
++ break;
++ case AndroidMediaPlayer::Initialized:
++ break;
++ case AndroidMediaPlayer::Preparing:
++ if (!mReloadingMedia)
++ setMediaStatus(QMediaPlayer::LoadingMedia);
++ break;
++ case AndroidMediaPlayer::Prepared:
++ setMediaStatus(QMediaPlayer::LoadedMedia);
++ if (mBuffering) {
++ setMediaStatus(mBufferPercent == 100 ? QMediaPlayer::BufferedMedia
++ : QMediaPlayer::BufferingMedia);
++ } else {
++ onBufferingChanged(100);
++ }
++ setPosition(0);
++ Q_EMIT metaDataChanged();
++ setAudioAvailable(true);
++ flushPendingStates();
++ break;
++ case AndroidMediaPlayer::Started:
++ stateChanged(QMediaPlayer::PlayingState);
++ if (mBuffering) {
++ setMediaStatus(mBufferPercent == 100 ? QMediaPlayer::BufferedMedia
++ : QMediaPlayer::BufferingMedia);
++ } else {
++ setMediaStatus(QMediaPlayer::BufferedMedia);
++ }
++ Q_EMIT positionChanged(position());
++ break;
++ case AndroidMediaPlayer::Paused:
++ stateChanged(QMediaPlayer::PausedState);
++ if (mediaStatus() == QMediaPlayer::EndOfMedia) {
++ setPosition(0);
++ setMediaStatus(QMediaPlayer::BufferedMedia);
++ } else {
++ Q_EMIT positionChanged(position());
++ }
++ break;
++ case AndroidMediaPlayer::Error:
++ stateChanged(QMediaPlayer::StoppedState);
++ setMediaStatus(QMediaPlayer::InvalidMedia);
++ mMediaPlayer->release();
++ Q_EMIT positionChanged(0);
++ break;
++ case AndroidMediaPlayer::Stopped:
++ stateChanged(QMediaPlayer::StoppedState);
++ setMediaStatus(QMediaPlayer::LoadedMedia);
++ Q_EMIT positionChanged(0);
++ break;
++ case AndroidMediaPlayer::PlaybackCompleted:
++ if (doLoop()) {
++ setPosition(0);
++ mMediaPlayer->play();
++ break;
++ }
++ stateChanged(QMediaPlayer::StoppedState);
++ setMediaStatus(QMediaPlayer::EndOfMedia);
++ break;
++ case AndroidMediaPlayer::Uninitialized:
++ // reset some properties (unless we reload the same media)
++ if (!mReloadingMedia) {
++ resetBufferingProgress();
++ mPendingPosition = -1;
++ mPendingSetMedia = false;
++ mPendingState = -1;
++
++ Q_EMIT durationChanged(0);
++ Q_EMIT positionChanged(0);
++
++ setAudioAvailable(false);
++ setVideoAvailable(false);
++ seekableChanged(true);
++ }
++ break;
++ default:
++ break;
++ }
++
++ if ((mState & (AndroidMediaPlayer::Stopped | AndroidMediaPlayer::Uninitialized)) != 0) {
++ mMediaPlayer->setDisplay(0);
++ if (mVideoOutput) {
++ mVideoOutput->stop();
++ }
++ }
++ }
++
++ int QAndroidMediaPlayer::trackCount(TrackType trackType)
++ {
++ if (!mTracksMetadata.contains(trackType))
++ return -1;
++
++ auto tracks = mTracksMetadata.value(trackType);
++ return tracks.count();
++ }
++
++ QMediaMetaData QAndroidMediaPlayer::trackMetaData(TrackType trackType, int streamNumber)
++ {
++ if (!mTracksMetadata.contains(trackType))
++ return QMediaMetaData();
++
++ auto tracks = mTracksMetadata.value(trackType);
++ if (tracks.count() < streamNumber)
++ return QMediaMetaData();
++
++ QAndroidMetaData trackInfo = tracks.at(streamNumber);
++ return static_cast<QMediaMetaData>(trackInfo);
++ }
++
++ std::optional<TrackType> convertTrackType(AndroidMediaPlayer::TrackType type)
++ {
++ switch (type) {
++ case AndroidMediaPlayer::TrackType::Video:
++ return TrackType::VideoStream;
++ case AndroidMediaPlayer::TrackType::Audio:
++ return TrackType::AudioStream;
++ case AndroidMediaPlayer::TrackType::TimedText:
++ return TrackType::SubtitleStream;
++ case AndroidMediaPlayer::TrackType::Subtitle:
++ return TrackType::SubtitleStream;
++ case AndroidMediaPlayer::TrackType::Unknown:
++ case AndroidMediaPlayer::TrackType::Metadata:
++ return std::nullopt;
++ }
++
++ return std::nullopt;
++ }
++
++ int QAndroidMediaPlayer::convertTrackNumber(int androidTrackNumber)
++ {
++ int trackNumber = androidTrackNumber;
++
++ int videoTrackCount = trackCount(TrackType::VideoStream);
++ if (trackNumber <= videoTrackCount)
++ return trackNumber;
++
++ trackNumber = trackNumber - videoTrackCount;
++
++ int audioTrackCount = trackCount(TrackType::AudioStream);
++ if (trackNumber <= audioTrackCount)
++ return trackNumber;
++
++ trackNumber = trackNumber - audioTrackCount;
++
++ auto subtitleTracks = mTracksMetadata.value(TrackType::SubtitleStream);
++ int timedTextCount = 0;
++ int subtitleTextCount = 0;
++ for (const auto &track : subtitleTracks) {
++ if (track.androidTrackType() == 3) // 3 == TimedText
++ timedTextCount++;
++
++ if (track.androidTrackType() == 4) // 4 == Subtitle
++ subtitleTextCount++;
++ }
++
++ if (trackNumber <= timedTextCount)
++ return trackNumber;
++
++ trackNumber = trackNumber - timedTextCount;
++
++ if (trackNumber <= subtitleTextCount)
++ return trackNumber;
++
++ return -1;
++ }
++
++ int QAndroidMediaPlayer::activeTrack(TrackType trackType)
++ {
++ int androidTrackNumber = -1;
++
++ switch (trackType) {
++ case TrackType::VideoStream: {
++ if (!mIsVideoTrackEnabled)
++ return -1;
++ androidTrackNumber = mMediaPlayer->activeTrack(AndroidMediaPlayer::TrackType::Video);
++ }
++ case TrackType::AudioStream: {
++ if (!mIsAudioTrackEnabled)
++ return -1;
++
++ androidTrackNumber = mMediaPlayer->activeTrack(AndroidMediaPlayer::TrackType::Audio);
++ }
++ case TrackType::SubtitleStream: {
++ int timedTextSelectedTrack =
++ mMediaPlayer->activeTrack(AndroidMediaPlayer::TrackType::TimedText);
++
++ if (timedTextSelectedTrack > -1) {
++ androidTrackNumber = timedTextSelectedTrack;
++ break;
++ }
++
++ int subtitleSelectedTrack =
++ mMediaPlayer->activeTrack(AndroidMediaPlayer::TrackType::Subtitle);
++ if (subtitleSelectedTrack > -1) {
++ androidTrackNumber = subtitleSelectedTrack;
++ break;
++ }
++
++ return -1;
++ }
++ }
++
++ return convertTrackNumber(androidTrackNumber);
++ }
++
++ void QAndroidMediaPlayer::disableTrack(TrackType trackType)
++ {
++ const auto track = activeTrack(trackType);
++
++ switch (trackType) {
++ case TrackType::VideoStream: {
++ if (track > -1) {
++ mMediaPlayer->setDisplay(nullptr);
++ mIsVideoTrackEnabled = false;
++ }
++ break;
++ }
++ case TrackType::AudioStream: {
++ if (track > -1) {
++ mMediaPlayer->setMuted(true);
++ mMediaPlayer->blockAudio();
++ mIsAudioTrackEnabled = false;
++ }
++ break;
++ }
++ case TrackType::SubtitleStream: {
++ // subtitles and timedtext tracks can be selected at the same time so deselect both
++ int subtitleSelectedTrack =
++ mMediaPlayer->activeTrack(AndroidMediaPlayer::TrackType::Subtitle);
++ if (subtitleSelectedTrack > -1)
++ mMediaPlayer->deselectTrack(subtitleSelectedTrack);
++
++ int timedTextSelectedTrack =
++ mMediaPlayer->activeTrack(AndroidMediaPlayer::TrackType::TimedText);
++ if (timedTextSelectedTrack > -1)
++ mMediaPlayer->deselectTrack(timedTextSelectedTrack);
++
++ break;
++ }
++ }
++ }
++
++ void QAndroidMediaPlayer::setActiveTrack(TrackType trackType, int streamNumber)
++ {
++
++ if (!mTracksMetadata.contains(trackType)) {
++ qCWarning(lcMediaPlayer)
++ << "Trying to set a active track which type has no available tracks.";
++ return;
++ }
++
++ const auto &tracks = mTracksMetadata.value(trackType);
++ if (streamNumber > tracks.count()) {
++ qCWarning(lcMediaPlayer) << "Trying to set a active track that does not exist.";
++ return;
++ }
++
++ // in case of < 0 deselect tracktype
++ if (streamNumber < 0) {
++ disableTrack(trackType);
++ return;
++ }
++
++ const auto currentTrack = activeTrack(trackType);
++ if (streamNumber == currentTrack) {
++ return;
++ }
++
++ if (trackType == TrackType::VideoStream && !mIsVideoTrackEnabled) {
++ // enable video stream
++ mMediaPlayer->setDisplay(mVideoOutput->surfaceTexture());
++ mIsVideoTrackEnabled = true;
++ }
++
++ if (trackType == TrackType::AudioStream && !mIsAudioTrackEnabled) {
++ // enable audio stream
++ mMediaPlayer->unblockAudio();
++ mMediaPlayer->setMuted(false);
++ mIsAudioTrackEnabled = true;
++ }
++
++ if (trackType == TrackType::SubtitleStream) {
++ // subtitles and timedtext tracks can be selected at the same time so deselect both before
++ // selecting a new one
++ disableTrack(TrackType::SubtitleStream);
++ }
++
++ const auto &trackInfo = tracks.at(streamNumber);
++ const auto &trackNumber = trackInfo.androidTrackNumber();
++ mMediaPlayer->selectTrack(trackNumber);
++
++ Q_EMIT activeTracksChanged();
++ }
++
++ void QAndroidMediaPlayer::positionChanged(qint64 position)
++ {
++ QPlatformMediaPlayer::positionChanged(position);
++ }
++
++ void QAndroidMediaPlayer::durationChanged(qint64 duration)
++ {
++ QPlatformMediaPlayer::durationChanged(duration);
++ }
++
++ void QAndroidMediaPlayer::onVideoOutputReady(bool ready)
++ {
++ if ((mMediaPlayer->display() == 0) && mVideoOutput && ready)
++ mMediaPlayer->setDisplay(mVideoOutput->surfaceTexture());
++
++ flushPendingStates();
++ }
++
++ void QAndroidMediaPlayer::setMediaStatus(QMediaPlayer::MediaStatus status)
++ {
++ mediaStatusChanged(status);
++
++ if (status == QMediaPlayer::NoMedia || status == QMediaPlayer::InvalidMedia) {
++ Q_EMIT durationChanged(0);
++ Q_EMIT metaDataChanged();
++ setAudioAvailable(false);
++ setVideoAvailable(false);
++ }
++
++ if (status == QMediaPlayer::EndOfMedia)
++ Q_EMIT positionChanged(position());
++
++ updateBufferStatus();
++ }
++
++ void QAndroidMediaPlayer::setAudioAvailable(bool available)
++ {
++ if (mAudioAvailable == available)
++ return;
++
++ mAudioAvailable = available;
++ Q_EMIT audioAvailableChanged(mAudioAvailable);
++ }
++
++ void QAndroidMediaPlayer::setVideoAvailable(bool available)
++ {
++ if (mVideoAvailable == available)
++ return;
++
++ if (!available)
++ mVideoSize = QSize();
++
++ mVideoAvailable = available;
++ Q_EMIT videoAvailableChanged(mVideoAvailable);
++ }
++
++ void QAndroidMediaPlayer::resetBufferingProgress()
++ {
++ mBuffering = false;
++ mBufferPercent = 0;
++ mAvailablePlaybackRange = QMediaTimeRange();
++ }
++
++ void QAndroidMediaPlayer::flushPendingStates()
++ {
++ if (mPendingSetMedia) {
++ setMedia(mMediaContent, 0);
++ mPendingSetMedia = false;
++ return;
++ }
++
++ const int newState = mPendingState;
++ mPendingState = -1;
++
++ if (mPendingPosition != -1)
++ setPosition(mPendingPosition);
++ if (mPendingVolume >= 0)
++ setVolume(mPendingVolume);
++ if (mPendingMute != -1)
++ setMuted((mPendingMute == 1));
++
++ switch (newState) {
++ case QMediaPlayer::PlayingState:
++ play();
++ break;
++ case QMediaPlayer::PausedState:
++ pause();
++ break;
++ case QMediaPlayer::StoppedState:
++ stop();
++ break;
++ default:
++ break;
++ }
++ }
++
++ void QAndroidMediaPlayer::updateBufferStatus()
++ {
++ const auto &status = mediaStatus();
++ bool bufferFilled = (status == QMediaPlayer::BufferedMedia || status == QMediaPlayer::BufferingMedia);
++
++ if (mBufferFilled != bufferFilled)
++ mBufferFilled = bufferFilled;
++
++ Q_EMIT bufferProgressChanged(bufferProgress());
++ }
++
++ void QAndroidMediaPlayer::updateTrackInfo()
++ {
++ const auto &androidTracksInfo = mMediaPlayer->tracksInfo();
++
++ // prepare mTracksMetadata
++ mTracksMetadata[TrackType::VideoStream] = QList<QAndroidMetaData>();
++ mTracksMetadata[TrackType::AudioStream] = QList<QAndroidMetaData>();
++ mTracksMetadata[TrackType::SubtitleStream] = QList<QAndroidMetaData>();
++
++ for (const auto &androidTrackInfo : androidTracksInfo) {
++
++ const auto mediaPlayerType = convertTrackType(androidTrackInfo.trackType);
++ if (!mediaPlayerType)
++ continue;
++
++ auto &tracks = mTracksMetadata[*mediaPlayerType];
++
++ const QAndroidMetaData metadata(*mediaPlayerType, androidTrackInfo.trackType,
++ androidTrackInfo.trackNumber, androidTrackInfo.mimeType,
++ androidTrackInfo.language);
++ tracks.append(metadata);
++ }
++
++ Q_EMIT tracksChanged();
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qandroidmediaplayer_p.cpp"