| @@ -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) | |||
| @@ -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<MediaSession.QueueItem> 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) | |||
| { | |||
| @@ -2840,6 +2840,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_product_unlocking\juce_product_unlocking.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4827,6 +4827,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -2840,6 +2840,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_product_unlocking\juce_product_unlocking.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4827,6 +4827,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -2840,6 +2840,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_product_unlocking\juce_product_unlocking.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4827,6 +4827,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -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."); | |||
| } | |||
| }); | |||
| } | |||
| //============================================================================== | |||
| @@ -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> 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> 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<AlertWindow> (aw)); | |||
| aw->enterModalState (true, callback, true); | |||
| } | |||
| static void videoUrlPromptClosed (int result, VideoDemo* owner, Component::SafePointer<AlertWindow> 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<int> (1000 * playPositionSeconds); | |||
| int posMinutes = positionMs / 60000; | |||
| int posSeconds = (positionMs % 60000) / 1000; | |||
| int posMillis = positionMs % 1000; | |||
| auto totalMs = static_cast<int> (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 | |||
| @@ -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<MediaSession.QueueItem> 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) | |||
| { | |||
| @@ -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) | |||
| @@ -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<MediaSession.QueueItem> 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) | |||
| { | |||
| @@ -2436,6 +2436,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_opengl\juce_opengl.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4065,6 +4065,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -2436,6 +2436,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_opengl\juce_opengl.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4065,6 +4065,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -2436,6 +2436,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_opengl\juce_opengl.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4065,6 +4065,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -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) | |||
| @@ -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); | |||
| } | |||
| //============================================================================== | |||
| @@ -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<int> (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<int> (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<int> (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"); | |||
| @@ -2642,6 +2642,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_product_unlocking\juce_product_unlocking.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4473,6 +4473,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -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) | |||
| @@ -2423,6 +2423,7 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_opengl\juce_opengl.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\capture\juce_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_CameraDevice.h"/> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_mac_Video.h"/> | |||
| @@ -4014,6 +4014,9 @@ | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_android_Video.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| <ClInclude Include="..\..\..\..\modules\juce_video\native\juce_ios_CameraDevice.h"> | |||
| <Filter>JUCE Modules\juce_video\native</Filter> | |||
| </ClInclude> | |||
| @@ -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) | |||
| @@ -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<MediaSession.QueueItem> 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$$ | |||
| @@ -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) | |||
| @@ -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 {}; | |||
| @@ -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, "<init>", "()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<jobject> exception (env->ExceptionOccurred()); | |||
| if (exception != nullptr) | |||
| { | |||
| env->ExceptionClear(); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| } | |||
| //============================================================================== | |||
| @@ -778,4 +812,24 @@ LocalRef<jobject> CreateJavaInterface (AndroidInterfaceImplementer* implementer, | |||
| LocalRef<jobject> 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 | |||
| @@ -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; | |||
| @@ -307,14 +307,10 @@ private: | |||
| auto inputStream = StreamCloser (LocalRef<jobject> (env->CallObjectMethod (assetFd, | |||
| AssetFileDescriptor.createInputStream))); | |||
| auto exception = LocalRef<jobject> (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<jobject> (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<jobject> (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<jobject> (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; | |||
| } | |||
| @@ -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, "<init>", "()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, "<init>", "(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<jobject> (env->NewObject (AudioAttributesBuilder, AudioAttributesBuilder.constructor)); | |||
| auto AndroidAudioAttributesBuilder = LocalRef<jobject> (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<jobject> (env->CallObjectMethod (audioAttributesBuilder, AudioAttributesBuilder.build)); | |||
| env->CallObjectMethod (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.setContentType, contentTypeSonification); | |||
| env->CallObjectMethod (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.setUsage, usageNotification); | |||
| auto audioAttributes = LocalRef<jobject> (env->CallObjectMethod (AndroidAudioAttributesBuilder, AndroidAudioAttributesBuilder.build)); | |||
| env->CallVoidMethod (channel, NotificationChannel.setSound, juceUrlToAndroidUri (c.soundToPlay).get(), audioAttributes.get()); | |||
| env->CallVoidMethod (notificationManager, NotificationManagerApi26.createNotificationChannel, channel.get()); | |||
| @@ -61,7 +61,8 @@ public: | |||
| // create a native surface view | |||
| surfaceView = GlobalRef (env->CallObjectMethod (android.activity.get(), | |||
| JuceAppActivity.createNativeSurfaceView, | |||
| reinterpret_cast<jlong> (this))); | |||
| reinterpret_cast<jlong> (this), | |||
| false)); | |||
| if (surfaceView.get() == nullptr) | |||
| return; | |||
| @@ -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" | |||
| @@ -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<jobject>& 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<jobject> (env->ExceptionOccurred()); | |||
| if (exception != 0) | |||
| env->ExceptionClear(); | |||
| jniCheckHasExceptionOccurredAndClear(); | |||
| unlockScreenOrientation(); | |||
| } | |||
| @@ -1630,16 +1583,12 @@ private: | |||
| } | |||
| } | |||
| auto exception = LocalRef<jobject> (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<jobject> (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<jobject> (env->ExceptionOccurred()); | |||
| if (exception != 0) | |||
| env->ExceptionClear(); | |||
| jniCheckHasExceptionOccurredAndClear(); | |||
| handlerThread.clear(); | |||
| handler.clear(); | |||
| } | |||
| static bool checkHasExceptionOccurred() | |||
| { | |||
| auto* env = getEnv(); | |||
| auto exception = LocalRef<jobject> (env->ExceptionOccurred()); | |||
| if (exception != 0) | |||
| { | |||
| env->ExceptionClear(); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| #endif | |||
| friend struct CameraDevice::ViewerComponent; | |||
| @@ -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<void (const URL&, Result)> 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<int> 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 <typename Derived> | |||
| class PlayerControllerBase | |||
| { | |||
| public: | |||
| ~PlayerControllerBase() | |||
| { | |||
| detachPlayerStatusObserver(); | |||
| detachPlaybackObserver(); | |||
| } | |||
| protected: | |||
| //============================================================================== | |||
| struct JucePlayerStatusObserverClass : public ObjCClass<NSObject> | |||
| { | |||
| JucePlayerStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerStatusObserverClass_") | |||
| { | |||
| #pragma clang diagnostic push | |||
| #pragma clang diagnostic ignored "-Wundeclared-selector" | |||
| addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?"); | |||
| #pragma clang diagnostic pop | |||
| addIvar<PlayerAsyncInitialiser*> ("owner"); | |||
| registerClass(); | |||
| } | |||
| //============================================================================== | |||
| static PlayerControllerBase& getOwner (id self) { return *getIvar<PlayerControllerBase*> (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<NSKeyValueChangeKey, id>* 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<NSObject> | |||
| { | |||
| JucePlayerItemPlaybackStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerItemPlaybackStatusObserverClass_") | |||
| { | |||
| #pragma clang diagnostic push | |||
| #pragma clang diagnostic ignored "-Wundeclared-selector" | |||
| addMethod (@selector (processNotification:), notificationReceived, "v@:@"); | |||
| #pragma clang diagnostic pop | |||
| addIvar<PlayerControllerBase*> ("owner"); | |||
| registerClass(); | |||
| } | |||
| //============================================================================== | |||
| static PlayerControllerBase& getOwner (id self) { return *getIvar<PlayerControllerBase*> (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<NSObject> | |||
| { | |||
| JucePlayerItemPreparationStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerItemStatusObserverClass_") | |||
| { | |||
| #pragma clang diagnostic push | |||
| #pragma clang diagnostic ignored "-Wundeclared-selector" | |||
| addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?"); | |||
| #pragma clang diagnostic pop | |||
| addIvar<PlayerAsyncInitialiser*> ("owner"); | |||
| registerClass(); | |||
| } | |||
| //============================================================================== | |||
| static PlayerAsyncInitialiser& getOwner (id self) { return *getIvar<PlayerAsyncInitialiser*> (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<NSKeyValueChangeKey, id>* 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<AVURLAsset, NSObjectDeleter> asset; | |||
| std::unique_ptr<NSArray<NSString*>, NSObjectDeleter> assetKeys; | |||
| std::unique_ptr<AVPlayerItem, NSObjectDeleter> playerItem; | |||
| std::unique_ptr<NSObject, NSObjectDeleter> playerItemPreparationStatusObserver; | |||
| std::unique_ptr<AVPlayer, NSObjectDeleter> 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<PlayerAsyncInitialiser> 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<NSObject, NSObjectDeleter> playerStatusObserver; | |||
| std::unique_ptr<NSObject, NSObjectDeleter> 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<Derived&> (*this); } | |||
| //============================================================================== | |||
| void playerPreparationFinished (const URL& url, Result r, AVPlayer* preparedPlayer) | |||
| { | |||
| if (preparedPlayer != nil) | |||
| crtp().setPlayer (preparedPlayer); | |||
| owner.playerPreparationFinished (url, r); | |||
| } | |||
| void playbackReachedEndTime() | |||
| { | |||
| WeakReference<PlayerControllerBase> 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<PlayerController> | |||
| { | |||
| 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<PlayerController> | |||
| { | |||
| 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<UIView> | |||
| { | |||
| JuceVideoViewerClass() : ObjCClass<UIView> ("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<AVPlayerViewController, NSObjectDeleter> playerViewController; | |||
| std::unique_ptr<UIView, NSObjectDeleter> playerView; | |||
| std::unique_ptr<AVPlayerLayer, NSObjectDeleter> playerLayer; | |||
| }; | |||
| #endif | |||
| AVPlayer* getAVPlayer() const noexcept { return [controller player]; } | |||
| //============================================================================== | |||
| VideoComponent& owner; | |||
| PlayerController playerController; | |||
| std::function<void (const URL&, Result)> 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) | |||
| }; | |||
| @@ -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<int> 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_COMPLETE: | |||
| component.stop(); | |||
| break; | |||
| case EC_USERABORT: | |||
| case EC_ERRORABORT: | |||
| case EC_ERRORABORTEX: | |||
| component.close(); | |||
| break; | |||
| default: | |||
| break; | |||
| case EC_REPAINT: | |||
| component.repaint(); | |||
| break; | |||
| case EC_COMPLETE: | |||
| component.stop(); | |||
| component.setPosition (0.0); | |||
| break; | |||
| case EC_ERRORABORT: | |||
| case EC_ERRORABORTEX: | |||
| component.errorOccurred (getErrorMessageFromResult ((HRESULT) p1).getErrorMessage()); | |||
| // intentional fallthrough | |||
| case EC_USERABORT: | |||
| component.close(); | |||
| 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; | |||
| @@ -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<void (const URL&, Result)> 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(); } | |||
| @@ -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<void (const URL&, Result)> 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<void()> onGlobalMediaVolumeChanged; | |||
| #endif | |||
| /** Set this callback to be notified whenever the playback starts. */ | |||
| std::function<void()> onPlaybackStarted; | |||
| /** Set this callback to be notified whenever the playback stops. */ | |||
| std::function<void()> onPlaybackStopped; | |||
| /** Set this callback to be notified whenever an error occurs. Upon error, you | |||
| may need to load the video again. */ | |||
| std::function<void (const String& /*error*/)> 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) | |||
| }; | |||