From 315326477d22cde60ac9dcd962aa57ccaa9040e5 Mon Sep 17 00:00:00 2001 From: Lukasz Kozakiewicz Date: Fri, 11 May 2018 17:57:26 +0200 Subject: [PATCH] Add video playback support for Android and iOS. Update VideoComponent API to support building custom UIs. --- .../Builds/Android/app/CMakeLists.txt | 2 + .../java/com/juce/demorunner/DemoRunner.java | 192 +- .../VisualStudio2013/DemoRunner_App.vcxproj | 1 + .../DemoRunner_App.vcxproj.filters | 3 + .../VisualStudio2015/DemoRunner_App.vcxproj | 1 + .../DemoRunner_App.vcxproj.filters | 3 + .../VisualStudio2017/DemoRunner_App.vcxproj | 1 + .../DemoRunner_App.vcxproj.filters | 3 + .../DemoRunner/JuceLibraryCode/AppConfig.h | 4 + .../DemoRunner/Source/Demos/DemoPIPs2.cpp | 4 +- examples/GUI/DialogsDemo.h | 12 + examples/GUI/VideoDemo.h | 470 +++- .../AudioPerformanceTest.java | 192 +- .../Builds/Android/app/CMakeLists.txt | 2 + .../roli/juce/pluginhost/AudioPluginHost.java | 192 +- .../AudioPluginHost_App.vcxproj | 1 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../AudioPluginHost_App.vcxproj | 1 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../AudioPluginHost_App.vcxproj | 1 + .../AudioPluginHost_App.vcxproj.filters | 3 + .../JuceLibraryCode/AppConfig.h | 4 + .../JUCENetworkGraphicsDemo.java | 44 +- .../jucer_ProjectExport_Android.h | 38 +- .../UnitTestRunner_ConsoleApp.vcxproj | 1 + .../UnitTestRunner_ConsoleApp.vcxproj.filters | 3 + .../JuceLibraryCode/AppConfig.h | 4 + .../WindowsDLL_StaticLibrary.vcxproj | 1 + .../WindowsDLL_StaticLibrary.vcxproj.filters | 3 + extras/WindowsDLL/JuceLibraryCode/AppConfig.h | 4 + .../juce_core/native/java/AndroidVideo.java | 146 ++ .../native/java/JuceAppActivity.java | 68 +- .../juce_core/native/juce_android_Files.cpp | 14 + .../native/juce_android_JNIHelpers.h | 152 +- .../native/juce_android_SystemStats.cpp | 29 + .../native/juce_android_ContentSharer.cpp | 28 +- .../native/juce_android_PushNotifications.cpp | 19 +- .../juce_opengl/native/juce_OpenGL_android.h | 3 +- modules/juce_video/juce_video.h | 19 + .../native/juce_android_CameraDevice.h | 102 +- .../juce_video/native/juce_android_Video.h | 1918 +++++++++++++++++ modules/juce_video/native/juce_mac_Video.h | 712 +++++- modules/juce_video/native/juce_win32_Video.h | 79 +- .../playback/juce_VideoComponent.cpp | 42 +- .../juce_video/playback/juce_VideoComponent.h | 74 +- 45 files changed, 4293 insertions(+), 308 deletions(-) create mode 100644 modules/juce_core/native/java/AndroidVideo.java create mode 100644 modules/juce_video/native/juce_android_Video.h diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index e6ad817f02..57fdd259dd 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -1467,6 +1467,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" "../../../../../modules/juce_video/capture/juce_CameraDevice.h" "../../../../../modules/juce_video/native/juce_android_CameraDevice.h" + "../../../../../modules/juce_video/native/juce_android_Video.h" "../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_Video.h" @@ -2932,6 +2933,7 @@ set_source_files_properties("../../../../../modules/juce_product_unlocking/juce_ set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_android_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_video/native/juce_android_Video.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_Video.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/examples/DemoRunner/Builds/Android/app/src/main/java/com/juce/demorunner/DemoRunner.java b/examples/DemoRunner/Builds/Android/app/src/main/java/com/juce/demorunner/DemoRunner.java index 6320a603db..dee7729cb2 100644 --- a/examples/DemoRunner/Builds/Android/app/src/main/java/com/juce/demorunner/DemoRunner.java +++ b/examples/DemoRunner/Builds/Android/app/src/main/java/com/juce/demorunner/DemoRunner.java @@ -31,6 +31,9 @@ import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.hardware.camera2.*; +import android.database.ContentObserver; +import android.media.session.*; +import android.media.MediaMetadata; import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -94,8 +97,11 @@ public class DemoRunner extends Activity //============================================================================== public boolean isPermissionDeclaredInManifest (int permissionID) { - String permissionToCheck = getAndroidPermissionName(permissionID); + return isPermissionDeclaredInManifest (getAndroidPermissionName (permissionID)); + } + public boolean isPermissionDeclaredInManifest (String permissionToCheck) + { try { PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); @@ -1997,11 +2003,13 @@ public class DemoRunner extends Activity implements SurfaceHolder.Callback { private long nativeContext = 0; + private boolean forVideo; - NativeSurfaceView (Context context, long nativeContextPtr) + NativeSurfaceView (Context context, long nativeContextPtr, boolean createdForVideo) { super (context); nativeContext = nativeContextPtr; + forVideo = createdForVideo; } public Surface getNativeSurface() @@ -2019,38 +2027,51 @@ public class DemoRunner extends Activity @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - surfaceChangedNative (nativeContext, holder, format, width, height); + if (forVideo) + surfaceChangedNativeVideo (nativeContext, holder, format, width, height); + else + surfaceChangedNative (nativeContext, holder, format, width, height); } @Override public void surfaceCreated (SurfaceHolder holder) { - surfaceCreatedNative (nativeContext, holder); + if (forVideo) + surfaceCreatedNativeVideo (nativeContext, holder); + else + surfaceCreatedNative (nativeContext, holder); } @Override public void surfaceDestroyed (SurfaceHolder holder) { - surfaceDestroyedNative (nativeContext, holder); + if (forVideo) + surfaceDestroyedNativeVideo (nativeContext, holder); + else + surfaceDestroyedNative (nativeContext, holder); } @Override protected void dispatchDraw (Canvas canvas) { super.dispatchDraw (canvas); - dispatchDrawNative (nativeContext, canvas); + + if (forVideo) + dispatchDrawNativeVideo (nativeContext, canvas); + else + dispatchDrawNative (nativeContext, canvas); } //============================================================================== @Override - protected void onAttachedToWindow () + protected void onAttachedToWindow() { super.onAttachedToWindow(); getHolder().addCallback (this); } @Override - protected void onDetachedFromWindow () + protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getHolder().removeCallback (this); @@ -2062,11 +2083,17 @@ public class DemoRunner extends Activity private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder, int format, int width, int height); + + private native void dispatchDrawNativeVideo (long nativeContextPtr, Canvas canvas); + private native void surfaceCreatedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceDestroyedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceChangedNativeVideo (long nativeContextptr, SurfaceHolder holder, + int format, int width, int height); } - public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr) + public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr, boolean forVideo) { - return new NativeSurfaceView (this, nativeSurfacePtr); + return new NativeSurfaceView (this, nativeSurfacePtr, forVideo); } //============================================================================== @@ -2826,6 +2853,151 @@ public class DemoRunner extends Activity } + //============================================================================== + public class MediaControllerCallback extends MediaController.Callback + { + private native void mediaControllerAudioInfoChanged (long host, MediaController.PlaybackInfo info); + private native void mediaControllerMetadataChanged (long host, MediaMetadata metadata); + private native void mediaControllerPlaybackStateChanged (long host, PlaybackState state); + private native void mediaControllerSessionDestroyed (long host); + + MediaControllerCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onAudioInfoChanged (MediaController.PlaybackInfo info) + { + mediaControllerAudioInfoChanged (host, info); + } + + @Override + public void onMetadataChanged (MediaMetadata metadata) + { + mediaControllerMetadataChanged (host, metadata); + } + + @Override + public void onPlaybackStateChanged (PlaybackState state) + { + mediaControllerPlaybackStateChanged (host, state); + } + + @Override + public void onQueueChanged (List queue) {} + + @Override + public void onSessionDestroyed() + { + mediaControllerSessionDestroyed (host); + } + + private long host; + } + + //============================================================================== + public class MediaSessionCallback extends MediaSession.Callback + { + private native void mediaSessionPause (long host); + private native void mediaSessionPlay (long host); + private native void mediaSessionPlayFromMediaId (long host, String mediaId, Bundle extras); + private native void mediaSessionSeekTo (long host, long pos); + private native void mediaSessionStop (long host); + + + MediaSessionCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onPause() + { + mediaSessionPause (host); + } + + @Override + public void onPlay() + { + mediaSessionPlay (host); + } + + @Override + public void onPlayFromMediaId (String mediaId, Bundle extras) + { + mediaSessionPlayFromMediaId (host, mediaId, extras); + } + + @Override + public void onSeekTo (long pos) + { + mediaSessionSeekTo (host, pos); + } + + @Override + public void onStop() + { + mediaSessionStop (host); + } + + @Override + public void onFastForward() {} + + @Override + public boolean onMediaButtonEvent (Intent mediaButtonIntent) + { + return true; + } + + @Override + public void onRewind() {} + + @Override + public void onSkipToNext() {} + + @Override + public void onSkipToPrevious() {} + + @Override + public void onSkipToQueueItem (long id) {} + + private long host; + } + + //============================================================================== + public class SystemVolumeObserver extends ContentObserver + { + private native void mediaSessionSystemVolumeChanged (long host); + + SystemVolumeObserver (Activity activityToUse, long hostToUse) + { + super (null); + + activity = activityToUse; + host = hostToUse; + } + + void setEnabled (boolean shouldBeEnabled) + { + if (shouldBeEnabled) + activity.getApplicationContext().getContentResolver().registerContentObserver (android.provider.Settings.System.CONTENT_URI, true, this); + else + activity.getApplicationContext().getContentResolver().unregisterContentObserver (this); + } + + @Override + public void onChange (boolean selfChange, Uri uri) + { + if (uri.toString().startsWith ("content://settings/system/volume_music")) + mediaSessionSystemVolumeChanged (host); + } + + private Activity activity; + private long host; + } + + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj index 2441e18c7c..f004cd121c 100644 --- a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj @@ -2840,6 +2840,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters index b1ec6a7918..04adb13e15 100644 --- a/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2013/DemoRunner_App.vcxproj.filters @@ -4827,6 +4827,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj index b96b1827b2..a9d23c0699 100644 --- a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj @@ -2840,6 +2840,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters index f2d69a6984..98462b557d 100644 --- a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters @@ -4827,6 +4827,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj index f5d8f06569..dabbf0de7b 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj @@ -2840,6 +2840,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters index 00170e0947..1f0deb70e9 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters @@ -4827,6 +4827,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/examples/DemoRunner/JuceLibraryCode/AppConfig.h b/examples/DemoRunner/JuceLibraryCode/AppConfig.h index 8312cc5afd..6d198a800c 100644 --- a/examples/DemoRunner/JuceLibraryCode/AppConfig.h +++ b/examples/DemoRunner/JuceLibraryCode/AppConfig.h @@ -291,6 +291,10 @@ #ifndef JUCE_USE_CAMERA #define JUCE_USE_CAMERA 1 #endif + +#ifndef JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + //#define JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME 1 +#endif //============================================================================== #ifndef JUCE_STANDALONE_APPLICATION #if defined(JucePlugin_Name) && defined(JucePlugin_Build_Standalone) diff --git a/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp b/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp index 98ed93834e..51074e1301 100644 --- a/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp +++ b/examples/DemoRunner/Source/Demos/DemoPIPs2.cpp @@ -61,7 +61,7 @@ #include "../../../GUI/OpenGLDemo2D.h" #endif #include "../../../GUI/PropertiesDemo.h" -#if JUCE_MAC || JUCE_WINDOWS +#if ! JUCE_LINUX #include "../../../GUI/VideoDemo.h" #endif #include "../../../GUI/WebBrowserDemo.h" @@ -100,7 +100,7 @@ void registerDemos_Two() noexcept REGISTER_DEMO_WITH_FILENAME (OpenGLDemoClasses::OpenGLDemo, GUI, OpenGLDemo, true) #endif REGISTER_DEMO (PropertiesDemo, GUI, false) - #if JUCE_MAC || JUCE_WINDOWS + #if ! JUCE_LINUX REGISTER_DEMO (VideoDemo, GUI, true) #endif REGISTER_DEMO (WebBrowserDemo, GUI, true) diff --git a/examples/GUI/DialogsDemo.h b/examples/GUI/DialogsDemo.h index 1c27591f27..91de0b68b6 100644 --- a/examples/GUI/DialogsDemo.h +++ b/examples/GUI/DialogsDemo.h @@ -159,6 +159,18 @@ public: } setSize (500, 500); + + RuntimePermissions::request (RuntimePermissions::readExternalStorage, + [] (bool granted) + { + if (! granted) + { + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Permissions warning", + "External storage access permission not granted, some files" + " may be inaccessible."); + } + }); } //============================================================================== diff --git a/examples/GUI/VideoDemo.h b/examples/GUI/VideoDemo.h index a05e4c9c84..5b1d9fed5d 100644 --- a/examples/GUI/VideoDemo.h +++ b/examples/GUI/VideoDemo.h @@ -46,6 +46,7 @@ #include "../Assets/DemoUtilities.h" +#if JUCE_MAC || JUCE_WINDOWS //============================================================================== // so that we can easily have two video windows each with a file browser, wrap this up as a class.. class MovieComponentWithFileBrowser : public Component, @@ -54,6 +55,7 @@ class MovieComponentWithFileBrowser : public Component, { public: MovieComponentWithFileBrowser() + : videoComp (true) { addAndMakeVisible (videoComp); @@ -110,8 +112,16 @@ private: void filenameComponentChanged (FilenameComponent*) override { + auto url = URL (fileChooser.getCurrentFile()); + // this is called when the user changes the filename in the file chooser box - auto result = videoComp.load (fileChooser.getCurrentFile()); + auto result = videoComp.load (url); + videoLoadingFinished (url, result); + } + + void videoLoadingFinished (const URL& url, Result result) + { + ignoreUnused (url); if (result.wasOk()) { @@ -209,6 +219,7 @@ public: } private: + std::unique_ptr fileChooser; WildcardFileFilter moviesWildcardFilter { "*", "*", "Movies File Filter" }; TimeSliceThread directoryThread { "Movie File Scanner Thread" }; DirectoryContentsList movieList { &moviesWildcardFilter, directoryThread }; @@ -231,5 +242,462 @@ private: void fileDoubleClicked (const File&) override {} void browserRootChanged (const File&) override {} + void selectVideoFile() + { + fileChooser.reset (new FileChooser ("Choose a file to open...", File::getCurrentWorkingDirectory(), + "*", false)); + + fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, + [this] (const FileChooser& chooser) + { + String chosen; + auto results = chooser.getURLResults(); + + // TODO: support non local files too + if (results.size() > 0) + movieCompLeft.setFile (results[0].getLocalFile()); + }); + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VideoDemo) }; +#elif JUCE_IOS || JUCE_ANDROID +//============================================================================== +class VideoDemo : public Component, + private Timer +{ +public: + VideoDemo() + : videoCompWithNativeControls (true), + videoCompNoNativeControls (false) + { + loadLocalButton .onClick = [this] { selectVideoFile(); }; + loadUrlButton .onClick = [this] { showVideoUrlPrompt(); }; + seekToStartButton.onClick = [this] { seekVideoToStart(); }; + playButton .onClick = [this] { playVideo(); }; + pauseButton .onClick = [this] { pauseVideo(); }; + unloadButton .onClick = [this] { unloadVideoFile(); }; + + volumeLabel .setColour (Label::textColourId, Colours::white); + currentPositionLabel.setColour (Label::textColourId, Colours::white); + + volumeLabel .setJustificationType (Justification::right); + currentPositionLabel.setJustificationType (Justification::right); + + volumeSlider .setRange (0.0, 1.0); + positionSlider.setRange (0.0, 1.0); + + volumeSlider .setSliderSnapsToMousePosition (false); + positionSlider.setSliderSnapsToMousePosition (false); + + volumeSlider.setSkewFactor (1.5); + volumeSlider.setValue (1.0, dontSendNotification); + #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + curVideoComp->onGlobalMediaVolumeChanged = [this]() { volumeSlider.setValue (curVideoComp->getAudioVolume(), dontSendNotification); }; + #endif + + volumeSlider .onValueChange = [this]() { curVideoComp->setAudioVolume ((float) volumeSlider.getValue()); }; + positionSlider.onValueChange = [this]() { seekVideoToNormalisedPosition (positionSlider.getValue()); }; + + positionSlider.onDragStart = [this]() + { + positionSliderDragging = true; + wasPlayingBeforeDragStart = curVideoComp->isPlaying(); + + if (wasPlayingBeforeDragStart) + curVideoComp->stop(); + }; + + positionSlider.onDragEnd = [this]() + { + if (wasPlayingBeforeDragStart) + curVideoComp->play(); + + wasPlayingBeforeDragStart = false; + + // Ensure the slider does not temporarily jump back on consecutive timer callback. + Timer::callAfterDelay (500, [this]() { positionSliderDragging = false; }); + }; + + playSpeedComboBox.addItem ("25%", 25); + playSpeedComboBox.addItem ("50%", 50); + playSpeedComboBox.addItem ("100%", 100); + playSpeedComboBox.addItem ("200%", 200); + playSpeedComboBox.addItem ("400%", 400); + playSpeedComboBox.setSelectedId (100, dontSendNotification); + playSpeedComboBox.onChange = [this]() { curVideoComp->setPlaySpeed (playSpeedComboBox.getSelectedId() / 100.0); }; + + setTransportControlsEnabled (false); + + addAndMakeVisible (loadLocalButton); + addAndMakeVisible (loadUrlButton); + addAndMakeVisible (volumeLabel); + addAndMakeVisible (volumeSlider); + addChildComponent (videoCompWithNativeControls); + addChildComponent (videoCompNoNativeControls); + addAndMakeVisible (positionSlider); + addAndMakeVisible (currentPositionLabel); + + addAndMakeVisible (playSpeedComboBox); + addAndMakeVisible (seekToStartButton); + addAndMakeVisible (playButton); + addAndMakeVisible (unloadButton); + addChildComponent (pauseButton); + + setSize (500, 500); + + RuntimePermissions::request (RuntimePermissions::readExternalStorage, + [] (bool granted) + { + if (! granted) + { + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Permissions warning", + "External storage access permission not granted, some files" + " may be inaccessible."); + } + }); + + setPortraitOrientationEnabled (true); + } + + ~VideoDemo() + { + curVideoComp->onPlaybackStarted = nullptr; + curVideoComp->onPlaybackStopped = nullptr; + curVideoComp->onErrorOccurred = nullptr; + curVideoComp->onGlobalMediaVolumeChanged = nullptr; + + setPortraitOrientationEnabled (false); + } + + void paint (Graphics& g) override + { + g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); + } + + void resized() override + { + auto area = getLocalBounds(); + + int marginSize = 5; + int buttonHeight = 20; + + area.reduce (0, marginSize); + + auto topArea = area.removeFromTop (buttonHeight); + loadLocalButton.setBounds (topArea.removeFromLeft (topArea.getWidth() / 6)); + loadUrlButton.setBounds (topArea.removeFromLeft (loadLocalButton.getWidth())); + volumeLabel.setBounds (topArea.removeFromLeft (loadLocalButton.getWidth())); + volumeSlider.setBounds (topArea.reduced (10, 0)); + + auto transportArea = area.removeFromBottom (buttonHeight); + auto positionArea = area.removeFromBottom (buttonHeight).reduced (marginSize, 0); + + playSpeedComboBox.setBounds (transportArea.removeFromLeft (jmax (50, transportArea.getWidth() / 5))); + + auto controlWidth = transportArea.getWidth() / 3; + + currentPositionLabel.setBounds (positionArea.removeFromRight (jmax (150, controlWidth))); + positionSlider.setBounds (positionArea); + + seekToStartButton.setBounds (transportArea.removeFromLeft (controlWidth)); + playButton .setBounds (transportArea.removeFromLeft (controlWidth)); + unloadButton .setBounds (transportArea.removeFromLeft (controlWidth)); + pauseButton.setBounds (playButton.getBounds()); + + area.removeFromTop (marginSize); + area.removeFromBottom (marginSize); + + videoCompWithNativeControls.setBounds (area); + videoCompNoNativeControls.setBounds (area); + + if (positionSlider.getWidth() > 0) + positionSlider.setMouseDragSensitivity (positionSlider.getWidth()); + } + +private: + TextButton loadLocalButton { "Load Local" }; + TextButton loadUrlButton { "Load URL" }; + Label volumeLabel { "volumeLabel", "Vol:" }; + Slider volumeSlider { Slider::LinearHorizontal, Slider::NoTextBox }; + + VideoComponent videoCompWithNativeControls; + VideoComponent videoCompNoNativeControls; + #if JUCE_IOS || JUCE_MAC + VideoComponent* curVideoComp = &videoCompWithNativeControls; + #else + VideoComponent* curVideoComp = &videoCompNoNativeControls; + #endif + bool isFirstSetup = true; + + Slider positionSlider { Slider::LinearHorizontal, Slider::NoTextBox }; + bool positionSliderDragging = false; + bool wasPlayingBeforeDragStart = false; + + Label currentPositionLabel { "currentPositionLabel", "-:- / -:-" }; + + ComboBox playSpeedComboBox { "playSpeedComboBox" }; + TextButton seekToStartButton { "|<" }; + TextButton playButton { "Play" }; + TextButton pauseButton { "Pause" }; + TextButton unloadButton { "Unload" }; + + std::unique_ptr fileChooser; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VideoDemo) + JUCE_DECLARE_WEAK_REFERENCEABLE (VideoDemo) + + //============================================================================== + void setPortraitOrientationEnabled (bool shouldBeEnabled) + { + auto allowedOrientations = Desktop::getInstance().getOrientationsEnabled(); + + if (shouldBeEnabled) + allowedOrientations |= Desktop::upright; + else + allowedOrientations &= ~Desktop::upright; + + Desktop::getInstance().setOrientationsEnabled (allowedOrientations); + } + + void setTransportControlsEnabled (bool shouldBeEnabled) + { + positionSlider .setEnabled (shouldBeEnabled); + playSpeedComboBox.setEnabled (shouldBeEnabled); + seekToStartButton.setEnabled (shouldBeEnabled); + playButton .setEnabled (shouldBeEnabled); + unloadButton .setEnabled (shouldBeEnabled); + pauseButton .setEnabled (shouldBeEnabled); + } + + void selectVideoFile() + { + fileChooser.reset (new FileChooser ("Choose a video file to open...", File::getCurrentWorkingDirectory(), + "*", true)); + + fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, + [this] (const FileChooser& chooser) + { + auto results = chooser.getURLResults(); + + if (results.size() > 0) + loadVideo (results[0]); + }); + } + + void loadVideo (const URL& url) + { + unloadVideoFile(); + + #if JUCE_IOS || JUCE_MAC + askIfUseNativeControls (url); + #else + loadUrl (url); + setupVideoComp (false); + #endif + } + + void askIfUseNativeControls (const URL& url) + { + auto* aw = new AlertWindow ("Choose viewer type", {}, AlertWindow::NoIcon); + + aw->addButton ("Yes", 1, KeyPress (KeyPress::returnKey)); + aw->addButton ("No", 0, KeyPress (KeyPress::escapeKey)); + aw->addTextBlock ("Do you want to use the viewer with native controls?"); + + auto callback = ModalCallbackFunction::forComponent (videoViewerTypeChosen, this, url); + aw->enterModalState (true, callback, true); + } + + static void videoViewerTypeChosen (int result, VideoDemo* owner, URL url) + { + if (owner != nullptr) + { + owner->setupVideoComp (result != 0); + owner->loadUrl (url); + } + } + + void setupVideoComp (bool useNativeViewerWithNativeControls) + { + auto* oldVideoComp = curVideoComp; + + if (useNativeViewerWithNativeControls) + curVideoComp = &videoCompWithNativeControls; + else + curVideoComp = &videoCompNoNativeControls; + + if (isFirstSetup || oldVideoComp != curVideoComp) + { + oldVideoComp->onPlaybackStarted = nullptr; + oldVideoComp->onPlaybackStopped = nullptr; + oldVideoComp->onErrorOccurred = nullptr; + oldVideoComp->setVisible (false); + + curVideoComp->onPlaybackStarted = [this]() { processPlaybackStarted(); }; + curVideoComp->onPlaybackStopped = [this]() { processPlaybackPaused(); }; + curVideoComp->onErrorOccurred = [this](const String& errorMessage) { errorOccurred (errorMessage); }; + curVideoComp->setVisible (true); + + #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + oldVideoComp->onGlobalMediaVolumeChanged = nullptr; + curVideoComp->onGlobalMediaVolumeChanged = [this]() { volumeSlider.setValue (curVideoComp->getAudioVolume(), dontSendNotification); }; + #endif + } + + isFirstSetup = false; + } + + void loadUrl (const URL& url) + { + curVideoComp->loadAsync (url, [this] (const URL& u, Result r) { videoLoadingFinished (u, r); }); + } + + void showVideoUrlPrompt() + { + auto* aw = new AlertWindow ("Enter URL for video to load", {}, AlertWindow::NoIcon); + + aw->addButton ("OK", 1, KeyPress (KeyPress::returnKey)); + aw->addButton ("Cancel", 0, KeyPress (KeyPress::escapeKey)); + aw->addTextEditor ("videoUrlTextEditor", "https://www.rmp-streaming.com/media/bbb-360p.mp4"); + + auto callback = ModalCallbackFunction::forComponent (videoUrlPromptClosed, this, Component::SafePointer (aw)); + aw->enterModalState (true, callback, true); + } + + static void videoUrlPromptClosed (int result, VideoDemo* owner, Component::SafePointer aw) + { + if (result != 0 && owner != nullptr && aw != nullptr) + { + auto url = aw->getTextEditorContents ("videoUrlTextEditor"); + + if (url.isNotEmpty()) + owner->loadVideo (url); + } + } + + void videoLoadingFinished (const URL& url, Result result) + { + ignoreUnused (url); + + if (result.wasOk()) + { + resized(); // update to reflect the video's aspect ratio + + setTransportControlsEnabled (true); + + currentPositionLabel.setText (getPositionString (0.0, curVideoComp->getVideoDuration()), sendNotification); + positionSlider.setValue (0.0, dontSendNotification); + playSpeedComboBox.setSelectedId (100, dontSendNotification); + } + else + { + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Couldn't load the file!", + result.getErrorMessage()); + } + } + + static String getPositionString (double playPositionSeconds, double durationSeconds) + { + auto positionMs = static_cast (1000 * playPositionSeconds); + int posMinutes = positionMs / 60000; + int posSeconds = (positionMs % 60000) / 1000; + int posMillis = positionMs % 1000; + + auto totalMs = static_cast (1000 * durationSeconds); + int totMinutes = totalMs / 60000; + int totSeconds = (totalMs % 60000) / 1000; + int totMillis = totalMs % 1000; + + return String::formatted ("%02d:%02d:%03d / %02d:%02d:%03d", + posMinutes, posSeconds, posMillis, + totMinutes, totSeconds, totMillis); + } + + void updatePositionSliderAndLabel() + { + auto position = curVideoComp->getPlayPosition(); + auto duration = curVideoComp->getVideoDuration(); + + currentPositionLabel.setText (getPositionString (position, duration), sendNotification); + + if (! positionSliderDragging) + positionSlider.setValue (duration != 0 ? (position / duration) : 0.0, dontSendNotification); + } + + void seekVideoToStart() + { + seekVideoToNormalisedPosition (0.0); + } + + void seekVideoToNormalisedPosition (double normalisedPos) + { + normalisedPos = jlimit (0.0, 1.0, normalisedPos); + + auto duration = curVideoComp->getVideoDuration(); + auto newPos = jlimit (0.0, duration, duration * normalisedPos); + + curVideoComp->setPlayPosition (newPos); + currentPositionLabel.setText (getPositionString (newPos, curVideoComp->getVideoDuration()), sendNotification); + positionSlider.setValue (normalisedPos, dontSendNotification); + } + + void playVideo() + { + curVideoComp->play(); + } + + void processPlaybackStarted() + { + playButton.setVisible (false); + pauseButton.setVisible (true); + + startTimer (20); + } + + void pauseVideo() + { + curVideoComp->stop(); + } + + void processPlaybackPaused() + { + // On seeking to a new pos, the playback may be temporarily paused. + if (positionSliderDragging) + return; + + pauseButton.setVisible (false); + playButton.setVisible (true); + } + + void errorOccurred (const String& errorMessage) + { + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "An error has occurred", + errorMessage + ", video will be unloaded."); + + unloadVideoFile(); + } + + void unloadVideoFile() + { + curVideoComp->closeVideo(); + + setTransportControlsEnabled (false); + stopTimer(); + + pauseButton.setVisible (false); + playButton.setVisible (true); + + currentPositionLabel.setText ("-:- / -:-", sendNotification); + positionSlider.setValue (0.0, dontSendNotification); + } + + void timerCallback() override + { + updatePositionSliderAndLabel(); + } +}; +#endif diff --git a/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java b/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java index 2ca9c6d929..18ee0a2eec 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java +++ b/extras/AudioPerformanceTest/Builds/Android/app/src/main/java/com/juce/audioperformancetest/AudioPerformanceTest.java @@ -31,6 +31,9 @@ import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.hardware.camera2.*; +import android.database.ContentObserver; +import android.media.session.*; +import android.media.MediaMetadata; import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -94,8 +97,11 @@ public class AudioPerformanceTest extends Activity //============================================================================== public boolean isPermissionDeclaredInManifest (int permissionID) { - String permissionToCheck = getAndroidPermissionName(permissionID); + return isPermissionDeclaredInManifest (getAndroidPermissionName (permissionID)); + } + public boolean isPermissionDeclaredInManifest (String permissionToCheck) + { try { PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); @@ -1997,11 +2003,13 @@ public class AudioPerformanceTest extends Activity implements SurfaceHolder.Callback { private long nativeContext = 0; + private boolean forVideo; - NativeSurfaceView (Context context, long nativeContextPtr) + NativeSurfaceView (Context context, long nativeContextPtr, boolean createdForVideo) { super (context); nativeContext = nativeContextPtr; + forVideo = createdForVideo; } public Surface getNativeSurface() @@ -2019,38 +2027,51 @@ public class AudioPerformanceTest extends Activity @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - surfaceChangedNative (nativeContext, holder, format, width, height); + if (forVideo) + surfaceChangedNativeVideo (nativeContext, holder, format, width, height); + else + surfaceChangedNative (nativeContext, holder, format, width, height); } @Override public void surfaceCreated (SurfaceHolder holder) { - surfaceCreatedNative (nativeContext, holder); + if (forVideo) + surfaceCreatedNativeVideo (nativeContext, holder); + else + surfaceCreatedNative (nativeContext, holder); } @Override public void surfaceDestroyed (SurfaceHolder holder) { - surfaceDestroyedNative (nativeContext, holder); + if (forVideo) + surfaceDestroyedNativeVideo (nativeContext, holder); + else + surfaceDestroyedNative (nativeContext, holder); } @Override protected void dispatchDraw (Canvas canvas) { super.dispatchDraw (canvas); - dispatchDrawNative (nativeContext, canvas); + + if (forVideo) + dispatchDrawNativeVideo (nativeContext, canvas); + else + dispatchDrawNative (nativeContext, canvas); } //============================================================================== @Override - protected void onAttachedToWindow () + protected void onAttachedToWindow() { super.onAttachedToWindow(); getHolder().addCallback (this); } @Override - protected void onDetachedFromWindow () + protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getHolder().removeCallback (this); @@ -2062,11 +2083,17 @@ public class AudioPerformanceTest extends Activity private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder, int format, int width, int height); + + private native void dispatchDrawNativeVideo (long nativeContextPtr, Canvas canvas); + private native void surfaceCreatedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceDestroyedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceChangedNativeVideo (long nativeContextptr, SurfaceHolder holder, + int format, int width, int height); } - public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr) + public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr, boolean forVideo) { - return new NativeSurfaceView (this, nativeSurfacePtr); + return new NativeSurfaceView (this, nativeSurfacePtr, forVideo); } //============================================================================== @@ -2826,6 +2853,151 @@ public class AudioPerformanceTest extends Activity } + //============================================================================== + public class MediaControllerCallback extends MediaController.Callback + { + private native void mediaControllerAudioInfoChanged (long host, MediaController.PlaybackInfo info); + private native void mediaControllerMetadataChanged (long host, MediaMetadata metadata); + private native void mediaControllerPlaybackStateChanged (long host, PlaybackState state); + private native void mediaControllerSessionDestroyed (long host); + + MediaControllerCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onAudioInfoChanged (MediaController.PlaybackInfo info) + { + mediaControllerAudioInfoChanged (host, info); + } + + @Override + public void onMetadataChanged (MediaMetadata metadata) + { + mediaControllerMetadataChanged (host, metadata); + } + + @Override + public void onPlaybackStateChanged (PlaybackState state) + { + mediaControllerPlaybackStateChanged (host, state); + } + + @Override + public void onQueueChanged (List queue) {} + + @Override + public void onSessionDestroyed() + { + mediaControllerSessionDestroyed (host); + } + + private long host; + } + + //============================================================================== + public class MediaSessionCallback extends MediaSession.Callback + { + private native void mediaSessionPause (long host); + private native void mediaSessionPlay (long host); + private native void mediaSessionPlayFromMediaId (long host, String mediaId, Bundle extras); + private native void mediaSessionSeekTo (long host, long pos); + private native void mediaSessionStop (long host); + + + MediaSessionCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onPause() + { + mediaSessionPause (host); + } + + @Override + public void onPlay() + { + mediaSessionPlay (host); + } + + @Override + public void onPlayFromMediaId (String mediaId, Bundle extras) + { + mediaSessionPlayFromMediaId (host, mediaId, extras); + } + + @Override + public void onSeekTo (long pos) + { + mediaSessionSeekTo (host, pos); + } + + @Override + public void onStop() + { + mediaSessionStop (host); + } + + @Override + public void onFastForward() {} + + @Override + public boolean onMediaButtonEvent (Intent mediaButtonIntent) + { + return true; + } + + @Override + public void onRewind() {} + + @Override + public void onSkipToNext() {} + + @Override + public void onSkipToPrevious() {} + + @Override + public void onSkipToQueueItem (long id) {} + + private long host; + } + + //============================================================================== + public class SystemVolumeObserver extends ContentObserver + { + private native void mediaSessionSystemVolumeChanged (long host); + + SystemVolumeObserver (Activity activityToUse, long hostToUse) + { + super (null); + + activity = activityToUse; + host = hostToUse; + } + + void setEnabled (boolean shouldBeEnabled) + { + if (shouldBeEnabled) + activity.getApplicationContext().getContentResolver().registerContentObserver (android.provider.Settings.System.CONTENT_URI, true, this); + else + activity.getApplicationContext().getContentResolver().unregisterContentObserver (this); + } + + @Override + public void onChange (boolean selfChange, Uri uri) + { + if (uri.toString().startsWith ("content://settings/system/volume_music")) + mediaSessionSystemVolumeChanged (host); + } + + private Activity activity; + private long host; + } + + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index 3b05e76a11..bfa191b627 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -1251,6 +1251,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" "../../../../../modules/juce_video/capture/juce_CameraDevice.h" "../../../../../modules/juce_video/native/juce_android_CameraDevice.h" + "../../../../../modules/juce_video/native/juce_android_Video.h" "../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" "../../../../../modules/juce_video/native/juce_mac_Video.h" @@ -2491,6 +2492,7 @@ set_source_files_properties("../../../../../modules/juce_opengl/juce_opengl.h" P set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/capture/juce_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_android_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_video/native/juce_android_Video.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_ios_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_CameraDevice.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_video/native/juce_mac_Video.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java b/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java index 2b62f5a596..f9fe65e908 100644 --- a/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java +++ b/extras/AudioPluginHost/Builds/Android/app/src/main/java/com/roli/juce/pluginhost/AudioPluginHost.java @@ -31,6 +31,9 @@ import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.hardware.camera2.*; +import android.database.ContentObserver; +import android.media.session.*; +import android.media.MediaMetadata; import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -94,8 +97,11 @@ public class AudioPluginHost extends Activity //============================================================================== public boolean isPermissionDeclaredInManifest (int permissionID) { - String permissionToCheck = getAndroidPermissionName(permissionID); + return isPermissionDeclaredInManifest (getAndroidPermissionName (permissionID)); + } + public boolean isPermissionDeclaredInManifest (String permissionToCheck) + { try { PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); @@ -1997,11 +2003,13 @@ public class AudioPluginHost extends Activity implements SurfaceHolder.Callback { private long nativeContext = 0; + private boolean forVideo; - NativeSurfaceView (Context context, long nativeContextPtr) + NativeSurfaceView (Context context, long nativeContextPtr, boolean createdForVideo) { super (context); nativeContext = nativeContextPtr; + forVideo = createdForVideo; } public Surface getNativeSurface() @@ -2019,38 +2027,51 @@ public class AudioPluginHost extends Activity @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - surfaceChangedNative (nativeContext, holder, format, width, height); + if (forVideo) + surfaceChangedNativeVideo (nativeContext, holder, format, width, height); + else + surfaceChangedNative (nativeContext, holder, format, width, height); } @Override public void surfaceCreated (SurfaceHolder holder) { - surfaceCreatedNative (nativeContext, holder); + if (forVideo) + surfaceCreatedNativeVideo (nativeContext, holder); + else + surfaceCreatedNative (nativeContext, holder); } @Override public void surfaceDestroyed (SurfaceHolder holder) { - surfaceDestroyedNative (nativeContext, holder); + if (forVideo) + surfaceDestroyedNativeVideo (nativeContext, holder); + else + surfaceDestroyedNative (nativeContext, holder); } @Override protected void dispatchDraw (Canvas canvas) { super.dispatchDraw (canvas); - dispatchDrawNative (nativeContext, canvas); + + if (forVideo) + dispatchDrawNativeVideo (nativeContext, canvas); + else + dispatchDrawNative (nativeContext, canvas); } //============================================================================== @Override - protected void onAttachedToWindow () + protected void onAttachedToWindow() { super.onAttachedToWindow(); getHolder().addCallback (this); } @Override - protected void onDetachedFromWindow () + protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getHolder().removeCallback (this); @@ -2062,11 +2083,17 @@ public class AudioPluginHost extends Activity private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder, int format, int width, int height); + + private native void dispatchDrawNativeVideo (long nativeContextPtr, Canvas canvas); + private native void surfaceCreatedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceDestroyedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceChangedNativeVideo (long nativeContextptr, SurfaceHolder holder, + int format, int width, int height); } - public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr) + public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr, boolean forVideo) { - return new NativeSurfaceView (this, nativeSurfacePtr); + return new NativeSurfaceView (this, nativeSurfacePtr, forVideo); } //============================================================================== @@ -2826,6 +2853,151 @@ public class AudioPluginHost extends Activity } + //============================================================================== + public class MediaControllerCallback extends MediaController.Callback + { + private native void mediaControllerAudioInfoChanged (long host, MediaController.PlaybackInfo info); + private native void mediaControllerMetadataChanged (long host, MediaMetadata metadata); + private native void mediaControllerPlaybackStateChanged (long host, PlaybackState state); + private native void mediaControllerSessionDestroyed (long host); + + MediaControllerCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onAudioInfoChanged (MediaController.PlaybackInfo info) + { + mediaControllerAudioInfoChanged (host, info); + } + + @Override + public void onMetadataChanged (MediaMetadata metadata) + { + mediaControllerMetadataChanged (host, metadata); + } + + @Override + public void onPlaybackStateChanged (PlaybackState state) + { + mediaControllerPlaybackStateChanged (host, state); + } + + @Override + public void onQueueChanged (List queue) {} + + @Override + public void onSessionDestroyed() + { + mediaControllerSessionDestroyed (host); + } + + private long host; + } + + //============================================================================== + public class MediaSessionCallback extends MediaSession.Callback + { + private native void mediaSessionPause (long host); + private native void mediaSessionPlay (long host); + private native void mediaSessionPlayFromMediaId (long host, String mediaId, Bundle extras); + private native void mediaSessionSeekTo (long host, long pos); + private native void mediaSessionStop (long host); + + + MediaSessionCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onPause() + { + mediaSessionPause (host); + } + + @Override + public void onPlay() + { + mediaSessionPlay (host); + } + + @Override + public void onPlayFromMediaId (String mediaId, Bundle extras) + { + mediaSessionPlayFromMediaId (host, mediaId, extras); + } + + @Override + public void onSeekTo (long pos) + { + mediaSessionSeekTo (host, pos); + } + + @Override + public void onStop() + { + mediaSessionStop (host); + } + + @Override + public void onFastForward() {} + + @Override + public boolean onMediaButtonEvent (Intent mediaButtonIntent) + { + return true; + } + + @Override + public void onRewind() {} + + @Override + public void onSkipToNext() {} + + @Override + public void onSkipToPrevious() {} + + @Override + public void onSkipToQueueItem (long id) {} + + private long host; + } + + //============================================================================== + public class SystemVolumeObserver extends ContentObserver + { + private native void mediaSessionSystemVolumeChanged (long host); + + SystemVolumeObserver (Activity activityToUse, long hostToUse) + { + super (null); + + activity = activityToUse; + host = hostToUse; + } + + void setEnabled (boolean shouldBeEnabled) + { + if (shouldBeEnabled) + activity.getApplicationContext().getContentResolver().registerContentObserver (android.provider.Settings.System.CONTENT_URI, true, this); + else + activity.getApplicationContext().getContentResolver().unregisterContentObserver (this); + } + + @Override + public void onChange (boolean selfChange, Uri uri) + { + if (uri.toString().startsWith ("content://settings/system/volume_music")) + mediaSessionSystemVolumeChanged (host); + } + + private Activity activity; + private long host; + } + + //============================================================================== public static final String getLocaleValue (boolean isRegion) { diff --git a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj index 5af7cd0b7c..9a586ae1e8 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj @@ -2436,6 +2436,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters index d73bb1e334..55370d1444 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2013/AudioPluginHost_App.vcxproj.filters @@ -4065,6 +4065,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj index 64a53f3415..071fbf1ace 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj @@ -2436,6 +2436,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters index 3425cca7a5..cb33a88a63 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters @@ -4065,6 +4065,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj index c8542405c1..2edcb9ef79 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj @@ -2436,6 +2436,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters index 33384a787e..12b15f3ee3 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters @@ -4065,6 +4065,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/AudioPluginHost/JuceLibraryCode/AppConfig.h b/extras/AudioPluginHost/JuceLibraryCode/AppConfig.h index 6b8cbf5efa..66346c23f6 100644 --- a/extras/AudioPluginHost/JuceLibraryCode/AppConfig.h +++ b/extras/AudioPluginHost/JuceLibraryCode/AppConfig.h @@ -264,6 +264,10 @@ #ifndef JUCE_USE_CAMERA #define JUCE_USE_CAMERA 0 #endif + +#ifndef JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + //#define JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME 1 +#endif //============================================================================== #ifndef JUCE_STANDALONE_APPLICATION #if defined(JucePlugin_Name) && defined(JucePlugin_Build_Standalone) diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java b/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java index fb24cf7354..80a25cb959 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/src/main/java/com/juce/networkgraphicsdemo/JUCENetworkGraphicsDemo.java @@ -87,8 +87,11 @@ public class JUCENetworkGraphicsDemo extends Activity //============================================================================== public boolean isPermissionDeclaredInManifest (int permissionID) { - String permissionToCheck = getAndroidPermissionName(permissionID); + return isPermissionDeclaredInManifest (getAndroidPermissionName (permissionID)); + } + public boolean isPermissionDeclaredInManifest (String permissionToCheck) + { try { PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); @@ -1064,11 +1067,13 @@ public class JUCENetworkGraphicsDemo extends Activity implements SurfaceHolder.Callback { private long nativeContext = 0; + private boolean forVideo; - NativeSurfaceView (Context context, long nativeContextPtr) + NativeSurfaceView (Context context, long nativeContextPtr, boolean createdForVideo) { super (context); nativeContext = nativeContextPtr; + forVideo = createdForVideo; } public Surface getNativeSurface() @@ -1086,38 +1091,51 @@ public class JUCENetworkGraphicsDemo extends Activity @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - surfaceChangedNative (nativeContext, holder, format, width, height); + if (forVideo) + surfaceChangedNativeVideo (nativeContext, holder, format, width, height); + else + surfaceChangedNative (nativeContext, holder, format, width, height); } @Override public void surfaceCreated (SurfaceHolder holder) { - surfaceCreatedNative (nativeContext, holder); + if (forVideo) + surfaceCreatedNativeVideo (nativeContext, holder); + else + surfaceCreatedNative (nativeContext, holder); } @Override public void surfaceDestroyed (SurfaceHolder holder) { - surfaceDestroyedNative (nativeContext, holder); + if (forVideo) + surfaceDestroyedNativeVideo (nativeContext, holder); + else + surfaceDestroyedNative (nativeContext, holder); } @Override protected void dispatchDraw (Canvas canvas) { super.dispatchDraw (canvas); - dispatchDrawNative (nativeContext, canvas); + + if (forVideo) + dispatchDrawNativeVideo (nativeContext, canvas); + else + dispatchDrawNative (nativeContext, canvas); } //============================================================================== @Override - protected void onAttachedToWindow () + protected void onAttachedToWindow() { super.onAttachedToWindow(); getHolder().addCallback (this); } @Override - protected void onDetachedFromWindow () + protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getHolder().removeCallback (this); @@ -1129,11 +1147,17 @@ public class JUCENetworkGraphicsDemo extends Activity private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder, int format, int width, int height); + + private native void dispatchDrawNativeVideo (long nativeContextPtr, Canvas canvas); + private native void surfaceCreatedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceDestroyedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceChangedNativeVideo (long nativeContextptr, SurfaceHolder holder, + int format, int width, int height); } - public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr) + public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr, boolean forVideo) { - return new NativeSurfaceView (this, nativeSurfacePtr); + return new NativeSurfaceView (this, nativeSurfacePtr, forVideo); } //============================================================================== diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h index 4e75af75d4..c25d93b0ba 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h @@ -1050,6 +1050,7 @@ private: auto midiCode = getMidiCode (javaSourceFolder, className); auto webViewCode = getWebViewCode (javaSourceFolder); auto cameraCode = getCameraCode (javaSourceFolder); + auto videoCode = getVideoCode (javaSourceFolder); auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java"); auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString()); @@ -1075,6 +1076,10 @@ private: newFile << cameraCode.imports; else if (line.contains ("$$JuceAndroidCameraCode$$")) newFile << cameraCode.main; + else if (line.contains ("$$JuceAndroidVideoImports$$")) + newFile << videoCode.imports; + else if (line.contains ("$$JuceAndroidVideoCode$$")) + newFile << videoCode.main; else newFile << line.replace ("$$JuceAppActivityBaseClass$$", androidActivityBaseClassName.get().toString()) .replace ("JuceAppActivity", className) @@ -1203,13 +1208,12 @@ private: String juceCameraImports, juceCameraCode; if (static_cast (androidMinimumSDK.get()) >= 21) + { juceCameraImports << "import android.hardware.camera2.*;" << newLine; - auto javaCameraFile = javaSourceFolder.getChildFile ("AndroidCamera.java"); - auto juceCameraCodeAll = javaCameraFile.loadFileAsString(); + auto javaCameraFile = javaSourceFolder.getChildFile ("AndroidCamera.java"); + auto juceCameraCodeAll = javaCameraFile.loadFileAsString(); - if (static_cast (androidMinimumSDK.get()) >= 21) - { juceCameraCode << juceCameraCodeAll.fromFirstOccurrenceOf ("$$CameraApi21", false, false) .upToFirstOccurrenceOf ("CameraApi21$$", false, false); } @@ -1217,6 +1221,32 @@ private: return { juceCameraImports, juceCameraCode }; } + struct VideoCode + { + String imports; + String main; + }; + + VideoCode getVideoCode (const File& javaSourceFolder) const + { + String juceVideoImports, juceVideoCode; + + if (static_cast (androidMinimumSDK.get()) >= 21) + { + juceVideoImports << "import android.database.ContentObserver;" << newLine; + juceVideoImports << "import android.media.session.*;" << newLine; + juceVideoImports << "import android.media.MediaMetadata;" << newLine; + + auto javaVideoFile = javaSourceFolder.getChildFile ("AndroidVideo.java"); + auto juceVideoCodeAll = javaVideoFile.loadFileAsString(); + + juceVideoCode << juceVideoCodeAll.fromFirstOccurrenceOf ("$$VideoApi21", false, false) + .upToFirstOccurrenceOf ("VideoApi21$$", false, false); + } + + return { juceVideoImports, juceVideoCode }; + } + void copyAdditionalJavaFiles (const File& sourceFolder, const File& targetFolder) const { auto inAppBillingJavaFileName = String ("IInAppBillingService.java"); diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj index 69ce1f0b38..3e28810fd5 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj @@ -2642,6 +2642,7 @@ + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters index d8d4e2f087..99ec1e7330 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -4473,6 +4473,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h b/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h index 059f7a28f8..4f86f8b15b 100644 --- a/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h +++ b/extras/UnitTestRunner/JuceLibraryCode/AppConfig.h @@ -290,6 +290,10 @@ #ifndef JUCE_USE_CAMERA //#define JUCE_USE_CAMERA 0 #endif + +#ifndef JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + //#define JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME 1 +#endif //============================================================================== #ifndef JUCE_STANDALONE_APPLICATION #if defined(JucePlugin_Name) && defined(JucePlugin_Build_Standalone) diff --git a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj index 6e81665879..8529654d2c 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj @@ -2423,6 +2423,7 @@ + diff --git a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters index f8c3447503..00a85ee174 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2017/WindowsDLL_StaticLibrary.vcxproj.filters @@ -4014,6 +4014,9 @@ JUCE Modules\juce_video\native + + JUCE Modules\juce_video\native + JUCE Modules\juce_video\native diff --git a/extras/WindowsDLL/JuceLibraryCode/AppConfig.h b/extras/WindowsDLL/JuceLibraryCode/AppConfig.h index 8510bde870..ff8c00ff4f 100644 --- a/extras/WindowsDLL/JuceLibraryCode/AppConfig.h +++ b/extras/WindowsDLL/JuceLibraryCode/AppConfig.h @@ -262,6 +262,10 @@ #ifndef JUCE_USE_CAMERA //#define JUCE_USE_CAMERA 0 #endif + +#ifndef JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + //#define JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME 1 +#endif //============================================================================== #ifndef JUCE_STANDALONE_APPLICATION #if defined(JucePlugin_Name) && defined(JucePlugin_Build_Standalone) diff --git a/modules/juce_core/native/java/AndroidVideo.java b/modules/juce_core/native/java/AndroidVideo.java new file mode 100644 index 0000000000..495e8ff372 --- /dev/null +++ b/modules/juce_core/native/java/AndroidVideo.java @@ -0,0 +1,146 @@ +$$VideoApi21 + //============================================================================== + public class MediaControllerCallback extends MediaController.Callback + { + private native void mediaControllerAudioInfoChanged (long host, MediaController.PlaybackInfo info); + private native void mediaControllerMetadataChanged (long host, MediaMetadata metadata); + private native void mediaControllerPlaybackStateChanged (long host, PlaybackState state); + private native void mediaControllerSessionDestroyed (long host); + + MediaControllerCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onAudioInfoChanged (MediaController.PlaybackInfo info) + { + mediaControllerAudioInfoChanged (host, info); + } + + @Override + public void onMetadataChanged (MediaMetadata metadata) + { + mediaControllerMetadataChanged (host, metadata); + } + + @Override + public void onPlaybackStateChanged (PlaybackState state) + { + mediaControllerPlaybackStateChanged (host, state); + } + + @Override + public void onQueueChanged (List queue) {} + + @Override + public void onSessionDestroyed() + { + mediaControllerSessionDestroyed (host); + } + + private long host; + } + + //============================================================================== + public class MediaSessionCallback extends MediaSession.Callback + { + private native void mediaSessionPause (long host); + private native void mediaSessionPlay (long host); + private native void mediaSessionPlayFromMediaId (long host, String mediaId, Bundle extras); + private native void mediaSessionSeekTo (long host, long pos); + private native void mediaSessionStop (long host); + + + MediaSessionCallback (long hostToUse) + { + host = hostToUse; + } + + @Override + public void onPause() + { + mediaSessionPause (host); + } + + @Override + public void onPlay() + { + mediaSessionPlay (host); + } + + @Override + public void onPlayFromMediaId (String mediaId, Bundle extras) + { + mediaSessionPlayFromMediaId (host, mediaId, extras); + } + + @Override + public void onSeekTo (long pos) + { + mediaSessionSeekTo (host, pos); + } + + @Override + public void onStop() + { + mediaSessionStop (host); + } + + @Override + public void onFastForward() {} + + @Override + public boolean onMediaButtonEvent (Intent mediaButtonIntent) + { + return true; + } + + @Override + public void onRewind() {} + + @Override + public void onSkipToNext() {} + + @Override + public void onSkipToPrevious() {} + + @Override + public void onSkipToQueueItem (long id) {} + + private long host; + } + + //============================================================================== + public class SystemVolumeObserver extends ContentObserver + { + private native void mediaSessionSystemVolumeChanged (long host); + + SystemVolumeObserver (Activity activityToUse, long hostToUse) + { + super (null); + + activity = activityToUse; + host = hostToUse; + } + + void setEnabled (boolean shouldBeEnabled) + { + if (shouldBeEnabled) + activity.getApplicationContext().getContentResolver().registerContentObserver (android.provider.Settings.System.CONTENT_URI, true, this); + else + activity.getApplicationContext().getContentResolver().unregisterContentObserver (this); + } + + @Override + public void onChange (boolean selfChange, Uri uri) + { + if (uri.toString().startsWith ("content://settings/system/volume_music")) + mediaSessionSystemVolumeChanged (host); + } + + private Activity activity; + private long host; + } + +VideoApi21$$ diff --git a/modules/juce_core/native/java/JuceAppActivity.java b/modules/juce_core/native/java/JuceAppActivity.java index 4fdeac0d9d..bce00f6192 100644 --- a/modules/juce_core/native/java/JuceAppActivity.java +++ b/modules/juce_core/native/java/JuceAppActivity.java @@ -30,7 +30,8 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -$$JuceAndroidCameraImports$$ // If you get an error here, you need to re-save your project with the Projucer! +$$JuceAndroidCameraImports$$ // If you get an error here, you need to re-save your project with the Projucer! +$$JuceAndroidVideoImports$$ // If you get an error here, you need to re-save your project with the Projucer! import android.net.http.SslError; import android.net.Uri; import android.os.Bundle; @@ -87,10 +88,13 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ } //============================================================================== - public boolean isPermissionDeclaredInManifest (int permissionID) + public boolean isPermissionDeclaredInManifest (int permissionID) + { + return isPermissionDeclaredInManifest (getAndroidPermissionName (permissionID)); + } + + public boolean isPermissionDeclaredInManifest (String permissionToCheck) { - String permissionToCheck = getAndroidPermissionName(permissionID); - try { PackageInfo info = getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS); @@ -982,12 +986,14 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ public static class NativeSurfaceView extends SurfaceView implements SurfaceHolder.Callback { - private long nativeContext = 0; + private long nativeContext = 0; + private boolean forVideo; - NativeSurfaceView (Context context, long nativeContextPtr) + NativeSurfaceView (Context context, long nativeContextPtr, boolean createdForVideo) { super (context); - nativeContext = nativeContextPtr; + nativeContext = nativeContextPtr; + forVideo = createdForVideo; } public Surface getNativeSurface() @@ -1004,39 +1010,52 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ //============================================================================== @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) - { - surfaceChangedNative (nativeContext, holder, format, width, height); + { + if (forVideo) + surfaceChangedNativeVideo (nativeContext, holder, format, width, height); + else + surfaceChangedNative (nativeContext, holder, format, width, height); } @Override public void surfaceCreated (SurfaceHolder holder) - { - surfaceCreatedNative (nativeContext, holder); + { + if (forVideo) + surfaceCreatedNativeVideo (nativeContext, holder); + else + surfaceCreatedNative (nativeContext, holder); } @Override public void surfaceDestroyed (SurfaceHolder holder) - { - surfaceDestroyedNative (nativeContext, holder); + { + if (forVideo) + surfaceDestroyedNativeVideo (nativeContext, holder); + else + surfaceDestroyedNative (nativeContext, holder); } @Override protected void dispatchDraw (Canvas canvas) { - super.dispatchDraw (canvas); - dispatchDrawNative (nativeContext, canvas); + super.dispatchDraw (canvas); + + if (forVideo) + dispatchDrawNativeVideo (nativeContext, canvas); + else + dispatchDrawNative (nativeContext, canvas); } //============================================================================== @Override - protected void onAttachedToWindow () + protected void onAttachedToWindow() { super.onAttachedToWindow(); getHolder().addCallback (this); } @Override - protected void onDetachedFromWindow () + protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getHolder().removeCallback (this); @@ -1047,12 +1066,18 @@ public class JuceAppActivity extends $$JuceAppActivityBaseClass$$ private native void surfaceCreatedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder); private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder, - int format, int width, int height); + int format, int width, int height); + + private native void dispatchDrawNativeVideo (long nativeContextPtr, Canvas canvas); + private native void surfaceCreatedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceDestroyedNativeVideo (long nativeContextptr, SurfaceHolder holder); + private native void surfaceChangedNativeVideo (long nativeContextptr, SurfaceHolder holder, + int format, int width, int height); } - public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr) + public NativeSurfaceView createNativeSurfaceView (long nativeSurfacePtr, boolean forVideo) { - return new NativeSurfaceView (this, nativeSurfacePtr); + return new NativeSurfaceView (this, nativeSurfacePtr, forVideo); } //============================================================================== @@ -1610,7 +1635,8 @@ $$JuceAndroidWebViewNativeCode$$ // If you get an error here, you need to re-sav private final Object hostLock = new Object(); } - $$JuceAndroidCameraCode$$ // If you get an error here, you need to re-save your project with the Projucer! + $$JuceAndroidCameraCode$$ // If you get an error here, you need to re-save your project with the Projucer! + $$JuceAndroidVideoCode$$ // If you get an error here, you need to re-save your project with the Projucer! //============================================================================== public static final String getLocaleValue (boolean isRegion) diff --git a/modules/juce_core/native/juce_android_Files.cpp b/modules/juce_core/native/juce_android_Files.cpp index a4c3458d4e..2b2c6457eb 100644 --- a/modules/juce_core/native/juce_android_Files.cpp +++ b/modules/juce_core/native/juce_android_Files.cpp @@ -188,6 +188,13 @@ private: uri.get(), projection.get(), jSelection.get(), args.get(), nullptr)); + if (jniCheckHasExceptionOccurredAndClear()) + { + // An exception has occurred, have you acquired RuntimePermission::readExternalStorage permission? + jassertfalse; + return {}; + } + if (cursor) { if (env->CallBooleanMethod (cursor.get(), AndroidCursor.moveToFirst) != 0) @@ -380,6 +387,13 @@ private: uri.get(), projection.get(), nullptr, nullptr, nullptr)); + if (jniCheckHasExceptionOccurredAndClear()) + { + // An exception has occurred, have you acquired RuntimePermission::readExternalStorage permission? + jassertfalse; + return {}; + } + if (cursor == 0) return {}; diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 1324649685..d171f41752 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -250,60 +250,79 @@ extern AndroidSystem android; //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \ - METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \ - METHOD (createNativeSurfaceView, "createNativeSurfaceView", "(J)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView;") \ - METHOD (finish, "finish", "()V") \ - METHOD (getWindowManager, "getWindowManager", "()Landroid/view/WindowManager;") \ - METHOD (setRequestedOrientation, "setRequestedOrientation", "(I)V") \ - METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \ - METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \ - METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \ - METHOD (renderGlyph, "renderGlyph", "(CCLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \ - STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;ILjava/lang/String;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \ - METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \ - METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ - METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;)V") \ - METHOD (showYesNoCancelBox, "showYesNoCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ - STATICMETHOD (getLocaleValue, "getLocaleValue", "(Z)Ljava/lang/String;") \ - STATICMETHOD (getDocumentsFolder, "getDocumentsFolder", "()Ljava/lang/String;") \ - STATICMETHOD (getPicturesFolder, "getPicturesFolder", "()Ljava/lang/String;") \ - STATICMETHOD (getMusicFolder, "getMusicFolder", "()Ljava/lang/String;") \ - STATICMETHOD (getDownloadsFolder, "getDownloadsFolder", "()Ljava/lang/String;") \ - STATICMETHOD (getMoviesFolder, "getMoviesFolder", "()Ljava/lang/String;") \ - METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \ - METHOD (getTypeFaceFromByteArray, "getTypeFaceFromByteArray", "([B)Landroid/graphics/Typeface;") \ - METHOD (setScreenSaver, "setScreenSaver", "(Z)V") \ - METHOD (getScreenSaver, "getScreenSaver", "()Z") \ - METHOD (getAndroidMidiDeviceManager, "getAndroidMidiDeviceManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$MidiDeviceManager;") \ - METHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager;") \ - STATICMETHOD (getAndroidSDKVersion, "getAndroidSDKVersion", "()I") \ - METHOD (audioManagerGetProperty, "audioManagerGetProperty", "(Ljava/lang/String;)Ljava/lang/String;") \ - METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \ - METHOD (requestRuntimePermission, "requestRuntimePermission", "(IJ)V" ) \ - METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \ - METHOD (isPermissionDeclaredInManifest, "isPermissionDeclaredInManifest", "(I)Z" ) \ - METHOD (getAssets, "getAssets", "()Landroid/content/res/AssetManager;") \ - METHOD (getSystemService, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;") \ - METHOD (getPackageManager, "getPackageManager", "()Landroid/content/pm/PackageManager;") \ - METHOD (getPackageName, "getPackageName", "()Ljava/lang/String;") \ - METHOD (getResources, "getResources", "()Landroid/content/res/Resources;") \ - METHOD (createInvocationHandler, "createInvocationHandler", "(J)Ljava/lang/reflect/InvocationHandler;") \ - METHOD (invocationHandlerContextDeleted, "invocationHandlerContextDeleted", "(Ljava/lang/reflect/InvocationHandler;)V") \ - METHOD (bindService, "bindService", "(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z") \ - METHOD (unbindService, "unbindService", "(Landroid/content/ServiceConnection;)V") \ - METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ - METHOD (moveTaskToBack, "moveTaskToBack", "(Z)Z") \ - METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ - METHOD (startActivityForResult, "startActivityForResult", "(Landroid/content/Intent;I)V") \ - METHOD (getContentResolver, "getContentResolver", "()Landroid/content/ContentResolver;") \ - METHOD (addAppPausedResumedListener, "addAppPausedResumedListener", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener;J)V") \ - METHOD (removeAppPausedResumedListener, "removeAppPausedResumedListener", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener;J)V") + METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \ + METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \ + METHOD (createNativeSurfaceView, "createNativeSurfaceView", "(JZ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView;") \ + METHOD (finish, "finish", "()V") \ + METHOD (getWindowManager, "getWindowManager", "()Landroid/view/WindowManager;") \ + METHOD (setRequestedOrientation, "setRequestedOrientation", "(I)V") \ + METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \ + METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \ + METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \ + METHOD (renderGlyph, "renderGlyph", "(CCLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \ + STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;ILjava/lang/String;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \ + METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \ + METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ + METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;)V") \ + METHOD (showYesNoCancelBox, "showYesNoCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \ + STATICMETHOD (getLocaleValue, "getLocaleValue", "(Z)Ljava/lang/String;") \ + STATICMETHOD (getDocumentsFolder, "getDocumentsFolder", "()Ljava/lang/String;") \ + STATICMETHOD (getPicturesFolder, "getPicturesFolder", "()Ljava/lang/String;") \ + STATICMETHOD (getMusicFolder, "getMusicFolder", "()Ljava/lang/String;") \ + STATICMETHOD (getDownloadsFolder, "getDownloadsFolder", "()Ljava/lang/String;") \ + STATICMETHOD (getMoviesFolder, "getMoviesFolder", "()Ljava/lang/String;") \ + METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \ + METHOD (getTypeFaceFromByteArray, "getTypeFaceFromByteArray", "([B)Landroid/graphics/Typeface;") \ + METHOD (setScreenSaver, "setScreenSaver", "(Z)V") \ + METHOD (getScreenSaver, "getScreenSaver", "()Z") \ + METHOD (getAndroidMidiDeviceManager, "getAndroidMidiDeviceManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$MidiDeviceManager;") \ + METHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager;") \ + STATICMETHOD (getAndroidSDKVersion, "getAndroidSDKVersion", "()I") \ + METHOD (audioManagerGetProperty, "audioManagerGetProperty", "(Ljava/lang/String;)Ljava/lang/String;") \ + METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \ + METHOD (requestRuntimePermission, "requestRuntimePermission", "(IJ)V" ) \ + METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \ + METHOD (isPermissionDeclaredInManifest, "isPermissionDeclaredInManifest", "(I)Z" ) \ + METHOD (isPermissionDeclaredInManifestString, "isPermissionDeclaredInManifest", "(Ljava/lang/String;)Z") \ + METHOD (getAssets, "getAssets", "()Landroid/content/res/AssetManager;") \ + METHOD (getSystemService, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;") \ + METHOD (getPackageManager, "getPackageManager", "()Landroid/content/pm/PackageManager;") \ + METHOD (getPackageName, "getPackageName", "()Ljava/lang/String;") \ + METHOD (getResources, "getResources", "()Landroid/content/res/Resources;") \ + METHOD (createInvocationHandler, "createInvocationHandler", "(J)Ljava/lang/reflect/InvocationHandler;") \ + METHOD (invocationHandlerContextDeleted, "invocationHandlerContextDeleted", "(Ljava/lang/reflect/InvocationHandler;)V") \ + METHOD (bindService, "bindService", "(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z") \ + METHOD (unbindService, "unbindService", "(Landroid/content/ServiceConnection;)V") \ + METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ + METHOD (moveTaskToBack, "moveTaskToBack", "(Z)Z") \ + METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ + METHOD (startActivityForResult, "startActivityForResult", "(Landroid/content/Intent;I)V") \ + METHOD (getContentResolver, "getContentResolver", "()Landroid/content/ContentResolver;") \ + METHOD (addAppPausedResumedListener, "addAppPausedResumedListener", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener;J)V") \ + METHOD (removeAppPausedResumedListener, "removeAppPausedResumedListener", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener;J)V") DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); #undef JNI_CLASS_MEMBERS //============================================================================== +#if __ANDROID_API__ >= 21 +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (build, "build", "()Landroid/media/AudioAttributes;") \ + METHOD (constructor, "", "()V") \ + METHOD (setContentType, "setContentType", "(I)Landroid/media/AudioAttributes$Builder;") \ + METHOD (setUsage, "setUsage", "(I)Landroid/media/AudioAttributes$Builder;") + +DECLARE_JNI_CLASS (AndroidAudioAttributesBuilder, "android/media/AudioAttributes$Builder") +#undef JNI_CLASS_MEMBERS +#endif + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (abandonAudioFocus, "abandonAudioFocus", "(Landroid/media/AudioManager$OnAudioFocusChangeListener;)I") \ + METHOD (requestAudioFocus, "requestAudioFocus", "(Landroid/media/AudioManager$OnAudioFocusChangeListener;II)I") + +DECLARE_JNI_CLASS (AndroidAudioManager, "android/media/AudioManager"); +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ STATICMETHOD (createBitmap, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;") \ STATICMETHOD (createBitmapFrom, "createBitmap", "(Landroid/graphics/Bitmap;IIIILandroid/graphics/Matrix;Z)Landroid/graphics/Bitmap;") \ @@ -741,6 +760,21 @@ namespace return result; } + + inline bool jniCheckHasExceptionOccurredAndClear() + { + auto* env = getEnv(); + + LocalRef exception (env->ExceptionOccurred()); + + if (exception != nullptr) + { + env->ExceptionClear(); + return true; + } + + return false; + } } //============================================================================== @@ -778,4 +812,24 @@ LocalRef CreateJavaInterface (AndroidInterfaceImplementer* implementer, LocalRef CreateJavaInterface (AndroidInterfaceImplementer* implementer, const String& interfaceName); +//============================================================================== +class AppPausedResumedListener : public AndroidInterfaceImplementer +{ +public: + struct Owner + { + virtual ~Owner() {} + + virtual void appPaused() = 0; + virtual void appResumed() = 0; + }; + + AppPausedResumedListener (Owner&); + + jobject invoke (jobject proxy, jobject method, jobjectArray args) override; + +private: + Owner& owner; +}; + } // namespace juce diff --git a/modules/juce_core/native/juce_android_SystemStats.cpp b/modules/juce_core/native/juce_android_SystemStats.cpp index 6089fd7a28..916cd871e1 100644 --- a/modules/juce_core/native/juce_android_SystemStats.cpp +++ b/modules/juce_core/native/juce_android_SystemStats.cpp @@ -200,6 +200,35 @@ JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024Nativ juce_dispatchDelete (env, thisPtr); } +//============================================================================== +AppPausedResumedListener::AppPausedResumedListener (Owner& ownerToUse) + : owner (ownerToUse) +{ +} + +jobject AppPausedResumedListener::invoke (jobject proxy, jobject method, jobjectArray args) +{ + auto* env = getEnv(); + + auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); + + int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; + + if (methodName == "appPaused" && numArgs == 0) + { + owner.appPaused(); + return nullptr; + } + + if (methodName == "appResumed" && numArgs == 0) + { + owner.appResumed(); + return nullptr; + } + + return AndroidInterfaceImplementer::invoke (proxy, method, args); +} + //============================================================================== JavaVM* androidJNIJavaVM = nullptr; diff --git a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp index 15b18d8fc0..bb86142fb3 100644 --- a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp +++ b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp @@ -307,14 +307,10 @@ private: auto inputStream = StreamCloser (LocalRef (env->CallObjectMethod (assetFd, AssetFileDescriptor.createInputStream))); - auto exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) + if (jniCheckHasExceptionOccurredAndClear()) { // Failed to open file stream for resource jassertfalse; - - env->ExceptionClear(); return {}; } @@ -326,14 +322,10 @@ private: JavaFileOutputStream.constructor, javaString (tempFile.getFullPathName()).get()))); - exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) + if (jniCheckHasExceptionOccurredAndClear()) { // Failed to open file stream for temporary file jassertfalse; - - env->ExceptionClear(); return {}; } @@ -347,14 +339,10 @@ private: bytesRead = env->CallIntMethod (inputStream.stream, JavaFileInputStream.read, buffer.get()); - exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) + if (jniCheckHasExceptionOccurredAndClear()) { // Failed to read from resource file. jassertfalse; - - env->ExceptionClear(); return {}; } @@ -363,12 +351,10 @@ private: env->CallVoidMethod (outputStream.stream, JavaFileOutputStream.write, buffer.get(), 0, bytesRead); - if (exception != 0) + if (jniCheckHasExceptionOccurredAndClear()) { // Failed to write to temporary file. jassertfalse; - - env->ExceptionClear(); return {}; } } @@ -714,14 +700,10 @@ private: ParcelFileDescriptor.open, javaFile.get(), modeReadOnly)); - auto exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) + if (jniCheckHasExceptionOccurredAndClear()) { // Failed to create file descriptor. Have you provided a valid file path/resource name? jassertfalse; - - env->ExceptionClear(); return nullptr; } diff --git a/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp b/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp index b6d6217db0..dbac03aba9 100644 --- a/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp +++ b/modules/juce_gui_extra/native/juce_android_PushNotifications.cpp @@ -27,17 +27,6 @@ namespace juce { -#if __ANDROID_API__ >= 21 -#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ - METHOD (build, "build", "()Landroid/media/AudioAttributes;") \ - METHOD (constructor, "", "()V") \ - METHOD (setContentType, "setContentType", "(I)Landroid/media/AudioAttributes$Builder;") \ - METHOD (setUsage, "setUsage", "(I)Landroid/media/AudioAttributes$Builder;") - -DECLARE_JNI_CLASS (AudioAttributesBuilder, "android/media/AudioAttributes$Builder") -#undef JNI_CLASS_MEMBERS -#endif - #if __ANDROID_API__ >= 26 #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ METHOD (constructor, "", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V") \ @@ -1504,12 +1493,12 @@ struct PushNotifications::Pimpl env->CallVoidMethod (channel, NotificationChannel.enableVibration, c.enableVibration); } - auto audioAttributesBuilder = LocalRef (env->NewObject (AudioAttributesBuilder, AudioAttributesBuilder.constructor)); + auto AndroidAudioAttributesBuilder = LocalRef (env->NewObject (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.constructor)); const int contentTypeSonification = 4; const int usageNotification = 5; - env->CallObjectMethod (audioAttributesBuilder, AudioAttributesBuilder.setContentType, contentTypeSonification); - env->CallObjectMethod (audioAttributesBuilder, AudioAttributesBuilder.setUsage, usageNotification); - auto audioAttributes = LocalRef (env->CallObjectMethod (audioAttributesBuilder, AudioAttributesBuilder.build)); + env->CallObjectMethod (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.setContentType, contentTypeSonification); + env->CallObjectMethod (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.setUsage, usageNotification); + auto audioAttributes = LocalRef (env->CallObjectMethod (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.build)); env->CallVoidMethod (channel, NotificationChannel.setSound, juceUrlToAndroidUri (c.soundToPlay).get(), audioAttributes.get()); env->CallVoidMethod (notificationManager, NotificationManagerApi26.createNotificationChannel, channel.get()); diff --git a/modules/juce_opengl/native/juce_OpenGL_android.h b/modules/juce_opengl/native/juce_OpenGL_android.h index a91d965db3..3ee0227c2f 100644 --- a/modules/juce_opengl/native/juce_OpenGL_android.h +++ b/modules/juce_opengl/native/juce_OpenGL_android.h @@ -61,7 +61,8 @@ public: // create a native surface view surfaceView = GlobalRef (env->CallObjectMethod (android.activity.get(), JuceAppActivity.createNativeSurfaceView, - reinterpret_cast (this))); + reinterpret_cast (this), + false)); if (surfaceView.get() == nullptr) return; diff --git a/modules/juce_video/juce_video.h b/modules/juce_video/juce_video.h index 77ccb89838..71b6ae67d8 100644 --- a/modules/juce_video/juce_video.h +++ b/modules/juce_video/juce_video.h @@ -79,6 +79,25 @@ #undef JUCE_USE_CAMERA #endif +//============================================================================= +/** Config: JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + Enables synchronisation between video playback volume and OS media volume. + Currently supported on Android only. + */ +#ifndef JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + #define JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME 1 +#endif + +#ifndef JUCE_VIDEO_LOG_ENABLED + #define JUCE_VIDEO_LOG_ENABLED 1 +#endif + +#if JUCE_VIDEO_LOG_ENABLED + #define JUCE_VIDEO_LOG(x) DBG(x) +#else + #define JUCE_VIDEO_LOG(x) {} +#endif + //============================================================================= #include "playback/juce_VideoComponent.h" #include "capture/juce_CameraDevice.h" diff --git a/modules/juce_video/native/juce_android_CameraDevice.h b/modules/juce_video/native/juce_android_CameraDevice.h index ee424b03ef..88334296d8 100644 --- a/modules/juce_video/native/juce_android_CameraDevice.h +++ b/modules/juce_video/native/juce_android_CameraDevice.h @@ -442,49 +442,6 @@ private: Owner& owner; }; -//============================================================================== -class AppPausedResumedListener : public AndroidInterfaceImplementer -{ -public: - struct Owner - { - virtual ~Owner() {} - - virtual void appPaused() = 0; - virtual void appResumed() = 0; - }; - - AppPausedResumedListener (Owner& ownerToUse) - : owner (ownerToUse) - {} - - jobject invoke (jobject proxy, jobject method, jobjectArray args) override - { - auto* env = getEnv(); - - auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName)); - - int numArgs = args != nullptr ? env->GetArrayLength (args) : 0; - - if (methodName == "appPaused" && numArgs == 0) - { - owner.appPaused(); - return nullptr; - } - - if (methodName == "appResumed" && numArgs == 0) - { - owner.appResumed(); - return nullptr; - } - - return AndroidInterfaceImplementer::invoke (proxy, method, args); - } - -private: - Owner& owner; -}; - //============================================================================== struct CameraDevice::Pimpl #if __ANDROID_API__ >= 21 @@ -506,7 +463,6 @@ struct CameraDevice::Pimpl appPausedResumedListener (*this), appPausedResumedListenerNative (CreateJavaInterface (&appPausedResumedListener, JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener").get()), - cameraManager (initialiseCameraManager()), cameraCharacteristics (initialiseCameraCharacteristics (cameraManager, cameraId)), streamConfigurationMap (cameraCharacteristics), @@ -869,7 +825,7 @@ private: bool isOutputSupportedForSurface (const LocalRef& surface) const { - return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get()); + return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get()) != 0; } static constexpr int jpegImageFormat = 256; @@ -1460,10 +1416,7 @@ private: // ... ignore RuntimeException that can be thrown if stop() was called after recording // has started but before any frame was written to a file. This is not an error. - auto exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) - env->ExceptionClear(); + jniCheckHasExceptionOccurredAndClear(); unlockScreenOrientation(); } @@ -1630,16 +1583,12 @@ private: } } - auto exception = LocalRef (env->ExceptionOccurred()); - // When exception occurs, CameraCaptureSession.close will never finish, so // we should not wait for it. For fatal error an exception does occur, but // it is catched internally in Java... - if (exception != 0 || scopedCameraDevice.fatalErrorOccurred.get()) + if (jniCheckHasExceptionOccurredAndClear() || scopedCameraDevice.fatalErrorOccurred.get()) { JUCE_CAMERA_LOG ("Exception or fatal error occurred while closing Capture Session, closing by force"); - - env->ExceptionClear(); } else if (calledClose) { @@ -1768,7 +1717,7 @@ private: void lockFocus() { - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; JUCE_CAMERA_LOG ("Performing auto-focus if possible..."); @@ -1796,7 +1745,7 @@ private: // IllegalStateException can be thrown when accessing CaptureSession, // claiming that capture session was already closed but we may not // get relevant callback yet, so check for this and bailout when needed. - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; auto* env = getEnv(); @@ -1902,7 +1851,7 @@ private: void captureStillPictureDelayed() { - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; JUCE_CAMERA_LOG ("Still picture capture, device ready, capturing now..."); @@ -1911,12 +1860,12 @@ private: env->CallVoidMethod (captureSession, CameraCaptureSession.stopRepeating); - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; env->CallVoidMethod (captureSession, CameraCaptureSession.abortCaptures); - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; // Delay still picture capture for devices that can't handle it right after @@ -1929,7 +1878,7 @@ private: void runPrecaptureSequence() { - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; auto* env = getEnv(); @@ -1950,7 +1899,7 @@ private: void unlockFocus() { - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; JUCE_CAMERA_LOG ("Unlocking focus..."); @@ -1970,7 +1919,7 @@ private: env->CallIntMethod (captureSession, CameraCaptureSession.capture, resetAutoFocusRequest.get(), nullptr, handler.get()); - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; // NB: for preview, using preview capture request again @@ -2233,10 +2182,7 @@ private: // If something went wrong we will be pinged in cameraDeviceStateError() // callback, silence the redundant exception. - auto exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) - env->ExceptionClear(); + jniCheckHasExceptionOccurredAndClear(); } void close() @@ -2500,12 +2446,12 @@ private: env->CallVoidMethod (session, CameraCaptureSession.stopRepeating); - if (Pimpl::checkHasExceptionOccurred()) + if (jniCheckHasExceptionOccurredAndClear()) return; env->CallVoidMethod (session, CameraCaptureSession.abortCaptures); - Pimpl::checkHasExceptionOccurred(); + jniCheckHasExceptionOccurredAndClear(); } } @@ -3064,29 +3010,11 @@ private: env->CallBooleanMethod (handlerThread, AndroidHandlerThread.quitSafely); env->CallVoidMethod (handlerThread, AndroidHandlerThread.join); - auto exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) - env->ExceptionClear(); + jniCheckHasExceptionOccurredAndClear(); handlerThread.clear(); handler.clear(); } - - static bool checkHasExceptionOccurred() - { - auto* env = getEnv(); - - auto exception = LocalRef (env->ExceptionOccurred()); - - if (exception != 0) - { - env->ExceptionClear(); - return true; - } - - return false; - } #endif friend struct CameraDevice::ViewerComponent; diff --git a/modules/juce_video/native/juce_android_Video.h b/modules/juce_video/native/juce_android_Video.h new file mode 100644 index 0000000000..789877585c --- /dev/null +++ b/modules/juce_video/native/juce_android_Video.h @@ -0,0 +1,1918 @@ +/* + ============================================================================== + + 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, "", "(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, "", "()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, "", "(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, "", "(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, "", "()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, "", "()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, "", "(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& mediaPlayer) = 0; + virtual void onBufferingUpdate (LocalRef& mediaPlayer, int progress) = 0; + virtual void onSeekComplete (LocalRef& mediaPlayer) = 0; + virtual void onCompletion (LocalRef& mediaPlayer) = 0; + virtual bool onInfo (LocalRef& mediaPlayer, int what, int extra) = 0; + virtual bool onError (LocalRef& 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 (env->GetObjectArrayElement (args, 0)); + + owner.onPrepared (mediaPlayer); + return nullptr; + } + + if (methodName == "onCompletion" && numArgs == 1) + { + auto mediaPlayer = LocalRef (env->GetObjectArrayElement (args, 0)); + + owner.onCompletion (mediaPlayer); + return nullptr; + } + + if (methodName == "onInfo" && numArgs == 3) + { + auto mediaPlayer = LocalRef (env->GetObjectArrayElement (args, 0)); + auto what = LocalRef (env->GetObjectArrayElement (args, 1)); + auto extra = LocalRef (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 (env->GetObjectArrayElement (args, 0)); + auto what = LocalRef (env->GetObjectArrayElement (args, 1)); + auto extra = LocalRef (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 (env->GetObjectArrayElement (args, 0)); + + owner.onSeekComplete (mediaPlayer); + return nullptr; + } + + if (methodName == "onBufferingUpdate" && numArgs == 2) + { + auto mediaPlayer = LocalRef (env->GetObjectArrayElement (args, 0)); + + auto progress = LocalRef (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 (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 (env->CallObjectMethod (android.activity.get(), + JuceAppActivity.createNativeSurfaceView, + reinterpret_cast (this), + true))); + + env->CallVoidMethod (android.activity, JuceAppActivity.addAppPausedResumedListener, + appPausedResumedListenerNative.get(), reinterpret_cast (this)); + #endif + } + + ~Pimpl() + { + #if __ANDROID_API__ >= 21 + getEnv()->CallVoidMethod (android.activity, JuceAppActivity.removeAppPausedResumedListener, + appPausedResumedListenerNative.get(), reinterpret_cast(this)); + #endif + } + + #if __ANDROID_API__ < 21 + // Dummy implementations for unsupported API levels. + void loadAsync (const URL&, std::function) {} + 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 getNativeSize() const { return {}; } + double getDuration() const { return 0.0; } + + File currentFile; + URL currentURL; + #else + void loadAsync (const URL& url, std::function 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 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 (getEnv()->NewObject (AndroidMediaSession, + AndroidMediaSession.constructor, + android.activity.get(), + javaString ("JuceVideoMediaSession").get()))), + mediaSessionCallback (LocalRef (getEnv()->NewObject (AndroidMediaSessionCallback, + AndroidMediaSessionCallback.constructor, + android.activity.get(), + reinterpret_cast (this)))), + playbackStateBuilder (LocalRef (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 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 (getEnv()->CallObjectMethod (nativeController, + AndroidMediaController.getTransportControls))), + controllerCallback (LocalRef (getEnv()->NewObject (AndroidMediaControllerCallback, + AndroidMediaControllerCallback.constructor, + android.activity.get(), + reinterpret_cast (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 (newPosition * 1000); + + getEnv()->CallVoidMethod (controllerTransportControls, AndroidMediaControllerTransportControls.seekTo, seekPos); + } + + double getPosition() const + { + auto* env = getEnv(); + + auto playbackState = LocalRef (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 (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 (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 (env->CallObjectMethod (nativeController, AndroidMediaController.getPlaybackInfo)); + + auto maxVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getMaxVolume)); + auto curVolume = (int) (env->CallIntMethod (playbackInfo, AndroidMediaControllerPlaybackInfo.getCurrentVolume)); + + return static_cast (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 (env->NewObject (AndroidMediaPlayer, AndroidMediaPlayer.constructor))); + + currentState = State::idle; + + auto uri = LocalRef (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 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 (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::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 (env->CallObjectMethod (nativeMediaPlayer, AndroidMediaPlayer.getPlaybackParams)); + LocalRef (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 (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 (currentState)]; } + + //============================================================================== + MediaSession& owner; + GlobalRef nativeMediaPlayer; + + MediaPlayerListener mediaPlayerListener; + GlobalRef nativeMediaPlayerListener; + + float lastAudioVolume = std::numeric_limits::min(); + + GlobalRef videoSurfaceHolder; + + State currentState = State::idle; + + //============================================================================== + void onPrepared (LocalRef& mediaPlayer) override + { + JUCE_VIDEO_LOG ("MediaPlayer::onPrepared()"); + + ignoreUnused (mediaPlayer); + + currentState = State::prepared; + + owner.playerPrepared(); + } + + void onBufferingUpdate (LocalRef& mediaPlayer, int progress) override + { + ignoreUnused (mediaPlayer); + + owner.playerBufferingUpdated (progress); + } + + void onSeekComplete (LocalRef& mediaPlayer) override + { + JUCE_VIDEO_LOG ("MediaPlayer::onSeekComplete()"); + + ignoreUnused (mediaPlayer); + + owner.playerSeekCompleted(); + } + + void onCompletion (LocalRef& 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& 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& 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 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 (durationMs); + + // NB: assuming the playback will start roughly when there is 5% of content loaded... + return ! bufferedRegions.containsRange (Range (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 (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setState, + (jint) playbackStateFlag, (jlong) playPos, (jfloat) playSpeed)); + + LocalRef (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 (durationMs); + + bufferedRegions.addRange (Range (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 (env->NewObject (AndroidMediaMetadataBuilder, + AndroidMediaMetadataBuilder.constructor)); + + auto durationMs = player.getVideoDuration(); + + auto jDurationKey = javaString ("android.media.metadata.DURATION"); + LocalRef (env->CallObjectMethod (metadataBuilder, + AndroidMediaMetadataBuilder.putLong, + jDurationKey.get(), + (jlong) durationMs)); + + auto jNumTracksKey = javaString ("android.media.metadata.NUM_TRACKS"); + LocalRef (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 (env->CallObjectMethod (playbackStateBuilder, AndroidPlaybackStateBuilder.setErrorMessage, + javaString (errorMessage).get())); + + auto state = LocalRef (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 (env->FindClass ("android/media/AudioFocusRequest$Builder")); + + static jmethodID constructor = env->GetMethodID (requestBuilderClass, "", "(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 (env->NewObject (requestBuilderClass, constructor, audioFocusGain)); + LocalRef (env->CallObjectMethod (requestBuilder, setAudioAttributesMethod, audioAttributes.get())); + LocalRef (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 (env->NewObject (AndroidAudioAttributesBuilder, + AndroidAudioAttributesBuilder.constructor)); + static constexpr jint contentTypeMovie = 3; + static constexpr jint usageMedia = 1; + + LocalRef (env->CallObjectMethod (audioAttribsBuilder, AndroidAudioAttributesBuilder.setContentType, contentTypeMovie)); + LocalRef (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 (getEnv()->NewObject (SystemVolumeObserver, + SystemVolumeObserver.constructor, + android.activity.get(), + android.activity.get(), + reinterpret_cast (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 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 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 (host)->videoSurfaceChanged (static_cast (surfaceHolder)); +} + +void juce_surfaceDestroyedNativeVideo (int64 host, void* surfaceHolder) +{ + reinterpret_cast (host)->videoSurfaceDestroyed (static_cast (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 (host)->pauseCallback(); +} + +void juce_mediaSessionPlay (int64 host) +{ + reinterpret_cast (host)->playCallback(); +} + +void juce_mediaSessionPlayFromMediaId (int64 host, void* mediaId, void* extras) +{ + reinterpret_cast (host)->playFromMediaIdCallback ((jstring) mediaId, (jobject) extras); +} + +void juce_mediaSessionSeekTo (int64 host, int64 pos) +{ + reinterpret_cast (host)->seekToCallback (pos); +} + +void juce_mediaSessionStop (int64 host) +{ + reinterpret_cast (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 (host)->audioInfoChanged ((jobject) info); +} + +void juce_mediaControllerMetadataChanged (int64 host, void* metadata) +{ + reinterpret_cast (host)->metadataChanged ((jobject) metadata); +} + +void juce_mediaControllerPlaybackStateChanged (int64 host, void* state) +{ + reinterpret_cast (host)->playbackStateChanged ((jobject) state); +} + +void juce_mediaControllerSessionDestroyed (int64 host) +{ + reinterpret_cast (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 (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 diff --git a/modules/juce_video/native/juce_mac_Video.h b/modules/juce_video/native/juce_mac_Video.h index c6d50e76ed..c13c649da4 100644 --- a/modules/juce_video/native/juce_mac_Video.h +++ b/modules/juce_video/native/juce_mac_Video.h @@ -2,7 +2,7 @@ ============================================================================== This file is part of the JUCE library. - Copyright (c) 2015 - ROLI Ltd. + 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) @@ -22,34 +22,26 @@ ============================================================================== */ -#if JUCE_IOS - using BaseClass = UIViewComponent; +#if JUCE_MAC +using Base = NSViewComponent; #else - using BaseClass = NSViewComponent; +using Base = UIViewComponent; #endif -struct VideoComponent::Pimpl : public BaseClass +struct VideoComponent::Pimpl : public Base { - Pimpl() + Pimpl (VideoComponent& ownerToUse, bool useNativeControlsIfAvailable) + : owner (ownerToUse), + playerController (*this, useNativeControlsIfAvailable) { setVisible (true); - #if JUCE_MAC && JUCE_32BIT - auto view = [[NSView alloc] init]; // 32-bit builds don't have AVPlayerView, so need to use a layer - controller = [[AVPlayerLayer alloc] init]; + auto* view = playerController.getView(); setView (view); + + #if JUCE_MAC [view setNextResponder: [view superview]]; [view setWantsLayer: YES]; - [view setLayer: controller]; - [view release]; - #elif JUCE_MAC - controller = [[AVPlayerView alloc] init]; - setView (controller); - [controller setNextResponder: [controller superview]]; - [controller setWantsLayer: YES]; - #else - controller = [[AVPlayerViewController alloc] init]; - setView ([controller view]); #endif } @@ -57,7 +49,6 @@ struct VideoComponent::Pimpl : public BaseClass { close(); setView (nil); - [controller release]; } Result load (const File& file) @@ -85,34 +76,46 @@ struct VideoComponent::Pimpl : public BaseClass if (url != nil) { close(); - - if (auto* player = [AVPlayer playerWithURL: url]) - { - [controller setPlayer: player]; - return Result::ok(); - } + return playerController.load (url); } return Result::fail ("Couldn't open movie"); } + void loadAsync (const URL& url, std::function callback) + { + if (url.isEmpty()) + { + jassertfalse; + return; + } + + currentURL = url; + + jassert (callback != nullptr); + + loadFinishedCallback = std::move (callback); + + playerController.loadAsync (url); + } + void close() { stop(); - [controller setPlayer: nil]; + playerController.close(); currentFile = File(); currentURL = {}; } - bool isOpen() const noexcept { return getAVPlayer() != nil; } + bool isOpen() const noexcept { return playerController.getPlayer() != nil; } bool isPlaying() const noexcept { return getSpeed() != 0; } - void play() noexcept { [getAVPlayer() play]; } - void stop() noexcept { [getAVPlayer() pause]; } + void play() noexcept { [playerController.getPlayer() play]; setSpeed (playSpeedMult); } + void stop() noexcept { [playerController.getPlayer() pause]; } void setPosition (double newPosition) { - if (auto* p = getAVPlayer()) + if (auto* p = playerController.getPlayer()) { CMTime t = { (CMTimeValue) (100000.0 * newPosition), (CMTimeScale) 100000, kCMTimeFlags_Valid }; @@ -125,7 +128,7 @@ struct VideoComponent::Pimpl : public BaseClass double getPosition() const { - if (auto* p = getAVPlayer()) + if (auto* p = playerController.getPlayer()) return toSeconds ([p currentTime]); return 0.0; @@ -133,12 +136,16 @@ struct VideoComponent::Pimpl : public BaseClass void setSpeed (double newSpeed) { - [getAVPlayer() setRate: (float) newSpeed]; + playSpeedMult = newSpeed; + + // Calling non 0.0 speed on a paused player would start it... + if (isPlaying()) + [playerController.getPlayer() setRate: (float) playSpeedMult]; } double getSpeed() const { - if (auto* p = getAVPlayer()) + if (auto* p = playerController.getPlayer()) return [p rate]; return 0.0; @@ -146,9 +153,9 @@ struct VideoComponent::Pimpl : public BaseClass Rectangle getNativeSize() const { - if (auto* player = getAVPlayer()) + if (auto* p = playerController.getPlayer()) { - auto s = [[player currentItem] presentationSize]; + auto s = [[p currentItem] presentationSize]; return { (int) s.width, (int) s.height }; } @@ -157,20 +164,20 @@ struct VideoComponent::Pimpl : public BaseClass double getDuration() const { - if (auto* player = getAVPlayer()) - return toSeconds ([[player currentItem] duration]); + if (auto* p = playerController.getPlayer()) + return toSeconds ([[p currentItem] duration]); return 0.0; } void setVolume (float newVolume) { - [getAVPlayer() setVolume: newVolume]; + [playerController.getPlayer() setVolume: newVolume]; } float getVolume() const { - if (auto* p = getAVPlayer()) + if (auto* p = playerController.getPlayer()) return [p volume]; return 0.0f; @@ -180,20 +187,633 @@ struct VideoComponent::Pimpl : public BaseClass URL currentURL; private: - #if JUCE_IOS - AVPlayerViewController* controller = nil; - #elif JUCE_32BIT - AVPlayerLayer* controller = nil; + //============================================================================== + template + class PlayerControllerBase + { + public: + ~PlayerControllerBase() + { + detachPlayerStatusObserver(); + detachPlaybackObserver(); + } + + protected: + //============================================================================== + struct JucePlayerStatusObserverClass : public ObjCClass + { + JucePlayerStatusObserverClass() : ObjCClass ("JucePlayerStatusObserverClass_") + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?"); + #pragma clang diagnostic pop + + addIvar ("owner"); + + registerClass(); + } + + //============================================================================== + static PlayerControllerBase& getOwner (id self) { return *getIvar (self, "owner"); } + static void setOwner (id self, PlayerControllerBase* p) { object_setInstanceVariable (self, "owner", p); } + + private: + static void valueChanged (id self, SEL, NSString* keyPath, id, + NSDictionary* change, void*) + { + auto& owner = getOwner (self); + + if ([keyPath isEqualToString: nsStringLiteral ("rate")]) + { + auto oldRate = [change[NSKeyValueChangeOldKey] floatValue]; + auto newRate = [change[NSKeyValueChangeNewKey] floatValue]; + + if (oldRate == 0 && newRate != 0) + owner.playbackStarted(); + else if (oldRate != 0 && newRate == 0) + owner.playbackStopped(); + } + else if ([keyPath isEqualToString: nsStringLiteral ("status")]) + { + auto status = [change[NSKeyValueChangeNewKey] intValue]; + + if (status == AVPlayerStatusFailed) + owner.errorOccurred(); + } + } + }; + + //============================================================================== + struct JucePlayerItemPlaybackStatusObserverClass : public ObjCClass + { + JucePlayerItemPlaybackStatusObserverClass() : ObjCClass ("JucePlayerItemPlaybackStatusObserverClass_") + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + addMethod (@selector (processNotification:), notificationReceived, "v@:@"); + #pragma clang diagnostic pop + + addIvar ("owner"); + + registerClass(); + } + + //============================================================================== + static PlayerControllerBase& getOwner (id self) { return *getIvar (self, "owner"); } + static void setOwner (id self, PlayerControllerBase* p) { object_setInstanceVariable (self, "owner", p); } + + private: + static void notificationReceived (id self, SEL, NSNotification* notification) + { + if ([notification.name isEqualToString: AVPlayerItemDidPlayToEndTimeNotification]) + getOwner (self).playbackReachedEndTime(); + } + }; + + //============================================================================== + class PlayerAsyncInitialiser + { + public: + PlayerAsyncInitialiser (PlayerControllerBase& ownerToUse) + : owner (ownerToUse), + assetKeys ([[NSArray alloc] initWithObjects: nsStringLiteral ("duration"), nsStringLiteral ("tracks"), + nsStringLiteral ("playable"), nil]) + { + static JucePlayerItemPreparationStatusObserverClass cls; + playerItemPreparationStatusObserver.reset ([cls.createInstance() init]); + JucePlayerItemPreparationStatusObserverClass::setOwner (playerItemPreparationStatusObserver.get(), this); + } + + ~PlayerAsyncInitialiser() + { + detachPreparationStatusObserver(); + } + + void loadAsync (URL url) + { + auto* nsUrl = [NSURL URLWithString: juceStringToNS (url.toString (true))]; + asset.reset ([[AVURLAsset alloc] initWithURL: nsUrl options: nil]); + + [asset.get() loadValuesAsynchronouslyForKeys: assetKeys.get() + completionHandler: ^() { checkAllKeysReadyFor (asset.get(), url); }]; + } + + private: + //============================================================================== + struct JucePlayerItemPreparationStatusObserverClass : public ObjCClass + { + JucePlayerItemPreparationStatusObserverClass() : ObjCClass ("JucePlayerItemStatusObserverClass_") + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?"); + #pragma clang diagnostic pop + + addIvar ("owner"); + + registerClass(); + } + + //============================================================================== + static PlayerAsyncInitialiser& getOwner (id self) { return *getIvar (self, "owner"); } + static void setOwner (id self, PlayerAsyncInitialiser* p) { object_setInstanceVariable (self, "owner", p); } + + private: + static void valueChanged (id self, SEL, NSString*, id object, + NSDictionary* change, void* context) + { + auto& owner = getOwner (self); + + if (context == &owner) + { + auto* playerItem = (AVPlayerItem*) object; + auto* urlAsset = (AVURLAsset*) playerItem.asset; + + URL url (nsStringToJuce (urlAsset.URL.absoluteString)); + auto oldStatus = [change[NSKeyValueChangeOldKey] intValue]; + auto newStatus = [change[NSKeyValueChangeNewKey] intValue]; + + // Ignore spurious notifications + if (oldStatus == newStatus) + return; + + if (newStatus == AVPlayerItemStatusFailed) + { + auto errorMessage = playerItem.error != nil + ? nsStringToJuce (playerItem.error.localizedDescription) + : String(); + + owner.notifyOwnerPreparationFinished (url, Result::fail (errorMessage), nullptr); + } + else if (newStatus == AVPlayerItemStatusReadyToPlay) + { + owner.notifyOwnerPreparationFinished (url, Result::ok(), owner.player.release()); + } + else + { + jassertfalse; + } + } + } + }; + + //============================================================================== + PlayerControllerBase& owner; + + std::unique_ptr asset; + std::unique_ptr, NSObjectDeleter> assetKeys; + std::unique_ptr playerItem; + std::unique_ptr playerItemPreparationStatusObserver; + std::unique_ptr player; + + //============================================================================== + void checkAllKeysReadyFor (AVAsset* assetToCheck, const URL& url) + { + NSError* error = nil; + + int successCount = 0; + + for (NSString* key : assetKeys.get()) + { + switch ([assetToCheck statusOfValueForKey: key error: &error]) + { + case AVKeyValueStatusLoaded: + { + ++successCount; + break; + } + case AVKeyValueStatusCancelled: + { + notifyOwnerPreparationFinished (url, Result::fail ("Loading cancelled"), nullptr); + return; + } + case AVKeyValueStatusFailed: + { + auto errorMessage = error != nil ? nsStringToJuce (error.localizedDescription) : String(); + notifyOwnerPreparationFinished (url, Result::fail (errorMessage), nullptr); + return; + } + default: + {} + } + } + + jassert (successCount == (int) [assetKeys.get() count]); + preparePlayerItem(); + } + + void preparePlayerItem() + { + playerItem.reset ([[AVPlayerItem alloc] initWithAsset: asset.get()]); + + attachPreparationStatusObserver(); + + player.reset ([[AVPlayer alloc] initWithPlayerItem: playerItem.get()]); + } + + //============================================================================== + void attachPreparationStatusObserver() + { + [playerItem.get() addObserver: playerItemPreparationStatusObserver.get() + forKeyPath: nsStringLiteral ("status") + options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context: this]; + } + + void detachPreparationStatusObserver() + { + if (playerItem != nullptr && playerItemPreparationStatusObserver != nullptr) + { + [playerItem.get() removeObserver: playerItemPreparationStatusObserver.get() + forKeyPath: nsStringLiteral ("status") + context: this]; + } + } + + //============================================================================== + void notifyOwnerPreparationFinished (const URL& url, Result r, AVPlayer* preparedPlayer) + { + WeakReference safeThis (this); + + MessageManager::callAsync ([safeThis, url, r, preparedPlayer]() mutable + { + if (safeThis != nullptr) + safeThis->owner.playerPreparationFinished (url, r, preparedPlayer); + }); + } + + JUCE_DECLARE_WEAK_REFERENCEABLE (PlayerAsyncInitialiser) + }; + + //============================================================================== + Pimpl& owner; + bool useNativeControls; + + PlayerAsyncInitialiser playerAsyncInitialiser; + std::unique_ptr playerStatusObserver; + std::unique_ptr playerItemPlaybackStatusObserver; + + //============================================================================== + PlayerControllerBase (Pimpl& ownerToUse, bool useNativeControlsIfAvailable) + : owner (ownerToUse), + useNativeControls (useNativeControlsIfAvailable), + playerAsyncInitialiser (*this) + { + static JucePlayerStatusObserverClass playerObserverClass; + playerStatusObserver.reset ([playerObserverClass.createInstance() init]); + JucePlayerStatusObserverClass::setOwner (playerStatusObserver.get(), this); + + static JucePlayerItemPlaybackStatusObserverClass itemObserverClass; + playerItemPlaybackStatusObserver.reset ([itemObserverClass.createInstance() init]); + JucePlayerItemPlaybackStatusObserverClass::setOwner (playerItemPlaybackStatusObserver.get(), this); + } + + //============================================================================== + void attachPlayerStatusObserver() + { + [crtp().getPlayer() addObserver: playerStatusObserver.get() + forKeyPath: nsStringLiteral ("rate") + options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context: this]; + + [crtp().getPlayer() addObserver: playerStatusObserver.get() + forKeyPath: nsStringLiteral ("status") + options: NSKeyValueObservingOptionNew + context: this]; + } + + void detachPlayerStatusObserver() + { + if (crtp().getPlayer() != nullptr && playerStatusObserver != nullptr) + { + [crtp().getPlayer() removeObserver: playerStatusObserver.get() + forKeyPath: nsStringLiteral ("rate") + context: this]; + + [crtp().getPlayer() removeObserver: playerStatusObserver.get() + forKeyPath: nsStringLiteral ("status") + context: this]; + } + } + + void attachPlaybackObserver() + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + [[NSNotificationCenter defaultCenter] addObserver: playerItemPlaybackStatusObserver.get() + selector: @selector (processNotification:) + name: AVPlayerItemDidPlayToEndTimeNotification + object: [crtp().getPlayer() currentItem]]; + #pragma clang diagnostic pop + } + + void detachPlaybackObserver() + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wundeclared-selector" + [[NSNotificationCenter defaultCenter] removeObserver: playerItemPlaybackStatusObserver.get()]; + #pragma clang diagnostic pop + } + + private: + //============================================================================== + Derived& crtp() { return static_cast (*this); } + + //============================================================================== + void playerPreparationFinished (const URL& url, Result r, AVPlayer* preparedPlayer) + { + if (preparedPlayer != nil) + crtp().setPlayer (preparedPlayer); + + owner.playerPreparationFinished (url, r); + } + + void playbackReachedEndTime() + { + WeakReference safeThis (this); + + MessageManager::callAsync ([safeThis]() mutable + { + if (safeThis != nullptr) + safeThis->owner.playbackReachedEndTime(); + }); + } + + //============================================================================== + void errorOccurred() + { + auto errorMessage = (crtp().getPlayer() != nil && crtp().getPlayer().error != nil) + ? nsStringToJuce (crtp().getPlayer().error.localizedDescription) + : String(); + + owner.errorOccurred (errorMessage); + } + + void playbackStarted() + { + owner.playbackStarted(); + } + + void playbackStopped() + { + owner.playbackStopped(); + } + + JUCE_DECLARE_WEAK_REFERENCEABLE (PlayerControllerBase) + }; + + #if JUCE_MAC + //============================================================================== + class PlayerController : public PlayerControllerBase + { + public: + PlayerController (Pimpl& ownerToUse, bool useNativeControlsIfAvailable) + : PlayerControllerBase (ownerToUse, useNativeControlsIfAvailable) + { + #if JUCE_32BIT + // 32-bit builds don't have AVPlayerView, so need to use a layer + useNativeControls = false; + #endif + + if (useNativeControls) + { + #if ! JUCE_32BIT + playerView = [[AVPlayerView alloc] init]; + #endif + } + else + { + view = [[NSView alloc] init]; + playerLayer = [[AVPlayerLayer alloc] init]; + [view setLayer: playerLayer]; + } + } + + ~PlayerController() + { + #if JUCE_32BIT + [view release]; + [playerLayer release]; + #else + [playerView release]; + #endif + } + + NSView* getView() + { + #if ! JUCE_32BIT + if (useNativeControls) + return playerView; + #endif + + return view; + } + + Result load (NSURL* url) + { + if (auto* player = [AVPlayer playerWithURL: url]) + { + setPlayer (player); + return Result::ok(); + } + + return Result::fail ("Couldn't open movie"); + } + + void loadAsync (URL url) + { + playerAsyncInitialiser.loadAsync (url); + } + + void close() { setPlayer (nil); } + + void setPlayer (AVPlayer* player) + { + #if ! JUCE_32BIT + if (useNativeControls) + { + [playerView setPlayer: player]; + attachPlayerStatusObserver(); + attachPlaybackObserver(); + return; + } + #endif + + [playerLayer setPlayer: player]; + attachPlayerStatusObserver(); + attachPlaybackObserver(); + } + + AVPlayer* getPlayer() const + { + #if ! JUCE_32BIT + if (useNativeControls) + return [playerView player]; + #endif + + return [playerLayer player]; + } + + private: + NSView* view = nil; + AVPlayerLayer* playerLayer = nil; + #if ! JUCE_32BIT + // 32-bit builds don't have AVPlayerView + AVPlayerView* playerView = nil; + #endif + }; #else - AVPlayerView* controller = nil; + //============================================================================== + class PlayerController : public PlayerControllerBase + { + public: + PlayerController (Pimpl& ownerToUse, bool useNativeControlsIfAvailable) + : PlayerControllerBase (ownerToUse, useNativeControlsIfAvailable) + { + if (useNativeControls) + { + playerViewController.reset ([[AVPlayerViewController alloc] init]); + } + else + { + static JuceVideoViewerClass cls; + playerView.reset ([cls.createInstance() init]); + + playerLayer.reset ([[AVPlayerLayer alloc] init]); + [playerView.get().layer addSublayer: playerLayer.get()]; + } + } + + UIView* getView() + { + if (useNativeControls) + return [playerViewController.get() view]; + + // Should call getView() only once. + jassert (playerView != nil); + return playerView.release(); + } + + Result load (NSURL*) + { + jassertfalse; + return Result::fail ("Synchronous loading is not supported on iOS, use loadAsync()"); + } + + void loadAsync (URL url) + { + playerAsyncInitialiser.loadAsync (url); + } + + void close() { setPlayer (nil); } + + AVPlayer* getPlayer() const + { + if (useNativeControls) + return [playerViewController.get() player]; + + return [playerLayer.get() player]; + } + + void setPlayer (AVPlayer* playerToUse) + { + if (useNativeControls) + [playerViewController.get() setPlayer: playerToUse]; + else + [playerLayer.get() setPlayer: playerToUse]; + + attachPlayerStatusObserver(); + attachPlaybackObserver(); + } + + private: + //============================================================================== + struct JuceVideoViewerClass : public ObjCClass + { + JuceVideoViewerClass() : ObjCClass ("JuceVideoViewerClass_") + { + addMethod (@selector (layoutSubviews), layoutSubviews, "v@:"); + + registerClass(); + } + + private: + static void layoutSubviews (id self, SEL) + { + sendSuperclassMessage (self, @selector (layoutSubviews)); + + UIView* asUIView = (UIView*) self; + + if (auto* previewLayer = getPreviewLayer (self)) + previewLayer.frame = asUIView.bounds; + } + + static AVPlayerLayer* getPreviewLayer (id self) + { + UIView* asUIView = (UIView*) self; + + if (asUIView.layer.sublayers != nil && [asUIView.layer.sublayers count] > 0) + if ([asUIView.layer.sublayers[0] isKindOfClass: [AVPlayerLayer class]]) + return (AVPlayerLayer*) asUIView.layer.sublayers[0]; + + return nil; + } + }; + + //============================================================================== + std::unique_ptr playerViewController; + + std::unique_ptr playerView; + std::unique_ptr playerLayer; + }; #endif - AVPlayer* getAVPlayer() const noexcept { return [controller player]; } + //============================================================================== + VideoComponent& owner; + + PlayerController playerController; + + std::function loadFinishedCallback; + + double playSpeedMult = 1.0; static double toSeconds (const CMTime& t) noexcept { return t.timescale != 0 ? (t.value / (double) t.timescale) : 0.0; } + void playerPreparationFinished (const URL& url, Result r) + { + owner.resized(); + + loadFinishedCallback (url, r); + loadFinishedCallback = nullptr; + } + + 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 playbackReachedEndTime() + { + stop(); + setPosition (0.0); + } + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) }; diff --git a/modules/juce_video/native/juce_win32_Video.h b/modules/juce_video/native/juce_win32_Video.h index dd25a2ea5f..ccbe0401cc 100644 --- a/modules/juce_video/native/juce_win32_Video.h +++ b/modules/juce_video/native/juce_win32_Video.h @@ -160,7 +160,9 @@ namespace VideoRenderers //============================================================================== struct VideoComponent::Pimpl : public Component { - Pimpl() : videoLoaded (false) + Pimpl (VideoComponent& ownerToUse, bool) + : owner (ownerToUse), + videoLoaded (false) { setOpaque (true); context.reset (new DirectShowContext (*this)); @@ -257,6 +259,11 @@ struct VideoComponent::Pimpl : public Component context->setSpeed (newSpeed); } + double getSpeed() const + { + return videoLoaded ? context->getSpeed() : 0.0; + } + Rectangle getNativeSize() const { return videoLoaded ? context->getVideoSize() @@ -307,10 +314,30 @@ struct VideoComponent::Pimpl : public Component repaint(); } + void playbackStarted() + { + if (owner.onPlaybackStarted != nullptr) + owner.onPlaybackStarted(); + } + + void playbackStopped() + { + if (owner.onPlaybackStopped != nullptr) + owner.onPlaybackStopped(); + } + + void errorOccurred (const String& errorMessage) + { + if (owner.onErrorOccurred != nullptr) + owner.onErrorOccurred (errorMessage); + } + File currentFile; URL currentURL; private: + VideoComponent& owner; + bool videoLoaded; //============================================================================== @@ -395,12 +422,15 @@ private: deleteNativeWindow(); mediaEvent->SetNotifyWindow (0, 0, 0); + if (videoRenderer != nullptr) videoRenderer->setVideoWindow (nullptr); createNativeWindow(); + mediaEvent->CancelDefaultHandling (EC_STATE_CHANGE); mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0); + if (videoRenderer != nullptr) videoRenderer->setVideoWindow (hwnd); } @@ -510,7 +540,10 @@ private: // set window to receive events if (SUCCEEDED (hr)) + { + mediaEvent->CancelDefaultHandling (EC_STATE_CHANGE); hr = mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0); + } if (SUCCEEDED (hr)) { @@ -586,22 +619,33 @@ private: switch (ec) { - case EC_REPAINT: - component.repaint(); - break; + case EC_REPAINT: + component.repaint(); + break; - case EC_COMPLETE: - component.stop(); - break; + case EC_COMPLETE: + component.stop(); + component.setPosition (0.0); + break; - case EC_USERABORT: - case EC_ERRORABORT: - case EC_ERRORABORTEX: - component.close(); - break; + case EC_ERRORABORT: + case EC_ERRORABORTEX: + component.errorOccurred (getErrorMessageFromResult ((HRESULT) p1).getErrorMessage()); + // intentional fallthrough + case EC_USERABORT: + component.close(); + break; - default: - break; + case EC_STATE_CHANGE: + switch (p1) + { + case State_Paused: component.playbackStopped(); break; + case State_Running: component.playbackStarted(); break; + default: break; + } + + default: + break; } } } @@ -644,6 +688,13 @@ private: return duration; } + double getSpeed() const + { + double speed; + mediaPosition->get_Rate (&speed); + return speed; + } + double getPosition() const { REFTIME seconds; diff --git a/modules/juce_video/playback/juce_VideoComponent.cpp b/modules/juce_video/playback/juce_VideoComponent.cpp index c83fea652a..79962ac722 100644 --- a/modules/juce_video/playback/juce_VideoComponent.cpp +++ b/modules/juce_video/playback/juce_VideoComponent.cpp @@ -25,16 +25,19 @@ namespace juce { -#if JUCE_MAC || JUCE_IOS || JUCE_MSVC +#if ! JUCE_LINUX #if JUCE_MAC || JUCE_IOS #include "../native/juce_mac_Video.h" #elif JUCE_WINDOWS #include "../native/juce_win32_Video.h" +#elif JUCE_ANDROID + #include "../native/juce_android_Video.h" #endif //============================================================================== -VideoComponent::VideoComponent() : pimpl (new Pimpl()) +VideoComponent::VideoComponent (bool useNativeControlsIfAvailable) + : pimpl (new Pimpl (*this, useNativeControlsIfAvailable)) { addAndMakeVisible (pimpl.get()); } @@ -46,22 +49,55 @@ VideoComponent::~VideoComponent() Result VideoComponent::load (const File& file) { + #if JUCE_ANDROID || JUCE_IOS + ignoreUnused (file); + jassertfalse; + return Result::fail ("load() is not supported on this platform. Use loadAsync() instead."); + #else auto r = pimpl->load (file); resized(); return r; + #endif } Result VideoComponent::load (const URL& url) { + #if JUCE_ANDROID || JUCE_IOS + // You need to use loadAsync on Android & iOS. + ignoreUnused (url); + jassertfalse; + return Result::fail ("load() is not supported on this platform. Use loadAsync() instead."); + #else auto r = pimpl->load (url); resized(); return r; + #endif +} + +void VideoComponent::loadAsync (const URL& url, std::function callback) +{ + if (callback == nullptr) + { + jassertfalse; + return; + } + + #if JUCE_ANDROID || JUCE_IOS || JUCE_MAC + pimpl->loadAsync (url, callback); + #else + auto result = load (url); + callback (url, result); + #endif } void VideoComponent::closeVideo() { pimpl->close(); + // Closing on Android is async and resized() will be called internally by pimpl once + // close operation finished. + #if ! JUCE_ANDROID// TODO JUCE_IOS too? resized(); + #endif } bool VideoComponent::isVideoOpen() const { return pimpl->isOpen(); } @@ -81,6 +117,8 @@ void VideoComponent::setPlayPosition (double newPos) { pimpl->setPosition double VideoComponent::getPlayPosition() const { return pimpl->getPosition(); } void VideoComponent::setPlaySpeed (double newSpeed) { pimpl->setSpeed (newSpeed); } +double VideoComponent::getPlaySpeed() const { return pimpl->getSpeed(); } + void VideoComponent::setAudioVolume (float newVolume) { pimpl->setVolume (newVolume); } float VideoComponent::getAudioVolume() const { return pimpl->getVolume(); } diff --git a/modules/juce_video/playback/juce_VideoComponent.h b/modules/juce_video/playback/juce_VideoComponent.h index b960cdfbac..901653dee5 100644 --- a/modules/juce_video/playback/juce_VideoComponent.h +++ b/modules/juce_video/playback/juce_VideoComponent.h @@ -44,25 +44,55 @@ public: //============================================================================== /** Creates an empty VideoComponent. - Use the load() method to open a video once you've added this component to - a parent (or put it on the desktop). + Use the loadAsync() or load() method to open a video once you've added + this component to a parent (or put it on the desktop). + + If useNativeControlsIfAvailable is enabled and a target OS has a video view with + dedicated controls for transport etc, that view will be used. In opposite + case a bare video view without any controls will be presented, allowing you to + tailor your own UI. Currently this flag is used on iOS and 64bit macOS. + Android, Windows and 32bit macOS will always use plain video views without + dedicated controls. */ - VideoComponent(); + VideoComponent (bool useNativeControlsIfAvailable); /** Destructor. */ ~VideoComponent(); //============================================================================== /** Tries to load a video from a local file. + + This function is supported on macOS and Windows. For iOS and Android, use + loadAsync() instead. + @returns an error if the file failed to be loaded correctly + + @see loadAsync */ Result load (const File& file); /** Tries to load a video from a URL. + + This function is supported on macOS and Windows. For iOS and Android, use + loadAsync() instead. + @returns an error if the file failed to be loaded correctly + + @see loadAsync */ Result load (const URL& url); + /** Tries to load a video from a URL asynchronously. When finished, invokes the + callback supplied to the function on the message thread. + + This is the preferred way of loading content, since it works not only on + macOS and Windows, but also on iOS and Android. On Windows, it will internally + call load(). + + @see load + */ + void loadAsync (const URL& url, std::function loadFinishedCallback); + /** Closes the video and resets the component. */ void closeVideo(); @@ -109,6 +139,9 @@ public: */ void setPlaySpeed (double newSpeed); + /** Returns the current play speed of the video. */ + double getPlaySpeed() const; + /** Changes the video's playback volume. @param newVolume the volume in the range 0 (silent) to 1.0 (full) */ @@ -119,6 +152,23 @@ public: */ float getAudioVolume() const; + #if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME + /** Set this callback to be notified whenever OS global media volume changes. + Currently used on Android only. + */ + std::function onGlobalMediaVolumeChanged; + #endif + + /** Set this callback to be notified whenever the playback starts. */ + std::function onPlaybackStarted; + + /** Set this callback to be notified whenever the playback stops. */ + std::function onPlaybackStopped; + + /** Set this callback to be notified whenever an error occurs. Upon error, you + may need to load the video again. */ + std::function onErrorOccurred; + private: //============================================================================== struct Pimpl; @@ -129,6 +179,24 @@ private: void resized() override; void timerCallback() override; + #if JUCE_ANDROID + 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 (VideoComponent) };