1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Add video playback support for Android and iOS. Update VideoComponent API to support building custom UIs.

This commit is contained in:
Lukasz Kozakiewicz 2018-05-11 17:57:26 +02:00
parent dc7217fbbb
commit 315326477d
45 changed files with 4293 additions and 308 deletions

View file

@ -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)

View file

@ -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)
{

View file

@ -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"/>

View file

@ -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>

View file

@ -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"/>

View file

@ -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>

View file

@ -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"/>

View file

@ -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>

View file

@ -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)

View file

@ -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)

View file

@ -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.");
}
});
}
//==============================================================================

View file

@ -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