Revision control
++ // 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 "androidmediaplayer_p.h"
++ #include "androidsurfacetexture_p.h"
++
++ #include <QList>
++ #include <QReadWriteLock>
++ #include <QString>
++ #include <QtCore/qcoreapplication.h>
++ #include <qloggingcategory.h>
++
++ static const char QtAndroidMediaPlayerClassName[] = "org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer";
++ typedef QList<AndroidMediaPlayer *> MediaPlayerList;
++ Q_GLOBAL_STATIC(MediaPlayerList, mediaPlayers)
++ Q_GLOBAL_STATIC(QReadWriteLock, rwLock)
++
++ QT_BEGIN_NAMESPACE
++
++ Q_STATIC_LOGGING_CATEGORY(lcAudio, "qt.multimedia.audio");
++
++ AndroidMediaPlayer::AndroidMediaPlayer()
++ : QObject()
++ {
++ QWriteLocker locker(rwLock);
++ auto context = QNativeInterface::QAndroidApplication::context();
++ const jlong id = reinterpret_cast<jlong>(this);
++ mMediaPlayer = QJniObject(QtAndroidMediaPlayerClassName,
++ "(Landroid/content/Context;J)V",
++ context.object(),
++ id);
++ mediaPlayers->append(this);
++ }
++
++ AndroidMediaPlayer::~AndroidMediaPlayer()
++ {
++ QWriteLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(this);
++ Q_ASSERT(i != -1);
++ mediaPlayers->remove(i);
++ }
++
++ void AndroidMediaPlayer::release()
++ {
++ mMediaPlayer.callMethod<void>("release");
++ }
++
++ void AndroidMediaPlayer::reset()
++ {
++ mMediaPlayer.callMethod<void>("reset");
++ }
++
++ int AndroidMediaPlayer::getCurrentPosition()
++ {
++ return mMediaPlayer.callMethod<jint>("getCurrentPosition");
++ }
++
++ int AndroidMediaPlayer::getDuration()
++ {
++ return mMediaPlayer.callMethod<jint>("getDuration");
++ }
++
++ bool AndroidMediaPlayer::isPlaying()
++ {
++ return mMediaPlayer.callMethod<jboolean>("isPlaying");
++ }
++
++ int AndroidMediaPlayer::volume()
++ {
++ return mMediaPlayer.callMethod<jint>("getVolume");
++ }
++
++ bool AndroidMediaPlayer::isMuted()
++ {
++ return mMediaPlayer.callMethod<jboolean>("isMuted");
++ }
++
++ qreal AndroidMediaPlayer::playbackRate()
++ {
++ qreal rate(1.0);
++
++ if (QNativeInterface::QAndroidApplication::sdkVersion() < 23)
++ return rate;
++
++ QJniObject player = mMediaPlayer.callObjectMethod("getMediaPlayerHandle",
++ "()Landroid/media/MediaPlayer;");
++ if (player.isValid()) {
++ QJniObject playbackParams = player.callObjectMethod("getPlaybackParams",
++ "()Landroid/media/PlaybackParams;");
++ if (playbackParams.isValid()) {
++ QJniEnvironment env;
++ auto methodId = env->GetMethodID(playbackParams.objectClass(), "getSpeed", "()F");
++ const qreal speed = env->CallFloatMethod(playbackParams.object(), methodId);
++ if (!env.checkAndClearExceptions())
++ rate = speed;
++ }
++ }
++
++ return rate;
++ }
++
++ jobject AndroidMediaPlayer::display()
++ {
++ return mMediaPlayer.callObjectMethod("display", "()Landroid/view/SurfaceHolder;").object();
++ }
++
++ AndroidMediaPlayer::TrackInfo convertTrackInfo(int streamNumber, QJniObject androidTrackInfo)
++ {
++ const QLatin1String unknownMimeType("application/octet-stream");
++ const QLatin1String undefinedLanguage("und");
++
++ if (!androidTrackInfo.isValid())
++ return { streamNumber, AndroidMediaPlayer::TrackType::Unknown, undefinedLanguage,
++ unknownMimeType };
++
++ QJniEnvironment env;
++ auto methodId = env->GetMethodID(androidTrackInfo.objectClass(), "getType", "()I");
++ const jint type = env->CallIntMethod(androidTrackInfo.object(), methodId);
++ if (env.checkAndClearExceptions())
++ return { streamNumber, AndroidMediaPlayer::TrackType::Unknown, undefinedLanguage,
++ unknownMimeType };
++
++ if (type < 0 || type > 5) {
++ return { streamNumber, AndroidMediaPlayer::TrackType::Unknown, undefinedLanguage,
++ unknownMimeType };
++ }
++
++ AndroidMediaPlayer::TrackType trackType = static_cast<AndroidMediaPlayer::TrackType>(type);
++
++ auto languageObject = androidTrackInfo.callObjectMethod("getLanguage", "()Ljava/lang/String;");
++ QString language = languageObject.isValid() ? languageObject.toString() : undefinedLanguage;
++
++ auto mimeTypeObject = androidTrackInfo.callObjectMethod("getMime", "()Ljava/lang/String;");
++ QString mimeType = mimeTypeObject.isValid() ? mimeTypeObject.toString() : unknownMimeType;
++
++ return { streamNumber, trackType, language, mimeType };
++ }
++
++ QList<AndroidMediaPlayer::TrackInfo> AndroidMediaPlayer::tracksInfo()
++ {
++ auto androidTracksInfoObject = mMediaPlayer.callObjectMethod(
++ "getAllTrackInfo",
++ "()[Lorg/qtproject/qt/android/multimedia/QtAndroidMediaPlayer$TrackInfo;");
++
++ if (!androidTracksInfoObject.isValid())
++ return QList<AndroidMediaPlayer::TrackInfo>();
++
++ auto androidTracksInfo = androidTracksInfoObject.object<jobjectArray>();
++ if (!androidTracksInfo)
++ return QList<AndroidMediaPlayer::TrackInfo>();
++
++ QJniEnvironment environment;
++ auto numberofTracks = environment->GetArrayLength(androidTracksInfo);
++
++ QList<AndroidMediaPlayer::TrackInfo> tracksInformation;
++
++ for (int index = 0; index < numberofTracks; index++) {
++ auto androidTrackInformation = environment->GetObjectArrayElement(androidTracksInfo, index);
++
++ if (environment.checkAndClearExceptions()) {
++ continue;
++ }
++
++ auto trackInfo = convertTrackInfo(index, androidTrackInformation);
++ tracksInformation.insert(index, trackInfo);
++
++ environment->DeleteLocalRef(androidTrackInformation);
++ }
++
++ return tracksInformation;
++ }
++
++ int AndroidMediaPlayer::activeTrack(TrackType androidTrackType)
++ {
++ int type = static_cast<int>(androidTrackType);
++ return mMediaPlayer.callMethod<jint>("getSelectedTrack", "(I)I", type);
++ }
++
++ void AndroidMediaPlayer::deselectTrack(int trackNumber)
++ {
++ mMediaPlayer.callMethod<void>("deselectTrack", "(I)V", trackNumber);
++ }
++
++ void AndroidMediaPlayer::selectTrack(int trackNumber)
++ {
++ mMediaPlayer.callMethod<void>("selectTrack", "(I)V", trackNumber);
++ }
++
++ void AndroidMediaPlayer::play()
++ {
++ mMediaPlayer.callMethod<void>("start");
++ }
++
++ void AndroidMediaPlayer::pause()
++ {
++ mMediaPlayer.callMethod<void>("pause");
++ }
++
++ void AndroidMediaPlayer::stop()
++ {
++ mMediaPlayer.callMethod<void>("stop");
++ }
++
++ void AndroidMediaPlayer::seekTo(qint32 msec)
++ {
++ mMediaPlayer.callMethod<void>("seekTo", "(I)V", jint(msec));
++ }
++
++ void AndroidMediaPlayer::setMuted(bool mute)
++ {
++ if (mAudioBlocked)
++ return;
++
++ mMediaPlayer.callMethod<void>("mute", "(Z)V", jboolean(mute));
++ }
++
++ void AndroidMediaPlayer::setDataSource(const QNetworkRequest &request)
++ {
++ QJniObject string = QJniObject::fromString(request.url().toString(QUrl::FullyEncoded));
++
++ mMediaPlayer.callMethod<void>("initHeaders", "()V");
++ for (auto &header : request.rawHeaderList()) {
++ auto value = request.rawHeader(header);
++ mMediaPlayer.callMethod<void>("setHeader", "(Ljava/lang/String;Ljava/lang/String;)V",
++ QJniObject::fromString(QLatin1String(header)).object(),
++ QJniObject::fromString(QLatin1String(value)).object());
++ }
++
++ mMediaPlayer.callMethod<void>("setDataSource", "(Ljava/lang/String;)V", string.object());
++ }
++
++ void AndroidMediaPlayer::prepareAsync()
++ {
++ mMediaPlayer.callMethod<void>("prepareAsync");
++ }
++
++ void AndroidMediaPlayer::setVolume(int volume)
++ {
++ if (mAudioBlocked)
++ return;
++
++ mMediaPlayer.callMethod<void>("setVolume", "(I)V", jint(volume));
++ }
++
++ void AndroidMediaPlayer::blockAudio()
++ {
++ mAudioBlocked = true;
++ }
++
++ void AndroidMediaPlayer::unblockAudio()
++ {
++ mAudioBlocked = false;
++ }
++
++ void AndroidMediaPlayer::startSoundStreaming(const int inputId, const int outputId)
++ {
++ QJniObject::callStaticMethod<void>("org/qtproject/qt/android/multimedia/QtAudioDeviceManager",
++ "startSoundStreaming",
++ inputId,
++ outputId);
++ }
++
++ void AndroidMediaPlayer::stopSoundStreaming()
++ {
++ QJniObject::callStaticMethod<void>(
++ "org/qtproject/qt/android/multimedia/QtAudioDeviceManager", "stopSoundStreaming");
++ }
++
++ bool AndroidMediaPlayer::setPlaybackRate(qreal rate)
++ {
++ if (QNativeInterface::QAndroidApplication::sdkVersion() < 23) {
++ qWarning() << "Setting the playback rate on a media player requires"
++ << "Android 6.0 (API level 23) or later";
++ return false;
++ }
++
++ return mMediaPlayer.callMethod<jboolean>("setPlaybackRate", jfloat(rate));
++ }
++
++ void AndroidMediaPlayer::setDisplay(AndroidSurfaceTexture *surfaceTexture)
++ {
++ mMediaPlayer.callMethod<void>("setDisplay",
++ "(Landroid/view/SurfaceHolder;)V",
++ surfaceTexture ? surfaceTexture->surfaceHolder() : 0);
++ }
++
++ bool AndroidMediaPlayer::setAudioOutput(const QByteArray &deviceId)
++ {
++ const bool ret = QJniObject::callStaticMethod<jboolean>(
++ "org/qtproject/qt/android/multimedia/QtAudioDeviceManager",
++ "prepareAudioOutput",
++ "(I)Z",
++ deviceId.toInt());
++
++ if (!ret)
++ qCWarning(lcAudio) << "Output device not set";
++
++ return ret;
++ }
++
++ #if 0
++ void AndroidMediaPlayer::setAudioRole(QAudio::Role role)
++ {
++ QString r;
++ switch (role) {
++ case QAudio::MusicRole:
++ r = QLatin1String("CONTENT_TYPE_MUSIC");
++ break;
++ case QAudio::VideoRole:
++ r = QLatin1String("CONTENT_TYPE_MOVIE");
++ break;
++ case QAudio::VoiceCommunicationRole:
++ r = QLatin1String("USAGE_VOICE_COMMUNICATION");
++ break;
++ case QAudio::AlarmRole:
++ r = QLatin1String("USAGE_ALARM");
++ break;
++ case QAudio::NotificationRole:
++ r = QLatin1String("USAGE_NOTIFICATION");
++ break;
++ case QAudio::RingtoneRole:
++ r = QLatin1String("USAGE_NOTIFICATION_RINGTONE");
++ break;
++ case QAudio::AccessibilityRole:
++ r = QLatin1String("USAGE_ASSISTANCE_ACCESSIBILITY");
++ break;
++ case QAudio::SonificationRole:
++ r = QLatin1String("CONTENT_TYPE_SONIFICATION");
++ break;
++ case QAudio::GameRole:
++ r = QLatin1String("USAGE_GAME");
++ break;
++ default:
++ return;
++ }
++
++ int type = 0; // CONTENT_TYPE_UNKNOWN
++ int usage = 0; // USAGE_UNKNOWN
++
++ if (r == QLatin1String("CONTENT_TYPE_MOVIE"))
++ type = 3;
++ else if (r == QLatin1String("CONTENT_TYPE_MUSIC"))
++ type = 2;
++ else if (r == QLatin1String("CONTENT_TYPE_SONIFICATION"))
++ type = 4;
++ else if (r == QLatin1String("CONTENT_TYPE_SPEECH"))
++ type = 1;
++ else if (r == QLatin1String("USAGE_ALARM"))
++ usage = 4;
++ else if (r == QLatin1String("USAGE_ASSISTANCE_ACCESSIBILITY"))
++ usage = 11;
++ else if (r == QLatin1String("USAGE_ASSISTANCE_NAVIGATION_GUIDANCE"))
++ usage = 12;
++ else if (r == QLatin1String("USAGE_ASSISTANCE_SONIFICATION"))
++ usage = 13;
++ else if (r == QLatin1String("USAGE_ASSISTANT"))
++ usage = 16;
++ else if (r == QLatin1String("USAGE_GAME"))
++ usage = 14;
++ else if (r == QLatin1String("USAGE_MEDIA"))
++ usage = 1;
++ else if (r == QLatin1String("USAGE_NOTIFICATION"))
++ usage = 5;
++ else if (r == QLatin1String("USAGE_NOTIFICATION_COMMUNICATION_DELAYED"))
++ usage = 9;
++ else if (r == QLatin1String("USAGE_NOTIFICATION_COMMUNICATION_INSTANT"))
++ usage = 8;
++ else if (r == QLatin1String("USAGE_NOTIFICATION_COMMUNICATION_REQUEST"))
++ usage = 7;
++ else if (r == QLatin1String("USAGE_NOTIFICATION_EVENT"))
++ usage = 10;
++ else if (r == QLatin1String("USAGE_NOTIFICATION_RINGTONE"))
++ usage = 6;
++ else if (r == QLatin1String("USAGE_VOICE_COMMUNICATION"))
++ usage = 2;
++ else if (r == QLatin1String("USAGE_VOICE_COMMUNICATION_SIGNALLING"))
++ usage = 3;
++
++ mMediaPlayer.callMethod<void>("setAudioAttributes", "(II)V", jint(type), jint(usage));
++ }
++ #endif
++
++ static void onErrorNative(JNIEnv *env, jobject thiz, jint what, jint extra, jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->error(what, extra);
++ }
++
++ static void onBufferingUpdateNative(JNIEnv *env, jobject thiz, jint percent, jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->bufferingChanged(percent);
++ }
++
++ static void onProgressUpdateNative(JNIEnv *env, jobject thiz, jint progress, jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->progressChanged(progress);
++ }
++
++ static void onDurationChangedNative(JNIEnv *env, jobject thiz, jint duration, jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->durationChanged(duration);
++ }
++
++ static void onInfoNative(JNIEnv *env, jobject thiz, jint what, jint extra, jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->info(what, extra);
++ }
++
++ static void onStateChangedNative(JNIEnv *env, jobject thiz, jint state, jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->stateChanged(state);
++ }
++
++ static void onVideoSizeChangedNative(JNIEnv *env,
++ jobject thiz,
++ jint width,
++ jint height,
++ jlong id)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ QReadLocker locker(rwLock);
++ const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id));
++ if (Q_UNLIKELY(i == -1))
++ return;
++
++ Q_EMIT (*mediaPlayers)[i]->videoSizeChanged(width, height);
++ }
++
++ static AndroidMediaPlayer *getMediaPlayer(jlong ptr)
++ {
++ auto mediaplayer = reinterpret_cast<AndroidMediaPlayer *>(ptr);
++ if (!mediaplayer || !mediaPlayers->contains(mediaplayer))
++ return nullptr;
++
++ return mediaplayer;
++ }
++
++ static void onTrackInfoChangedNative(JNIEnv *env, jobject thiz, jlong ptr)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ QReadLocker locker(rwLock);
++ auto mediaplayer = getMediaPlayer(ptr);
++ if (!mediaplayer)
++ return;
++
++ Q_EMIT mediaplayer->tracksInfoChanged();
++ }
++
++ static void onTimedTextChangedNative(JNIEnv *env, jobject thiz, jstring timedText, jint time,
++ jlong ptr)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ Q_UNUSED(time);
++
++ QReadLocker locker(rwLock);
++
++ auto mediaplayer = getMediaPlayer(ptr);
++ if (!mediaplayer)
++ return;
++
++ QString subtitleText;
++ if (timedText != nullptr)
++ subtitleText = QString::fromUtf8(env->GetStringUTFChars(timedText, 0));
++
++ Q_EMIT mediaplayer->timedTextChanged(subtitleText);
++ }
++
++ bool AndroidMediaPlayer::registerNativeMethods()
++ {
++ static const JNINativeMethod methods[] = {
++ { "onErrorNative", "(IIJ)V", reinterpret_cast<void *>(onErrorNative) },
++ { "onBufferingUpdateNative", "(IJ)V", reinterpret_cast<void *>(onBufferingUpdateNative) },
++ { "onProgressUpdateNative", "(IJ)V", reinterpret_cast<void *>(onProgressUpdateNative) },
++ { "onDurationChangedNative", "(IJ)V", reinterpret_cast<void *>(onDurationChangedNative) },
++ { "onInfoNative", "(IIJ)V", reinterpret_cast<void *>(onInfoNative) },
++ { "onVideoSizeChangedNative", "(IIJ)V",
++ reinterpret_cast<void *>(onVideoSizeChangedNative) },
++ { "onStateChangedNative", "(IJ)V", reinterpret_cast<void *>(onStateChangedNative) },
++ { "onTrackInfoChangedNative", "(J)V", reinterpret_cast<void *>(onTrackInfoChangedNative) },
++ { "onTimedTextChangedNative", "(Ljava/lang/String;IJ)V",
++ reinterpret_cast<void *>(onTimedTextChangedNative) }
++ };
++
++ const int size = std::size(methods);
++ return QJniEnvironment().registerNativeMethods(QtAndroidMediaPlayerClassName, methods, size);
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_androidmediaplayer_p.cpp"