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

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