|
- /*
- ==============================================================================
-
- This file is part of the JUCE library.
- 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)
- b) the Affero GPL v3
-
- Details of these licenses can be found at: www.gnu.org/licenses
-
- JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- ------------------------------------------------------------------------------
-
- To release a closed-source product which uses JUCE, commercial licenses are
- available: visit www.juce.com for more information.
-
- ==============================================================================
- */
-
- #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_")
- {
- #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
- //==============================================================================
- 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
-
- //==============================================================================
- 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)
- };
|