Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor tweaks #6

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions PersistentStreamPlayer.h
Original file line number Diff line number Diff line change
@@ -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
371 changes: 235 additions & 136 deletions PersistentStreamPlayer.m
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ @interface PersistentStreamPlayer () <NSURLConnectionDataDelegate, AVAssetResour
@property (nonatomic, strong) NSURL *tempURL;

@property (nonatomic, assign) NSUInteger fullAudioDataLength;
@property (nonatomic, assign) NSUInteger loadedAudioDataLength;

@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) NSURLConnection *connection;
@@ -33,6 +34,8 @@ @interface PersistentStreamPlayer () <NSURLConnectionDataDelegate, AVAssetResour

@property (nonatomic, assign) BOOL isDestroyed;

@property (nonatomic, assign) BOOL isResumed;

@end

@implementation PersistentStreamPlayer
@@ -57,10 +60,7 @@ - (instancetype)initWithRemoteURL:(NSURL *)remoteURL
return self;
}

- (void)dealloc
{
[self destroy];
}
#pragma mark - Actions

- (void)prepareToPlay
{
@@ -74,67 +74,9 @@ - (void)prepareToPlay
forKeyPath:@"status"
options:NSKeyValueObservingOptionNew
context:NULL];
}

- (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];
}

- (void)pause
{
[self.player pause];
[self stopHealthCheckTimer];
[self.loopingLocalAudioPlayer pause];
}

- (void)destroy
{
if (self.isDestroyed) {
return;
}
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;
}

- (NSURL *)audioRemoteStreamingURL
{
if (!self.remoteURL) {
return nil;
}

NSURLComponents *components = [[NSURLComponents alloc] initWithURL:self.remoteURL resolvingAgainstBaseURL:NO];
self.originalURLScheme = components.scheme;
components.scheme = @"streaming";
return components.URL;

//uncomment if it will help
// self.player.automaticallyWaitsToMinimizeStalling = NO;
}

- (void)play
@@ -144,35 +86,100 @@ - (void)play
[self.loopingLocalAudioPlayer play];
}

- (BOOL)playing
- (void)pause
{
if (self.loopingLocalAudioPlayer) {
return self.loopingLocalAudioPlayer.playing;
}
return self.player.rate != 0 && !self.player.error;
[self.player pause];
[self stopHealthCheckTimer];
[self.loopingLocalAudioPlayer pause];
}

/* See "in beta" warning in header file. */
#pragma mark - seeking
- (void)seekToTime:(NSTimeInterval)time
{
CMTime seekTime = CMTimeMakeWithSeconds(MAX(time, 0), self.player.currentTime.timescale);
[self.player seekToTime:seekTime];
}

#pragma mark - NSURLConnection delegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.fullAudioDataLength = 0;
self.response = (NSHTTPURLResponse *)response;
if (!self.isResumed) {
self.response = (NSHTTPURLResponse *)response;
self.loadedAudioDataLength = 0;
self.fullAudioDataLength = self.response.expectedContentLength;
}

[self processPendingRequests];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
self.fullAudioDataLength += data.length;
self.loadedAudioDataLength += data.length;
[self appendDataToTempFile:data];
[self processPendingRequests];

if (self.isResumed) {
self.isResumed = NO;
self.isStalled = NO;
}
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.connectionHasFinishedLoading = YES;

[self processPendingRequests];
[FileUtils moveFileFromURL:self.tempURL toURL:self.localURL];

if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidPersistAsset:)]) {
[self.delegate persistentStreamPlayerDidPersistAsset:self];
}
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

NSArray *pendingRequests = [self.pendingRequests copy];
for (AVAssetResourceLoadingRequest *loadingRequest in pendingRequests) {
NSURLComponents * component = [[NSURLComponents alloc] initWithURL:loadingRequest.request.URL resolvingAgainstBaseURL:NO];
component.scheme = self.originalURLScheme ?: @"http";

if ([component.URL.absoluteString isEqualToString: connection.currentRequest.URL.absoluteString] ) {
[loadingRequest finishLoadingWithError:error];
[self.pendingRequests removeObject:loadingRequest];
}
}

if (!self.connectionHasFinishedLoading) {

[self.connection cancel];
self.connection = nil;

if (self.pendingRequests.count == 0) {
if ([self.delegate respondsToSelector:@selector(persistentStreamPlayerDidFailToLoadAsset:)]) {
[self.delegate persistentStreamPlayerDidFailToLoadAsset:self];
}
}

}
}

- (void)resumeConnection {
NSURL *interceptedURL = self.audioRemoteStreamingURL;
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = self.originalURLScheme ?: @"http";

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:actualURLComponents.URL];
if (self.loadedAudioDataLength > 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