diff --git a/PersistentStreamPlayer.h b/PersistentStreamPlayer.h index 0e8fe49..acb4de5 100644 --- a/PersistentStreamPlayer.h +++ b/PersistentStreamPlayer.h @@ -24,6 +24,9 @@ @property (nonatomic, assign) BOOL looping; @property (nonatomic, readonly) BOOL playing; @property (nonatomic, assign) float volume; +@property (nonatomic, assign) BOOL muted; + +@property (nonatomic, readonly, nullable) AVPlayer *player; - (void)play; - (void)pause; @@ -40,4 +43,6 @@ */ - (void)seekToTime:(NSTimeInterval)time; +- (void)resumeConnection; + @end diff --git a/PersistentStreamPlayer.m b/PersistentStreamPlayer.m index d61de1e..414ddbc 100644 --- a/PersistentStreamPlayer.m +++ b/PersistentStreamPlayer.m @@ -13,6 +13,7 @@ @interface PersistentStreamPlayer () 0) { + NSString *range = [NSString stringWithFormat:@"bytes=%i-", self.loadedAudioDataLength]; + [request setValue:range forHTTPHeaderField:@"Range"]; + } + + self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO]; + [self.connection setDelegateQueue:[NSOperationQueue mainQueue]]; + [self.connection start]; + + self.isResumed = YES; } - (void)appendDataToTempFile:(NSData *)data @@ -186,6 +193,16 @@ - (void)appendDataToTempFile:(NSData *)data } } +#pragma mark - Getters & Setters + +- (BOOL)playing +{ + if (self.loopingLocalAudioPlayer) { + return self.loopingLocalAudioPlayer.playing; + } + return self.player.rate != 0 && !self.player.error; +} + - (BOOL)tempFileExists { return [[NSFileManager defaultManager] fileExistsAtPath:self.tempURL.path]; @@ -203,16 +220,16 @@ - (NSURL *)currentURLForDataFile return self.connectionHasFinishedLoading ? self.localURL : self.tempURL; } -- (void)connectionDidFinishLoading:(NSURLConnection *)connection +- (NSURL *)audioRemoteStreamingURL { - self.connectionHasFinishedLoading = YES; - - [self processPendingRequests]; - [FileUtils moveFileFromURL:self.tempURL toURL:self.localURL]; - - if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidPersistAsset:)]) { - [self.delegate persistentStreamPlayerDidPersistAsset:self]; + if (!self.remoteURL) { + return nil; } + + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:self.remoteURL resolvingAgainstBaseURL:NO]; + self.originalURLScheme = components.scheme; + components.scheme = @"streaming"; + return components.URL; } - (float)volume @@ -229,7 +246,49 @@ - (void)setVolume:(float)volume } } +- (BOOL)muted +{ + return self.player.muted; +} + +- (void)setMuted:(BOOL)muted +{ + self.player.muted = muted; +} + +- (BOOL)isAssetLoaded +{ + AVKeyValueStatus durationStatus = [self.player.currentItem.asset statusOfValueForKey:@"duration" error:NULL]; + return durationStatus == AVKeyValueStatusLoaded && self.player.status == AVPlayerStatusReadyToPlay; +} + +- (NSTimeInterval)duration +{ + if (!self.isAssetLoaded) { + return self.fullAudioDataLength; + /* + return 5 * 60.0; // give it a good guess of 5 min before asset loads... + */ + } + return CMTimeGetSeconds(self.player.currentItem.asset.duration); +} + +- (NSTimeInterval)timeBuffered +{ + CMTimeRange timeRange = [[self.player.currentItem.loadedTimeRanges lastObject] CMTimeRangeValue]; + return CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration); +} + +- (NSTimeInterval)currentTime +{ + if (self.loopingLocalAudioPlayer) { + return self.loopingLocalAudioPlayer.currentTime; + } + return CMTimeGetSeconds(self.player.currentTime); +} + #pragma mark - AVURLAsset resource loading + - (void)processPendingRequests { NSMutableArray *requestsCompleted = [NSMutableArray array]; @@ -319,20 +378,62 @@ - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingR [self.pendingRequests removeObject:loadingRequest]; } -#pragma mark - KVO +#pragma mark - Observing + - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) { + if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay + && self.playing) { [self.player play]; } } -#pragma mark - health check timer +- (void)addObservers +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playerItemDidReachEnd:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:[self.player currentItem]]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playerItemDidStall:) + name:AVPlayerItemPlaybackStalledNotification + object:[self.player currentItem]]; +} + +- (void)removeObservers +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - AVPlayerItem + +- (void)playerItemDidStall:(NSNotification *)notification +{ + self.isStalled = YES; + + if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerStreamingDidStall:)]) { + [self.delegate persistentStreamPlayerStreamingDidStall:self]; + } +} + +- (void)playerItemDidReachEnd:(NSNotification *)notification +{ + if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidFinishPlaying:)]) { + [self.delegate persistentStreamPlayerDidFinishPlaying:self]; + } + [self stopHealthCheckTimer]; + [self tryToStartLocalLoop]; +} + - (void)startHealthCheckTimer { + if (self.healthCheckTimer) { + [self stopHealthCheckTimer]; + } + [self healthCheckTimerDidFire]; // fires once immediately self.healthCheckTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self @@ -360,6 +461,13 @@ - (void)healthCheckTimerDidFire - (void)tryToPlayIfStalled { if (!self.isStalled) { + + if (self.player.rate != 0 + && self.player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate + && [self.player.reasonForWaitingToPlay isEqualToString:AVPlayerWaitingToMinimizeStallsReason]) { + [self.player playImmediatelyAtRate:self.player.rate]; + } + return; } @@ -371,14 +479,6 @@ - (void)tryToPlayIfStalled } } -- (void)playerItemDidReachEnd:(NSNotification *)notification -{ - if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidFinishPlaying:)]) { - [self.delegate persistentStreamPlayerDidFinishPlaying:self]; - } - [self tryToStartLocalLoop]; -} - - (void)tryToStartLocalLoop { if (!self.looping) { @@ -393,29 +493,12 @@ - (void)tryToStartLocalLoop [self.loopingLocalAudioPlayer play]; } -- (void)playerItemDidStall:(NSNotification *)notification -{ - self.isStalled = YES; - - if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerStreamingDidStall:)]) { - [self.delegate persistentStreamPlayerStreamingDidStall:self]; - } -} - -#pragma mark - asset and duration -- (NSTimeInterval)duration -{ - if (!self.isAssetLoaded) { - return 5 * 60.0; // give it a good guess of 5 min before asset loads... - } - return CMTimeGetSeconds(self.player.currentItem.asset.duration); -} - - (void)loadAssetIfNecessary { if (self.hasForcedDurationLoad) { return; } + if (self.isAssetLoaded) { return; } @@ -433,40 +516,56 @@ - (void)loadAssetIfNecessary } } -- (BOOL)isAssetLoaded -{ - AVKeyValueStatus durationStatus = [self.player.currentItem.asset statusOfValueForKey:@"duration" error:NULL]; - return durationStatus == AVKeyValueStatusLoaded && self.player.status == AVPlayerStatusReadyToPlay; -} - - (void)forceLoadOfDuration { + __weak typeof(self) weakSelf = self; [self.player.currentItem.asset loadValuesAsynchronouslyForKeys:@[@"duration"] - completionHandler:^{ - if (self.isAssetLoaded) { - if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidLoadAsset:)]) { - [self.delegate persistentStreamPlayerDidLoadAsset:self]; - } - } else { - if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidFailToLoadAsset:)]) { - [self.delegate persistentStreamPlayerDidFailToLoadAsset:self]; - } - } - }]; -} + completionHandler:^{ + if(weakSelf) { + PersistentStreamPlayer* strongSelf = weakSelf; + if (strongSelf.isAssetLoaded) { + if ([strongSelf.delegate respondsToSelector:@selector(persistentStreamPlayerDidLoadAsset:)]) { + [strongSelf.delegate persistentStreamPlayerDidLoadAsset:strongSelf]; + } + } else { + if ([strongSelf.delegate respondsToSelector:@selector(persistentStreamPlayerDidFailToLoadAsset:)]) { + [strongSelf.delegate persistentStreamPlayerDidFailToLoadAsset:strongSelf]; + } + } + } + }]; +} + +#pragma mark - Memory management -- (NSTimeInterval)timeBuffered +- (void)dealloc { - CMTimeRange timeRange = [[self.player.currentItem.loadedTimeRanges lastObject] CMTimeRangeValue]; - return CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration); + [self destroy]; } -- (NSTimeInterval)currentTime +- (void)destroy { - if (self.loopingLocalAudioPlayer) { - return self.loopingLocalAudioPlayer.currentTime; + if (self.isDestroyed) { + return; } - return CMTimeGetSeconds(self.player.currentTime); + self.isDestroyed = YES; + + [self removeObservers]; + + [self stopHealthCheckTimer]; + [self.player pause]; + + [self.player.currentItem removeObserver:self forKeyPath:@"status"]; + [self.player.currentItem cancelPendingSeeks]; + [self.player.currentItem.asset cancelLoading]; + self.player.rate = 0.0; + self.player = nil; + + [self.connection cancel]; + self.connection = nil; + + [self.loopingLocalAudioPlayer stop]; + self.loopingLocalAudioPlayer = nil; } @end