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:
parent
dc7217fbbb
commit
315326477d
45 changed files with 4293 additions and 308 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue