mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-17 00:44:19 +00:00
1918 lines
80 KiB
C++
1918 lines
80 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2018 - ROLI Ltd.
|
|
|
|
Permission is granted to use this software under the terms of either:
|
|
a) the GPL v2 (or any later version)
|
|
b) the Affero GPL v3
|
|
|
|
Details of these licenses can be found at: www.gnu.org/licenses
|
|
|
|
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
To release a closed-source product which uses JUCE, commercial licenses are
|
|
available: visit www.juce.com for more information.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
#if __ANDROID_API__ >= 21
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (getPlaybackInfo, "getPlaybackInfo", "()Landroid/media/session/MediaController$PlaybackInfo;") \
|
|
METHOD (getPlaybackState, "getPlaybackState", "()Landroid/media/session/PlaybackState;") \
|
|
METHOD (getTransportControls, "getTransportControls", "()Landroid/media/session/MediaController$TransportControls;") \
|
|
METHOD (registerCallback, "registerCallback", "(Landroid/media/session/MediaController$Callback;)V") \
|
|
METHOD (setVolumeTo, "setVolumeTo", "(II)V") \
|
|
METHOD (unregisterCallback, "unregisterCallback", "(Landroid/media/session/MediaController$Callback;)V")
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaController, "android/media/session/MediaController");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";J)V") \
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaControllerCallback, JUCE_ANDROID_ACTIVITY_CLASSPATH "$MediaControllerCallback");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (getAudioAttributes, "getAudioAttributes", "()Landroid/media/AudioAttributes;") \
|
|
METHOD (getCurrentVolume, "getCurrentVolume", "()I") \
|
|
METHOD (getMaxVolume, "getMaxVolume", "()I")
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaControllerPlaybackInfo, "android/media/session/MediaController$PlaybackInfo");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (pause, "pause", "()V") \
|
|
METHOD (play, "play", "()V") \
|
|
METHOD (playFromMediaId, "playFromMediaId", "(Ljava/lang/String;Landroid/os/Bundle;)V") \
|
|
METHOD (seekTo, "seekTo", "(J)V") \
|
|
METHOD (stop, "stop", "()V")
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaControllerTransportControls, "android/media/session/MediaController$TransportControls");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (constructor, "<init>", "()V") \
|
|
METHOD (getCurrentPosition, "getCurrentPosition", "()I") \
|
|
METHOD (getDuration, "getDuration", "()I") \
|
|
METHOD (getPlaybackParams, "getPlaybackParams", "()Landroid/media/PlaybackParams;") \
|
|
METHOD (getVideoHeight, "getVideoHeight", "()I") \
|
|
METHOD (getVideoWidth, "getVideoWidth", "()I") \
|
|
METHOD (isPlaying, "isPlaying", "()Z") \
|
|
METHOD (pause, "pause", "()V") \
|
|
METHOD (prepareAsync, "prepareAsync", "()V") \
|
|
METHOD (release, "release", "()V") \
|
|
METHOD (seekTo, "seekTo", "(I)V") \
|
|
METHOD (setAudioAttributes, "setAudioAttributes", "(Landroid/media/AudioAttributes;)V") \
|
|
METHOD (setDataSource, "setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V") \
|
|
METHOD (setDisplay, "setDisplay", "(Landroid/view/SurfaceHolder;)V") \
|
|
METHOD (setOnBufferingUpdateListener, "setOnBufferingUpdateListener", "(Landroid/media/MediaPlayer$OnBufferingUpdateListener;)V") \
|
|
METHOD (setOnCompletionListener, "setOnCompletionListener", "(Landroid/media/MediaPlayer$OnCompletionListener;)V") \
|
|
METHOD (setOnErrorListener, "setOnErrorListener", "(Landroid/media/MediaPlayer$OnErrorListener;)V") \
|
|
METHOD (setOnInfoListener, "setOnInfoListener", "(Landroid/media/MediaPlayer$OnInfoListener;)V") \
|
|
METHOD (setOnPreparedListener, "setOnPreparedListener", "(Landroid/media/MediaPlayer$OnPreparedListener;)V") \
|
|
METHOD (setOnSeekCompleteListener, "setOnSeekCompleteListener", "(Landroid/media/MediaPlayer$OnSeekCompleteListener;)V") \
|
|
METHOD (setPlaybackParams, "setPlaybackParams", "(Landroid/media/PlaybackParams;)V") \
|
|
METHOD (setVolume, "setVolume", "(FF)V") \
|
|
METHOD (start, "start", "()V") \
|
|
METHOD (stop, "stop", "()V")
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaPlayer, "android/media/MediaPlayer");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (constructor, "<init>", "(Landroid/content/Context;Ljava/lang/String;)V") \
|
|
METHOD (getController, "getController", "()Landroid/media/session/MediaController;") \
|
|
METHOD (release, "release", "()V") \
|
|
METHOD (setActive, "setActive", "(Z)V") \
|
|
METHOD (setCallback, "setCallback", "(Landroid/media/session/MediaSession$Callback;)V") \
|
|
METHOD (setFlags, "setFlags", "(I)V") \
|
|
METHOD (setMediaButtonReceiver, "setMediaButtonReceiver", "(Landroid/app/PendingIntent;)V") \
|
|
METHOD (setMetadata, "setMetadata", "(Landroid/media/MediaMetadata;)V") \
|
|
METHOD (setPlaybackState, "setPlaybackState", "(Landroid/media/session/PlaybackState;)V") \
|
|
METHOD (setPlaybackToLocal, "setPlaybackToLocal", "(Landroid/media/AudioAttributes;)V")
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaSession, "android/media/session/MediaSession");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";J)V") \
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaSessionCallback, JUCE_ANDROID_ACTIVITY_CLASSPATH "$MediaSessionCallback");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (build, "build", "()Landroid/media/MediaMetadata;") \
|
|
METHOD (constructor, "<init>", "()V") \
|
|
METHOD (putLong, "putLong", "(Ljava/lang/String;J)Landroid/media/MediaMetadata$Builder;")
|
|
|
|
DECLARE_JNI_CLASS (AndroidMediaMetadataBuilder, "android/media/MediaMetadata$Builder");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (getSpeed, "getSpeed", "()F") \
|
|
METHOD (setSpeed, "setSpeed", "(F)Landroid/media/PlaybackParams;")
|
|
|
|
DECLARE_JNI_CLASS (AndroidPlaybackParams, "android/media/PlaybackParams");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (getActions, "getActions", "()J") \
|
|
METHOD (getErrorMessage, "getErrorMessage", "()Ljava/lang/CharSequence;") \
|
|
METHOD (getPlaybackSpeed, "getPlaybackSpeed", "()F") \
|
|
METHOD (getPosition, "getPosition", "()J") \
|
|
METHOD (getState, "getState", "()I")
|
|
|
|
DECLARE_JNI_CLASS (AndroidPlaybackState, "android/media/session/PlaybackState");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (build, "build", "()Landroid/media/session/PlaybackState;") \
|
|
METHOD (constructor, "<init>", "()V") \
|
|
METHOD (setActions, "setActions", "(J)Landroid/media/session/PlaybackState$Builder;") \
|
|
METHOD (setErrorMessage, "setErrorMessage", "(Ljava/lang/CharSequence;)Landroid/media/session/PlaybackState$Builder;") \
|
|
METHOD (setState, "setState", "(IJF)Landroid/media/session/PlaybackState$Builder;")
|
|
|
|
DECLARE_JNI_CLASS (AndroidPlaybackStateBuilder, "android/media/session/PlaybackState$Builder");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
|
METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";Landroid/app/Activity;J)V") \
|
|
METHOD (setEnabled, "setEnabled", "(Z)V")
|
|
|
|
DECLARE_JNI_CLASS (SystemVolumeObserver, JUCE_ANDROID_ACTIVITY_CLASSPATH "$SystemVolumeObserver");
|
|
#undef JNI_CLASS_MEMBERS
|
|
|
|
#endif
|
|
|
|
//==============================================================================
|
|
class MediaPlayerListener : public AndroidInterfaceImplementer
|
|
{
|
|
public:
|
|
struct Owner
|
|
{
|
|
virtual ~Owner() {}
|
|
|
|
virtual void onPrepared (LocalRef<jobject>& mediaPlayer) = 0;
|
|
virtual void onBufferingUpdate (LocalRef<jobject>& mediaPlayer, int progress) = 0;
|
|
virtual void onSeekComplete (LocalRef<jobject>& mediaPlayer) = 0;
|
|
virtual void onCompletion (LocalRef<jobject>& mediaPlayer) = 0;
|
|
virtual bool onInfo (LocalRef<jobject>& mediaPlayer, int what, int extra) = 0;
|
|
virtual bool onError (LocalRef<jobject>& mediaPlayer, int what, int extra) = 0;
|
|
};
|
|
|
|
MediaPlayerListener (Owner& ownerToUse) : owner (ownerToUse) {}
|
|
|
|
private:
|
|
Owner& owner;
|
|
|
|
jobject invoke (jobject proxy, jobject method, jobjectArray args) override
|
|
{
|
|
auto* env = getEnv();
|
|
auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));
|
|
|
|
int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;
|
|
|
|
if (methodName == "onPrepared" && numArgs == 1)
|
|
{
|
|
auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
|
|
owner.onPrepared (mediaPlayer);
|
|
return nullptr;
|
|
}
|
|
|
|
if (methodName == "onCompletion" && numArgs == 1)
|
|
{
|
|
auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
|
|
owner.onCompletion (mediaPlayer);
|
|
return nullptr;
|
|
}
|
|
|
|
if (methodName == "onInfo" && numArgs == 3)
|
|
{
|
|
auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
auto what = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
|
auto extra = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));
|
|
|
|
auto whatInt = (int) env->CallIntMethod (what, JavaInteger.intValue);
|
|
auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue);
|
|
|
|
auto res = owner.onInfo (mediaPlayer, whatInt, extraInt);
|
|
return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, (jboolean) res);
|
|
}
|
|
|
|
if (methodName == "onError" && numArgs == 3)
|
|
{
|
|
auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
auto what = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
|
auto extra = LocalRef<jobject> (env->GetObjectArrayElement (args, 2));
|
|
|
|
auto whatInt = (int) env->CallIntMethod (what, JavaInteger.intValue);
|
|
auto extraInt = (int) env->CallIntMethod (extra, JavaInteger.intValue);
|
|
|
|
auto res = owner.onError (mediaPlayer, whatInt, extraInt);
|
|
return env->CallStaticObjectMethod (JavaBoolean, JavaBoolean.valueOf, (jboolean) res);
|
|
}
|
|
|
|
if (methodName == "onSeekComplete" && numArgs == 1)
|
|
{
|
|
auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
|
|
owner.onSeekComplete (mediaPlayer);
|
|
return nullptr;
|
|
}
|
|
|
|
if (methodName == "onBufferingUpdate" && numArgs == 2)
|
|
{
|
|
auto mediaPlayer = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
|
|
auto progress = LocalRef<jobject> (env->GetObjectArrayElement (args, 1));
|
|
auto progressInt = (int) env->CallIntMethod (progress, JavaInteger.intValue);
|
|
|
|
owner.onBufferingUpdate (mediaPlayer, progressInt);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
return AndroidInterfaceImplementer::invoke (proxy, method, args);
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
class AudioManagerOnAudioFocusChangeListener : public AndroidInterfaceImplementer
|
|
{
|
|
public:
|
|
struct Owner
|
|
{
|
|
virtual ~Owner() {}
|
|
|
|
virtual void onAudioFocusChange (int changeType) = 0;
|
|
};
|
|
|
|
AudioManagerOnAudioFocusChangeListener (Owner& ownerToUse) : owner (ownerToUse) {}
|
|
|
|
private:
|
|
Owner& owner;
|
|
|
|
jobject invoke (jobject proxy, jobject method, jobjectArray args) override
|
|
{
|
|
auto* env = getEnv();
|
|
auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));
|
|
|
|
int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;
|
|
|
|
if (methodName == "onAudioFocusChange" && numArgs == 1)
|
|
{
|
|
auto changeType = LocalRef<jobject> (env->GetObjectArrayElement (args, 0));
|
|
|
|
auto changeTypeInt = (int) env->CallIntMethod (changeType, JavaInteger.intValue);
|
|
|
|
owner.onAudioFocusChange (changeTypeInt);
|
|
return nullptr;
|
|
}
|
|
|
|
return AndroidInterfaceImplementer::invoke (proxy, method, args);
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
struct VideoComponent::Pimpl
|
|
: public AndroidViewComponent
|
|
#if __ANDROID_API__ >= 21
|
|
, private AppPausedResumedListener::Owner
|
|
#endif
|
|
{
|
|
Pimpl (VideoComponent& ownerToUse, bool)
|
|
#if __ANDROID_API__ >= 21
|
|
: owner (ownerToUse),
|
|
mediaSession (*this),
|
|
appPausedResumedListener (*this),
|
|
appPausedResumedListenerNative (CreateJavaInterface (&appPausedResumedListener,
|
|
JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener").get())
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
, systemVolumeListener (*this)
|
|
#endif
|
|
#endif
|
|
{
|
|
#if __ANDROID_API__ >= 21
|
|
setVisible (true);
|
|
|
|
auto* env = getEnv();
|
|
|
|
setView (LocalRef<jobject> (env->CallObjectMethod (android.activity.get(),
|
|
JuceAppActivity.createNativeSurfaceView,
|
|
reinterpret_cast<jlong> (this),
|
|
true)));
|
|
|
|
env->CallVoidMethod (android.activity, JuceAppActivity.addAppPausedResumedListener,
|
|
appPausedResumedListenerNative.get(), reinterpret_cast<jlong> (this));
|
|
#endif
|
|
}
|
|
|
|
~Pimpl()
|
|
{
|
|
#if __ANDROID_API__ >= 21
|
|
getEnv()->CallVoidMethod (android.activity, JuceAppActivity.removeAppPausedResumedListener,
|
|
appPausedResumedListenerNative.get(), reinterpret_cast<jlong>(this));
|
|
#endif
|
|
}
|
|
|
|
#if __ANDROID_API__ < 21
|
|
// Dummy implementations for unsupported API levels.
|
|
void loadAsync (const URL&, std::function<void (const URL&, Result)>) {}
|
|
void close() {}
|
|
bool isOpen() const noexcept { return false; }
|
|
bool isPlaying() const noexcept { return false; }
|
|
void play() {}
|
|
void stop() {}
|
|
void setPosition (double) {}
|
|
void setSpeed (double) {}
|
|
void setVolume (float) {}
|
|
float getVolume() const { return 0.0f; }
|
|
double getPosition() const { return 0.0; }
|
|
double getSpeed() const { return 0.0; }
|
|
Rectangle<int> getNativeSize() const { return {}; }
|
|
double getDuration() const { return 0.0; }
|
|
|
|
File currentFile;
|
|
URL currentURL;
|
|
#else
|
|
void loadAsync (const URL& url, std::function<void (const URL&, Result)> callback)
|
|
{
|
|
close();
|
|
wasOpen = false;
|
|
|
|
if (url.isEmpty())
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
if (! url.isLocalFile())
|
|
{
|
|
auto granted = android.activity.callBooleanMethod (JuceAppActivity.isPermissionDeclaredInManifestString,
|
|
javaString ("android.permission.INTERNET").get()) != 0;
|
|
|
|
if (! granted)
|
|
{
|
|
// In order to access videos from the Internet, the Internet permission has to be specified in
|
|
// Android Manifest.
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
}
|
|
|
|
currentURL = url;
|
|
|
|
jassert (callback != nullptr);
|
|
|
|
loadFinishedCallback = std::move (callback);
|
|
|
|
static constexpr jint visible = 0;
|
|
getEnv()->CallVoidMethod ((jobject) getView(), AndroidView.setVisibility, visible);
|
|
|
|
mediaSession.load (url);
|
|
}
|
|
|
|
void close()
|
|
{
|
|
if (! isOpen())
|
|
return;
|
|
|
|
mediaSession.closeVideo();
|
|
|
|
static constexpr jint invisible = 4;
|
|
getEnv()->CallVoidMethod ((jobject) getView(), AndroidView.setVisibility, invisible);
|
|
}
|
|
|
|
bool isOpen() const noexcept { return mediaSession.isVideoOpen(); }
|
|
bool isPlaying() const noexcept { return mediaSession.isPlaying(); }
|
|
|
|
void play() { mediaSession.play(); }
|
|
void stop() { mediaSession.stop(); }
|
|
|
|
void setPosition (double newPosition) { mediaSession.setPosition (newPosition); }
|
|
double getPosition() const { return mediaSession.getPosition(); }
|
|
|
|
void setSpeed (double newSpeed) { mediaSession.setSpeed (newSpeed); }
|
|
double getSpeed() const { return mediaSession.getSpeed(); }
|
|
|
|
Rectangle<int> getNativeSize() const { return mediaSession.getNativeSize(); }
|
|
|
|
double getDuration() const { return mediaSession.getDuration(); }
|
|
|
|
void setVolume (float newVolume) { mediaSession.setVolume (newVolume); }
|
|
float getVolume() const { return mediaSession.getVolume(); }
|
|
|
|
File currentFile;
|
|
URL currentURL;
|
|
|
|
private:
|
|
//==============================================================================
|
|
class MediaSession : private AudioManagerOnAudioFocusChangeListener::Owner
|
|
{
|
|
public:
|
|
MediaSession (Pimpl& ownerToUse)
|
|
: owner (ownerToUse),
|
|
sdkVersion (getEnv()->CallStaticIntMethod (JuceAppActivity, JuceAppActivity.getAndroidSDKVersion)),
|
|
audioAttributes (getAudioAttributes()),
|
|
nativeMediaSession (LocalRef<jobject> (getEnv()->NewObject (AndroidMediaSession,
|
|
AndroidMediaSession.constructor,
|
|
android.activity.get(),
|
|
javaString ("JuceVideoMediaSession").get()))),
|
|
mediaSessionCallback (LocalRef<jobject> (getEnv()->NewObject (AndroidMediaSessionCallback,
|
|
AndroidMediaSessionCallback.constructor,
|
|
android.activity.get(),
|
|
reinterpret_cast<jlong> (this)))),
|
|
playbackStateBuilder (LocalRef<jobject> (getEnv()->NewObject (AndroidPlaybackStateBuilder,
|
|
AndroidPlaybackStateBuilder.constructor))),
|
|
controller (*this, getEnv()->CallObjectMethod (nativeMediaSession,
|
|
AndroidMediaSession.getController)),
|
|
player (*this),
|
|
audioManager (android.activity.callObjectMethod (JuceAppActivity.getSystemService, javaString ("audio").get())),
|
|
audioFocusChangeListener (*this),
|
|
nativeAudioFocusChangeListener (GlobalRef (CreateJavaInterface (&audioFocusChangeListener,
|
|
"android/media/AudioManager$OnAudioFocusChangeListener").get())),
|
|
audioFocusRequest (createAudioFocusRequestIfNecessary (sdkVersion, audioAttributes,
|
|
nativeAudioFocusChangeListener))
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackToLocal, audioAttributes.get());
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setMediaButtonReceiver, nullptr);
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setCallback, mediaSessionCallback.get());
|
|
}
|
|
|
|
~MediaSession()
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setCallback, nullptr);
|
|
|
|
controller.stop();
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.release);
|
|
}
|
|
|
|
bool isVideoOpen() const { return player.isVideoOpen(); }
|
|
bool isPlaying() const { return player.isPlaying(); }
|
|
|
|
void load (const URL& url) { controller.load (url); }
|
|
|
|
void closeVideo()
|
|
{
|
|
resetState();
|
|
controller.closeVideo();
|
|
}
|
|
|
|
void setDisplay (jobject surfaceHolder) { player.setDisplay (surfaceHolder); }
|
|
|
|
void play() { controller.play(); }
|
|
void stop() { controller.stop(); }
|
|
|
|
void setPosition (double newPosition) { controller.setPosition (newPosition); }
|
|
double getPosition() const { return controller.getPosition(); }
|
|
|
|
void setSpeed (double newSpeed)
|
|
{
|
|
playSpeedMult = newSpeed;
|
|
|
|
// Calling non 0.0 speed on a paused player would start it...
|
|
if (player.isPlaying())
|
|
{
|
|
player.setPlaySpeed (playSpeedMult);
|
|
updatePlaybackState();
|
|
}
|
|
}
|
|
|
|
double getSpeed() const { return controller.getPlaySpeed(); }
|
|
Rectangle<int> getNativeSize() const { return player.getVideoNativeSize(); }
|
|
double getDuration() const { return player.getVideoDuration() / 1000.0; }
|
|
|
|
void setVolume (float newVolume)
|
|
{
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
controller.setVolume (newVolume);
|
|
#else
|
|
player.setAudioVolume (newVolume);
|
|
#endif
|
|
}
|
|
|
|
float getVolume() const
|
|
{
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
return controller.getVolume();
|
|
#else
|
|
return player.getAudioVolume();
|
|
#endif
|
|
}
|
|
|
|
void storeState()
|
|
{
|
|
storedPlaybackState.clear();
|
|
storedPlaybackState = GlobalRef (getCurrentPlaybackState());
|
|
}
|
|
|
|
void restoreState()
|
|
{
|
|
if (storedPlaybackState.get() == nullptr)
|
|
return;
|
|
|
|
auto* env = getEnv();
|
|
|
|
auto pos = env->CallLongMethod (storedPlaybackState, AndroidPlaybackState.getPosition);
|
|
setPosition (pos / 1000.0);
|
|
|
|
setSpeed (playSpeedMult);
|
|
|
|
auto state = env->CallIntMethod (storedPlaybackState, AndroidPlaybackState.getState);
|
|
|
|
if (state != PlaybackState::STATE_NONE && state != PlaybackState::STATE_STOPPED
|
|
&& state != PlaybackState::STATE_PAUSED && state != PlaybackState::STATE_ERROR)
|
|
{
|
|
play();
|
|
}
|
|
}
|
|
|
|
private:
|
|
struct PlaybackState
|
|
{
|
|
enum
|
|
{
|
|
STATE_NONE = 0,
|
|
STATE_STOPPED = 1,
|
|
STATE_PAUSED = 2,
|
|
STATE_PLAYING = 3,
|
|
STATE_FAST_FORWARDING = 4,
|
|
STATE_REWINDING = 5,
|
|
STATE_BUFFERING = 6,
|
|
STATE_ERROR = 7,
|
|
STATE_CONNECTING = 8,
|
|
STATE_SKIPPING_TO_PREVIOUS = 9,
|
|
STATE_SKIPPING_TO_NEXT = 10,
|
|
STATE_SKIPPING_TO_QUEUE_ITEM = 11,
|
|
};
|
|
|
|
enum
|
|
{
|
|
ACTION_PAUSE = 0x2,
|
|
ACTION_PLAY = 0x4,
|
|
ACTION_PLAY_FROM_MEDIA_ID = 0x8000,
|
|
ACTION_PLAY_PAUSE = 0x200,
|
|
ACTION_SEEK_TO = 0x100,
|
|
ACTION_STOP = 0x1,
|
|
};
|
|
};
|
|
|
|
//==============================================================================
|
|
class Controller
|
|
{
|
|
public:
|
|
Controller (MediaSession& ownerToUse, jobject nativeController)
|
|
: owner (ownerToUse),
|
|
nativeController (GlobalRef (nativeController)),
|
|
controllerTransportControls (LocalRef<jobject> (getEnv()->CallObjectMethod (nativeController,
|
|
AndroidMediaController.getTransportControls))),
|
|
controllerCallback (LocalRef<jobject> (getEnv()->NewObject (AndroidMediaControllerCallback,
|
|
AndroidMediaControllerCallback.constructor,
|
|
android.activity.get(),
|
|
reinterpret_cast<jlong> (this))))
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
env->CallVoidMethod (nativeController, AndroidMediaController.registerCallback, controllerCallback.get());
|
|
}
|
|
|
|
~Controller()
|
|
{
|
|
auto* env = getEnv();
|
|
env->CallVoidMethod (nativeController, AndroidMediaController.unregisterCallback, controllerCallback.get());
|
|
}
|
|
|
|
void load (const URL& url)
|
|
{
|
|
// NB: would use playFromUri, but it was only introduced in API 23...
|
|
getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.playFromMediaId,
|
|
javaString (url.toString (true)).get(), nullptr);
|
|
}
|
|
|
|
void closeVideo()
|
|
{
|
|
getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.stop);
|
|
}
|
|
|
|
void play()
|
|
{
|
|
getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.play);
|
|
}
|
|
|
|
void stop()
|
|
{
|
|
// NB: calling pause, rather than stop, because after calling stop, we would have to call load() again.
|
|
getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.pause);
|
|
}
|
|
|
|
void setPosition (double newPosition)
|
|
{
|
|
auto seekPos = static_cast<jlong> (newPosition * 1000);
|
|
|
|
getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.seekTo, seekPos);
|
|
}
|
|
|
|
double getPosition() const
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
auto playbackState = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackState));
|
|
|
|
if (playbackState != nullptr)
|
|
return env->CallLongMethod (playbackState, AndroidPlaybackState.getPosition) / 1000.0;
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
double getPlaySpeed() const
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
auto playbackState = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackState));
|
|
|
|
if (playbackState != nullptr)
|
|
return (double) env->CallFloatMethod (playbackState, AndroidPlaybackState.getPlaybackSpeed);
|
|
|
|
return 1.0;
|
|
}
|
|
|
|
void setVolume (float newVolume)
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
auto playbackInfo = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo));
|
|
|
|
auto maxVolume = env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume);
|
|
|
|
auto targetVolume = jmin (jint (maxVolume * newVolume), maxVolume);
|
|
|
|
static constexpr jint flagShowUI = 1;
|
|
env->CallVoidMethod (nativeController, AndroidMediaController.setVolumeTo, targetVolume, flagShowUI);
|
|
}
|
|
|
|
float getVolume() const
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
auto playbackInfo = LocalRef<jobject> (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo));
|
|
|
|
auto maxVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume));
|
|
auto curVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getCurrentVolume));
|
|
|
|
return static_cast<float> (curVolume) / maxVolume;
|
|
}
|
|
|
|
private:
|
|
MediaSession& owner;
|
|
|
|
GlobalRef nativeController;
|
|
GlobalRef controllerTransportControls;
|
|
GlobalRef controllerCallback;
|
|
bool wasPlaying = false;
|
|
bool wasPaused = true;
|
|
|
|
//==============================================================================
|
|
// MediaSessionController callbacks
|
|
|
|
void audioInfoChanged (jobject info)
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSessionController::audioInfoChanged()");
|
|
ignoreUnused (info);
|
|
}
|
|
|
|
void metadataChanged (jobject metadata)
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSessionController::metadataChanged()");
|
|
ignoreUnused (metadata);
|
|
}
|
|
|
|
void playbackStateChanged (jobject playbackState)
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSessionController::playbackStateChanged()");
|
|
|
|
if (playbackState == nullptr)
|
|
return;
|
|
|
|
auto state = getEnv()->CallIntMethod (playbackState, AndroidPlaybackState.getState);
|
|
|
|
static constexpr jint statePaused = 2;
|
|
static constexpr jint statePlaying = 3;
|
|
|
|
if (wasPlaying == false && state == statePlaying)
|
|
owner.playbackStarted();
|
|
else if (wasPaused == false && state == statePaused)
|
|
owner.playbackStopped();
|
|
|
|
wasPlaying = state == statePlaying;
|
|
wasPaused = state == statePaused;
|
|
}
|
|
|
|
void sessionDestroyed()
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSessionController::sessionDestroyed()");
|
|
}
|
|
|
|
friend void juce_mediaControllerAudioInfoChanged (int64, void*);
|
|
friend void juce_mediaControllerMetadataChanged (int64, void*);
|
|
friend void juce_mediaControllerPlaybackStateChanged (int64, void*);
|
|
friend void juce_mediaControllerSessionDestroyed (int64);
|
|
};
|
|
|
|
//==============================================================================
|
|
class Player : private MediaPlayerListener::Owner
|
|
{
|
|
public:
|
|
Player (MediaSession& ownerToUse)
|
|
: owner (ownerToUse),
|
|
mediaPlayerListener (*this),
|
|
nativeMediaPlayerListener (GlobalRef (CreateJavaInterface (&mediaPlayerListener,
|
|
getNativeMediaPlayerListenerInterfaces())))
|
|
|
|
{}
|
|
|
|
void setDisplay (jobject surfaceHolder)
|
|
{
|
|
if (surfaceHolder == nullptr)
|
|
{
|
|
videoSurfaceHolder.clear();
|
|
|
|
if (nativeMediaPlayer.get() != nullptr)
|
|
getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, nullptr);
|
|
|
|
return;
|
|
}
|
|
|
|
videoSurfaceHolder = GlobalRef (surfaceHolder);
|
|
|
|
if (nativeMediaPlayer.get() != nullptr)
|
|
getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, videoSurfaceHolder.get());
|
|
}
|
|
|
|
void load (jstring mediaId, jobject extras)
|
|
{
|
|
ignoreUnused (extras);
|
|
|
|
closeVideo();
|
|
|
|
auto* env = getEnv();
|
|
|
|
nativeMediaPlayer = GlobalRef (LocalRef<jobject> (env->NewObject (AndroidMediaPlayer, AndroidMediaPlayer.constructor)));
|
|
|
|
currentState = State::idle;
|
|
|
|
auto uri = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse, mediaId));
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDataSource, android.activity.get(), uri.get());
|
|
|
|
if (jniCheckHasExceptionOccurredAndClear())
|
|
{
|
|
owner.errorOccurred ("Could not find video under path provided (" + juceString (mediaId) + ")");
|
|
return;
|
|
}
|
|
|
|
currentState = State::initialised;
|
|
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnBufferingUpdateListener, nativeMediaPlayerListener.get());
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnCompletionListener, nativeMediaPlayerListener.get());
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnErrorListener, nativeMediaPlayerListener.get());
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnInfoListener, nativeMediaPlayerListener.get());
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnPreparedListener, nativeMediaPlayerListener.get());
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setOnSeekCompleteListener, nativeMediaPlayerListener.get());
|
|
|
|
if (videoSurfaceHolder != nullptr)
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setDisplay, videoSurfaceHolder.get());
|
|
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.prepareAsync);
|
|
|
|
currentState = State::preparing;
|
|
}
|
|
|
|
void closeVideo()
|
|
{
|
|
if (nativeMediaPlayer.get() == nullptr)
|
|
return;
|
|
|
|
auto* env = getEnv();
|
|
|
|
if (getCurrentStateInfo().canCallStop)
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.stop);
|
|
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.release);
|
|
nativeMediaPlayer.clear();
|
|
|
|
currentState = State::end;
|
|
}
|
|
|
|
bool isVideoOpen() const noexcept
|
|
{
|
|
return currentState == State::prepared || currentState == State::started
|
|
|| currentState == State::paused || currentState == State::complete;
|
|
}
|
|
|
|
int getPlaybackStateFlag() const noexcept { return getCurrentStateInfo().playbackStateFlag; }
|
|
int getAllowedActions() const noexcept { return getCurrentStateInfo().allowedActions; }
|
|
|
|
jlong getVideoDuration() const
|
|
{
|
|
if (! getCurrentStateInfo().canCallGetVideoDuration)
|
|
return 0;
|
|
|
|
return getEnv()->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getDuration);
|
|
}
|
|
|
|
Rectangle<int> getVideoNativeSize() const
|
|
{
|
|
if (! getCurrentStateInfo().canCallGetVideoHeight)
|
|
{
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
auto* env = getEnv();
|
|
|
|
auto width = (int) env->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getVideoWidth);
|
|
auto height = (int) env->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getVideoHeight);
|
|
|
|
return Rectangle<int> (0, 0, width, height);
|
|
}
|
|
|
|
void play()
|
|
{
|
|
if (! getCurrentStateInfo().canCallStart)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
auto* env = getEnv();
|
|
|
|
// Perform a potentially pending volume setting
|
|
if (lastAudioVolume != std::numeric_limits<float>::min())
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setVolume, (jfloat) lastAudioVolume, (jfloat) lastAudioVolume);
|
|
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.start);
|
|
|
|
currentState = State::started;
|
|
}
|
|
|
|
void pause()
|
|
{
|
|
if (! getCurrentStateInfo().canCallPause)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.pause);
|
|
|
|
currentState = State::paused;
|
|
}
|
|
|
|
bool isPlaying() const
|
|
{
|
|
return getCurrentStateInfo().isPlaying;
|
|
}
|
|
|
|
void setPlayPosition (jint newPositionMs)
|
|
{
|
|
if (! getCurrentStateInfo().canCallSeekTo)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.seekTo, (jint) newPositionMs);
|
|
}
|
|
|
|
jint getPlayPosition() const
|
|
{
|
|
if (! getCurrentStateInfo().canCallGetCurrentPosition)
|
|
return 0.0;
|
|
|
|
return getEnv()->CallIntMethod (nativeMediaPlayer, AndroidMediaPlayer.getCurrentPosition);
|
|
}
|
|
|
|
void setPlaySpeed (double newSpeed)
|
|
{
|
|
if (! getCurrentStateInfo().canCallSetPlaybackParams)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
auto* env = getEnv();
|
|
|
|
auto playbackParams = LocalRef<jobject> (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams));
|
|
LocalRef<jobject> (env->CallObjectMethod (playbackParams, AndroidPlaybackParams.setSpeed, (jfloat) newSpeed));
|
|
env->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setPlaybackParams, playbackParams.get());
|
|
|
|
if (jniCheckHasExceptionOccurredAndClear())
|
|
{
|
|
// MediaPlayer can't handle speed provided!
|
|
jassertfalse;
|
|
}
|
|
}
|
|
|
|
double getPlaySpeed() const
|
|
{
|
|
if (! getCurrentStateInfo().canCallGetPlaybackParams)
|
|
return 0.0;
|
|
|
|
auto* env = getEnv();
|
|
|
|
auto playbackParams = LocalRef<jobject> (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams));
|
|
return (double) env->CallFloatMethod (playbackParams, AndroidPlaybackParams.getSpeed);
|
|
}
|
|
|
|
void setAudioVolume (float newVolume)
|
|
{
|
|
if (! getCurrentStateInfo().canCallSetVolume)
|
|
{
|
|
jassertfalse;
|
|
return;
|
|
}
|
|
|
|
lastAudioVolume = jlimit (0.0f, 1.0f, newVolume);
|
|
|
|
if (nativeMediaPlayer.get() != nullptr)
|
|
getEnv()->CallVoidMethod (nativeMediaPlayer, AndroidMediaPlayer.setVolume, (jfloat) lastAudioVolume, (jfloat) lastAudioVolume);
|
|
}
|
|
|
|
float getAudioVolume() const
|
|
{
|
|
// There is NO getVolume() in MediaPlayer, so the value returned here can be incorrect!
|
|
return lastAudioVolume;
|
|
}
|
|
|
|
private:
|
|
//=============================================================================
|
|
struct StateInfo
|
|
{
|
|
int playbackStateFlag = 0, allowedActions = 0;
|
|
|
|
bool isPlaying, canCallGetCurrentPosition, canCallGetVideoDuration,
|
|
canCallGetVideoHeight, canCallGetVideoWidth, canCallGetPlaybackParams,
|
|
canCallPause, canCallPrepare, canCallSeekTo, canCallSetAudioAttributes,
|
|
canCallSetDataSource, canCallSetPlaybackParams, canCallSetVolume,
|
|
canCallStart, canCallStop;
|
|
};
|
|
|
|
enum class State
|
|
{
|
|
idle, initialised, preparing, prepared, started, paused, stopped, complete, error, end
|
|
};
|
|
|
|
static constexpr StateInfo stateInfos[] = {
|
|
/* idle */
|
|
{PlaybackState::STATE_NONE, PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
false, true, false, true, true, false, false, false, false, true,
|
|
true, false, true, false, false},
|
|
/* initialised */
|
|
{PlaybackState::STATE_NONE, 0, // NB: could use action prepare, but that's API 24 onwards only
|
|
false, true, false, true, true, true, false, true, false, true,
|
|
false, true, true, false, false},
|
|
/* preparing */
|
|
{PlaybackState::STATE_BUFFERING, 0,
|
|
false, false, false, false, false, true, false, false, false, false,
|
|
false, false, false, false, false},
|
|
/* prepared */
|
|
{PlaybackState::STATE_PAUSED,
|
|
PlaybackState::ACTION_PLAY | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID | PlaybackState::ACTION_STOP | PlaybackState::ACTION_SEEK_TO,
|
|
false, true, true, true, true, true, false, false, true, true,
|
|
false, true, true, true, true},
|
|
/* started */
|
|
{PlaybackState::STATE_PLAYING,
|
|
PlaybackState::ACTION_PAUSE | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
true, true, true, true, true, true, true, false, true, true,
|
|
false, true, true, true, true},
|
|
/* paused */
|
|
{PlaybackState::STATE_PAUSED,
|
|
PlaybackState::ACTION_PLAY | PlaybackState::ACTION_PLAY_PAUSE | PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
false, true, true, true, true, true, true, false, true, true,
|
|
false, true, true, true, true},
|
|
/* stopped */
|
|
{PlaybackState::STATE_STOPPED,
|
|
PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
false, true, true, true, true, true, false, true, false, true,
|
|
false, false, true, false, true},
|
|
/* complete */
|
|
{PlaybackState::STATE_PAUSED,
|
|
PlaybackState::ACTION_SEEK_TO | PlaybackState::ACTION_STOP | PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
false, true, true, true, true, true, true, false, true, true,
|
|
false, true, true, true, true},
|
|
/* error */
|
|
{PlaybackState::STATE_ERROR,
|
|
PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
false, false, false, false, false, false, false, false, false, false,
|
|
false, false, false, false, false},
|
|
/* end */
|
|
{PlaybackState::STATE_NONE,
|
|
PlaybackState::ACTION_PLAY_FROM_MEDIA_ID,
|
|
false, false, false, false, false, false, false, false, false, false,
|
|
false, false, false, false, false}
|
|
};
|
|
|
|
StateInfo getCurrentStateInfo() const noexcept { return stateInfos[static_cast<int> (currentState)]; }
|
|
|
|
//==============================================================================
|
|
MediaSession& owner;
|
|
GlobalRef nativeMediaPlayer;
|
|
|
|
MediaPlayerListener mediaPlayerListener;
|
|
GlobalRef nativeMediaPlayerListener;
|
|
|
|
float lastAudioVolume = std::numeric_limits<float>::min();
|
|
|
|
GlobalRef videoSurfaceHolder;
|
|
|
|
State currentState = State::idle;
|
|
|
|
//==============================================================================
|
|
void onPrepared (LocalRef<jobject>& mediaPlayer) override
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaPlayer::onPrepared()");
|
|
|
|
ignoreUnused (mediaPlayer);
|
|
|
|
currentState = State::prepared;
|
|
|
|
owner.playerPrepared();
|
|
}
|
|
|
|
void onBufferingUpdate (LocalRef<jobject>& mediaPlayer, int progress) override
|
|
{
|
|
ignoreUnused (mediaPlayer);
|
|
|
|
owner.playerBufferingUpdated (progress);
|
|
}
|
|
|
|
void onSeekComplete (LocalRef<jobject>& mediaPlayer) override
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaPlayer::onSeekComplete()");
|
|
|
|
ignoreUnused (mediaPlayer);
|
|
|
|
owner.playerSeekCompleted();
|
|
}
|
|
|
|
void onCompletion (LocalRef<jobject>& mediaPlayer) override
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaPlayer::onCompletion()");
|
|
|
|
ignoreUnused (mediaPlayer);
|
|
|
|
currentState = State::complete;
|
|
|
|
owner.playerPlaybackCompleted();
|
|
}
|
|
|
|
enum
|
|
{
|
|
MEDIA_INFO_UNKNOWN = 1,
|
|
MEDIA_INFO_VIDEO_RENDERING_START = 3,
|
|
MEDIA_INFO_VIDEO_TRACK_LAGGING = 700,
|
|
MEDIA_INFO_BUFFERING_START = 701,
|
|
MEDIA_INFO_BUFFERING_END = 702,
|
|
MEDIA_INFO_NETWORK_BANDWIDTH = 703,
|
|
MEDIA_INFO_BAD_INTERLEAVING = 800,
|
|
MEDIA_INFO_NOT_SEEKABLE = 801,
|
|
MEDIA_INFO_METADATA_UPDATE = 802,
|
|
MEDIA_INFO_AUDIO_NOT_PLAYING = 804,
|
|
MEDIA_INFO_VIDEO_NOT_PLAYING = 805,
|
|
MEDIA_INFO_UNSUPPORTED_SUBTITE = 901,
|
|
MEDIA_INFO_SUBTITLE_TIMED_OUT = 902
|
|
};
|
|
|
|
bool onInfo (LocalRef<jobject>& mediaPlayer, int what, int extra) override
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaPlayer::onInfo(), infoCode: " + String (what) + " (" + infoCodeToString (what) + ")"
|
|
+ ", extraCode: " + String (extra));
|
|
|
|
ignoreUnused (mediaPlayer, extra);
|
|
|
|
if (what == MEDIA_INFO_BUFFERING_START)
|
|
owner.playerBufferingStarted();
|
|
else if (what == MEDIA_INFO_BUFFERING_END)
|
|
owner.playerBufferingEnded();
|
|
|
|
return true;
|
|
}
|
|
|
|
static String infoCodeToString (int code)
|
|
{
|
|
switch (code)
|
|
{
|
|
case MEDIA_INFO_UNKNOWN: return "Unknown";
|
|
case MEDIA_INFO_VIDEO_RENDERING_START: return "Rendering start";
|
|
case MEDIA_INFO_VIDEO_TRACK_LAGGING: return "Video track lagging";
|
|
case MEDIA_INFO_BUFFERING_START: return "Buffering start";
|
|
case MEDIA_INFO_BUFFERING_END: return "Buffering end";
|
|
case MEDIA_INFO_NETWORK_BANDWIDTH: return "Network bandwidth info available";
|
|
case MEDIA_INFO_BAD_INTERLEAVING: return "Bad interleaving";
|
|
case MEDIA_INFO_NOT_SEEKABLE: return "Video not seekable";
|
|
case MEDIA_INFO_METADATA_UPDATE: return "Metadata updated";
|
|
case MEDIA_INFO_AUDIO_NOT_PLAYING: return "Audio not playing";
|
|
case MEDIA_INFO_VIDEO_NOT_PLAYING: return "Video not playing";
|
|
case MEDIA_INFO_UNSUPPORTED_SUBTITE: return "Unsupported subtitle";
|
|
case MEDIA_INFO_SUBTITLE_TIMED_OUT: return "Subtitle timed out";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
bool onError (LocalRef<jobject>& mediaPlayer, int what, int extra) override
|
|
{
|
|
auto errorMessage = errorCodeToString (what);
|
|
auto extraMessage = errorCodeToString (extra);
|
|
|
|
if (extraMessage.isNotEmpty())
|
|
errorMessage << ", " << extraMessage;
|
|
|
|
JUCE_VIDEO_LOG ("MediaPlayer::onError(), errorCode: " + String (what) + " (" + errorMessage + ")"
|
|
+ ", extraCode: " + String (extra) + " (" + extraMessage + ")");
|
|
|
|
ignoreUnused (mediaPlayer);
|
|
|
|
currentState = State::error;
|
|
|
|
owner.errorOccurred (errorMessage);
|
|
return true;
|
|
}
|
|
|
|
static String errorCodeToString (int code)
|
|
{
|
|
enum
|
|
{
|
|
MEDIA_ERROR_UNSUPPORTED = -1010,
|
|
MEDIA_ERROR_MALFORMED = -1007,
|
|
MEDIA_ERROR_IO = -1004,
|
|
MEDIA_ERROR_TIMED_OUT = -110,
|
|
MEDIA_ERROR_UNKNOWN = 1,
|
|
MEDIA_ERROR_SERVER_DIED = 100,
|
|
MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200
|
|
};
|
|
|
|
switch (code)
|
|
{
|
|
case MEDIA_ERROR_UNSUPPORTED: return "Unsupported bitstream";
|
|
case MEDIA_ERROR_MALFORMED: return "Malformed bitstream";
|
|
case MEDIA_ERROR_IO: return "File/Network I/O error";
|
|
case MEDIA_ERROR_TIMED_OUT: return "Timed out";
|
|
case MEDIA_ERROR_UNKNOWN: return "Unknown error";
|
|
case MEDIA_ERROR_SERVER_DIED: return "Media server died (playback restart required)";
|
|
case MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: return "Video container not valid for progressive playback";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
static StringArray getNativeMediaPlayerListenerInterfaces()
|
|
{
|
|
#define IFPREFIX "android/media/MediaPlayer$"
|
|
|
|
return { IFPREFIX "OnCompletionListener", IFPREFIX "OnErrorListener",
|
|
IFPREFIX "OnInfoListener", IFPREFIX "OnPreparedListener",
|
|
IFPREFIX "OnBufferingUpdateListener", IFPREFIX "OnSeekCompleteListener"
|
|
};
|
|
|
|
#undef IFPREFIX
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
Pimpl& owner;
|
|
|
|
int sdkVersion;
|
|
|
|
GlobalRef audioAttributes;
|
|
GlobalRef nativeMediaSession;
|
|
GlobalRef mediaSessionCallback;
|
|
GlobalRef playbackStateBuilder;
|
|
|
|
Controller controller;
|
|
Player player;
|
|
|
|
GlobalRef audioManager;
|
|
AudioManagerOnAudioFocusChangeListener audioFocusChangeListener;
|
|
GlobalRef nativeAudioFocusChangeListener;
|
|
GlobalRef audioFocusRequest;
|
|
|
|
GlobalRef storedPlaybackState;
|
|
|
|
bool pendingSeekRequest = false;
|
|
|
|
bool playerBufferingInProgress = false;
|
|
bool usesBuffering = false;
|
|
SparseSet<int> bufferedRegions;
|
|
|
|
double playSpeedMult = 1.0;
|
|
bool hasAudioFocus = false;
|
|
|
|
//==============================================================================
|
|
// MediaSession callbacks
|
|
|
|
void pauseCallback()
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSession::pauseCallback()");
|
|
|
|
player.pause();
|
|
updatePlaybackState();
|
|
|
|
abandonAudioFocus();
|
|
}
|
|
|
|
void playCallback()
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSession::playCallback()");
|
|
|
|
requestAudioFocus();
|
|
|
|
if (! hasAudioFocus)
|
|
{
|
|
errorOccurred ("Application has been denied audio focus. Try again later.");
|
|
return;
|
|
}
|
|
|
|
getEnv()->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setActive, true);
|
|
|
|
player.play();
|
|
setSpeed (playSpeedMult);
|
|
updatePlaybackState();
|
|
}
|
|
|
|
void playFromMediaIdCallback (jstring mediaId, jobject extras)
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSession::playFromMediaIdCallback()");
|
|
|
|
player.load (mediaId, extras);
|
|
updatePlaybackState();
|
|
}
|
|
|
|
void seekToCallback (jlong pos)
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSession::seekToCallback()");
|
|
|
|
pendingSeekRequest = true;
|
|
player.setPlayPosition ((jint) pos);
|
|
updatePlaybackState();
|
|
}
|
|
|
|
void stopCallback()
|
|
{
|
|
JUCE_VIDEO_LOG ("MediaSession::stopCallback()");
|
|
|
|
auto* env = getEnv();
|
|
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setActive, false);
|
|
|
|
player.closeVideo();
|
|
updatePlaybackState();
|
|
|
|
abandonAudioFocus();
|
|
|
|
owner.closeVideoFinished();
|
|
}
|
|
|
|
//==============================================================================
|
|
bool isSeekInProgress() const noexcept
|
|
{
|
|
if (pendingSeekRequest)
|
|
return true;
|
|
|
|
if (! usesBuffering)
|
|
return false;
|
|
|
|
// NB: player sometimes notifies us about buffering, but only for regions that
|
|
// were previously buffered already. For buffering happening for the first time,
|
|
// we don't get such notification...
|
|
if (playerBufferingInProgress)
|
|
return true;
|
|
|
|
auto playPos = player.getPlayPosition();
|
|
auto durationMs = player.getVideoDuration();
|
|
int playPosPercent = 100 * playPos / static_cast<double> (durationMs);
|
|
|
|
// NB: assuming the playback will start roughly when there is 5% of content loaded...
|
|
return ! bufferedRegions.containsRange (Range<int> (playPosPercent, jmin (101, playPosPercent + 5)));
|
|
}
|
|
|
|
void updatePlaybackState()
|
|
{
|
|
getEnv()->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackState, getCurrentPlaybackState());
|
|
}
|
|
|
|
jobject getCurrentPlaybackState()
|
|
{
|
|
static constexpr int bufferingState = 6;
|
|
|
|
auto playbackStateFlag = isSeekInProgress() ? bufferingState : player.getPlaybackStateFlag();
|
|
auto playPos = player.getPlayPosition();
|
|
auto playSpeed = player.getPlaySpeed();
|
|
auto allowedActions = player.getAllowedActions();
|
|
|
|
auto* env = getEnv();
|
|
|
|
LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setState,
|
|
(jint) playbackStateFlag, (jlong) playPos, (jfloat) playSpeed));
|
|
|
|
LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setActions, (jint) allowedActions));
|
|
|
|
return env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.build);
|
|
}
|
|
|
|
//==============================================================================
|
|
void playerPrepared()
|
|
{
|
|
resetState();
|
|
|
|
updateMetadata();
|
|
|
|
owner.loadFinished();
|
|
}
|
|
|
|
void playerBufferingStarted() { playerBufferingInProgress = true; }
|
|
void playerBufferingEnded() { playerBufferingInProgress = false; }
|
|
|
|
void playerBufferingUpdated (int progress)
|
|
{
|
|
usesBuffering = true;
|
|
|
|
updatePlaybackState();
|
|
|
|
auto playPos = player.getPlayPosition();
|
|
auto durationMs = player.getVideoDuration();
|
|
int playPosPercent = 100 * playPos / static_cast<double> (durationMs);
|
|
|
|
bufferedRegions.addRange (Range<int> (playPosPercent, progress + 1));
|
|
|
|
String ranges;
|
|
|
|
for (auto& r : bufferedRegions.getRanges())
|
|
ranges << "[" << r.getStart() << "%, " << r.getEnd() - 1 << "%] ";
|
|
|
|
JUCE_VIDEO_LOG ("Buffering status update, seek pos: " + String (playPosPercent) + "%, buffered regions: " + ranges);
|
|
}
|
|
|
|
void playerSeekCompleted()
|
|
{
|
|
pendingSeekRequest = false;
|
|
updatePlaybackState();
|
|
}
|
|
|
|
void playerPlaybackCompleted()
|
|
{
|
|
pauseCallback();
|
|
seekToCallback ((jlong) 0);
|
|
}
|
|
|
|
void updateMetadata()
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
auto metadataBuilder = LocalRef<jobject> (env->NewObject (AndroidMediaMetadataBuilder,
|
|
AndroidMediaMetadataBuilder.constructor));
|
|
|
|
auto durationMs = player.getVideoDuration();
|
|
|
|
auto jDurationKey = javaString ("android.media.metadata.DURATION");
|
|
LocalRef<jobject> (env->CallObjectMethod (metadataBuilder,
|
|
AndroidMediaMetadataBuilder.putLong,
|
|
jDurationKey.get(),
|
|
(jlong) durationMs));
|
|
|
|
auto jNumTracksKey = javaString ("android.media.metadata.NUM_TRACKS");
|
|
LocalRef<jobject> (env->CallObjectMethod (metadataBuilder,
|
|
AndroidMediaMetadataBuilder.putLong,
|
|
jNumTracksKey.get(),
|
|
(jlong) 1));
|
|
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setMetadata,
|
|
env->CallObjectMethod (metadataBuilder, AndroidMediaMetadataBuilder.build));
|
|
}
|
|
|
|
void errorOccurred (const String& errorMessage)
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
// Propagate error to session controller(s) and ...
|
|
LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setErrorMessage,
|
|
javaString (errorMessage).get()));
|
|
|
|
auto state = LocalRef<jobject> (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.build));
|
|
env->CallVoidMethod (nativeMediaSession, AndroidMediaSession.setPlaybackState, state.get());
|
|
|
|
// ...also notify JUCE side client
|
|
owner.errorOccurred (errorMessage);
|
|
}
|
|
|
|
//==============================================================================
|
|
static jobject createAudioFocusRequestIfNecessary (int sdkVersion, const GlobalRef& audioAttributes,
|
|
const GlobalRef& nativeAudioFocusChangeListener)
|
|
{
|
|
if (sdkVersion < 26)
|
|
return nullptr;
|
|
|
|
auto* env = getEnv();
|
|
|
|
auto requestBuilderClass = LocalRef<jclass> (env->FindClass ("android/media/AudioFocusRequest$Builder"));
|
|
|
|
static jmethodID constructor = env->GetMethodID (requestBuilderClass, "<init>", "(I)V");
|
|
static jmethodID buildMethod = env->GetMethodID (requestBuilderClass, "build", "()Landroid/media/AudioFocusRequest;");
|
|
static jmethodID setAudioAttributesMethod = env->GetMethodID (requestBuilderClass, "setAudioAttributes",
|
|
"(Landroid/media/AudioAttributes;)Landroid/media/AudioFocusRequest$Builder;");
|
|
static jmethodID setOnAudioFocusChangeListenerMethod = env->GetMethodID (requestBuilderClass, "setOnAudioFocusChangeListener",
|
|
"(Landroid/media/AudioManager$OnAudioFocusChangeListener;)Landroid/media/AudioFocusRequest$Builder;");
|
|
|
|
static constexpr jint audioFocusGain = 1;
|
|
|
|
auto requestBuilder = LocalRef<jobject> (env->NewObject (requestBuilderClass, constructor, audioFocusGain));
|
|
LocalRef<jobject> (env->CallObjectMethod (requestBuilder, setAudioAttributesMethod, audioAttributes.get()));
|
|
LocalRef<jobject> (env->CallObjectMethod (requestBuilder, setOnAudioFocusChangeListenerMethod, nativeAudioFocusChangeListener.get()));
|
|
|
|
return env->CallObjectMethod (requestBuilder, buildMethod);
|
|
}
|
|
|
|
void requestAudioFocus()
|
|
{
|
|
static constexpr jint audioFocusGain = 1;
|
|
static constexpr jint streamMusic = 3;
|
|
static constexpr jint audioFocusRequestGranted = 1;
|
|
|
|
jint result = audioFocusRequestGranted;
|
|
|
|
if (sdkVersion >= 26)
|
|
{
|
|
static jmethodID requestAudioFocusMethod = getEnv()->GetMethodID (AndroidAudioManager, "requestAudioFocus",
|
|
"(Landroid/media/AudioFocusRequest;)I");
|
|
|
|
result = getEnv()->CallIntMethod (audioManager, requestAudioFocusMethod, audioFocusRequest.get());
|
|
}
|
|
else
|
|
{
|
|
result = getEnv()->CallIntMethod (audioManager, AndroidAudioManager.requestAudioFocus,
|
|
nativeAudioFocusChangeListener.get(), streamMusic, audioFocusGain);
|
|
}
|
|
|
|
hasAudioFocus = result == audioFocusRequestGranted;
|
|
}
|
|
|
|
void abandonAudioFocus()
|
|
{
|
|
if (! hasAudioFocus)
|
|
return;
|
|
|
|
static constexpr jint audioFocusRequestGranted = 1;
|
|
|
|
jint result = audioFocusRequestGranted;
|
|
|
|
if (sdkVersion >= 26)
|
|
{
|
|
static jmethodID abandonAudioFocusMethod = getEnv()->GetMethodID (AndroidAudioManager, "abandonAudioFocusRequest",
|
|
"(Landroid/media/AudioFocusRequest;)I");
|
|
|
|
result = getEnv()->CallIntMethod (audioManager, abandonAudioFocusMethod, audioFocusRequest.get());
|
|
}
|
|
else
|
|
{
|
|
result = getEnv()->CallIntMethod (audioManager, AndroidAudioManager.abandonAudioFocus,
|
|
nativeAudioFocusChangeListener.get());
|
|
}
|
|
|
|
// NB: granted in this case means "granted to change the focus to abandoned"...
|
|
hasAudioFocus = result != audioFocusRequestGranted;
|
|
}
|
|
|
|
void onAudioFocusChange (int changeType) override
|
|
{
|
|
static constexpr jint audioFocusGain = 1;
|
|
|
|
if (changeType == audioFocusGain)
|
|
JUCE_VIDEO_LOG ("Audio focus gained");
|
|
else
|
|
JUCE_VIDEO_LOG ("Audio focus lost");
|
|
|
|
if (changeType != audioFocusGain)
|
|
{
|
|
if (isPlaying())
|
|
{
|
|
JUCE_VIDEO_LOG ("Received a request to abandon audio focus. Stopping playback...");
|
|
stop();
|
|
}
|
|
|
|
abandonAudioFocus();
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
void playbackStarted()
|
|
{
|
|
owner.playbackStarted();
|
|
}
|
|
|
|
void playbackStopped()
|
|
{
|
|
owner.playbackStopped();
|
|
}
|
|
|
|
//==============================================================================
|
|
void resetState()
|
|
{
|
|
usesBuffering = false;
|
|
bufferedRegions.clear();
|
|
playerBufferingInProgress = false;
|
|
|
|
pendingSeekRequest = false;
|
|
|
|
playSpeedMult = 1.0;
|
|
hasAudioFocus = false;
|
|
}
|
|
|
|
//==============================================================================
|
|
static jobject getAudioAttributes()
|
|
{
|
|
auto* env = getEnv();
|
|
|
|
auto audioAttribsBuilder = LocalRef<jobject> (env->NewObject (AndroidAudioAttributesBuilder,
|
|
AndroidAudioAttributesBuilder.constructor));
|
|
static constexpr jint contentTypeMovie = 3;
|
|
static constexpr jint usageMedia = 1;
|
|
|
|
LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setContentType, contentTypeMovie));
|
|
LocalRef<jobject> (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setUsage, usageMedia));
|
|
|
|
return env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.build);
|
|
}
|
|
|
|
friend void juce_mediaSessionPause (int64);
|
|
friend void juce_mediaSessionPlay (int64);
|
|
friend void juce_mediaSessionPlayFromMediaId (int64, void*, void*);
|
|
friend void juce_mediaSessionSeekTo (int64, int64);
|
|
friend void juce_mediaSessionStop (int64);
|
|
|
|
friend void juce_mediaControllerAudioInfoChanged (int64, void*);
|
|
friend void juce_mediaControllerMetadataChanged (int64, void*);
|
|
friend void juce_mediaControllerPlaybackStateChanged (int64, void*);
|
|
friend void juce_mediaControllerSessionDestroyed (int64);
|
|
};
|
|
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
//==============================================================================
|
|
class SystemVolumeListener
|
|
{
|
|
public:
|
|
SystemVolumeListener (Pimpl& ownerToUse)
|
|
: owner (ownerToUse),
|
|
nativeObserver (LocalRef<jobject> (getEnv()->NewObject (SystemVolumeObserver,
|
|
SystemVolumeObserver.constructor,
|
|
android.activity.get(),
|
|
android.activity.get(),
|
|
reinterpret_cast<jlong> (this))))
|
|
{
|
|
setEnabled (true);
|
|
}
|
|
|
|
~SystemVolumeListener()
|
|
{
|
|
setEnabled (false);
|
|
}
|
|
|
|
void setEnabled (bool shouldBeEnabled)
|
|
{
|
|
getEnv()->CallVoidMethod (nativeObserver, SystemVolumeObserver.setEnabled, shouldBeEnabled);
|
|
|
|
// Send first notification instantly to ensure sync.
|
|
if (shouldBeEnabled)
|
|
systemVolumeChanged();
|
|
}
|
|
|
|
private:
|
|
Pimpl& owner;
|
|
GlobalRef nativeObserver;
|
|
|
|
void systemVolumeChanged()
|
|
{
|
|
WeakReference<SystemVolumeListener> weakThis (this);
|
|
|
|
MessageManager::callAsync ([weakThis]() mutable
|
|
{
|
|
if (weakThis == nullptr)
|
|
return;
|
|
|
|
if (weakThis->owner.owner.onGlobalMediaVolumeChanged != nullptr)
|
|
weakThis->owner.owner.onGlobalMediaVolumeChanged();
|
|
});
|
|
}
|
|
|
|
friend void juce_mediaSessionSystemVolumeChanged (int64);
|
|
|
|
JUCE_DECLARE_WEAK_REFERENCEABLE (SystemVolumeListener)
|
|
};
|
|
#endif
|
|
|
|
//==============================================================================
|
|
VideoComponent& owner;
|
|
|
|
MediaSession mediaSession;
|
|
AppPausedResumedListener appPausedResumedListener;
|
|
GlobalRef appPausedResumedListenerNative;
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
SystemVolumeListener systemVolumeListener;
|
|
#endif
|
|
|
|
std::function<void (const URL&, Result)> loadFinishedCallback;
|
|
|
|
bool wasOpen = false;
|
|
|
|
//==============================================================================
|
|
void loadFinished()
|
|
{
|
|
owner.resized();
|
|
|
|
if (loadFinishedCallback != nullptr)
|
|
{
|
|
loadFinishedCallback (currentURL, Result::ok());
|
|
loadFinishedCallback = nullptr;
|
|
}
|
|
}
|
|
|
|
void closeVideoFinished()
|
|
{
|
|
owner.resized();
|
|
}
|
|
|
|
void errorOccurred (const String& errorMessage)
|
|
{
|
|
if (owner.onErrorOccurred != nullptr)
|
|
owner.onErrorOccurred (errorMessage);
|
|
}
|
|
|
|
void playbackStarted()
|
|
{
|
|
if (owner.onPlaybackStarted != nullptr)
|
|
owner.onPlaybackStarted();
|
|
}
|
|
|
|
void playbackStopped()
|
|
{
|
|
if (owner.onPlaybackStopped != nullptr)
|
|
owner.onPlaybackStopped();
|
|
}
|
|
|
|
void videoSurfaceChanged (jobject surfaceHolder)
|
|
{
|
|
mediaSession.setDisplay (surfaceHolder);
|
|
}
|
|
|
|
void videoSurfaceDestroyed (jobject surfaceHolder)
|
|
{
|
|
mediaSession.setDisplay (nullptr);
|
|
}
|
|
|
|
//==============================================================================
|
|
void appPaused() override
|
|
{
|
|
wasOpen = isOpen();
|
|
|
|
if (! wasOpen)
|
|
return;
|
|
|
|
JUCE_VIDEO_LOG ("App paused, releasing media player...");
|
|
|
|
mediaSession.storeState();
|
|
mediaSession.closeVideo();
|
|
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
systemVolumeListener.setEnabled (false);
|
|
#endif
|
|
}
|
|
|
|
void appResumed() override
|
|
{
|
|
if (! wasOpen)
|
|
return;
|
|
|
|
JUCE_VIDEO_LOG ("App resumed, restoring media player...");
|
|
|
|
loadAsync (currentURL, [this](const URL&, Result r)
|
|
{
|
|
if (r.wasOk())
|
|
mediaSession.restoreState();
|
|
});
|
|
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
systemVolumeListener.setEnabled (true);
|
|
#endif
|
|
}
|
|
|
|
//==============================================================================
|
|
friend void juce_surfaceChangedNativeVideo (int64, void*);
|
|
friend void juce_surfaceDestroyedNativeVideo (int64, void*);
|
|
|
|
friend void juce_mediaSessionPause (int64);
|
|
friend void juce_mediaSessionPlay (int64);
|
|
friend void juce_mediaSessionPlayFromMediaId (int64, void*, void*);
|
|
friend void juce_mediaSessionSeekTo (int64, int64);
|
|
friend void juce_mediaSessionStop (int64);
|
|
|
|
friend void juce_mediaControllerAudioInfoChanged (int64, void*);
|
|
friend void juce_mediaControllerMetadataChanged (int64, void*);
|
|
friend void juce_mediaControllerPlaybackStateChanged (int64, void*);
|
|
friend void juce_mediaControllerSessionDestroyed (int64);
|
|
|
|
friend void juce_mediaSessionSystemVolumeChanged (int64);
|
|
#endif
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
|
};
|
|
|
|
#if __ANDROID_API__ >= 21
|
|
//==============================================================================
|
|
void juce_surfaceChangedNativeVideo (int64 host, void* surfaceHolder)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl*> (host)->videoSurfaceChanged (static_cast<jobject> (surfaceHolder));
|
|
}
|
|
|
|
void juce_surfaceDestroyedNativeVideo (int64 host, void* surfaceHolder)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl*> (host)->videoSurfaceDestroyed (static_cast<jobject> (surfaceHolder));
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), dispatchDrawNativeVideo,
|
|
void, (JNIEnv* env, jobject nativeView, jlong host, jobject canvas))
|
|
{
|
|
ignoreUnused (nativeView, host, canvas);
|
|
setEnv (env);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), surfaceChangedNativeVideo,
|
|
void, (JNIEnv* env, jobject nativeView, jlong host, jobject holder, jint format, jint width, jint height))
|
|
{
|
|
ignoreUnused (nativeView, format, width, height);
|
|
setEnv (env);
|
|
|
|
JUCE_VIDEO_LOG ("video surface changed");
|
|
|
|
juce_surfaceChangedNativeVideo (host, holder);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), surfaceCreatedNativeVideo,
|
|
void, (JNIEnv* env, jobject nativeView, jlong host, jobject holder))
|
|
{
|
|
ignoreUnused (nativeView, host, holder);
|
|
setEnv (env);
|
|
|
|
JUCE_VIDEO_LOG ("video surface created");
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), surfaceDestroyedNativeVideo,
|
|
void, (JNIEnv* env, jobject nativeView, jlong host, jobject holder))
|
|
{
|
|
ignoreUnused (nativeView, host, holder);
|
|
setEnv (env);
|
|
|
|
JUCE_VIDEO_LOG ("video surface destroyed");
|
|
juce_surfaceDestroyedNativeVideo (host, holder);
|
|
}
|
|
|
|
//==============================================================================
|
|
void juce_mediaSessionPause (int64 host)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host)->pauseCallback();
|
|
}
|
|
|
|
void juce_mediaSessionPlay (int64 host)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host)->playCallback();
|
|
}
|
|
|
|
void juce_mediaSessionPlayFromMediaId (int64 host, void* mediaId, void* extras)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host)->playFromMediaIdCallback ((jstring) mediaId, (jobject) extras);
|
|
}
|
|
|
|
void juce_mediaSessionSeekTo (int64 host, int64 pos)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host)->seekToCallback (pos);
|
|
}
|
|
|
|
void juce_mediaSessionStop (int64 host)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession*> (host)->stopCallback();
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaSessionCallback), mediaSessionPause,
|
|
void, (JNIEnv* env, jobject /*mediaSessionCallback*/, jlong host))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaSessionPause (host);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaSessionCallback), mediaSessionPlay,
|
|
void, (JNIEnv* env, jobject /*mediaSessionCallback*/, jlong host))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaSessionPlay (host);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaSessionCallback), mediaSessionPlayFromMediaId,
|
|
void, (JNIEnv* env, jobject /*mediaSessionCallback*/, jlong host, jobject mediaId, jobject extras))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaSessionPlayFromMediaId (host, mediaId, extras);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaSessionCallback), mediaSessionSeekTo,
|
|
void, (JNIEnv* env, jobject /*mediaSessionCallback*/, jlong host, jlong pos))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaSessionSeekTo (host, pos);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaSessionCallback), mediaSessionStop,
|
|
void, (JNIEnv* env, jobject /*mediaSessionCallback*/, jlong host))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaSessionStop (host);
|
|
}
|
|
|
|
//==============================================================================
|
|
void juce_mediaControllerAudioInfoChanged (int64 host, void* info)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host)->audioInfoChanged ((jobject) info);
|
|
}
|
|
|
|
void juce_mediaControllerMetadataChanged (int64 host, void* metadata)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host)->metadataChanged ((jobject) metadata);
|
|
}
|
|
|
|
void juce_mediaControllerPlaybackStateChanged (int64 host, void* state)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host)->playbackStateChanged ((jobject) state);
|
|
}
|
|
|
|
void juce_mediaControllerSessionDestroyed (int64 host)
|
|
{
|
|
reinterpret_cast<VideoComponent::Pimpl::MediaSession::Controller*> (host)->sessionDestroyed();
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaControllerCallback), mediaControllerAudioInfoChanged,
|
|
void, (JNIEnv* env, jobject /*mediaControllerCallback*/, jlong host, jobject playbackInfo))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaControllerAudioInfoChanged (host, playbackInfo);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaControllerCallback), mediaControllerMetadataChanged,
|
|
void, (JNIEnv* env, jobject /*mediaControllerCallback*/, jlong host, jobject metadata))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaControllerMetadataChanged (host, metadata);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaControllerCallback), mediaControllerPlaybackStateChanged,
|
|
void, (JNIEnv* env, jobject /*mediaControllerCallback*/, jlong host, jobject playbackState))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaControllerPlaybackStateChanged (host, playbackState);
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024MediaControllerCallback), mediaControllerSessionDestroyed,
|
|
void, (JNIEnv* env, jobject /*mediaControllerCallback*/, jlong host))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaControllerSessionDestroyed (host);
|
|
}
|
|
|
|
//==============================================================================
|
|
void juce_mediaSessionSystemVolumeChanged (int64 host)
|
|
{
|
|
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
|
reinterpret_cast<VideoComponent::Pimpl::SystemVolumeListener*> (host)->systemVolumeChanged();
|
|
#else
|
|
ignoreUnused (host);
|
|
#endif
|
|
}
|
|
|
|
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024SystemVolumeObserver), mediaSessionSystemVolumeChanged,
|
|
void, (JNIEnv* env, jobject /*systemSettingsObserver*/, jlong host))
|
|
{
|
|
setEnv (env);
|
|
juce_mediaSessionSystemVolumeChanged (host);
|
|
}
|
|
|
|
//==============================================================================
|
|
constexpr VideoComponent::Pimpl::MediaSession::Player::StateInfo VideoComponent::Pimpl::MediaSession::Player::stateInfos[];
|
|
#endif
|