832 lines
29 KiB
C
832 lines
29 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE library.
|
||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||
|
|
||
|
JUCE is an open source library subject to commercial or open-source
|
||
|
licensing.
|
||
|
|
||
|
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||
|
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||
|
|
||
|
End User License Agreement: www.juce.com/juce-6-licence
|
||
|
Privacy Policy: www.juce.com/juce-privacy-policy
|
||
|
|
||
|
Or: You may also use this code under the terms of the GPL v3 (see
|
||
|
www.gnu.org/licenses).
|
||
|
|
||
|
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||
|
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||
|
DISCLAIMED.
|
||
|
|
||
|
==============================================================================
|
||
|
*/
|
||
|
|
||
|
#if JUCE_MAC
|
||
|
using Base = NSViewComponent;
|
||
|
#else
|
||
|
using Base = UIViewComponent;
|
||
|
#endif
|
||
|
|
||
|
struct VideoComponent::Pimpl : public Base
|
||
|
{
|
||
|
Pimpl (VideoComponent& ownerToUse, bool useNativeControlsIfAvailable)
|
||
|
: owner (ownerToUse),
|
||
|
playerController (*this, useNativeControlsIfAvailable)
|
||
|
{
|
||
|
setVisible (true);
|
||
|
|
||
|
auto* view = playerController.getView();
|
||
|
setView (view);
|
||
|
|
||
|
#if JUCE_MAC
|
||
|
[view setNextResponder: [view superview]];
|
||
|
[view setWantsLayer: YES];
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
~Pimpl()
|
||
|
{
|
||
|
close();
|
||
|
setView (nil);
|
||
|
}
|
||
|
|
||
|
Result load (const File& file)
|
||
|
{
|
||
|
auto r = load (createNSURLFromFile (file));
|
||
|
|
||
|
if (r.wasOk())
|
||
|
currentFile = file;
|
||
|
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
Result load (const URL& url)
|
||
|
{
|
||
|
auto r = load ([NSURL URLWithString: juceStringToNS (url.toString (true))]);
|
||
|
|
||
|
if (r.wasOk())
|
||
|
currentURL = url;
|
||
|
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
Result load (NSURL* url)
|
||
|
{
|
||
|
if (url != nil)
|
||
|
{
|
||
|
close();
|
||
|
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();
|
||
|
playerController.close();
|
||
|
currentFile = File();
|
||
|
currentURL = {};
|
||
|
}
|
||
|
|
||
|
bool isOpen() const noexcept { return playerController.getPlayer() != nil; }
|
||
|
bool isPlaying() const noexcept { return getSpeed() != 0; }
|
||
|
|
||
|
void play() noexcept { [playerController.getPlayer() play]; setSpeed (playSpeedMult); }
|
||
|
void stop() noexcept { [playerController.getPlayer() pause]; }
|
||
|
|
||
|
void setPosition (double newPosition)
|
||
|
{
|
||
|
if (auto* p = playerController.getPlayer())
|
||
|
{
|
||
|
CMTime t = { (CMTimeValue) (100000.0 * newPosition),
|
||
|
(CMTimeScale) 100000, kCMTimeFlags_Valid, {} };
|
||
|
|
||
|
[p seekToTime: t
|
||
|
toleranceBefore: kCMTimeZero
|
||
|
toleranceAfter: kCMTimeZero];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
double getPosition() const
|
||
|
{
|
||
|
if (auto* p = playerController.getPlayer())
|
||
|
return toSeconds ([p currentTime]);
|
||
|
|
||
|
return 0.0;
|
||
|
}
|
||
|
|
||
|
void setSpeed (double 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 = playerController.getPlayer())
|
||
|
return [p rate];
|
||
|
|
||
|
return 0.0;
|
||
|
}
|
||
|
|
||
|
Rectangle<int> getNativeSize() const
|
||
|
{
|
||
|
if (auto* p = playerController.getPlayer())
|
||
|
{
|
||
|
auto s = [[p currentItem] presentationSize];
|
||
|
return { (int) s.width, (int) s.height };
|
||
|
}
|
||
|
|
||
|
return {};
|
||
|
}
|
||
|
|
||
|
double getDuration() const
|
||
|
{
|
||
|
if (auto* p = playerController.getPlayer())
|
||
|
return toSeconds ([[p currentItem] duration]);
|
||
|
|
||
|
return 0.0;
|
||
|
}
|
||
|
|
||
|
void setVolume (float newVolume)
|
||
|
{
|
||
|
[playerController.getPlayer() setVolume: newVolume];
|
||
|
}
|
||
|
|
||
|
float getVolume() const
|
||
|
{
|
||
|
if (auto* p = playerController.getPlayer())
|
||
|
return [p volume];
|
||
|
|
||
|
return 0.0f;
|
||
|
}
|
||
|
|
||
|
File currentFile;
|
||
|
URL currentURL;
|
||
|
|
||
|
private:
|
||
|
//==============================================================================
|
||
|
template <typename Derived>
|
||
|
class PlayerControllerBase
|
||
|
{
|
||
|
public:
|
||
|
~PlayerControllerBase()
|
||
|
{
|
||
|
detachPlayerStatusObserver();
|
||
|
detachPlaybackObserver();
|
||
|
}
|
||
|
|
||
|
protected:
|
||
|
//==============================================================================
|
||
|
struct JucePlayerStatusObserverClass : public ObjCClass<NSObject>
|
||
|
{
|
||
|
JucePlayerStatusObserverClass() : ObjCClass<NSObject> ("JucePlayerStatusObserverClass_")
|
||
|
{
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||
|
addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?");
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
|
||
|
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<NSString*, 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_")
|
||
|
{
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||
|
addMethod (@selector (processNotification:), notificationReceived, "v@:@");
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
|
||
|
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_")
|
||
|
{
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||
|
addMethod (@selector (observeValueForKeyPath:ofObject:change:context:), valueChanged, "v@:@@@?");
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
|
||
|
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<NSString*, 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.get());
|
||
|
}
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
case AVKeyValueStatusUnknown:
|
||
|
case AVKeyValueStatusLoading:
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
{
|
||
|
MessageManager::callAsync ([url, preparedPlayer, r,
|
||
|
safeThis = WeakReference<PlayerAsyncInitialiser> { this }]() 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()
|
||
|
{
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||
|
[[NSNotificationCenter defaultCenter] addObserver: playerItemPlaybackStatusObserver.get()
|
||
|
selector: @selector (processNotification:)
|
||
|
name: AVPlayerItemDidPlayToEndTimeNotification
|
||
|
object: [crtp().getPlayer() currentItem]];
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
}
|
||
|
|
||
|
void detachPlaybackObserver()
|
||
|
{
|
||
|
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||
|
[[NSNotificationCenter defaultCenter] removeObserver: playerItemPlaybackStatusObserver.get()];
|
||
|
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||
|
}
|
||
|
|
||
|
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()
|
||
|
{
|
||
|
MessageManager::callAsync ([safeThis = WeakReference<PlayerControllerBase> { this }]() 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 (getPlayer() != nil && player != getPlayer()) {
|
||
|
// must detach from this player properly
|
||
|
detachPlayerStatusObserver();
|
||
|
detachPlaybackObserver();
|
||
|
}
|
||
|
|
||
|
#if ! JUCE_32BIT
|
||
|
if (useNativeControls)
|
||
|
[playerView setPlayer: player];
|
||
|
else
|
||
|
#endif
|
||
|
[playerLayer setPlayer: player];
|
||
|
|
||
|
if (player != nil)
|
||
|
{
|
||
|
attachPlayerStatusObserver();
|
||
|
attachPlaybackObserver();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
detachPlayerStatusObserver();
|
||
|
detachPlaybackObserver();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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
|
||
|
//==============================================================================
|
||
|
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 (getPlayer() != nil && playerToUse != getPlayer()) {
|
||
|
// must detach from this player properly
|
||
|
detachPlayerStatusObserver();
|
||
|
detachPlaybackObserver();
|
||
|
}
|
||
|
|
||
|
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<void> (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
|
||
|
|
||
|
//==============================================================================
|
||
|
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)
|
||
|
};
|