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

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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_REPAINT:
component.repaint();
break;
case EC_COMPLETE:
component.stop();
break;
case EC_COMPLETE:
component.stop();
component.setPosition (0.0);
break;
case EC_USERABORT:
case EC_ERRORABORT:
case EC_ERRORABORTEX:
component.close();
break;
case EC_ERRORABORT:
case EC_ERRORABORTEX:
component.errorOccurred (getErrorMessageFromResult ((HRESULT) p1).getErrorMessage());
// intentional fallthrough
case EC_USERABORT:
component.close();
break;
default:
break;
case EC_STATE_CHANGE:
switch (p1)
{
case State_Paused: component.playbackStopped(); break;
case State_Running: component.playbackStarted(); break;
default: break;
}
default:
break;
}
}
}
@ -644,6 +688,13 @@ private:
return duration;
}
double getSpeed() const
{
double speed;
mediaPosition->get_Rate (&speed);
return speed;
}
double getPosition() const
{
REFTIME seconds;

View file

@ -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(); }

View file

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