├── .gitignore ├── PRXPlayer.podspec ├── LICENSE ├── PRXPlayer_private.h ├── PRXQueuePlayer.h ├── README.md ├── PRXPlayer.h ├── PRXPlayerQueue.h ├── PRXPlayerQueue.m ├── PRXQueuePlayer.m └── PRXPlayer.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /PRXPlayer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "PRXPlayer" 3 | s.version = "2.0.21" 4 | s.summary = "An iOS audio player" 5 | 6 | s.description = <<-DESC 7 | PRXPlayer is a lightweight wrapper around AVPlayer. It's job is simply to provide a standard set of tools, for free, that most implementations of an media player would have anyway. This includes things like: retry logic, error handling, observing basic state changes, and monitoring playback. 8 | 9 | DESC 10 | 11 | s.homepage = "https://github.com/PRX/PRXPlayer" 12 | 13 | s.license = { :type => 'MIT', :file => 'LICENSE' } 14 | 15 | s.author = { "Chris Kalafarski" => "chris.kalafarski@prx.org", "Rebecca Nesson" => "rebecca@prx.org" } 16 | s.social_media_url = "http://twitter.com/prx" 17 | 18 | s.platform = :ios, '7.0' 19 | 20 | s.source = { :git => "https://github.com/PRX/PRXPlayer.git", :tag => "2.0.21" } 21 | s.source_files = '**/*.{h,m}' 22 | s.requires_arc = true 23 | 24 | s.dependency 'Reachability' 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 PRX. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | -------------------------------------------------------------------------------- /PRXPlayer_private.h: -------------------------------------------------------------------------------- 1 | // 2 | // PRXPlayer_private.h 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | @import MediaPlayer; 26 | 27 | #import "PRXPlayer.h" 28 | 29 | @interface PRXPlayer () 30 | 31 | + (dispatch_queue_t)sharedObserverQueue; 32 | + (dispatch_queue_t)sharedAssignmentQueue; 33 | 34 | @property (nonatomic, strong, readonly) Reachability *reach; 35 | @property (nonatomic, strong) AVPlayer *player; 36 | 37 | @property (nonatomic, readonly) float rateForFilePlayback; 38 | @property (nonatomic, readonly) float rateForPlayback; 39 | 40 | @property (nonatomic, readonly) BOOL allowsPlaybackViaWWAN; 41 | 42 | @property (nonatomic, strong, readonly) NSDictionary *MPNowPlayingInfoCenterNowPlayingInfo; 43 | 44 | - (void)postGeneralChangeNotification; 45 | 46 | - (void)mediaPlayerCurrentItemStatusDidChange:(NSDictionary *)change; 47 | - (void)mediaPlayerCurrentItemDidPlayToEndTime:(NSNotification *)notification; 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /PRXQueuePlayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // PRXAudioPlayer.h 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #import "PRXPlayer.h" 26 | #import "PRXPlayerQueue.h" 27 | 28 | @interface PRXQueuePlayer : PRXPlayer 29 | 30 | @property (strong, nonatomic, readonly) PRXPlayerQueue *queue; 31 | 32 | @property (nonatomic, readonly) BOOL hasNext; 33 | @property (nonatomic, readonly) BOOL hasPrevious; 34 | 35 | @property (nonatomic, readonly) NSUInteger previousPosition; 36 | @property (nonatomic, readonly) NSUInteger nextPosition; 37 | 38 | - (BOOL)canMoveToQueuePosition:(NSUInteger)position; 39 | - (void)moveToQueuePosition:(NSUInteger)position; 40 | - (void)seekToQueuePosition:(NSUInteger)position; 41 | 42 | - (void)seekForward; 43 | - (void)seekBackward; 44 | 45 | - (void)enqueue:(id)playerItem; 46 | - (void)enqueue:(id)playerItem atPosition:(NSUInteger)position; 47 | - (void)enqueueAfterCurrentPosition:(id)playerItem; 48 | 49 | - (void)dequeue:(id)playerItem; 50 | - (void)dequeueFromPosition:(NSUInteger)position; 51 | - (void)requeue:(id)playerItem atPosition:(NSUInteger)position; 52 | - (void)movePlayerItemFromPosition:(NSUInteger)inPosition toPosition:(NSUInteger)outPosition; 53 | 54 | - (void)enqueuePlayerItems:(NSArray *)playerItems; 55 | - (void)enqueuePlayerItems:(NSArray *)playerItems atPosition:(NSUInteger)position; 56 | 57 | - (void)emptyQueue; 58 | 59 | - (BOOL)queueContainsPlayerItem:(id)playerItem; 60 | - (NSUInteger)firstQueuePositionForPlayerItem:(id)playerItem; 61 | - (NSUInteger)nextQueuePositionForPlayerItem:(id)playerItem; 62 | - (NSIndexSet *)allQueuePositionsForPlayerItem:(id)playerItem; 63 | - (id)playerItemAtCurrentQueuePosition; 64 | - (id)playerItemAtQueuePosition:(NSUInteger)position; 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PRXPlayer 2 | 3 | PRXPlayer currently depends on a modified Reachability. 4 | 5 | pod 'PRXPlayer', '~> 2.0' 6 | 7 | #### How to use 8 | 9 | PRXPlayer is a lightweight wrapper around AVPlayer. It's job is simply to provide a standard set of tools, for free, that most implementations of an media player would have anyway. This includes things like: retry logic, error handling, observing basic state changes, and monitoring playback. PRXPlayer provides an abstracted set of NSNotifications, which make it easy for your app to respond to nearly all relevant changes. More precise interaction with the AVPlayer and its items and assets can be achieved by implementing a PRXPlayerDelegate. Beyond that, you always have the ability to deal with the underlying AVPlayer instances directly, allowing for as much customization as you need. 10 | 11 | ### PRXPlayerItem 12 | 13 | The `PRXPlayer` API expects objects that implement the `PRXPlayerItem` protocol. This protocol ensures that objects contain the most basic set of properties that are required for a consistent media player experience. Every `PRXPlayerItem` must provide an `AVAsset`, which is what actually gets loaded into the underlying `AVPlayer`, and implement the `isEqualToPlayerItem:` to the player can accurately determine when player items have changed. 14 | 15 | In some cases, the `AVAsset` of a given object may change over time. For instance, an episode object may by default have an `AVURLAsset` corresponding to a file on a remote server, but then returns an `AVURLAsset` corresponding to a file in local storage after the episode has been downloaded. In cases such as this, `isEqualToPlayerItem:` should be able to distnguish between these various situations, and return `NO` when a `PRXPlayerItem` is compared to itself, but with differing `AVAssets`. 16 | 17 | ### PRXPlayer 18 | 19 | There are two groups of methods that a `PRXPlayer` has to allow for controlling playback: indifferent and explicit. The indifferent methods are: `play`, `pause`, `toggle`, and `stop`. These control the player regardless of what is loaded into the player. The explict controls, `playPlayerItem:`, `loadPlayerItem:`, and `togglePlayerItem:` deal with specific media objects. If an explict control message is sent to a `PRXPlayer` and the given object is not the currently loaded object, the player will take steps to clear out any existing media object and load the new one. `loadPlayerItem:` will prepare a media object as much as possible, up until the point where it can begin playback (ie it will start to buffer), but will keep the player paused once it's ready. 20 | 21 | ### PRXPlayerDelegate 22 | 23 | Work in progress… 24 | 25 | ## Responding to changes 26 | 27 | One of the primary functions of `PRXPlayer` is to make responding to changes for an `AVPlayer` and the associated media very easy. One way this is accomplished is by posting several very general notifications as changes come about. In most cases, parts of an app (eg UI elements, persistance layers, etc) are not concerned with what changed or what caused the change, simply that the change occured. 28 | 29 | A common example would be the player controls UI presented in a music app. The controller managing the UI does not necessarily care why the player became paused, simply that the player changed state, and that the UI should be updated. 30 | 31 | `PRXPlayer` will post a `PRXPlayerChangeNotification` any time the state of the player, it's underlying `AVPlayer`, or an asset loaded into the `AVPlayer` changes. Observing this notification, using the `sharedPlayer` as the object, should cover all situations needed to keep a player UI in sync with the player. 32 | 33 | Additionally the `PRXPlayerTimeIntervalNotification` and `PRXPlayerLongTimeIntervalNotification` will be posted at one and 10 second intervals respectively any time playback is occuring through the `PRXPlayer`. Time jumps will also cause these notifications to be posted. For general use, the object registered for these notifications should be the `sharedPlayer`. 34 | 35 | In cases where the asset that is playing back is an `AVURLAsset`, two additional notifications will be posted. They are also `PRXPlayerTimeIntervalNotification` and `PRXPlayerLongTimeIntervalNotification` notifications, but the object is the `absoluteString` of the `URL` of the `AVURLAsset`. 36 | 37 | `PRXPlayer` also allows for the enforcement of WWAN (eg 3G/4G cellular connection) policy, and will post a `PRXPlayerReachabilityPolicyPreventedPlayback` if playback fails due to network connectivity conditions. 38 | -------------------------------------------------------------------------------- /PRXPlayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // PRXPlayer.h 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | @import UIKit; 26 | @import AVFoundation; 27 | 28 | #import "Reachability.h" 29 | 30 | @protocol PRXPlayerItem, PRXPlayerDelegate; 31 | 32 | extern NSString * const PRXPlayerChangeNotification; 33 | extern NSString * const PRXPlayerTimeIntervalNotification; 34 | extern NSString * const PRXPlayerLongTimeIntervalNotification; 35 | 36 | extern NSString * const PRXPlayerReachabilityPolicyPreventedPlayback; 37 | 38 | typedef NS_ENUM(NSUInteger, PRXPlayerState) { 39 | PRXPlayerStateUnknown, 40 | PRXPlayerStateEmpty, 41 | PRXPlayerStateLoading, 42 | PRXPlayerStateBuffering, 43 | PRXPlayerStateWaiting, 44 | PRXPlayerStateReady 45 | }; 46 | 47 | @interface PRXPlayer : UIResponder { 48 | BOOL holdPlayback; 49 | 50 | NSUInteger retryCount; 51 | 52 | id playerPlaybackStartBoundaryTimeObserver; 53 | id playerPeriodicTimeObserver; 54 | id playerSoftEndBoundaryTimeObserver; 55 | 56 | BOOL ignoreTimeObservations; 57 | 58 | NSUInteger backgroundKeepAliveTaskID; 59 | NSDate *dateAtAudioPlaybackInterruption; 60 | 61 | NetworkStatus previousReachabilityStatus; 62 | NSString *previousReachabilityString; 63 | } 64 | 65 | + (instancetype)sharedPlayer; 66 | 67 | @property (nonatomic, strong, readonly) AVPlayer *player; 68 | 69 | @property (nonatomic, readonly) PRXPlayerState state; 70 | @property (nonatomic, readonly) NSTimeInterval buffer; 71 | 72 | @property (nonatomic, strong) id playerItem; 73 | 74 | @property (nonatomic, weak) id delegate; 75 | 76 | - (void)loadPlayerItem:(id)playerItem; 77 | - (void)playPlayerItem:(id)playerItem; 78 | - (void)togglePlayerItem:(id)playerItem orCancel:(BOOL)cancel; 79 | - (void)togglePlayerItem:(id)playerItem; 80 | 81 | - (void)play; 82 | - (void)pause; 83 | - (void)toggle; 84 | - (void)toggleOrCancel; 85 | - (void)stop; 86 | 87 | // this will go away 88 | - (NSDate *)dateAtAudioPlaybackInterruption; 89 | 90 | @end 91 | 92 | @protocol PRXPlayerItem 93 | 94 | @property (nonatomic, strong, readonly) AVAsset *playerAsset; 95 | 96 | - (BOOL)isEqualToPlayerItem:(id)aPlayerItem; 97 | 98 | @optional 99 | 100 | @property (nonatomic, strong, readonly) NSDictionary *mediaItemProperties; 101 | 102 | // TODO better names 103 | @property (nonatomic, readonly) CMTime playerTime; 104 | @property (nonatomic, readonly) CMTime playerDuration; 105 | 106 | - (void)setPlayerTime:(CMTime)playerTime; 107 | - (void)setPlayerDuration:(CMTime)playerDuration; 108 | 109 | @end 110 | 111 | @protocol PRXPlayerDelegate 112 | 113 | //- (void)player:(AVPlayer *)player changedToTime:(CMTime); 114 | //- (void)playerDidTraverseSoftEndBoundaryTime:(PRXPlayer *)player; 115 | 116 | @optional 117 | 118 | - (AVAsset *)player:(PRXPlayer *)player assetForPlayerItem:(id)playerItem; 119 | 120 | - (void)player:(PRXPlayer *)player failedToLoadTracksForAsset:(AVAsset *)asset holdPlayback:(BOOL)holdPlayback; 121 | - (void)playerFailedToBecomeReadyToPlay:(PRXPlayer *)player holdPlayback:(BOOL)holdPlayback; 122 | 123 | - (void)player:(PRXPlayer *)player softBoundaryTimeReachedForPlayerItem:(AVPlayerItem *)playerItem; 124 | - (void)player:(PRXPlayer *)player endTimeReachedForPlayerItem:(AVPlayerItem *)playerItem; 125 | - (void)player:(PRXPlayer *)player playerItemDidChange:(NSDictionary *)change; 126 | - (void)player:(PRXPlayer *)player currentItemStatusDidChange:(NSDictionary *)change; 127 | - (void)player:(PRXPlayer *)player rateDidChange:(NSDictionary *)change; 128 | 129 | - (float)filePlaybackRateForPlayer:(PRXPlayer *)player; 130 | - (BOOL)playerAllowsPlaybackViaWWAN:(PRXPlayer *)player; 131 | - (float)softEndBoundaryProgressForPlayer:(PRXPlayer *)player; 132 | - (NSUInteger)retryLimitForPlayer:(PRXPlayer *)player; 133 | 134 | @end 135 | -------------------------------------------------------------------------------- /PRXPlayerQueue.h: -------------------------------------------------------------------------------- 1 | // 2 | // PRXPlayerQueue.h 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #import 26 | 27 | /** 28 | `PRXPlayerQueue` is an array-like collection that is used to manage a list of playble objects intended for use in a `PRXQueuePlayer`. It implements the NSArray primative methods and all methods NSMutableArray requires for subclassing. Any other NSArray or NSMutableArray methods may not work as expected if used directly on a `PRXPlayerQueue`. 29 | 30 | ## Cursor 31 | 32 | The main difference between the `PRXAudioQueue` and a standard `NSMutableArray` is the cursor that is maintained by `PRXAudioQueue` to signify a position within the queue representing the currently playing item. The cursor is specifically managed such that it will never point to a position outside the current range of the collection. If something tries to move the cursor beyond the bounds of the collection, it will be set to the closet valid value. 33 | 34 | When the cursor is not defined, it will return `NSNotFound`. Any time the queue is empty the cursor will return `NSNotFound`. It is possible for the queue to include one or more items and have an undefined cursor. 35 | 36 | ## Intended use 37 | 38 | The only place a `PRXPlayerQueue` should be interacted with is inside a `PRXQueuePlayer` subclass. Application code is not intended to be aware of this class. Even though the `cursor` is exposed, it should not be get or set outside of a `PRXQueuePlayer` or subclass. 39 | 40 | ## Delegate 41 | 42 | When a delegate has been set for a `PRXAudioQueue`, it will be notified of changes to the following conditions of the queue: 43 | 44 | - The `cursor` position 45 | - The size of the queue 46 | - The position of objects within the queue 47 | */ 48 | 49 | @protocol PRXPlayerQueueDelegate; 50 | 51 | @interface PRXPlayerQueue : NSMutableArray 52 | 53 | @property (nonatomic, weak) id delegate; 54 | @property (nonatomic) NSUInteger position; 55 | 56 | @property (nonatomic, readonly) BOOL isEmpty; 57 | 58 | 59 | ///--------------------------------------- 60 | /// @name NSArray primative methods 61 | ///--------------------------------------- 62 | 63 | /** 64 | Returns the object located at _index_. 65 | 66 | @param index An index within or outside the bounds of the array. 67 | 68 | @return The object located at _index_, or `nil`. 69 | 70 | @discussion If _index_ is beyond the end of the array `nil` is returned. 71 | */ 72 | - (id)objectAtIndex:(NSUInteger)index; 73 | 74 | ///--------------------------------------- 75 | /// @name NSMutableArray methods 76 | ///--------------------------------------- 77 | 78 | /** 79 | Inserts the given object at the specified index of the mutable ordered set. 80 | 81 | @discussion Unlike `insertObject:atIndex:` for `NSMutableArray`, if _idx_ is greater than the number of elements in the queue _object_ will be inserted at the end of the collection. 82 | 83 | If the index is less than or equal to the current queue position, the position will be incremented so the selected element does not change. 84 | */ 85 | - (void)insertObject:(id)anObject atIndex:(NSUInteger)index; 86 | 87 | /** 88 | Removes the object at _index_. 89 | 90 | @discussion If the queue becomes empty after remove the object, the queue's position becomes `NSNotFound`. 91 | 92 | If the index is less than the current queue position, the position will be decremented so the selected element does not change. 93 | */ 94 | - (void)removeObjectAtIndex:(NSUInteger)index; 95 | 96 | @end 97 | 98 | /** 99 | The `PRXPlayerQueueDelegate` protocol defines a method that allows an object, usually a `PRXQueuePlayer`, to be notified when the queue changes. 100 | */ 101 | @protocol PRXPlayerQueueDelegate 102 | 103 | /** 104 | Tells the delegate when the queue or queue position changes. 105 | 106 | @param queue The queue object in which the change occured 107 | 108 | @discussion The delegate typically implements this method to respond to changes resulting from user actions, or from triggered internally within the queue or the queue player. 109 | */ 110 | - (void)queueDidChange:(PRXPlayerQueue *)queue; 111 | 112 | @end 113 | -------------------------------------------------------------------------------- /PRXPlayerQueue.m: -------------------------------------------------------------------------------- 1 | // 2 | // PRXPlayerQueue.m 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #import "PRXPlayerQueue.h" 26 | 27 | @interface PRXPlayerQueue () 28 | 29 | @property (nonatomic, strong) NSMutableArray *collection; 30 | 31 | @property (nonatomic) NSUInteger lastPosition; 32 | 33 | - (void)incrementPosition; 34 | - (void)decrementPosition; 35 | 36 | - (void)notifyDelegate; 37 | 38 | @end 39 | 40 | @implementation PRXPlayerQueue 41 | 42 | - (id)init { 43 | self = [super init]; 44 | if (self) { 45 | self.collection = [NSMutableArray arrayWithCapacity:10]; 46 | } 47 | return self; 48 | } 49 | 50 | #pragma mark - Backing collection 51 | 52 | - (void)setCollection:(NSMutableArray *)collection { 53 | _collection = collection; 54 | self.position = (collection.count == 0 ? NSNotFound : 0); 55 | } 56 | 57 | - (BOOL)isEmpty { 58 | return (self.count == 0); 59 | } 60 | 61 | #pragma - Position manipulation 62 | 63 | - (void)setPosition:(NSUInteger)position { 64 | _position = ((position == NSNotFound || self.isEmpty) ? NSNotFound : MAX(0, MIN(position, self.lastPosition))); 65 | [self notifyDelegate]; 66 | } 67 | 68 | - (void)incrementPosition { 69 | self.position = (self.position == NSNotFound ? 0 : (self.position + 1)); 70 | } 71 | 72 | - (void)decrementPosition { 73 | self.position = (self.position == NSNotFound ? NSNotFound : (self.position - 1));; 74 | } 75 | 76 | - (NSUInteger)lastPosition { 77 | return (self.count == 0 ? NSNotFound : (self.count - 1)); 78 | } 79 | 80 | #pragma mark - NSArray primative methods 81 | 82 | - (NSUInteger)count { 83 | return self.collection.count; 84 | } 85 | 86 | - (id)objectAtIndex:(NSUInteger)index { 87 | @synchronized(self.collection) { 88 | if (index != NSNotFound && index < self.collection.count) { 89 | return [self.collection objectAtIndex:index]; 90 | } 91 | } 92 | 93 | return nil; 94 | } 95 | 96 | #pragma mark - NSMutableArray methods 97 | 98 | - (void)insertObject:(id)anObject atIndex:(NSUInteger)index { 99 | @synchronized(self.collection) { 100 | [self.collection insertObject:anObject atIndex:MIN(index, self.collection.count)]; 101 | } 102 | 103 | // TODO If things are allowed to play without being in the queue 104 | // (like if the current item is dequeued), incrementing the counter 105 | // here when NSNotFound could be bad 106 | if (index <= self.position || self.position == NSNotFound) { 107 | // If an item is inserted before the current item, 108 | // shift the cursor to follow the current item. 109 | // Or if the cursor hasn't been set, increment it 110 | // to get it to 0. 111 | [self incrementPosition]; 112 | } 113 | 114 | [self notifyDelegate]; 115 | } 116 | 117 | - (void)removeObjectAtIndex:(NSUInteger)index { 118 | if (index >= self.collection.count) { return; } 119 | 120 | [self.collection removeObjectAtIndex:index]; 121 | 122 | if (self.position != NSNotFound) { 123 | if (index < self.position) { 124 | // When removing an item below the current cursor, 125 | // the current item will shift down one, so we need 126 | // to make the cursor follow it 127 | [self decrementPosition]; 128 | } 129 | 130 | if (self.position > self.lastPosition) { 131 | // If the cursor ever gets out of bounds, move it 132 | // back in bounds. This can happen when the cursor 133 | // is on the last item, and it is removed. 134 | self.position = self.lastPosition; 135 | } 136 | } 137 | 138 | if (self.isEmpty) { 139 | self.position = NSNotFound; 140 | } 141 | 142 | [self notifyDelegate]; 143 | } 144 | 145 | - (void)addObject:(id)anObject { 146 | [self.collection addObject:anObject]; 147 | 148 | [self notifyDelegate]; 149 | } 150 | 151 | - (void)removeAllObjects { 152 | @synchronized(self.collection) { 153 | [self.collection removeAllObjects]; 154 | self.position = NSNotFound; 155 | } 156 | 157 | [self notifyDelegate]; 158 | } 159 | 160 | - (void)removeLastObject { 161 | [self removeObjectAtIndex:self.lastPosition]; 162 | } 163 | 164 | #pragma mark - Delegate and observers 165 | 166 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context { 167 | [self notifyDelegate]; 168 | } 169 | 170 | - (void)notifyDelegate { 171 | if (self.delegate) { 172 | [self.delegate queueDidChange:self]; 173 | } 174 | } 175 | 176 | @end 177 | -------------------------------------------------------------------------------- /PRXQueuePlayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // PRXAudioPlayer.m 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #import "PRXPlayer_private.h" 26 | #import "PRXQueuePlayer.h" 27 | 28 | @implementation PRXQueuePlayer 29 | 30 | - (id)init { 31 | self = [super init]; 32 | if (self) { 33 | _queue = [[PRXPlayerQueue alloc] init]; 34 | self.queue.delegate = self; 35 | } 36 | return self; 37 | } 38 | 39 | - (void)setPlayerItem:(id)playerItem { 40 | if ([self queueContainsPlayerItem:playerItem]) { 41 | self.queue.position = [self nextQueuePositionForPlayerItem:playerItem]; 42 | super.playerItem = playerItem; 43 | } else { 44 | if (playerItem) { 45 | [self enqueueAfterCurrentPosition:playerItem]; 46 | } 47 | 48 | if ([self queueContainsPlayerItem:playerItem]) { 49 | super.playerItem = playerItem; 50 | } 51 | } 52 | } 53 | 54 | - (void)play { 55 | if (self.queue.isEmpty) { 56 | [super play]; 57 | } else { 58 | if (self.queue.position == NSNotFound) { 59 | [self moveToQueuePosition:0]; 60 | } 61 | [self playPlayerItem:self.queue[self.queue.position]]; 62 | } 63 | } 64 | 65 | - (void)playerItemStatusDidChange:(NSDictionary *)change { 66 | NSUInteger keyValueChangeKind = [change[NSKeyValueChangeKindKey] integerValue]; 67 | 68 | if (keyValueChangeKind == NSKeyValueChangeSetting && self.player.currentItem.status == AVPlayerStatusFailed) { 69 | // PRXLog(@"Player status failed %@", self.player.currentItem.error); 70 | // the AVPlayer has trouble switching from stream to file and vice versa 71 | // if we get an error condition, start over playing the thing it tried to play. 72 | // Once a player fails it can't be used for playback anymore! 73 | 74 | NSUInteger _retryLimit = 3; 75 | 76 | if ([self.delegate respondsToSelector:@selector(retryLimitForPlayer:)]) { 77 | _retryLimit = [self.delegate retryLimitForPlayer:self]; 78 | } 79 | 80 | if (retryCount < _retryLimit) { 81 | [super mediaPlayerCurrentItemStatusDidChange:change]; 82 | } else { 83 | [self postGeneralChangeNotification]; 84 | [self seekForward]; 85 | } 86 | } else { 87 | [super mediaPlayerCurrentItemStatusDidChange:change]; 88 | } 89 | } 90 | 91 | #pragma mark - Next and previous 92 | 93 | - (BOOL)hasPrevious { 94 | return (self.queue.position != NSNotFound && self.queue.position > 0 && self.queue.count > 1); 95 | } 96 | 97 | - (BOOL)hasNext { 98 | return (self.queue.position != NSNotFound && self.queue.position < (self.queue.count - 1)); 99 | } 100 | 101 | - (NSUInteger)previousPosition { 102 | return self.hasPrevious ? (self.queue.position - 1) : NSNotFound; 103 | } 104 | 105 | - (NSUInteger)nextPosition { 106 | return self.hasNext ? (self.queue.position + 1) : NSNotFound; 107 | } 108 | 109 | #pragma mark - Queue movement 110 | 111 | - (BOOL)canMoveToQueuePosition:(NSUInteger)position { 112 | if (self.queue.count == 0) { return NO; } 113 | 114 | return (position <= (self.queue.count - 1)); 115 | } 116 | 117 | - (void)moveToQueuePosition:(NSUInteger)position { 118 | if ([self canMoveToQueuePosition:position]) { 119 | self.queue.position = position; 120 | } 121 | } 122 | 123 | - (void)seekToQueuePosition:(NSUInteger)position { 124 | if ([self canMoveToQueuePosition:position]) { 125 | [self moveToQueuePosition:position]; 126 | self.playerItem = self.queue[self.queue.position]; 127 | } 128 | } 129 | 130 | - (void)seekForward { 131 | if (self.hasNext) { 132 | [self seekToQueuePosition:self.nextPosition]; 133 | } 134 | } 135 | 136 | - (void)seekBackward { 137 | if (self.hasPrevious) { 138 | [self seekToQueuePosition:self.previousPosition]; 139 | } 140 | } 141 | 142 | - (void)moveToPrevious { 143 | if (self.hasPrevious) { 144 | [self moveToQueuePosition:self.previousPosition]; 145 | } 146 | } 147 | 148 | - (void)moveToNext { 149 | if (self.hasNext) { 150 | [self moveToQueuePosition:self.nextPosition]; 151 | } 152 | } 153 | 154 | #pragma mark - Queue manipulation 155 | 156 | - (void)enqueue:(id)playerItem atPosition:(NSUInteger)position { 157 | [self.queue insertObject:playerItem atIndex:position]; 158 | 159 | if (!self.playerItem) { 160 | if (self.queue.position == NSNotFound) { 161 | self.queue.position = 0; 162 | } 163 | 164 | [self loadPlayerItem:self.queue[self.queue.position]]; 165 | } 166 | } 167 | 168 | - (void)enqueue:(id)playerItem { 169 | [self enqueue:playerItem atPosition:self.queue.count]; 170 | } 171 | 172 | - (void)enqueueAfterCurrentPosition:(id)playerItem { 173 | NSUInteger position = (self.queue.count == 0 ? 0 : (self.queue.position + 1)); 174 | [self enqueue:playerItem atPosition:position]; 175 | } 176 | 177 | - (void)dequeueFromPosition:(NSUInteger)position { 178 | [self.queue removeObjectAtIndex:position]; 179 | } 180 | 181 | - (void)dequeue:(id)playerItem { 182 | NSUInteger position = [self firstQueuePositionForPlayerItem:playerItem]; 183 | if (position != NSNotFound) { 184 | [self dequeueFromPosition:position]; 185 | } 186 | } 187 | 188 | - (void)movePlayerItemFromPosition:(NSUInteger)position toPosition:(NSUInteger)newPosition { 189 | if ([self canMoveToQueuePosition:position] && [self canMoveToQueuePosition:newPosition]) { 190 | // If the current item is being moved, we 191 | // want to make sure the position in the queue 192 | // follows it. 193 | BOOL moveQueuePositionToNewPosition = (position == self.queue.position); 194 | 195 | id playerItem = self.queue[position]; 196 | 197 | [self.queue removeObjectAtIndex:position]; 198 | [self.queue insertObject:playerItem atIndex:newPosition]; 199 | 200 | if (moveQueuePositionToNewPosition) { 201 | self.queue.position = newPosition; 202 | } 203 | } 204 | } 205 | 206 | - (void)requeue:(id)playerItem atPosition:(NSUInteger)position { 207 | NSUInteger index = [self firstQueuePositionForPlayerItem:playerItem]; 208 | if (index != NSNotFound) { 209 | [self movePlayerItemFromPosition:index toPosition:position]; 210 | } 211 | } 212 | 213 | - (void)enqueuePlayerItems:(NSArray *)playerItems atPosition:(NSUInteger)position { 214 | NSUInteger iPosition = position; 215 | 216 | for (id playerItem in playerItems) { 217 | [self enqueue:playerItem atPosition:iPosition]; 218 | iPosition++; 219 | } 220 | } 221 | 222 | - (void)enqueuePlayerItems:(NSArray *)playerItems { 223 | [self enqueuePlayerItems:playerItems atPosition:self.queue.count]; 224 | } 225 | 226 | - (void)emptyQueue { 227 | @synchronized(self.queue) { 228 | [self.queue removeAllObjects]; 229 | 230 | if (self.player.rate != 0.0f) { 231 | [self enqueue:self.playerItem]; 232 | [self postGeneralChangeNotification]; 233 | } 234 | } 235 | } 236 | 237 | #pragma mark - Queue queries 238 | 239 | - (BOOL)queueContainsPlayerItem:(id)playerItem { 240 | return ([self firstQueuePositionForPlayerItem:playerItem] != NSNotFound); 241 | } 242 | 243 | - (id)playerItemAtQueuePosition:(NSUInteger)position { 244 | return [self.queue objectAtIndex:position]; 245 | } 246 | 247 | - (id)playerItemAtCurrentQueuePosition { 248 | return [self.queue objectAtIndex:self.queue.position]; 249 | } 250 | 251 | - (NSUInteger)firstQueuePositionForPlayerItem:(id)playerItem { 252 | @synchronized(self.queue) { 253 | return [self.queue indexOfObjectPassingTest:^BOOL(id aPlayerItem, NSUInteger idx, BOOL *stop) { 254 | return [aPlayerItem isEqualToPlayerItem:playerItem]; 255 | }]; 256 | } 257 | } 258 | 259 | - (NSUInteger)nextQueuePositionForPlayerItem:(id)playerItem { 260 | @synchronized(self.queue) { 261 | NSUInteger position; 262 | 263 | position = [self.queue indexOfObjectPassingTest:^BOOL(id aPlayerItem, NSUInteger idx, BOOL* stop) { 264 | return ([aPlayerItem isEqualToPlayerItem:playerItem] && idx >= self.queue.position); 265 | }]; 266 | 267 | if (position == NSNotFound) { 268 | position = [self firstQueuePositionForPlayerItem:playerItem]; 269 | } 270 | 271 | return position; 272 | } 273 | } 274 | 275 | - (NSIndexSet *)allQueuePositionsForPlayerItem:(id)playerItem { 276 | return [self.queue indexesOfObjectsPassingTest:^BOOL(idaPlayerItem, NSUInteger idx, BOOL *stop) { 277 | return [aPlayerItem isEqualToPlayerItem:playerItem]; 278 | }]; 279 | } 280 | 281 | #pragma mark - Remote control 282 | 283 | - (void)remoteControlReceivedWithEvent:(UIEvent *)event { 284 | [super remoteControlReceivedWithEvent:event]; 285 | 286 | switch (event.subtype) { 287 | case UIEventSubtypeRemoteControlNextTrack: 288 | [self seekForward]; 289 | break; 290 | case UIEventSubtypeRemoteControlPreviousTrack: 291 | [self seekBackward]; 292 | break; 293 | default: 294 | break; 295 | } 296 | } 297 | 298 | - (NSDictionary *)MPNowPlayingInfoCenterNowPlayingInfo { 299 | NSMutableDictionary *info = super.MPNowPlayingInfoCenterNowPlayingInfo.mutableCopy; 300 | 301 | if (!info[MPMediaItemPropertyAlbumTrackNumber]) { 302 | NSUInteger position = (self.queue.position == NSNotFound ? 0 : self.queue.position); 303 | NSUInteger count = (position + 1); 304 | 305 | info[MPMediaItemPropertyAlbumTrackNumber] = @(count); 306 | } 307 | 308 | if (!info[MPMediaItemPropertyAlbumTrackCount]) { 309 | info[MPMediaItemPropertyAlbumTrackCount] = @(self.queue.count); 310 | } 311 | 312 | return info; 313 | } 314 | 315 | #pragma mark - PRXAudioQueue delegate 316 | 317 | - (void)queueDidChange:(PRXPlayerQueue*)queue { 318 | [self postGeneralChangeNotification]; 319 | } 320 | 321 | #pragma mark -- Overrides 322 | 323 | - (void)playerItemDidPlayToEndTime:(NSNotification *)notification { 324 | [super mediaPlayerCurrentItemDidPlayToEndTime:notification]; 325 | [self seekForward]; 326 | } 327 | 328 | @end 329 | -------------------------------------------------------------------------------- /PRXPlayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // PRXPlayer.m 3 | // PRXPlayer 4 | // 5 | // Copyright (c) 2013 PRX. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #import "PRXPlayer_private.h" 26 | 27 | NSString * const PRXPlayerChangeNotification = @"PRXPlayerChangeNotification"; 28 | NSString * const PRXPlayerTimeIntervalNotification = @"PRXPlayerTimeIntervalNotification"; 29 | NSString * const PRXPlayerLongTimeIntervalNotification = @"PRXPlayerLongTimeIntervalNotification"; 30 | 31 | NSString * const PRXPlayerReachabilityPolicyPreventedPlayback = @"PRXPlayerReachabilityPolicyPreventedPlayback"; 32 | 33 | static const char *periodicTimeObserverQueueLabel = "PRXPlayerPeriodicTimeObserverQueueLabel"; 34 | static const char *playerAssignmentQueueLabel = "PRXPlayerAVPlayerAssignmentQueueLabel"; 35 | 36 | @implementation PRXPlayer 37 | 38 | + (instancetype)sharedPlayer { 39 | static dispatch_once_t predicate; 40 | static id _instance = nil; 41 | 42 | dispatch_once(&predicate, ^{ 43 | _instance = self.new; 44 | }); 45 | 46 | return _instance; 47 | } 48 | 49 | + (dispatch_queue_t)sharedObserverQueue { 50 | static dispatch_queue_t sharedObserverQueue; 51 | static dispatch_once_t onceToken; 52 | 53 | dispatch_once(&onceToken, ^{ 54 | sharedObserverQueue = dispatch_queue_create(periodicTimeObserverQueueLabel, DISPATCH_QUEUE_SERIAL); 55 | }); 56 | 57 | return sharedObserverQueue; 58 | } 59 | 60 | + (dispatch_queue_t)sharedAssignmentQueue { 61 | static dispatch_queue_t sharedAssignmentQueue; 62 | static dispatch_once_t onceToken; 63 | 64 | dispatch_once(&onceToken, ^{ 65 | sharedAssignmentQueue = dispatch_queue_create(playerAssignmentQueueLabel, DISPATCH_QUEUE_SERIAL); 66 | }); 67 | 68 | return sharedAssignmentQueue; 69 | } 70 | 71 | static void * const PRXPlayerAVPlayerStatusContext = (void*)&PRXPlayerAVPlayerStatusContext; 72 | static void * const PRXPlayerAVPlayerRateContext = (void*)&PRXPlayerAVPlayerRateContext; 73 | static void * const PRXPlayerAVPlayerErrorContext = (void*)&PRXPlayerAVPlayerErrorContext; 74 | static void * const PRXPlayerAVPlayerCurrentItemContext = (void*)&PRXPlayerAVPlayerCurrentItemContext; 75 | static void * const PRXPlayerAVPlayerCurrentItemStatusContext = (void*)&PRXPlayerAVPlayerCurrentItemStatusContext; 76 | static void * const PRXPlayerAVPlayerCurrentItemBufferEmptyContext = (void*)&PRXPlayerAVPlayerCurrentItemBufferEmptyContext; 77 | 78 | - (id)init { 79 | self = [super init]; 80 | if (self) { 81 | _reach = [Reachability reachabilityWithHostname:@"www.google.com"]; 82 | previousReachabilityStatus = -1; 83 | [self.reach startNotifier]; 84 | 85 | [[NSNotificationCenter defaultCenter] addObserver:self 86 | selector:@selector(reachabilityDidChange:) 87 | name:kReachabilityChangedNotification 88 | object:nil]; 89 | 90 | [NSNotificationCenter.defaultCenter addObserver:self 91 | selector:@selector(audioSessionInterruption:) 92 | name:AVAudioSessionInterruptionNotification 93 | object:nil]; 94 | 95 | [NSNotificationCenter.defaultCenter addObserver:self 96 | selector:@selector(audioSessionRouteChange:) 97 | name:AVAudioSessionRouteChangeNotification 98 | object:nil]; 99 | } 100 | return self; 101 | } 102 | 103 | - (void)setDelegate:(id)delegate { 104 | _delegate = delegate; 105 | } 106 | 107 | - (void)setPlayer:(AVPlayer *)player { 108 | // Player assignment is serialized on the assignment queue to ensure that 109 | // multiple successive assignments do not result in half-initialized observers 110 | // or players deallocated before observers have been cleaned up. We need a 111 | // separate queue from the one used by the TimeObservers, since after removing 112 | // those observers we need to wait on the observer queue to drain before 113 | // deallocating the old player. 114 | dispatch_async(self.class.sharedAssignmentQueue, ^{ 115 | if (self.player) { 116 | NSLog(@"Stopping to observe AVPlayer"); 117 | 118 | [self.player removeObserver:self forKeyPath:@"currentItem"]; 119 | [self.player removeObserver:self forKeyPath:@"status"]; 120 | [self.player removeObserver:self forKeyPath:@"rate"]; 121 | [self.player removeObserver:self forKeyPath:@"error"]; 122 | 123 | if (self.player.currentItem) { 124 | [self.player.currentItem removeObserver:self forKeyPath:@"status"]; 125 | [self.player.currentItem removeObserver:self forKeyPath:@"playbackBufferEmpty"]; 126 | } 127 | 128 | if (playerPeriodicTimeObserver) { 129 | [self.player removeTimeObserver:playerPeriodicTimeObserver]; 130 | playerPeriodicTimeObserver = nil; 131 | } 132 | 133 | if (playerSoftEndBoundaryTimeObserver) { 134 | [self.player removeTimeObserver:playerSoftEndBoundaryTimeObserver]; 135 | playerSoftEndBoundaryTimeObserver = nil; 136 | } 137 | 138 | if (playerPlaybackStartBoundaryTimeObserver) { 139 | [self.player removeTimeObserver:playerPlaybackStartBoundaryTimeObserver]; 140 | playerPlaybackStartBoundaryTimeObserver = nil; 141 | } 142 | } 143 | 144 | // Ensure that all time observer messages in flight have cleared the queue: 145 | dispatch_sync(self.class.sharedObserverQueue, ^{}); 146 | 147 | _player = player; 148 | 149 | if (self.player) { 150 | NSKeyValueObservingOptions options = (NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld); 151 | 152 | [self.player addObserver:self forKeyPath:@"currentItem" options:options context:PRXPlayerAVPlayerCurrentItemContext]; 153 | 154 | [self.player addObserver:self forKeyPath:@"status" options:options context:PRXPlayerAVPlayerStatusContext]; 155 | [self.player addObserver:self forKeyPath:@"rate" options:options context:PRXPlayerAVPlayerRateContext]; 156 | [self.player addObserver:self forKeyPath:@"error" options:options context:PRXPlayerAVPlayerRateContext]; 157 | 158 | __block id _self = self; 159 | 160 | playerPeriodicTimeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, 1000) queue:self.class.sharedObserverQueue usingBlock:^(CMTime time) { 161 | [_self didObservePeriodicTimeChange:time]; 162 | }]; 163 | 164 | // when using playerWithPlayerItem: the player will come with an item, and the 165 | // current item context wont actually "change" 166 | AVPlayerItem *currentItem = self.player.currentItem; 167 | if (currentItem) { 168 | NSLog(@"AVPlayer arrived with a current playerItem; treating it like an observed change"); 169 | // construct a dummy change dict to pass along 170 | NSDictionary *change = @{ NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting), 171 | NSKeyValueChangeNewKey : currentItem }; 172 | [self mediaPlayerCurrentItemDidChange:change]; 173 | } 174 | 175 | [self postGeneralChangeNotification]; 176 | } 177 | 178 | }); 179 | } 180 | 181 | - (void)setPlayerItem:(id)playerItem { 182 | NSLog(@"PRXPlayerItem was set"); 183 | ignoreTimeObservations = YES; 184 | 185 | // If setting to anything other than current PlayerItem 186 | // now is a good time to silence any existing playback 187 | if (![playerItem isEqualToPlayerItem:self.playerItem]) { 188 | self.player.rate = 0.0f; 189 | } 190 | 191 | // Update playerItem but keep previous item around for conditionals below 192 | id oldPlayerItem = _playerItem; 193 | _playerItem = playerItem; 194 | 195 | // Send a change notification to our delegate: 196 | if ([self.delegate respondsToSelector:@selector(player:playerItemDidChange:)]) { 197 | NSDictionary *change = @{ NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting), 198 | NSKeyValueChangeOldKey : oldPlayerItem ? (id)oldPlayerItem : [NSNull null], 199 | NSKeyValueChangeNewKey : playerItem ? (id)playerItem : [NSNull null]}; 200 | [self.delegate player:self playerItemDidChange:change]; 201 | } 202 | 203 | [self postGeneralChangeNotification]; 204 | 205 | // Setting the playerItem to nil is the same as calling stop, 206 | // everything should get dumped; 207 | if (!playerItem) { 208 | if (self.player) { 209 | NSLog(@"Tearing down existing AVPlayer"); 210 | [self.player replaceCurrentItemWithPlayerItem:nil]; 211 | self.player = nil; 212 | } 213 | } else { 214 | AVAsset *playerItemAsset = self.playerItemAsset; 215 | 216 | // most prxplayer properties can/should be reset somewhere in here 217 | // dateAtAudioPlaybackInterruption = nil; //this really can't happen here 218 | 219 | // 220 | // Nothing can happen after these checks; most call async methods 221 | // and we need to wait for the results 222 | // 223 | 224 | // If there was no previous valid PlayerItem, we can always load the new one 225 | if (![oldPlayerItem conformsToProtocol:@protocol(PRXPlayerItem)]) { 226 | NSLog(@"No previous player item was set; no reason not to load tracks for new one"); 227 | [self loadTracksForAsset:playerItemAsset]; 228 | } else if (![playerItemAsset isKindOfClass:AVURLAsset.class]) { 229 | NSLog(@"New asset isn't a URL asset, so we can't make any checks; just load the tracks"); 230 | [self loadTracksForAsset:playerItemAsset]; 231 | } else { 232 | AVURLAsset *newURLAsset = (AVURLAsset *)playerItemAsset; 233 | 234 | // 235 | // Knowing the new PlayerItem asset is a URL asset lets us be smart 236 | // about some specific situations 237 | // 238 | 239 | // if the new and old items are the same, we're probably dealing with a case 240 | // where the PlayerItem's asset resource changed from local to remote. 241 | // Since old and new are the same, checking for a change in that object's 242 | // asset would always be false, so we should check against the asset 243 | // actually loaded into the player 244 | // (this only can be checked if the current asset is a URL asset) 245 | if ([oldPlayerItem isEqualToPlayerItem:playerItem] 246 | && [self.player.currentItem.asset isKindOfClass:AVURLAsset.class]) { 247 | AVURLAsset *currentAsset = (AVURLAsset *)self.player.currentItem.asset; 248 | 249 | if (![currentAsset.URL isEqual:newURLAsset.URL]) { 250 | NSLog(@"New PlayerItem matches currentPlayer item, but URLs differ. Resource likely changed; loading tracks"); 251 | // This will not seemlessly transition between resources unless you are maintaning 252 | // position state on the PRXPlayerItem, which will be used after the tracks load. 253 | // Otherwise each transition will restart from the beginning. 254 | [self loadTracksForAsset:playerItemAsset]; 255 | } else { 256 | // Old and new PlayerItem were equal, and the new item's asset 257 | // matches the currently loaded asset, 258 | // just make sure it's playing/holding as requested 259 | NSLog(@"New PlayerItem matches current PlayerItem exactly. No reason to load, just deal with playback"); 260 | [self bar]; 261 | } 262 | } else if ([oldPlayerItem.playerAsset isKindOfClass:AVURLAsset.class]) { 263 | AVURLAsset *oldURLAsset = (AVURLAsset *)oldPlayerItem.playerAsset; 264 | 265 | // If the new and old PlayerItems are not the same, but their 266 | // asset URLs are, we can't assume it's a remote/local switchover, 267 | // and there's no need to reload the already loaded tracks, so 268 | // just make sure it's playing/holding as requested 269 | if ([oldURLAsset.URL isEqual:newURLAsset.URL]) { 270 | NSLog(@"PlayerItem changed, but asset resource (URL) did not. No reason to load, just deal with playback"); 271 | [self bar]; 272 | } else { 273 | NSLog(@"PlayerItems and Asset URLs have changed; load tracks for new PlayerItem"); 274 | [self loadTracksForAsset:playerItemAsset]; 275 | } 276 | } else { 277 | // PlayerItem changed but old item's asset isn't a URL asset, so we 278 | // can't make any good checks; just load the tracks 279 | NSLog(@"Couldn't compare new PlayerItem with old asset, so load its tracks"); 280 | [self loadTracksForAsset:playerItemAsset]; 281 | } 282 | } 283 | } 284 | 285 | return; 286 | } 287 | 288 | #pragma mark - Properties 289 | 290 | - (NSTimeInterval)buffer { 291 | // TODO this should probably check for asset parity 292 | if (self.player.currentItem) { 293 | CMTimeRange tr; 294 | [self.player.currentItem.loadedTimeRanges.lastObject getValue:&tr]; 295 | 296 | CMTime duration = tr.duration; 297 | return MAX(0.0f, CMTimeGetSeconds(duration)); 298 | } 299 | 300 | return 0.0f; 301 | } 302 | 303 | - (PRXPlayerState)state { 304 | if (!self.playerItem) { 305 | return PRXPlayerStateEmpty; 306 | } 307 | 308 | AVAsset *playerAssset = self.player.currentItem.asset; 309 | AVAsset *itemAsset = self.playerItemAsset; 310 | 311 | BOOL isPlayerAssetURLAsset = [playerAssset isKindOfClass:AVURLAsset.class]; 312 | BOOL isItemAssetURLAsset = [itemAsset isKindOfClass:AVURLAsset.class]; 313 | 314 | BOOL haveAssetParity = (isItemAssetURLAsset && isPlayerAssetURLAsset && [((AVURLAsset *)itemAsset).URL isEqual:((AVURLAsset *)playerAssset).URL] ? YES : NO); 315 | 316 | if (!haveAssetParity 317 | && [self allowsLoadingOfAsset:itemAsset]) { 318 | // We can assume that the current asset (asset of the current PlayerItem) is "loading" if 319 | // it's not the asset that is currently in the player, and the reachability policy allows 320 | // it be loaded 321 | return PRXPlayerStateLoading; 322 | } else if (haveAssetParity 323 | && dateAtAudioPlaybackInterruption) { 324 | return PRXPlayerStateWaiting; 325 | } else if (haveAssetParity 326 | && self.player.currentItem 327 | && self.player.currentItem.status == AVPlayerStatusReadyToPlay) { 328 | // If we have asset parity and the AVPlayer's asset is ready, we can consider the PRXPlayer 329 | // state to be ready as well 330 | return PRXPlayerStateReady; 331 | } else if (haveAssetParity) { 332 | // This likely isn't accurate... 333 | // 334 | return PRXPlayerStateBuffering; 335 | } 336 | 337 | // If we get here we don't really know what's going on with the player and we should feel bad 338 | return PRXPlayerStateUnknown; 339 | } 340 | 341 | // this will go away 342 | - (NSDate *)dateAtAudioPlaybackInterruption { 343 | return dateAtAudioPlaybackInterruption; 344 | } 345 | 346 | #pragma mark - Indifferent controls 347 | 348 | - (void)play { 349 | holdPlayback = NO; 350 | dateAtAudioPlaybackInterruption = nil; 351 | 352 | // If the current player item isn't ready, nothing good can happen from trying 353 | // to start playback 354 | if (!self.player.currentItem 355 | || (self.player.currentItem && self.player.currentItem.status != AVPlayerStatusReadyToPlay)) { 356 | NSLog(@"Asked to play but no item is ready; if something it loading there is no hold so it will start"); 357 | return; 358 | } 359 | 360 | // If the current player asset doesn't match the current item asset 361 | // simply starting playback is almost certainly unexpected behavior 362 | if ([self.playerItemAsset isKindOfClass:AVURLAsset.class] 363 | && [self.player.currentItem.asset isKindOfClass:AVURLAsset.class]) { 364 | AVURLAsset *currentAsset = (AVURLAsset *)self.player.currentItem.asset; 365 | AVURLAsset *itemAsset = (AVURLAsset *)self.playerItemAsset; 366 | 367 | if (![currentAsset.URL isEqual:itemAsset.URL]) { 368 | NSLog(@"Current item does not match the loaded item, starting playback now would likely result in unexpected behavior"); 369 | return; 370 | } 371 | } 372 | 373 | if (self.player.rate == 0.0f 374 | || (self.player.rate != 0.0f && self.player.rate != self.rateForPlayback)) { 375 | self.player.rate = self.rateForPlayback; 376 | } 377 | } 378 | 379 | - (void)pause { 380 | if (self.player.rate != 0.0f) { 381 | self.player.rate = 0.0f; 382 | } 383 | 384 | holdPlayback = YES; 385 | dateAtAudioPlaybackInterruption = nil; 386 | } 387 | 388 | - (void)toggle { 389 | (self.player.rate == 0.0f) ? [self play] : [self pause]; 390 | } 391 | 392 | - (void)toggleOrCancel { 393 | if (self.state == PRXPlayerStateLoading || 394 | self.state == PRXPlayerStateBuffering || 395 | self.state == PRXPlayerStateWaiting) { 396 | [self pause]; 397 | } else { 398 | [self toggle]; 399 | } 400 | } 401 | 402 | - (void)stop { 403 | self.playerItem = nil; 404 | } 405 | 406 | #pragma mark - Remote control 407 | 408 | - (BOOL)canBecomeFirstResponder { 409 | return YES; 410 | } 411 | 412 | - (BOOL)becomeFirstResponder { 413 | return YES; 414 | } 415 | 416 | - (NSDictionary *)MPNowPlayingInfoCenterNowPlayingInfo { 417 | NSMutableDictionary *info = NSMutableDictionary.dictionary; 418 | 419 | if ([self.playerItem respondsToSelector:@selector(mediaItemProperties)]) { 420 | [info setValuesForKeysWithDictionary:self.playerItem.mediaItemProperties]; 421 | } 422 | 423 | // We'll use the asset metadata (eg ID3 tags) as default values for any properties 424 | // that didnt get set explicitly by the current PRXPlayerItem 425 | NSArray* metadata = self.player.currentItem.asset.commonMetadata; 426 | 427 | // Reporting times when we don't have a duration can get messy 428 | if (self.player.currentItem.duration.value > 0) { 429 | if (!info[MPMediaItemPropertyPlaybackDuration]) { 430 | float _playbackDuration = self.player.currentItem ? CMTimeGetSeconds(self.player.currentItem.duration) : 0.0f; 431 | NSNumber* playbackDuration = @(_playbackDuration); 432 | info[MPMediaItemPropertyPlaybackDuration] = playbackDuration; 433 | } 434 | 435 | if (!info[MPNowPlayingInfoPropertyElapsedPlaybackTime]) { 436 | float _elapsedPlaybackTime = self.player.currentItem ? CMTimeGetSeconds(self.player.currentItem.currentTime) : 0.0f; 437 | NSNumber* elapsedPlaybackTime = @(_elapsedPlaybackTime); 438 | info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedPlaybackTime; 439 | } 440 | } 441 | 442 | if (!info[MPNowPlayingInfoPropertyPlaybackRate]) { 443 | info[MPNowPlayingInfoPropertyPlaybackRate] = @(self.rateForPlayback); 444 | } 445 | 446 | if (!info[MPMediaItemPropertyArtwork]) { 447 | NSArray* artworkMetadata = [AVMetadataItem metadataItemsFromArray:metadata 448 | withKey:AVMetadataCommonKeyArtwork 449 | keySpace:AVMetadataKeySpaceCommon]; 450 | if (artworkMetadata.count > 0) { 451 | AVMetadataItem* artworkMetadataItem = artworkMetadata[0]; 452 | UIImage* artworkImage; 453 | 454 | if ([artworkMetadataItem.value respondsToSelector:@selector(objectForKeyedSubscript:)]) { 455 | artworkImage = [UIImage imageWithData:artworkMetadataItem.value[@"data"]]; 456 | } else if ([artworkMetadataItem.value isKindOfClass:NSData.class]) { 457 | artworkImage = [UIImage imageWithData:artworkMetadataItem.dataValue]; 458 | } 459 | 460 | MPMediaItemArtwork* artwork = [[MPMediaItemArtwork alloc] initWithImage:artworkImage]; 461 | 462 | info[MPMediaItemPropertyArtwork] = artwork; 463 | } 464 | } 465 | 466 | if (!info[MPMediaItemPropertyTitle]) { 467 | NSArray* _metadata = [AVMetadataItem metadataItemsFromArray:metadata withKey:AVMetadataCommonKeyTitle keySpace:AVMetadataKeySpaceCommon]; 468 | 469 | if (_metadata.count > 0) { 470 | AVMetadataItem* _metadataItem = _metadata[0]; 471 | info[MPMediaItemPropertyTitle] = _metadataItem.value; 472 | } 473 | } 474 | 475 | if (!info[MPMediaItemPropertyAlbumTitle]) { 476 | NSArray* _metadata = [AVMetadataItem metadataItemsFromArray:metadata withKey:AVMetadataCommonKeyAlbumName keySpace:AVMetadataKeySpaceCommon]; 477 | 478 | if (_metadata.count > 0) { 479 | AVMetadataItem* _metadataItem = _metadata[0]; 480 | info[MPMediaItemPropertyAlbumTitle] = _metadataItem.value; 481 | } 482 | } 483 | 484 | if (!info[MPMediaItemPropertyArtist]) { 485 | NSArray* _metadata = [AVMetadataItem metadataItemsFromArray:metadata withKey:AVMetadataCommonKeyArtist keySpace:AVMetadataKeySpaceCommon]; 486 | 487 | if (_metadata.count > 0) { 488 | AVMetadataItem* _metadataItem = _metadata[0]; 489 | info[MPMediaItemPropertyArtist] = _metadataItem.value; 490 | } 491 | } 492 | 493 | return info; 494 | } 495 | 496 | - (void)publishMPNowPlayingInfoCenterNowPlayingInfo { 497 | NSLog(@"Publishing media item properties to MPNowPlayingInfoCenter"); 498 | MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = self.MPNowPlayingInfoCenterNowPlayingInfo; 499 | } 500 | 501 | - (void)remoteControlReceivedWithEvent:(UIEvent *)event { 502 | switch (event.subtype) { 503 | case UIEventSubtypeNone: 504 | break; 505 | case UIEventSubtypeMotionShake: 506 | break; 507 | case UIEventSubtypeRemoteControlPlay: 508 | [self play]; 509 | break; 510 | case UIEventSubtypeRemoteControlPause: 511 | [self pause]; 512 | break; 513 | case UIEventSubtypeRemoteControlStop: 514 | [self stop]; 515 | break; 516 | case UIEventSubtypeRemoteControlTogglePlayPause: 517 | [self toggle]; 518 | break; 519 | case UIEventSubtypeRemoteControlNextTrack: 520 | break; 521 | case UIEventSubtypeRemoteControlPreviousTrack: 522 | break; 523 | case UIEventSubtypeRemoteControlBeginSeekingBackward: 524 | break; 525 | case UIEventSubtypeRemoteControlEndSeekingBackward: 526 | break; 527 | case UIEventSubtypeRemoteControlBeginSeekingForward: 528 | break; 529 | case UIEventSubtypeRemoteControlEndSeekingForward: 530 | break; 531 | default: 532 | break; 533 | } 534 | } 535 | 536 | #pragma mark - Target playback rates 537 | 538 | - (float)rateForFilePlayback { 539 | if ([self.delegate respondsToSelector:@selector(filePlaybackRateForPlayer:)]) { 540 | return [self.delegate filePlaybackRateForPlayer:self]; 541 | } 542 | 543 | return 1.0f; 544 | } 545 | 546 | - (float)rateForPlayback { 547 | // It's dangerous to try to play to play unbounded (eg non-file, ie streams) 548 | // assets faster than 1x, as it will almost always play at 1x anyway 549 | 550 | return (CMTIME_IS_INDEFINITE(self.player.currentItem.duration) ? 1.0f : self.rateForFilePlayback); 551 | } 552 | 553 | #pragma mark - Reachablity 554 | 555 | - (BOOL)allowsPlaybackViaWWAN { 556 | if ([self.delegate respondsToSelector:@selector(playerAllowsPlaybackViaWWAN:)]) { 557 | return [self.delegate playerAllowsPlaybackViaWWAN:self]; 558 | } 559 | 560 | return YES; 561 | } 562 | 563 | - (BOOL)allowsLoadingOfAsset:(AVAsset *)asset { 564 | self.reach.reachableOnWWAN = self.allowsPlaybackViaWWAN; 565 | BOOL isAssetLocal = NO; 566 | 567 | if ([asset isKindOfClass:AVURLAsset.class]) { 568 | AVURLAsset *_asset = (AVURLAsset *)asset; 569 | 570 | if (_asset.URL.isFileURL) { 571 | isAssetLocal = YES; 572 | } 573 | } 574 | 575 | if (!self.reach.isReachable && !isAssetLocal) { 576 | NSLog(@"Reachability policy doesn't allow for WWAN playback of remote assets; tracks will not be loaded"); 577 | [NSNotificationCenter.defaultCenter postNotificationName:PRXPlayerReachabilityPolicyPreventedPlayback object:self]; 578 | return NO; 579 | } else { 580 | NSLog(@"Reachability policy allows for playback under current conditions: Local file: %i, Reach: %@", isAssetLocal, self.reach.currentReachabilityString); 581 | return YES; 582 | } 583 | } 584 | 585 | #pragma mark - Loading assets 586 | 587 | - (AVAsset *)playerItemAsset { 588 | AVAsset *asset = self.playerItem.playerAsset; 589 | 590 | if ([self.delegate respondsToSelector:@selector(player:assetForPlayerItem:)]) { 591 | asset = [self.delegate player:self assetForPlayerItem:self.playerItem]; 592 | } 593 | 594 | return asset; 595 | } 596 | 597 | - (void)loadTracksForAsset:(AVAsset *)asset { 598 | if (![self allowsLoadingOfAsset:asset]) { 599 | return; 600 | } 601 | 602 | ignoreTimeObservations = YES; 603 | 604 | static NSString *AVKeyAssetTracks = @"tracks"; 605 | 606 | NSLog(@"Attempting to load tracks for asset"); 607 | [asset loadValuesAsynchronouslyForKeys:@[ AVKeyAssetTracks ] completionHandler:^{ 608 | NSLog(@"Done trying to load tracks for asset..."); 609 | 610 | dispatch_async(dispatch_get_main_queue(), ^{ 611 | NSError *error; 612 | AVKeyValueStatus status = [asset statusOfValueForKey:AVKeyAssetTracks error:&error]; 613 | 614 | if (status == AVKeyValueStatusLoaded) { 615 | NSLog(@"...Loaded tracks for asset, passing to AVPlayer if necessary"); 616 | 617 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 618 | // TODO clean up these if/elses 619 | if (!self.playerItem) { 620 | NSLog(@"PlayerItem was removed before tracks could load, so they're being ignored"); 621 | return; 622 | } else if ([asset isKindOfClass:AVURLAsset.class] && [self.playerItemAsset isKindOfClass:AVURLAsset.class]) { 623 | AVURLAsset *urlAsset = (AVURLAsset *)asset; 624 | AVURLAsset *itemURLAsset = (AVURLAsset *)self.playerItemAsset; 625 | 626 | if (![urlAsset.URL.absoluteString isEqualToString:itemURLAsset.URL.absoluteString]) { 627 | NSLog(@"PlayerItem asset (%@) no longer matches this asset (%@), so the loaded tracks are being ignored", urlAsset.URL.absoluteString, itemURLAsset.URL.absoluteString); 628 | return; 629 | } 630 | } 631 | 632 | AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset]; 633 | 634 | // Using replaceCurrentItemWithPlayerItem: was problematic 635 | // but may be more better if the issues are fixed 636 | NSLog(@"Setting up a new AVPlayer"); 637 | self.player = [AVPlayer playerWithPlayerItem:playerItem]; 638 | }); 639 | } else { 640 | BOOL _hold = holdPlayback; 641 | NSLog(@"...Failed to load tracks for asset %@", asset); 642 | holdPlayback = YES; // until there's something better to do; may actually be worth stopping 643 | 644 | if ([self.delegate respondsToSelector:@selector(player:failedToLoadTracksForAsset:holdPlayback:)]) { 645 | [self.delegate player:self failedToLoadTracksForAsset:asset holdPlayback:_hold]; 646 | } 647 | 648 | // TODO 649 | // loading the tracks using a player url asset is more reliable and has already been tried 650 | // by the time we get here. but if it fails we can still try to set the player item directly. 651 | // self.currentPlayerItem = [AVPlayerItem playerItemWithURL:self.currentPlayable.audioURL]; 652 | } 653 | }); 654 | }]; 655 | } 656 | 657 | #pragma mark - Routing observations 658 | 659 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 660 | if (context == &PRXPlayerAVPlayerCurrentItemContext) { 661 | NSLog(@"Observed AVPlayer currentItem change"); 662 | [self mediaPlayerCurrentItemDidChange:change]; 663 | return; 664 | } else if (context == &PRXPlayerAVPlayerStatusContext) { 665 | // TODO figure out a way to trigger this 666 | NSLog(@"PRXPlayerAVPlayerStatusContext"); 667 | return; 668 | } else if (context == &PRXPlayerAVPlayerRateContext) { 669 | NSLog(@"Observed AVPlayer rate change"); 670 | [self mediaPlayerRateDidChange:change]; 671 | return; 672 | } else if (context == &PRXPlayerAVPlayerErrorContext) { 673 | // TODO I dont think this is actually worth observing, since the player 674 | // will never recover from an error, and the value is only worth 675 | // checking after a status change, which is observed separately 676 | NSLog(@"PRXPlayerAVPlayerErrorContext"); 677 | [self mediaPlayerErrorDidChange:change]; 678 | return; 679 | } else if (context == &PRXPlayerAVPlayerCurrentItemStatusContext) { 680 | NSLog(@"Observed player item status change"); 681 | [self mediaPlayerCurrentItemStatusDidChange:change]; 682 | return; 683 | } else if (context == &PRXPlayerAVPlayerCurrentItemBufferEmptyContext) { 684 | NSLog(@"Observed player item buffer state change"); 685 | [self mediaPlayerCurrentItemBufferEmptied:change]; 686 | return; 687 | } 688 | 689 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 690 | return; 691 | } 692 | 693 | #pragma mark - Responding to observations 694 | 695 | - (void)mediaPlayerRateDidChange:(NSDictionary *)change { 696 | if ([change[NSKeyValueChangeNewKey] isKindOfClass:NSClassFromString(@"NSError")]) { return; } 697 | float newValue = [change[NSKeyValueChangeNewKey] floatValue]; 698 | 699 | // When the rate becomes non-zero we should check to make sure the 700 | // NowPlayingInfo's rate is correct, in case the target playback rate 701 | // has changed since it was last published, and republish if necessary 702 | if (newValue != 0.0) { 703 | NSDictionary *nowPlayingInfo = MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo; 704 | NSNumber *_playbackRate = nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate]; 705 | 706 | if (_playbackRate && ![_playbackRate isEqual:NSNull.null]) { 707 | float playbackRate = _playbackRate.floatValue; 708 | 709 | if (newValue != playbackRate) { 710 | NSLog(@"Target playback rate appears to have changed from %f to %f", playbackRate, newValue); 711 | [self publishMPNowPlayingInfoCenterNowPlayingInfo]; 712 | } 713 | } 714 | } 715 | 716 | if ([self.delegate respondsToSelector:@selector(player:rateDidChange:)]) { 717 | [self.delegate player:self rateDidChange:change]; 718 | } 719 | 720 | [self postGeneralChangeNotification]; 721 | } 722 | 723 | - (void)mediaPlayerErrorDidChange:(NSDictionary *)change { 724 | } 725 | 726 | - (void)mediaPlayerCurrentItemDidChange:(NSDictionary *)change { 727 | NSUInteger valueChangeKind = [change[NSKeyValueChangeKindKey] integerValue]; 728 | 729 | if (valueChangeKind == NSKeyValueChangeSetting) { 730 | AVPlayerItem *oldItem = change[NSKeyValueChangeOldKey]; 731 | AVPlayerItem *newItem = change[NSKeyValueChangeNewKey]; 732 | NSLog(@"oldItem: %@, newItem: %@", oldItem, newItem); 733 | 734 | if (![oldItem isEqual:[NSNull null]]) { 735 | NSLog(@"Unregistering observers from AVPlayer's old playerItem"); 736 | 737 | [oldItem removeObserver:self forKeyPath:@"status"]; 738 | [oldItem removeObserver:self forKeyPath:@"playbackBufferEmpty"]; 739 | 740 | [NSNotificationCenter.defaultCenter removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; 741 | [NSNotificationCenter.defaultCenter removeObserver:self name:AVPlayerItemTimeJumpedNotification object:nil]; 742 | } 743 | 744 | if (![newItem isEqual:[NSNull null]]) { 745 | NSLog(@"Starting to observe AVPlayer's new playerItem: %@", newItem); 746 | 747 | NSKeyValueObservingOptions options = (NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld); 748 | 749 | [newItem addObserver:self forKeyPath:@"status" options:options context:PRXPlayerAVPlayerCurrentItemStatusContext]; 750 | [newItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:options context:PRXPlayerAVPlayerCurrentItemBufferEmptyContext]; 751 | 752 | [NSNotificationCenter.defaultCenter addObserver:self 753 | selector:@selector(mediaPlayerCurrentItemDidPlayToEndTime:) 754 | name:AVPlayerItemDidPlayToEndTimeNotification 755 | object:newItem]; 756 | 757 | [NSNotificationCenter.defaultCenter addObserver:self 758 | selector:@selector(mediaPlayerCurrentItemDidJumpTime:) 759 | name:AVPlayerItemTimeJumpedNotification 760 | object:newItem]; 761 | 762 | // Sometimes I think this change doesn't get observed until after 763 | // after the status has already changed, so we need to invoke some 764 | // handlers manually 765 | if (newItem.status != AVPlayerStatusUnknown) { 766 | NSLog(@"Newly set AVPlayerItem arrived with a meaningful status; handling appropriately"); 767 | 768 | if (newItem.status == AVPlayerStatusReadyToPlay) { 769 | [self mediaPlayerCurrentItemDidBecomeReadyToPlay]; 770 | } else if (newItem.status == AVPlayerStatusFailed) { 771 | [self mediaPlayerCurrentItemFailedToBecomeReadyToPlay]; 772 | } 773 | } 774 | } 775 | } 776 | } 777 | 778 | - (void)mediaPlayerCurrentItemStatusDidChange:(NSDictionary *)change { 779 | NSUInteger valueChangeKind = [change[NSKeyValueChangeKindKey] integerValue]; 780 | 781 | if (valueChangeKind == NSKeyValueChangeSetting) { 782 | id _new = change[NSKeyValueChangeNewKey]; 783 | id _old = change[NSKeyValueChangeOldKey]; 784 | 785 | // Only if an actual change happened. 786 | if (![_new isEqual:_old]) { 787 | if (self.player.currentItem.status == AVPlayerStatusReadyToPlay) { 788 | NSLog(@"Item status is ReadyToPlay"); 789 | [self mediaPlayerCurrentItemDidBecomeReadyToPlay]; 790 | } else if (self.player.currentItem.status == AVPlayerStatusFailed) { 791 | NSLog(@"Item status is failed: %@", self.player.currentItem.error); 792 | // force this with an HTTP200 .m3u that contains an HTTP404 793 | [self mediaPlayerCurrentItemFailedToBecomeReadyToPlay]; 794 | } else { 795 | NSLog(@"Item status is not ready or failed: %li", (long)self.player.currentItem.status); 796 | } 797 | } 798 | } 799 | 800 | if ([self.delegate respondsToSelector:@selector(player:currentItemStatusDidChange:)]) { 801 | [self.delegate player:self currentItemStatusDidChange:change]; 802 | } 803 | 804 | [self postGeneralChangeNotification]; 805 | } 806 | 807 | - (void)mediaPlayerCurrentItemDidBecomeReadyToPlay { 808 | [self publishMPNowPlayingInfoCenterNowPlayingInfo]; 809 | 810 | if (self.player.currentItem.duration.value > 0) { 811 | Float64 duration = CMTimeGetSeconds(self.player.currentItem.duration); 812 | 813 | Float64 progress = 0.95f; 814 | 815 | if ([self.delegate respondsToSelector:@selector(softEndBoundaryProgressForPlayer:)]) { 816 | progress = [self.delegate softEndBoundaryProgressForPlayer:self]; 817 | } 818 | 819 | int64_t boundaryTime = (duration * progress); 820 | CMTime boundary = CMTimeMakeWithSeconds(boundaryTime, 10); 821 | 822 | NSValue* _boundary = [NSValue valueWithCMTime:boundary]; 823 | 824 | __block id _self = self; 825 | 826 | dispatch_async(self.class.sharedObserverQueue, ^{ 827 | if (playerSoftEndBoundaryTimeObserver) { 828 | [self.player removeTimeObserver:playerSoftEndBoundaryTimeObserver]; 829 | playerSoftEndBoundaryTimeObserver = nil; 830 | } 831 | 832 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 833 | NSLog(@"Adding soft end boundary observer: %@s (%f)", @(CMTimeGetSeconds(boundary)), progress); 834 | playerSoftEndBoundaryTimeObserver = [self.player addBoundaryTimeObserverForTimes:@[ _boundary ] 835 | queue:self.class.sharedObserverQueue 836 | usingBlock:^{ 837 | [_self didObserveSoftBoundaryTime]; 838 | }]; 839 | 840 | }); 841 | }); 842 | } 843 | [self bar]; 844 | } 845 | 846 | - (void)mediaPlayerCurrentItemFailedToBecomeReadyToPlay { 847 | NSUInteger _retryLimit = 3; 848 | 849 | if ([self.delegate respondsToSelector:@selector(retryLimitForPlayer:)]) { 850 | _retryLimit = [self.delegate retryLimitForPlayer:self]; 851 | } 852 | 853 | if (retryCount < _retryLimit) { 854 | retryCount++; 855 | NSLog(@"Retry %lu of %lu", (unsigned long)retryCount, (unsigned long)_retryLimit); 856 | 857 | id retryPlayerItem = self.playerItem; 858 | [self stop]; 859 | self.playerItem = retryPlayerItem; 860 | } else { 861 | NSLog(@"Retries failed, stopping."); 862 | BOOL _hold = holdPlayback; 863 | [self stop]; 864 | 865 | if ([self.delegate respondsToSelector:@selector(playerFailedToBecomeReadyToPlay:holdPlayback:)]) { 866 | [self.delegate playerFailedToBecomeReadyToPlay:self holdPlayback:_hold]; 867 | } 868 | } 869 | } 870 | 871 | - (void)mediaPlayerCurrentItemBufferEmptied:(NSDictionary *)change { 872 | BOOL oldValue = [change[NSKeyValueChangeOldKey] boolValue]; 873 | BOOL newValue = [change[NSKeyValueChangeNewKey] boolValue]; 874 | 875 | if (oldValue != newValue && self.playerItem) { 876 | if (newValue) { 877 | NSLog(@"Buffer went from not empty to empty..."); 878 | 879 | if ([self.player.currentItem.asset isKindOfClass:AVURLAsset.class] 880 | && [[((AVURLAsset *)self.player.currentItem.asset) URL] isFileURL] ) { 881 | NSLog(@"...but was a local file. This isn't considered a problem; no need to restart."); 882 | } else if (!self.reach.isReachable) { 883 | NSLog(@"...and we don't have connectivity for a remote file/stream; flag for a restart when we do..."); 884 | // TODO flag for retry 885 | } else { 886 | if (self.player.externalPlaybackActive) { 887 | NSLog(@"...still have connectivity, but AirPlay is borked, NOT trying again."); 888 | } else { 889 | NSLog(@"...but we still have connectivity, reloading remote files/streams to try again"); 890 | [self reloadPlayerItemWithRemoteAsset:self.playerItem]; 891 | } 892 | } 893 | } else { 894 | NSLog(@"Buffer went from empty to not empty"); 895 | } 896 | } 897 | } 898 | 899 | - (void)mediaPlayerCurrentItemDidPlayToEndTime:(NSNotification *)notification { 900 | if ([self.delegate respondsToSelector:@selector(player:endTimeReachedForPlayerItem:)]) { 901 | [self.delegate player:self endTimeReachedForPlayerItem:notification.object]; 902 | } 903 | } 904 | 905 | - (void)mediaPlayerCurrentItemDidJumpTime:(NSNotification *)notification { 906 | if (!ignoreTimeObservations) { 907 | // called when seeking and when setting rate >0 908 | [self publishMPNowPlayingInfoCenterNowPlayingInfo]; 909 | } 910 | } 911 | 912 | - (void)didObservePeriodicTimeChange:(CMTime)time { 913 | if (ignoreTimeObservations) { 914 | NSLog(@"Ignoring out of sequence time change"); 915 | } else { 916 | NSValue *time_v = [NSValue valueWithCMTime:time]; 917 | NSDictionary *userInfo = @{ @"time": time_v }; 918 | 919 | AVURLAsset *asset; 920 | 921 | [NSNotificationCenter.defaultCenter postNotificationName:PRXPlayerTimeIntervalNotification 922 | object:self 923 | userInfo:userInfo]; 924 | 925 | if ([self.player.currentItem.asset isKindOfClass:AVURLAsset.class]) { 926 | asset = (AVURLAsset *)self.player.currentItem.asset; 927 | 928 | [NSNotificationCenter.defaultCenter postNotificationName:PRXPlayerTimeIntervalNotification 929 | object:asset.URL.absoluteString 930 | userInfo:userInfo]; 931 | } 932 | 933 | if ([self.playerItem respondsToSelector:@selector(setPlayerTime:)]) { 934 | 935 | self.playerItem.playerTime = time; 936 | } 937 | 938 | if (fmodf(round(CMTimeGetSeconds(time)), 10.0f) == 9.0f) { 939 | [NSNotificationCenter.defaultCenter postNotificationName:PRXPlayerLongTimeIntervalNotification 940 | object:self 941 | userInfo:userInfo]; 942 | 943 | if (asset) { 944 | [NSNotificationCenter.defaultCenter postNotificationName:PRXPlayerLongTimeIntervalNotification 945 | object:asset.URL.absoluteString 946 | userInfo:userInfo]; 947 | } 948 | } 949 | } 950 | } 951 | 952 | - (void)didObserveSoftBoundaryTime { 953 | if (!ignoreTimeObservations) { 954 | if ([self.delegate respondsToSelector:@selector(player:softBoundaryTimeReachedForPlayerItem:)]) { 955 | [self.delegate player:self softBoundaryTimeReachedForPlayerItem:self.player.currentItem]; 956 | } 957 | 958 | [self publishMPNowPlayingInfoCenterNowPlayingInfo]; 959 | } 960 | } 961 | 962 | - (void)didObservePlaybackStartBoundaryTime { 963 | ignoreTimeObservations = NO; 964 | NSLog(@"[Player] No longer ignoring time observations"); 965 | 966 | if (playerPlaybackStartBoundaryTimeObserver) { 967 | dispatch_async(self.class.sharedObserverQueue, ^{ 968 | [self.player removeTimeObserver:playerPlaybackStartBoundaryTimeObserver]; 969 | playerPlaybackStartBoundaryTimeObserver = nil; 970 | }); 971 | } 972 | } 973 | 974 | - (void)reachabilityDidChange:(NSNotification *)notification { 975 | Reachability *reach = notification.object; 976 | 977 | // if (previousReachabilityStatus == -1) { 978 | // NSLog(@"Reachability status became available, it is: %@", reach.currentReachabilityString); 979 | // } else 980 | if (reach.currentReachabilityStatus != previousReachabilityStatus) { 981 | NSLog(@"Reachability changed from %@ to %@", previousReachabilityString, reach.currentReachabilityString); 982 | 983 | if (self.playerItem) { 984 | if (reach.currentReachabilityStatus == NotReachable) { 985 | NSLog(@"No longer have a network connection. Keeping app alive as long as possible to watch for reconnect."); 986 | [self keepAliveInBackground]; 987 | } else if (reach.currentReachabilityStatus == ReachableViaWiFi) { 988 | NSLog(@"Connected to WiFi; reload remote files/streams (either to fix player failure, or reduce WWAN bandwidth)"); 989 | [self reloadPlayerItemWithRemoteAsset:self.playerItem]; 990 | } else if (reach.currentReachabilityStatus == ReachableViaWWAN 991 | && previousReachabilityStatus == NotReachable) { 992 | NSLog(@"Connected to WWAN after losing connection; reload the player"); 993 | [self reloadPlayerItemWithRemoteAsset:self.playerItem]; 994 | } else if (reach.currentReachabilityStatus == ReachableViaWWAN 995 | && previousReachabilityStatus == ReachableViaWiFi) { 996 | NSLog(@"Lost Wifi connection but maintained WWAN; reload the player to continue playback"); 997 | [self reloadPlayerItemWithRemoteAsset:self.playerItem]; 998 | } 999 | } 1000 | } else { 1001 | NSLog(@"Reachability status change was triggered, but the value didn't actually change"); 1002 | } 1003 | 1004 | previousReachabilityStatus = reach.currentReachabilityStatus; 1005 | previousReachabilityString = reach.currentReachabilityString; 1006 | } 1007 | 1008 | #pragma mark - Playback vector 1009 | 1010 | - (void)loadPlayerItem:(id)playerItem { 1011 | holdPlayback = YES; 1012 | self.playerItem = playerItem; 1013 | } 1014 | 1015 | - (void)playPlayerItem:(id)playerItem { 1016 | holdPlayback = NO; 1017 | self.playerItem = playerItem; 1018 | } 1019 | 1020 | - (void)togglePlayerItem:(id)playerItem orCancel:(BOOL)cancel { 1021 | AVAsset *playerItemAsset = playerItem.playerAsset; 1022 | 1023 | if ([self.delegate respondsToSelector:@selector(player:assetForPlayerItem:)]) { 1024 | playerItemAsset = [self.delegate player:self assetForPlayerItem:playerItem]; 1025 | } 1026 | 1027 | if (!cancel) { 1028 | if ([playerItemAsset isKindOfClass:AVURLAsset.class] 1029 | && [self.player.currentItem.asset isKindOfClass:AVURLAsset.class] 1030 | && [((AVURLAsset *)self.player.currentItem.asset).URL.absoluteString isEqual:((AVURLAsset *)playerItemAsset).URL.absoluteString] 1031 | && self.player.rate != 0.0f) { 1032 | [self pause]; 1033 | } else { 1034 | [self playPlayerItem:playerItem]; 1035 | } 1036 | 1037 | } else { // try to cancel 1038 | 1039 | if ((self.state == PRXPlayerStateLoading || 1040 | self.state == PRXPlayerStateBuffering || 1041 | self.state == PRXPlayerStateWaiting)) { 1042 | [self stop]; 1043 | } else if ([playerItemAsset isKindOfClass:AVURLAsset.class] 1044 | && [self.player.currentItem.asset isKindOfClass:AVURLAsset.class] 1045 | && [((AVURLAsset *)self.player.currentItem.asset).URL isEqual:((AVURLAsset *)playerItemAsset).URL] 1046 | && self.player.rate != 0.0f) { 1047 | [self pause]; 1048 | } else { 1049 | [self playPlayerItem:playerItem]; 1050 | } 1051 | 1052 | } 1053 | return; 1054 | } 1055 | 1056 | - (void)togglePlayerItem:(id)playerItem { 1057 | [self togglePlayerItem:playerItem orCancel:NO]; 1058 | } 1059 | 1060 | - (void)reloadPlayerItem:(id)playerItem { 1061 | // reloading an item will lose the current time position unless it's being persisted 1062 | // this is avoidable but not currently implemented 1063 | NSLog(@"Reloading player item, holdPlayback is set to %i", holdPlayback); 1064 | BOOL hold = holdPlayback; 1065 | [self stop]; 1066 | holdPlayback = hold; 1067 | self.playerItem = playerItem; 1068 | } 1069 | 1070 | - (void)reloadPlayerItemWithRemoteAsset:(id)playerItem { 1071 | AVAsset *playerItemAsset = playerItem.playerAsset; 1072 | 1073 | if ([self.delegate respondsToSelector:@selector(player:assetForPlayerItem:)]) { 1074 | playerItemAsset = [self.delegate player:self assetForPlayerItem:playerItem]; 1075 | } 1076 | 1077 | if ([playerItemAsset isKindOfClass:AVURLAsset.class] 1078 | && ![[(AVURLAsset *)playerItemAsset URL] isFileURL]) { 1079 | NSLog(@"Reloading PlayerItem with remote URL asset"); 1080 | [self reloadPlayerItem:playerItem]; 1081 | } 1082 | } 1083 | 1084 | - (void)setupPlaybackStartBoundaryObserverCompletionHandler:(void (^)())completionHandler { 1085 | if (self.player.currentItem) { 1086 | 1087 | dispatch_async(self.class.sharedObserverQueue, ^{ 1088 | if (playerPlaybackStartBoundaryTimeObserver) { 1089 | [self.player removeTimeObserver:playerPlaybackStartBoundaryTimeObserver]; 1090 | playerPlaybackStartBoundaryTimeObserver = nil; 1091 | } 1092 | 1093 | AVPlayerItem *currentItem = self.player.currentItem; 1094 | CMTime duration = currentItem.duration; 1095 | 1096 | if (CMTIME_IS_VALID(duration)) { 1097 | // Boundary needs to be after playhead 1098 | 1099 | CMTime time = kCMTimeZero; 1100 | if ([self.playerItem respondsToSelector:@selector(playerTime)]) { 1101 | time = self.playerItem.playerTime; 1102 | 1103 | if (CMTimeCompare(time, kCMTimeZero) == 0 1104 | || CMTIME_IS_INVALID(time) 1105 | || CMTIME_IS_INDEFINITE(time) 1106 | || CMTIME_IS_NEGATIVE_INFINITY(time) 1107 | || CMTIME_IS_POSITIVE_INFINITY(time)) { 1108 | time = kCMTimeZero; 1109 | } 1110 | } 1111 | 1112 | CMTime boundaryTimePadding = CMTimeMake(1, 3); 1113 | 1114 | CMTime boundaryTime = CMTimeAdd(time, boundaryTimePadding); 1115 | NSValue *boundaryTime_v = [NSValue valueWithCMTime:boundaryTime]; 1116 | 1117 | __block id _self = self; 1118 | 1119 | playerPlaybackStartBoundaryTimeObserver = [self.player addBoundaryTimeObserverForTimes:@[ boundaryTime_v ] 1120 | queue:self.class.sharedObserverQueue 1121 | usingBlock:^{ 1122 | [_self didObservePlaybackStartBoundaryTime]; 1123 | }]; 1124 | 1125 | if (completionHandler) { 1126 | completionHandler(); 1127 | } 1128 | } 1129 | }); 1130 | } 1131 | } 1132 | 1133 | - (void)bar { 1134 | if (self.player.currentItem.status != AVPlayerStatusReadyToPlay) { 1135 | NSLog(@"Couldn't finalize playback because current player item wasn't ready to play"); 1136 | } else { 1137 | 1138 | [self setupPlaybackStartBoundaryObserverCompletionHandler:^{ 1139 | 1140 | // If there was an audio interrupt 1141 | if (dateAtAudioPlaybackInterruption) { 1142 | NSTimeInterval intervalSinceInterrupt = [NSDate.date timeIntervalSinceDate:dateAtAudioPlaybackInterruption]; 1143 | 1144 | NSLog(@"Appear to be recovering from an interrupt that's %fs old", intervalSinceInterrupt); 1145 | 1146 | NSTimeInterval limit = (60.0f * 4.0f); 1147 | BOOL withinResumeTimeLimit = (limit < 0) || (intervalSinceInterrupt <= limit); 1148 | 1149 | if (!withinResumeTimeLimit) { 1150 | NSLog(@"Internal playback request after an interrupt, but waited too long; exiting."); 1151 | holdPlayback = YES; 1152 | } 1153 | 1154 | dateAtAudioPlaybackInterruption = nil; 1155 | } 1156 | 1157 | if ([self.playerItem respondsToSelector:@selector(playerTime)]) { 1158 | [self.player seekToTime:self.playerItem.playerTime completionHandler:^(BOOL finished) { 1159 | if (finished && !holdPlayback) { 1160 | NSLog(@"Current item is ready and will start playing; seeked to %f", CMTimeGetSeconds(self.playerItem.playerTime)); 1161 | self.player.rate = self.rateForPlayback; 1162 | } else { 1163 | NSLog(@"Current item is ready, but playback is being help or the initial seek failed"); 1164 | } 1165 | }]; 1166 | } else if (!holdPlayback) { 1167 | NSLog(@"Current item is ready and will start playing from current player time"); 1168 | self.player.rate = self.rateForPlayback; 1169 | } else { 1170 | NSLog(@"Current item is ready, but playback is being held"); 1171 | } 1172 | 1173 | }]; 1174 | } 1175 | } 1176 | 1177 | #pragma mark - Notifications 1178 | 1179 | - (void)postGeneralChangeNotification { 1180 | [NSNotificationCenter.defaultCenter postNotificationName:PRXPlayerChangeNotification object:self]; 1181 | } 1182 | 1183 | #pragma mark Keep Alive 1184 | 1185 | - (void)keepAliveInBackground { 1186 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 1187 | [self beginBackgroundKeepAlive]; 1188 | for (int i = 0; i < 24; i++) { 1189 | // NSLog(@"keeping alive %d", i * 10); 1190 | [NSThread sleepForTimeInterval:10]; 1191 | } 1192 | [self endBackgroundKeepAlive]; 1193 | }); 1194 | } 1195 | 1196 | - (void)beginBackgroundKeepAlive { 1197 | backgroundKeepAliveTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ 1198 | [self endBackgroundKeepAlive]; 1199 | }]; 1200 | } 1201 | 1202 | - (void)endBackgroundKeepAlive { 1203 | [[UIApplication sharedApplication] endBackgroundTask:backgroundKeepAliveTaskID]; 1204 | backgroundKeepAliveTaskID = UIBackgroundTaskInvalid; 1205 | } 1206 | 1207 | #pragma mark Audio Session Interruption 1208 | 1209 | - (void)audioSessionInterruption:(NSNotification *)notification { 1210 | NSLog(@"An audioSessionInterruption notification was received"); 1211 | 1212 | id interruptionTypeKey = notification.userInfo[AVAudioSessionInterruptionTypeKey]; 1213 | 1214 | if ([interruptionTypeKey isEqual:@(AVAudioSessionInterruptionTypeBegan)]) { 1215 | [self audioSessionDidBeginInterruption:notification]; 1216 | } else if ([interruptionTypeKey isEqual:@(AVAudioSessionInterruptionTypeEnded)]) { 1217 | [self audioSessionDidEndInterruption:notification]; 1218 | } 1219 | } 1220 | 1221 | - (void)audioSessionDidBeginInterruption:(NSNotification *)notification { 1222 | NSLog(@"Audio session has been interrupted (this does not mean audio playback was interrupted)"); 1223 | [self keepAliveInBackground]; 1224 | dateAtAudioPlaybackInterruption = NSDate.date; 1225 | } 1226 | 1227 | - (void)audioSessionDidEndInterruption:(NSNotification *)notification { 1228 | NSLog(@"Audio session has interruption ended..."); 1229 | 1230 | // Because of various bugs and unpredictable behavior, it is unreliable to 1231 | // try and recover from audio session interrupts. 1232 | // 1233 | // When something is loaded into AVPlayer and the interrupt ends, even without 1234 | // us doing anything, the player item's status will change. We need to make 1235 | // sure our handling of that change is appropriate 1236 | // 1237 | // If AVPlayer changes to consistently report player rate at the time of the 1238 | // interrupt, or it is able to report interrupts when the rate is 0, this 1239 | // could be handled more directly. 1240 | // 1241 | // As it is now, if the player is paused going into the interrupt, we know 1242 | // the hold flag is set, so when the status changes, even though it will 1243 | // go through the play handler, it won't start playback. 1244 | // In cases where the audio was playing at the interrupt, the hold flag 1245 | // simply won't be set, so it will resume in the play handler. 1246 | 1247 | // Apparently sometimes the status change does not get reported as soon as 1248 | // the intr. ends, so we do need to coerce it in some cases. 1249 | // REAL DUMB. 1250 | 1251 | if (dateAtAudioPlaybackInterruption && self.playerItem) { 1252 | // TODO might just need to call bar here 1253 | self.playerItem = self.playerItem; 1254 | // [self bar]; 1255 | } 1256 | 1257 | } 1258 | 1259 | #pragma mark Route changes 1260 | 1261 | - (void)audioSessionRouteChange:(NSNotification *)notification { 1262 | NSUInteger reason = [notification.userInfo[AVAudioSessionRouteChangeReasonKey] integerValue]; 1263 | 1264 | // For reference only 1265 | // typedef enum : NSUInteger { 1266 | // AVAudioSessionRouteChangeReasonUnknown = 0, 1267 | // AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1, 1268 | // AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2, 1269 | // AVAudioSessionRouteChangeReasonCategoryChange = 3, 1270 | // AVAudioSessionRouteChangeReasonOverride = 4, 1271 | // AVAudioSessionRouteChangeReasonWakeFromSleep = 6, 1272 | // AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7, 1273 | // AVAudioSessionRouteChangeReasonRouteConfigurationChange = 8, 1274 | // } AVAudioSessionRouteChangeReason; 1275 | 1276 | NSLog(@"Audio session route changed: %lu", (unsigned long)reason); 1277 | // AVAudioSessionRouteDescription* previousRoute = notification.userInfo[AVAudioSessionRouteChangePreviousRouteKey]; 1278 | // AVAudioSessionRouteDescription* currentRoute = [AVAudioSession.sharedInstance currentRoute]; 1279 | 1280 | switch (reason) { 1281 | case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: 1282 | [self pause]; 1283 | break; 1284 | default: 1285 | break; 1286 | } 1287 | } 1288 | 1289 | @end 1290 | --------------------------------------------------------------------------------