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
|
|
@ -79,6 +79,25 @@
|
|||
#undef JUCE_USE_CAMERA
|
||||
#endif
|
||||
|
||||
//=============================================================================
|
||||
/** Config: JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
||||
Enables synchronisation between video playback volume and OS media volume.
|
||||
Currently supported on Android only.
|
||||
*/
|
||||
#ifndef JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
||||
#define JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME 1
|
||||
#endif
|
||||
|
||||
#ifndef JUCE_VIDEO_LOG_ENABLED
|
||||
#define JUCE_VIDEO_LOG_ENABLED 1
|
||||
#endif
|
||||
|
||||
#if JUCE_VIDEO_LOG_ENABLED
|
||||
#define JUCE_VIDEO_LOG(x) DBG(x)
|
||||
#else
|
||||
#define JUCE_VIDEO_LOG(x) {}
|
||||
#endif
|
||||
|
||||
//=============================================================================
|
||||
#include "playback/juce_VideoComponent.h"
|
||||
#include "capture/juce_CameraDevice.h"
|
||||
|
|
|
|||
|
|
@ -442,49 +442,6 @@ private:
|
|||
Owner& owner;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AppPausedResumedListener : public AndroidInterfaceImplementer
|
||||
{
|
||||
public:
|
||||
struct Owner
|
||||
{
|
||||
virtual ~Owner() {}
|
||||
|
||||
virtual void appPaused() = 0;
|
||||
virtual void appResumed() = 0;
|
||||
};
|
||||
|
||||
AppPausedResumedListener (Owner& ownerToUse)
|
||||
: owner (ownerToUse)
|
||||
{}
|
||||
|
||||
jobject invoke (jobject proxy, jobject method, jobjectArray args) override
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));
|
||||
|
||||
int numArgs = args != nullptr ? env->GetArrayLength (args) : 0;
|
||||
|
||||
if (methodName == "appPaused" && numArgs == 0)
|
||||
{
|
||||
owner.appPaused();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (methodName == "appResumed" && numArgs == 0)
|
||||
{
|
||||
owner.appResumed();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return AndroidInterfaceImplementer::invoke (proxy, method, args);
|
||||
}
|
||||
|
||||
private:
|
||||
Owner& owner;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct CameraDevice::Pimpl
|
||||
#if __ANDROID_API__ >= 21
|
||||
|
|
@ -506,7 +463,6 @@ struct CameraDevice::Pimpl
|
|||
appPausedResumedListener (*this),
|
||||
appPausedResumedListenerNative (CreateJavaInterface (&appPausedResumedListener,
|
||||
JUCE_ANDROID_ACTIVITY_CLASSPATH "$AppPausedResumedListener").get()),
|
||||
|
||||
cameraManager (initialiseCameraManager()),
|
||||
cameraCharacteristics (initialiseCameraCharacteristics (cameraManager, cameraId)),
|
||||
streamConfigurationMap (cameraCharacteristics),
|
||||
|
|
@ -869,7 +825,7 @@ private:
|
|||
|
||||
bool isOutputSupportedForSurface (const LocalRef<jobject>& surface) const
|
||||
{
|
||||
return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get());
|
||||
return getEnv()->CallBooleanMethod (scalerStreamConfigurationMap, AndroidStreamConfigurationMap.isOutputSupportedForSurface, surface.get()) != 0;
|
||||
}
|
||||
|
||||
static constexpr int jpegImageFormat = 256;
|
||||
|
|
@ -1460,10 +1416,7 @@ private:
|
|||
|
||||
// ... ignore RuntimeException that can be thrown if stop() was called after recording
|
||||
// has started but before any frame was written to a file. This is not an error.
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
env->ExceptionClear();
|
||||
jniCheckHasExceptionOccurredAndClear();
|
||||
|
||||
unlockScreenOrientation();
|
||||
}
|
||||
|
|
@ -1630,16 +1583,12 @@ private:
|
|||
}
|
||||
}
|
||||
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
// When exception occurs, CameraCaptureSession.close will never finish, so
|
||||
// we should not wait for it. For fatal error an exception does occur, but
|
||||
// it is catched internally in Java...
|
||||
if (exception != 0 || scopedCameraDevice.fatalErrorOccurred.get())
|
||||
if (jniCheckHasExceptionOccurredAndClear() || scopedCameraDevice.fatalErrorOccurred.get())
|
||||
{
|
||||
JUCE_CAMERA_LOG ("Exception or fatal error occurred while closing Capture Session, closing by force");
|
||||
|
||||
env->ExceptionClear();
|
||||
}
|
||||
else if (calledClose)
|
||||
{
|
||||
|
|
@ -1768,7 +1717,7 @@ private:
|
|||
|
||||
void lockFocus()
|
||||
{
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
JUCE_CAMERA_LOG ("Performing auto-focus if possible...");
|
||||
|
|
@ -1796,7 +1745,7 @@ private:
|
|||
// IllegalStateException can be thrown when accessing CaptureSession,
|
||||
// claiming that capture session was already closed but we may not
|
||||
// get relevant callback yet, so check for this and bailout when needed.
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
auto* env = getEnv();
|
||||
|
|
@ -1902,7 +1851,7 @@ private:
|
|||
|
||||
void captureStillPictureDelayed()
|
||||
{
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
JUCE_CAMERA_LOG ("Still picture capture, device ready, capturing now...");
|
||||
|
|
@ -1911,12 +1860,12 @@ private:
|
|||
|
||||
env->CallVoidMethod (captureSession, CameraCaptureSession.stopRepeating);
|
||||
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
env->CallVoidMethod (captureSession, CameraCaptureSession.abortCaptures);
|
||||
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
// Delay still picture capture for devices that can't handle it right after
|
||||
|
|
@ -1929,7 +1878,7 @@ private:
|
|||
|
||||
void runPrecaptureSequence()
|
||||
{
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
auto* env = getEnv();
|
||||
|
|
@ -1950,7 +1899,7 @@ private:
|
|||
|
||||
void unlockFocus()
|
||||
{
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
JUCE_CAMERA_LOG ("Unlocking focus...");
|
||||
|
|
@ -1970,7 +1919,7 @@ private:
|
|||
env->CallIntMethod (captureSession, CameraCaptureSession.capture, resetAutoFocusRequest.get(),
|
||||
nullptr, handler.get());
|
||||
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
// NB: for preview, using preview capture request again
|
||||
|
|
@ -2233,10 +2182,7 @@ private:
|
|||
|
||||
// If something went wrong we will be pinged in cameraDeviceStateError()
|
||||
// callback, silence the redundant exception.
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
env->ExceptionClear();
|
||||
jniCheckHasExceptionOccurredAndClear();
|
||||
}
|
||||
|
||||
void close()
|
||||
|
|
@ -2500,12 +2446,12 @@ private:
|
|||
|
||||
env->CallVoidMethod (session, CameraCaptureSession.stopRepeating);
|
||||
|
||||
if (Pimpl::checkHasExceptionOccurred())
|
||||
if (jniCheckHasExceptionOccurredAndClear())
|
||||
return;
|
||||
|
||||
env->CallVoidMethod (session, CameraCaptureSession.abortCaptures);
|
||||
|
||||
Pimpl::checkHasExceptionOccurred();
|
||||
jniCheckHasExceptionOccurredAndClear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3064,29 +3010,11 @@ private:
|
|||
env->CallBooleanMethod (handlerThread, AndroidHandlerThread.quitSafely);
|
||||
env->CallVoidMethod (handlerThread, AndroidHandlerThread.join);
|
||||
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
env->ExceptionClear();
|
||||
jniCheckHasExceptionOccurredAndClear();
|
||||
|
||||
handlerThread.clear();
|
||||
handler.clear();
|
||||
}
|
||||
|
||||
static bool checkHasExceptionOccurred()
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto exception = LocalRef<jobject> (env->ExceptionOccurred());
|
||||
|
||||
if (exception != 0)
|
||||
{
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
friend struct CameraDevice::ViewerComponent;
|
||||
|
|
|
|||
1918
modules/juce_video/native/juce_android_Video.h
Normal file
1918
modules/juce_video/native/juce_android_Video.h
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2015 - ROLI Ltd.
|
||||
Copyright (c) 2018 - ROLI Ltd.
|
||||
|
||||
Permission is granted to use this software under the terms of either:
|
||||
a) the GPL v2 (or any later version)
|
||||
|
|
@ -22,34 +22,26 @@
|
|||
==============================================================================
|
||||
*/
|
||||
|
||||
#if JUCE_IOS
|
||||
using BaseClass = UIViewComponent;
|
||||
#if JUCE_MAC
|
||||
using Base = NSViewComponent;
|
||||
#else
|
||||
using BaseClass = NSViewComponent;
|
||||
using Base = UIViewComponent;
|
||||
#endif
|
||||
|
||||
struct VideoComponent::Pimpl : public BaseClass
|
||||
struct VideoComponent::Pimpl : public Base
|
||||
{
|
||||
Pimpl()
|
||||
Pimpl (VideoComponent& ownerToUse, bool useNativeControlsIfAvailable)
|
||||
: owner (ownerToUse),
|
||||
playerController (*this, useNativeControlsIfAvailable)
|
||||
{
|
||||
setVisible (true);
|
||||
|
||||
#if JUCE_MAC && JUCE_32BIT
|
||||
auto view = [[NSView alloc] init]; // 32-bit builds don't have AVPlayerView, so need to use a layer
|
||||
controller = [[AVPlayerLayer alloc] init];
|
||||
auto* view = playerController.getView();
|
||||
setView (view);
|
||||
|
||||
#if JUCE_MAC
|
||||
[view setNextResponder: [view superview]];
|
||||
[view setWantsLayer: YES];
|
||||
[view setLayer: controller];
|
||||
[view release];
|
||||
#elif JUCE_MAC
|
||||
controller = [[AVPlayerView alloc] init];
|
||||
setView (controller);
|
||||
[controller setNextResponder: [controller superview]];
|
||||
[controller setWantsLayer: YES];
|
||||
#else
|
||||
controller = [[AVPlayerViewController alloc] init];
|
||||
setView ([controller view]);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +49,6 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
{
|
||||
close();
|
||||
setView (nil);
|
||||
[controller release];
|
||||
}
|
||||
|
||||
Result load (const File& file)
|
||||
|
|
@ -85,34 +76,46 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
if (url != nil)
|
||||
{
|
||||
close();
|
||||
|
||||
if (auto* player = [AVPlayer playerWithURL: url])
|
||||
{
|
||||
[controller setPlayer: player];
|
||||
return Result::ok();
|
||||
}
|
||||
return playerController.load (url);
|
||||
}
|
||||
|
||||
return Result::fail ("Couldn't open movie");
|
||||
}
|
||||
|
||||
void loadAsync (const URL& url, std::function<void (const URL&, Result)> callback)
|
||||
{
|
||||
if (url.isEmpty())
|
||||
{
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
currentURL = url;
|
||||
|
||||
jassert (callback != nullptr);
|
||||
|
||||
loadFinishedCallback = std::move (callback);
|
||||
|
||||
playerController.loadAsync (url);
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
stop();
|
||||
[controller setPlayer: nil];
|
||||
playerController.close();
|
||||
currentFile = File();
|
||||
currentURL = {};
|
||||
}
|
||||
|
||||
bool isOpen() const noexcept { return getAVPlayer() != nil; }
|
||||
bool isOpen() const noexcept { return playerController.getPlayer() != nil; }
|
||||
bool isPlaying() const noexcept { return getSpeed() != 0; }
|
||||
|
||||
void play() noexcept { [getAVPlayer() play]; }
|
||||
void stop() noexcept { [getAVPlayer() pause]; }
|
||||
void play() noexcept { [playerController.getPlayer() play]; setSpeed (playSpeedMult); }
|
||||
void stop() noexcept { [playerController.getPlayer() pause]; }
|
||||
|
||||
void setPosition (double newPosition)
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
if (auto* p = playerController.getPlayer())
|
||||
{
|
||||
CMTime t = { (CMTimeValue) (100000.0 * newPosition),
|
||||
(CMTimeScale) 100000, kCMTimeFlags_Valid };
|
||||
|
|
@ -125,7 +128,7 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
|
||||
double getPosition() const
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
if (auto* p = playerController.getPlayer())
|
||||
return toSeconds ([p currentTime]);
|
||||
|
||||
return 0.0;
|
||||
|
|
@ -133,12 +136,16 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
|
||||
void setSpeed (double newSpeed)
|
||||
{
|
||||
[getAVPlayer() setRate: (float) newSpeed];
|
||||
playSpeedMult = newSpeed;
|
||||
|
||||
// Calling non 0.0 speed on a paused player would start it...
|
||||
if (isPlaying())
|
||||
[playerController.getPlayer() setRate: (float) playSpeedMult];
|
||||
}
|
||||
|
||||
double getSpeed() const
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
if (auto* p = playerController.getPlayer())
|
||||
return [p rate];
|
||||
|
||||
return 0.0;
|
||||
|
|
@ -146,9 +153,9 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
|
||||
Rectangle<int> getNativeSize() const
|
||||
{
|
||||
if (auto* player = getAVPlayer())
|
||||
if (auto* p = playerController.getPlayer())
|
||||
{
|
||||
auto s = [[player currentItem] presentationSize];
|
||||
auto s = [[p currentItem] presentationSize];
|
||||
return { (int) s.width, (int) s.height };
|
||||
}
|
||||
|
||||
|
|
@ -157,20 +164,20 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
|
||||
double getDuration() const
|
||||
{
|
||||
if (auto* player = getAVPlayer())
|
||||
return toSeconds ([[player currentItem] duration]);
|
||||
if (auto* p = playerController.getPlayer())
|
||||
return toSeconds ([[p currentItem] duration]);
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void setVolume (float newVolume)
|
||||
{
|
||||
[getAVPlayer() setVolume: newVolume];
|
||||
[playerController.getPlayer() setVolume: newVolume];
|
||||
}
|
||||
|
||||
float getVolume() const
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
if (auto* p = playerController.getPlayer())
|
||||
return [p volume];
|
||||
|
||||
return 0.0f;
|
||||
|
|
@ -180,20 +187,633 @@ struct VideoComponent::Pimpl : public BaseClass
|
|||
URL currentURL;
|
||||
|
||||
private:
|
||||
#if JUCE_IOS
|
||||
AVPlayerViewController* controller = nil;
|
||||
#elif JUCE_32BIT
|
||||
AVPlayerLayer* controller = nil;
|
||||
//==============================================================================
|
||||
template <typename Derived>
|
||||
class PlayerControllerBase
|
||||
{
|
||||
public:
|
||||
~PlayerControllerBase()
|
||||
{
|
||||
detachPlayerStatusObserver();
|
||||
detachPlaybackObserver();
|
||||
}
|
||||
|
||||
protected:
|
||||
//==============================================================================
|
||||
struct JucePlayerStatusObserverClass : public ObjCClass<NSObject>
|
||||
{
|
||||
JucePlayerStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerStatusObserverClass_")
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?");
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
addIvar<PlayerAsyncInitialiser*> ("owner");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static PlayerControllerBase& getOwner (id self) { return *getIvar<PlayerControllerBase*> (self, "owner"); }
|
||||
static void setOwner (id self, PlayerControllerBase* p) { object_setInstanceVariable (self, "owner", p); }
|
||||
|
||||
private:
|
||||
static void valueChanged (id self, SEL, NSString* keyPath, id,
|
||||
NSDictionary<NSKeyValueChangeKey, id>* change, void*)
|
||||
{
|
||||
auto& owner = getOwner (self);
|
||||
|
||||
if ([keyPath isEqualToString: nsStringLiteral ("rate")])
|
||||
{
|
||||
auto oldRate = [change[NSKeyValueChangeOldKey] floatValue];
|
||||
auto newRate = [change[NSKeyValueChangeNewKey] floatValue];
|
||||
|
||||
if (oldRate == 0 && newRate != 0)
|
||||
owner.playbackStarted();
|
||||
else if (oldRate != 0 && newRate == 0)
|
||||
owner.playbackStopped();
|
||||
}
|
||||
else if ([keyPath isEqualToString: nsStringLiteral ("status")])
|
||||
{
|
||||
auto status = [change[NSKeyValueChangeNewKey] intValue];
|
||||
|
||||
if (status == AVPlayerStatusFailed)
|
||||
owner.errorOccurred();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct JucePlayerItemPlaybackStatusObserverClass : public ObjCClass<NSObject>
|
||||
{
|
||||
JucePlayerItemPlaybackStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerItemPlaybackStatusObserverClass_")
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
addMethod (@selector (processNotification:), notificationReceived, "v@:@");
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
addIvar<PlayerControllerBase*> ("owner");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static PlayerControllerBase& getOwner (id self) { return *getIvar<PlayerControllerBase*> (self, "owner"); }
|
||||
static void setOwner (id self, PlayerControllerBase* p) { object_setInstanceVariable (self, "owner", p); }
|
||||
|
||||
private:
|
||||
static void notificationReceived (id self, SEL, NSNotification* notification)
|
||||
{
|
||||
if ([notification.name isEqualToString: AVPlayerItemDidPlayToEndTimeNotification])
|
||||
getOwner (self).playbackReachedEndTime();
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class PlayerAsyncInitialiser
|
||||
{
|
||||
public:
|
||||
PlayerAsyncInitialiser (PlayerControllerBase& ownerToUse)
|
||||
: owner (ownerToUse),
|
||||
assetKeys ([[NSArray alloc] initWithObjects: nsStringLiteral ("duration"), nsStringLiteral ("tracks"),
|
||||
nsStringLiteral ("playable"), nil])
|
||||
{
|
||||
static JucePlayerItemPreparationStatusObserverClass cls;
|
||||
playerItemPreparationStatusObserver.reset ([cls.createInstance() init]);
|
||||
JucePlayerItemPreparationStatusObserverClass::setOwner (playerItemPreparationStatusObserver.get(), this);
|
||||
}
|
||||
|
||||
~PlayerAsyncInitialiser()
|
||||
{
|
||||
detachPreparationStatusObserver();
|
||||
}
|
||||
|
||||
void loadAsync (URL url)
|
||||
{
|
||||
auto* nsUrl = [NSURL URLWithString: juceStringToNS (url.toString (true))];
|
||||
asset.reset ([[AVURLAsset alloc] initWithURL: nsUrl options: nil]);
|
||||
|
||||
[asset.get() loadValuesAsynchronouslyForKeys: assetKeys.get()
|
||||
completionHandler: ^() { checkAllKeysReadyFor (asset.get(), url); }];
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct JucePlayerItemPreparationStatusObserverClass : public ObjCClass<NSObject>
|
||||
{
|
||||
JucePlayerItemPreparationStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerItemStatusObserverClass_")
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?");
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
addIvar<PlayerAsyncInitialiser*> ("owner");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static PlayerAsyncInitialiser& getOwner (id self) { return *getIvar<PlayerAsyncInitialiser*> (self, "owner"); }
|
||||
static void setOwner (id self, PlayerAsyncInitialiser* p) { object_setInstanceVariable (self, "owner", p); }
|
||||
|
||||
private:
|
||||
static void valueChanged (id self, SEL, NSString*, id object,
|
||||
NSDictionary<NSKeyValueChangeKey, id>* change, void* context)
|
||||
{
|
||||
auto& owner = getOwner (self);
|
||||
|
||||
if (context == &owner)
|
||||
{
|
||||
auto* playerItem = (AVPlayerItem*) object;
|
||||
auto* urlAsset = (AVURLAsset*) playerItem.asset;
|
||||
|
||||
URL url (nsStringToJuce (urlAsset.URL.absoluteString));
|
||||
auto oldStatus = [change[NSKeyValueChangeOldKey] intValue];
|
||||
auto newStatus = [change[NSKeyValueChangeNewKey] intValue];
|
||||
|
||||
// Ignore spurious notifications
|
||||
if (oldStatus == newStatus)
|
||||
return;
|
||||
|
||||
if (newStatus == AVPlayerItemStatusFailed)
|
||||
{
|
||||
auto errorMessage = playerItem.error != nil
|
||||
? nsStringToJuce (playerItem.error.localizedDescription)
|
||||
: String();
|
||||
|
||||
owner.notifyOwnerPreparationFinished (url, Result::fail (errorMessage), nullptr);
|
||||
}
|
||||
else if (newStatus == AVPlayerItemStatusReadyToPlay)
|
||||
{
|
||||
owner.notifyOwnerPreparationFinished (url, Result::ok(), owner.player.release());
|
||||
}
|
||||
else
|
||||
{
|
||||
jassertfalse;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
PlayerControllerBase& owner;
|
||||
|
||||
std::unique_ptr<AVURLAsset, NSObjectDeleter> asset;
|
||||
std::unique_ptr<NSArray<NSString*>, NSObjectDeleter> assetKeys;
|
||||
std::unique_ptr<AVPlayerItem, NSObjectDeleter> playerItem;
|
||||
std::unique_ptr<NSObject, NSObjectDeleter> playerItemPreparationStatusObserver;
|
||||
std::unique_ptr<AVPlayer, NSObjectDeleter> player;
|
||||
|
||||
//==============================================================================
|
||||
void checkAllKeysReadyFor (AVAsset* assetToCheck, const URL& url)
|
||||
{
|
||||
NSError* error = nil;
|
||||
|
||||
int successCount = 0;
|
||||
|
||||
for (NSString* key : assetKeys.get())
|
||||
{
|
||||
switch ([assetToCheck statusOfValueForKey: key error: &error])
|
||||
{
|
||||
case AVKeyValueStatusLoaded:
|
||||
{
|
||||
++successCount;
|
||||
break;
|
||||
}
|
||||
case AVKeyValueStatusCancelled:
|
||||
{
|
||||
notifyOwnerPreparationFinished (url, Result::fail ("Loading cancelled"), nullptr);
|
||||
return;
|
||||
}
|
||||
case AVKeyValueStatusFailed:
|
||||
{
|
||||
auto errorMessage = error != nil ? nsStringToJuce (error.localizedDescription) : String();
|
||||
notifyOwnerPreparationFinished (url, Result::fail (errorMessage), nullptr);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
jassert (successCount == (int) [assetKeys.get() count]);
|
||||
preparePlayerItem();
|
||||
}
|
||||
|
||||
void preparePlayerItem()
|
||||
{
|
||||
playerItem.reset ([[AVPlayerItem alloc] initWithAsset: asset.get()]);
|
||||
|
||||
attachPreparationStatusObserver();
|
||||
|
||||
player.reset ([[AVPlayer alloc] initWithPlayerItem: playerItem.get()]);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void attachPreparationStatusObserver()
|
||||
{
|
||||
[playerItem.get() addObserver: playerItemPreparationStatusObserver.get()
|
||||
forKeyPath: nsStringLiteral ("status")
|
||||
options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
|
||||
context: this];
|
||||
}
|
||||
|
||||
void detachPreparationStatusObserver()
|
||||
{
|
||||
if (playerItem != nullptr && playerItemPreparationStatusObserver != nullptr)
|
||||
{
|
||||
[playerItem.get() removeObserver: playerItemPreparationStatusObserver.get()
|
||||
forKeyPath: nsStringLiteral ("status")
|
||||
context: this];
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void notifyOwnerPreparationFinished (const URL& url, Result r, AVPlayer* preparedPlayer)
|
||||
{
|
||||
WeakReference<PlayerAsyncInitialiser> safeThis (this);
|
||||
|
||||
MessageManager::callAsync ([safeThis, url, r, preparedPlayer]() mutable
|
||||
{
|
||||
if (safeThis != nullptr)
|
||||
safeThis->owner.playerPreparationFinished (url, r, preparedPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
JUCE_DECLARE_WEAK_REFERENCEABLE (PlayerAsyncInitialiser)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
Pimpl& owner;
|
||||
bool useNativeControls;
|
||||
|
||||
PlayerAsyncInitialiser playerAsyncInitialiser;
|
||||
std::unique_ptr<NSObject, NSObjectDeleter> playerStatusObserver;
|
||||
std::unique_ptr<NSObject, NSObjectDeleter> playerItemPlaybackStatusObserver;
|
||||
|
||||
//==============================================================================
|
||||
PlayerControllerBase (Pimpl& ownerToUse, bool useNativeControlsIfAvailable)
|
||||
: owner (ownerToUse),
|
||||
useNativeControls (useNativeControlsIfAvailable),
|
||||
playerAsyncInitialiser (*this)
|
||||
{
|
||||
static JucePlayerStatusObserverClass playerObserverClass;
|
||||
playerStatusObserver.reset ([playerObserverClass.createInstance() init]);
|
||||
JucePlayerStatusObserverClass::setOwner (playerStatusObserver.get(), this);
|
||||
|
||||
static JucePlayerItemPlaybackStatusObserverClass itemObserverClass;
|
||||
playerItemPlaybackStatusObserver.reset ([itemObserverClass.createInstance() init]);
|
||||
JucePlayerItemPlaybackStatusObserverClass::setOwner (playerItemPlaybackStatusObserver.get(), this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void attachPlayerStatusObserver()
|
||||
{
|
||||
[crtp().getPlayer() addObserver: playerStatusObserver.get()
|
||||
forKeyPath: nsStringLiteral ("rate")
|
||||
options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
|
||||
context: this];
|
||||
|
||||
[crtp().getPlayer() addObserver: playerStatusObserver.get()
|
||||
forKeyPath: nsStringLiteral ("status")
|
||||
options: NSKeyValueObservingOptionNew
|
||||
context: this];
|
||||
}
|
||||
|
||||
void detachPlayerStatusObserver()
|
||||
{
|
||||
if (crtp().getPlayer() != nullptr && playerStatusObserver != nullptr)
|
||||
{
|
||||
[crtp().getPlayer() removeObserver: playerStatusObserver.get()
|
||||
forKeyPath: nsStringLiteral ("rate")
|
||||
context: this];
|
||||
|
||||
[crtp().getPlayer() removeObserver: playerStatusObserver.get()
|
||||
forKeyPath: nsStringLiteral ("status")
|
||||
context: this];
|
||||
}
|
||||
}
|
||||
|
||||
void attachPlaybackObserver()
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
[[NSNotificationCenter defaultCenter] addObserver: playerItemPlaybackStatusObserver.get()
|
||||
selector: @selector (processNotification:)
|
||||
name: AVPlayerItemDidPlayToEndTimeNotification
|
||||
object: [crtp().getPlayer() currentItem]];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
void detachPlaybackObserver()
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
[[NSNotificationCenter defaultCenter] removeObserver: playerItemPlaybackStatusObserver.get()];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
Derived& crtp() { return static_cast<Derived&> (*this); }
|
||||
|
||||
//==============================================================================
|
||||
void playerPreparationFinished (const URL& url, Result r, AVPlayer* preparedPlayer)
|
||||
{
|
||||
if (preparedPlayer != nil)
|
||||
crtp().setPlayer (preparedPlayer);
|
||||
|
||||
owner.playerPreparationFinished (url, r);
|
||||
}
|
||||
|
||||
void playbackReachedEndTime()
|
||||
{
|
||||
WeakReference<PlayerControllerBase> safeThis (this);
|
||||
|
||||
MessageManager::callAsync ([safeThis]() mutable
|
||||
{
|
||||
if (safeThis != nullptr)
|
||||
safeThis->owner.playbackReachedEndTime();
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void errorOccurred()
|
||||
{
|
||||
auto errorMessage = (crtp().getPlayer() != nil && crtp().getPlayer().error != nil)
|
||||
? nsStringToJuce (crtp().getPlayer().error.localizedDescription)
|
||||
: String();
|
||||
|
||||
owner.errorOccurred (errorMessage);
|
||||
}
|
||||
|
||||
void playbackStarted()
|
||||
{
|
||||
owner.playbackStarted();
|
||||
}
|
||||
|
||||
void playbackStopped()
|
||||
{
|
||||
owner.playbackStopped();
|
||||
}
|
||||
|
||||
JUCE_DECLARE_WEAK_REFERENCEABLE (PlayerControllerBase)
|
||||
};
|
||||
|
||||
#if JUCE_MAC
|
||||
//==============================================================================
|
||||
class PlayerController : public PlayerControllerBase<PlayerController>
|
||||
{
|
||||
public:
|
||||
PlayerController (Pimpl& ownerToUse, bool useNativeControlsIfAvailable)
|
||||
: PlayerControllerBase (ownerToUse, useNativeControlsIfAvailable)
|
||||
{
|
||||
#if JUCE_32BIT
|
||||
// 32-bit builds don't have AVPlayerView, so need to use a layer
|
||||
useNativeControls = false;
|
||||
#endif
|
||||
|
||||
if (useNativeControls)
|
||||
{
|
||||
#if ! JUCE_32BIT
|
||||
playerView = [[AVPlayerView alloc] init];
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
view = [[NSView alloc] init];
|
||||
playerLayer = [[AVPlayerLayer alloc] init];
|
||||
[view setLayer: playerLayer];
|
||||
}
|
||||
}
|
||||
|
||||
~PlayerController()
|
||||
{
|
||||
#if JUCE_32BIT
|
||||
[view release];
|
||||
[playerLayer release];
|
||||
#else
|
||||
[playerView release];
|
||||
#endif
|
||||
}
|
||||
|
||||
NSView* getView()
|
||||
{
|
||||
#if ! JUCE_32BIT
|
||||
if (useNativeControls)
|
||||
return playerView;
|
||||
#endif
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
Result load (NSURL* url)
|
||||
{
|
||||
if (auto* player = [AVPlayer playerWithURL: url])
|
||||
{
|
||||
setPlayer (player);
|
||||
return Result::ok();
|
||||
}
|
||||
|
||||
return Result::fail ("Couldn't open movie");
|
||||
}
|
||||
|
||||
void loadAsync (URL url)
|
||||
{
|
||||
playerAsyncInitialiser.loadAsync (url);
|
||||
}
|
||||
|
||||
void close() { setPlayer (nil); }
|
||||
|
||||
void setPlayer (AVPlayer* player)
|
||||
{
|
||||
#if ! JUCE_32BIT
|
||||
if (useNativeControls)
|
||||
{
|
||||
[playerView setPlayer: player];
|
||||
attachPlayerStatusObserver();
|
||||
attachPlaybackObserver();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
[playerLayer setPlayer: player];
|
||||
attachPlayerStatusObserver();
|
||||
attachPlaybackObserver();
|
||||
}
|
||||
|
||||
AVPlayer* getPlayer() const
|
||||
{
|
||||
#if ! JUCE_32BIT
|
||||
if (useNativeControls)
|
||||
return [playerView player];
|
||||
#endif
|
||||
|
||||
return [playerLayer player];
|
||||
}
|
||||
|
||||
private:
|
||||
NSView* view = nil;
|
||||
AVPlayerLayer* playerLayer = nil;
|
||||
#if ! JUCE_32BIT
|
||||
// 32-bit builds don't have AVPlayerView
|
||||
AVPlayerView* playerView = nil;
|
||||
#endif
|
||||
};
|
||||
#else
|
||||
AVPlayerView* controller = nil;
|
||||
//==============================================================================
|
||||
class PlayerController : public PlayerControllerBase<PlayerController>
|
||||
{
|
||||
public:
|
||||
PlayerController (Pimpl& ownerToUse, bool useNativeControlsIfAvailable)
|
||||
: PlayerControllerBase (ownerToUse, useNativeControlsIfAvailable)
|
||||
{
|
||||
if (useNativeControls)
|
||||
{
|
||||
playerViewController.reset ([[AVPlayerViewController alloc] init]);
|
||||
}
|
||||
else
|
||||
{
|
||||
static JuceVideoViewerClass cls;
|
||||
playerView.reset ([cls.createInstance() init]);
|
||||
|
||||
playerLayer.reset ([[AVPlayerLayer alloc] init]);
|
||||
[playerView.get().layer addSublayer: playerLayer.get()];
|
||||
}
|
||||
}
|
||||
|
||||
UIView* getView()
|
||||
{
|
||||
if (useNativeControls)
|
||||
return [playerViewController.get() view];
|
||||
|
||||
// Should call getView() only once.
|
||||
jassert (playerView != nil);
|
||||
return playerView.release();
|
||||
}
|
||||
|
||||
Result load (NSURL*)
|
||||
{
|
||||
jassertfalse;
|
||||
return Result::fail ("Synchronous loading is not supported on iOS, use loadAsync()");
|
||||
}
|
||||
|
||||
void loadAsync (URL url)
|
||||
{
|
||||
playerAsyncInitialiser.loadAsync (url);
|
||||
}
|
||||
|
||||
void close() { setPlayer (nil); }
|
||||
|
||||
AVPlayer* getPlayer() const
|
||||
{
|
||||
if (useNativeControls)
|
||||
return [playerViewController.get() player];
|
||||
|
||||
return [playerLayer.get() player];
|
||||
}
|
||||
|
||||
void setPlayer (AVPlayer* playerToUse)
|
||||
{
|
||||
if (useNativeControls)
|
||||
[playerViewController.get() setPlayer: playerToUse];
|
||||
else
|
||||
[playerLayer.get() setPlayer: playerToUse];
|
||||
|
||||
attachPlayerStatusObserver();
|
||||
attachPlaybackObserver();
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct JuceVideoViewerClass : public ObjCClass<UIView>
|
||||
{
|
||||
JuceVideoViewerClass() : ObjCClass<UIView> ("JuceVideoViewerClass_")
|
||||
{
|
||||
addMethod (@selector (layoutSubviews), layoutSubviews, "v@:");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
private:
|
||||
static void layoutSubviews (id self, SEL)
|
||||
{
|
||||
sendSuperclassMessage (self, @selector (layoutSubviews));
|
||||
|
||||
UIView* asUIView = (UIView*) self;
|
||||
|
||||
if (auto* previewLayer = getPreviewLayer (self))
|
||||
previewLayer.frame = asUIView.bounds;
|
||||
}
|
||||
|
||||
static AVPlayerLayer* getPreviewLayer (id self)
|
||||
{
|
||||
UIView* asUIView = (UIView*) self;
|
||||
|
||||
if (asUIView.layer.sublayers != nil && [asUIView.layer.sublayers count] > 0)
|
||||
if ([asUIView.layer.sublayers[0] isKindOfClass: [AVPlayerLayer class]])
|
||||
return (AVPlayerLayer*) asUIView.layer.sublayers[0];
|
||||
|
||||
return nil;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
std::unique_ptr<AVPlayerViewController, NSObjectDeleter> playerViewController;
|
||||
|
||||
std::unique_ptr<UIView, NSObjectDeleter> playerView;
|
||||
std::unique_ptr<AVPlayerLayer, NSObjectDeleter> playerLayer;
|
||||
};
|
||||
#endif
|
||||
|
||||
AVPlayer* getAVPlayer() const noexcept { return [controller player]; }
|
||||
//==============================================================================
|
||||
VideoComponent& owner;
|
||||
|
||||
PlayerController playerController;
|
||||
|
||||
std::function<void (const URL&, Result)> loadFinishedCallback;
|
||||
|
||||
double playSpeedMult = 1.0;
|
||||
|
||||
static double toSeconds (const CMTime& t) noexcept
|
||||
{
|
||||
return t.timescale != 0 ? (t.value / (double) t.timescale) : 0.0;
|
||||
}
|
||||
|
||||
void playerPreparationFinished (const URL& url, Result r)
|
||||
{
|
||||
owner.resized();
|
||||
|
||||
loadFinishedCallback (url, r);
|
||||
loadFinishedCallback = nullptr;
|
||||
}
|
||||
|
||||
void errorOccurred (const String& errorMessage)
|
||||
{
|
||||
if (owner.onErrorOccurred != nullptr)
|
||||
owner.onErrorOccurred (errorMessage);
|
||||
}
|
||||
|
||||
void playbackStarted()
|
||||
{
|
||||
if (owner.onPlaybackStarted != nullptr)
|
||||
owner.onPlaybackStarted();
|
||||
}
|
||||
|
||||
void playbackStopped()
|
||||
{
|
||||
if (owner.onPlaybackStopped != nullptr)
|
||||
owner.onPlaybackStopped();
|
||||
}
|
||||
|
||||
void playbackReachedEndTime()
|
||||
{
|
||||
stop();
|
||||
setPosition (0.0);
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -160,7 +160,9 @@ namespace VideoRenderers
|
|||
//==============================================================================
|
||||
struct VideoComponent::Pimpl : public Component
|
||||
{
|
||||
Pimpl() : videoLoaded (false)
|
||||
Pimpl (VideoComponent& ownerToUse, bool)
|
||||
: owner (ownerToUse),
|
||||
videoLoaded (false)
|
||||
{
|
||||
setOpaque (true);
|
||||
context.reset (new DirectShowContext (*this));
|
||||
|
|
@ -257,6 +259,11 @@ struct VideoComponent::Pimpl : public Component
|
|||
context->setSpeed (newSpeed);
|
||||
}
|
||||
|
||||
double getSpeed() const
|
||||
{
|
||||
return videoLoaded ? context->getSpeed() : 0.0;
|
||||
}
|
||||
|
||||
Rectangle<int> getNativeSize() const
|
||||
{
|
||||
return videoLoaded ? context->getVideoSize()
|
||||
|
|
@ -307,10 +314,30 @@ struct VideoComponent::Pimpl : public Component
|
|||
repaint();
|
||||
}
|
||||
|
||||
void playbackStarted()
|
||||
{
|
||||
if (owner.onPlaybackStarted != nullptr)
|
||||
owner.onPlaybackStarted();
|
||||
}
|
||||
|
||||
void playbackStopped()
|
||||
{
|
||||
if (owner.onPlaybackStopped != nullptr)
|
||||
owner.onPlaybackStopped();
|
||||
}
|
||||
|
||||
void errorOccurred (const String& errorMessage)
|
||||
{
|
||||
if (owner.onErrorOccurred != nullptr)
|
||||
owner.onErrorOccurred (errorMessage);
|
||||
}
|
||||
|
||||
File currentFile;
|
||||
URL currentURL;
|
||||
|
||||
private:
|
||||
VideoComponent& owner;
|
||||
|
||||
bool videoLoaded;
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -395,12 +422,15 @@ private:
|
|||
deleteNativeWindow();
|
||||
|
||||
mediaEvent->SetNotifyWindow (0, 0, 0);
|
||||
|
||||
if (videoRenderer != nullptr)
|
||||
videoRenderer->setVideoWindow (nullptr);
|
||||
|
||||
createNativeWindow();
|
||||
|
||||
mediaEvent->CancelDefaultHandling (EC_STATE_CHANGE);
|
||||
mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0);
|
||||
|
||||
if (videoRenderer != nullptr)
|
||||
videoRenderer->setVideoWindow (hwnd);
|
||||
}
|
||||
|
|
@ -510,7 +540,10 @@ private:
|
|||
|
||||
// set window to receive events
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
mediaEvent->CancelDefaultHandling (EC_STATE_CHANGE);
|
||||
hr = mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0);
|
||||
}
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
|
|
@ -586,22 +619,33 @@ private:
|
|||
|
||||
switch (ec)
|
||||
{
|
||||
case EC_REPAINT:
|
||||
component.repaint();
|
||||
break;
|
||||
case EC_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;
|
||||
|
|
|
|||
|
|
@ -25,16 +25,19 @@
|
|||
namespace juce
|
||||
{
|
||||
|
||||
#if JUCE_MAC || JUCE_IOS || JUCE_MSVC
|
||||
#if ! JUCE_LINUX
|
||||
|
||||
#if JUCE_MAC || JUCE_IOS
|
||||
#include "../native/juce_mac_Video.h"
|
||||
#elif JUCE_WINDOWS
|
||||
#include "../native/juce_win32_Video.h"
|
||||
#elif JUCE_ANDROID
|
||||
#include "../native/juce_android_Video.h"
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
VideoComponent::VideoComponent() : pimpl (new Pimpl())
|
||||
VideoComponent::VideoComponent (bool useNativeControlsIfAvailable)
|
||||
: pimpl (new Pimpl (*this, useNativeControlsIfAvailable))
|
||||
{
|
||||
addAndMakeVisible (pimpl.get());
|
||||
}
|
||||
|
|
@ -46,22 +49,55 @@ VideoComponent::~VideoComponent()
|
|||
|
||||
Result VideoComponent::load (const File& file)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS
|
||||
ignoreUnused (file);
|
||||
jassertfalse;
|
||||
return Result::fail ("load() is not supported on this platform. Use loadAsync() instead.");
|
||||
#else
|
||||
auto r = pimpl->load (file);
|
||||
resized();
|
||||
return r;
|
||||
#endif
|
||||
}
|
||||
|
||||
Result VideoComponent::load (const URL& url)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS
|
||||
// You need to use loadAsync on Android & iOS.
|
||||
ignoreUnused (url);
|
||||
jassertfalse;
|
||||
return Result::fail ("load() is not supported on this platform. Use loadAsync() instead.");
|
||||
#else
|
||||
auto r = pimpl->load (url);
|
||||
resized();
|
||||
return r;
|
||||
#endif
|
||||
}
|
||||
|
||||
void VideoComponent::loadAsync (const URL& url, std::function<void (const URL&, Result)> callback)
|
||||
{
|
||||
if (callback == nullptr)
|
||||
{
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->loadAsync (url, callback);
|
||||
#else
|
||||
auto result = load (url);
|
||||
callback (url, result);
|
||||
#endif
|
||||
}
|
||||
|
||||
void VideoComponent::closeVideo()
|
||||
{
|
||||
pimpl->close();
|
||||
// Closing on Android is async and resized() will be called internally by pimpl once
|
||||
// close operation finished.
|
||||
#if ! JUCE_ANDROID// TODO JUCE_IOS too?
|
||||
resized();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool VideoComponent::isVideoOpen() const { return pimpl->isOpen(); }
|
||||
|
|
@ -81,6 +117,8 @@ void VideoComponent::setPlayPosition (double newPos) { pimpl->setPosition
|
|||
double VideoComponent::getPlayPosition() const { return pimpl->getPosition(); }
|
||||
|
||||
void VideoComponent::setPlaySpeed (double newSpeed) { pimpl->setSpeed (newSpeed); }
|
||||
double VideoComponent::getPlaySpeed() const { return pimpl->getSpeed(); }
|
||||
|
||||
void VideoComponent::setAudioVolume (float newVolume) { pimpl->setVolume (newVolume); }
|
||||
float VideoComponent::getAudioVolume() const { return pimpl->getVolume(); }
|
||||
|
||||
|
|
|
|||
|
|
@ -44,25 +44,55 @@ public:
|
|||
//==============================================================================
|
||||
/** Creates an empty VideoComponent.
|
||||
|
||||
Use the load() method to open a video once you've added this component to
|
||||
a parent (or put it on the desktop).
|
||||
Use the loadAsync() or load() method to open a video once you've added
|
||||
this component to a parent (or put it on the desktop).
|
||||
|
||||
If useNativeControlsIfAvailable is enabled and a target OS has a video view with
|
||||
dedicated controls for transport etc, that view will be used. In opposite
|
||||
case a bare video view without any controls will be presented, allowing you to
|
||||
tailor your own UI. Currently this flag is used on iOS and 64bit macOS.
|
||||
Android, Windows and 32bit macOS will always use plain video views without
|
||||
dedicated controls.
|
||||
*/
|
||||
VideoComponent();
|
||||
VideoComponent (bool useNativeControlsIfAvailable);
|
||||
|
||||
/** Destructor. */
|
||||
~VideoComponent();
|
||||
|
||||
//==============================================================================
|
||||
/** Tries to load a video from a local file.
|
||||
|
||||
This function is supported on macOS and Windows. For iOS and Android, use
|
||||
loadAsync() instead.
|
||||
|
||||
@returns an error if the file failed to be loaded correctly
|
||||
|
||||
@see loadAsync
|
||||
*/
|
||||
Result load (const File& file);
|
||||
|
||||
/** Tries to load a video from a URL.
|
||||
|
||||
This function is supported on macOS and Windows. For iOS and Android, use
|
||||
loadAsync() instead.
|
||||
|
||||
@returns an error if the file failed to be loaded correctly
|
||||
|
||||
@see loadAsync
|
||||
*/
|
||||
Result load (const URL& url);
|
||||
|
||||
/** Tries to load a video from a URL asynchronously. When finished, invokes the
|
||||
callback supplied to the function on the message thread.
|
||||
|
||||
This is the preferred way of loading content, since it works not only on
|
||||
macOS and Windows, but also on iOS and Android. On Windows, it will internally
|
||||
call load().
|
||||
|
||||
@see load
|
||||
*/
|
||||
void loadAsync (const URL& url, std::function<void (const URL&, Result)> loadFinishedCallback);
|
||||
|
||||
/** Closes the video and resets the component. */
|
||||
void closeVideo();
|
||||
|
||||
|
|
@ -109,6 +139,9 @@ public:
|
|||
*/
|
||||
void setPlaySpeed (double newSpeed);
|
||||
|
||||
/** Returns the current play speed of the video. */
|
||||
double getPlaySpeed() const;
|
||||
|
||||
/** Changes the video's playback volume.
|
||||
@param newVolume the volume in the range 0 (silent) to 1.0 (full)
|
||||
*/
|
||||
|
|
@ -119,6 +152,23 @@ public:
|
|||
*/
|
||||
float getAudioVolume() const;
|
||||
|
||||
#if JUCE_SYNC_VIDEO_VOLUME_WITH_OS_MEDIA_VOLUME
|
||||
/** Set this callback to be notified whenever OS global media volume changes.
|
||||
Currently used on Android only.
|
||||
*/
|
||||
std::function<void()> onGlobalMediaVolumeChanged;
|
||||
#endif
|
||||
|
||||
/** Set this callback to be notified whenever the playback starts. */
|
||||
std::function<void()> onPlaybackStarted;
|
||||
|
||||
/** Set this callback to be notified whenever the playback stops. */
|
||||
std::function<void()> onPlaybackStopped;
|
||||
|
||||
/** Set this callback to be notified whenever an error occurs. Upon error, you
|
||||
may need to load the video again. */
|
||||
std::function<void (const String& /*error*/)> onErrorOccurred;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct Pimpl;
|
||||
|
|
@ -129,6 +179,24 @@ private:
|
|||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
#if JUCE_ANDROID
|
||||
friend void juce_surfaceChangedNativeVideo (int64, void*);
|
||||
friend void juce_surfaceDestroyedNativeVideo (int64, void*);
|
||||
|
||||
friend void juce_mediaSessionPause (int64);
|
||||
friend void juce_mediaSessionPlay (int64);
|
||||
friend void juce_mediaSessionPlayFromMediaId (int64, void*, void*);
|
||||
friend void juce_mediaSessionSeekTo (int64, int64);
|
||||
friend void juce_mediaSessionStop (int64);
|
||||
|
||||
friend void juce_mediaControllerAudioInfoChanged (int64, void*);
|
||||
friend void juce_mediaControllerMetadataChanged (int64, void*);
|
||||
friend void juce_mediaControllerPlaybackStateChanged (int64, void*);
|
||||
friend void juce_mediaControllerSessionDestroyed (int64);
|
||||
|
||||
friend void juce_mediaSessionSystemVolumeChanged (int64);
|
||||
#endif
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VideoComponent)
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue