Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions Audio/AudioPlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
- (void)endOfInputPlayed;
- (void)reportPlayCount;
- (void)reportScrobble;
- (void)setError:(BOOL)status forTrack:(id)userInfo;
- (void)sendDelegateMethod:(SEL)selector withVoid:(void *)obj waitUntilDone:(BOOL)wait;
- (void)sendDelegateMethod:(SEL)selector withObject:(id)obj waitUntilDone:(BOOL)wait;
- (void)sendDelegateMethod:(SEL)selector withObject:(id)obj withObject:(id)obj2 waitUntilDone:(BOOL)wait;
Expand Down
52 changes: 47 additions & 5 deletions Audio/AudioPlayer.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@

#import "Logging.h"

static NSURL *streamURLWithoutFragment(NSURL *url) {
if(!url || ![[url fragment] length]) {
return url;
}

NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
components.fragment = nil;
return components.URL;
}

static BOOL streamURLsShareUnderlyingResource(NSURL *firstURL, NSURL *secondURL) {
NSURL *firstResource = streamURLWithoutFragment(firstURL);
NSURL *secondResource = streamURLWithoutFragment(secondURL);
if(!firstResource || !secondResource) {
return NO;
}
return [firstResource isEqualTo:secondResource];
}

@implementation AudioPlayer {
BOOL stoppedRecently;
}
Expand Down Expand Up @@ -106,7 +125,23 @@ - (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)r
[self notifyStreamChanged:userInfo];
}

while(![bufferChain open:url withOutputFormat:[output format] withUserInfo:userInfo withRGInfo:rgi resetBuffers:YES]) {
NSURL *failedFragmentResource = nil;
BOOL advancedPastFailedStream = NO;
while(YES) {
BOOL skipRepeatedProbe = failedFragmentResource && streamURLsShareUnderlyingResource(failedFragmentResource, url);
if(!skipRepeatedProbe && [bufferChain open:url withOutputFormat:[output format] withUserInfo:userInfo withRGInfo:rgi resetBuffers:YES]) {
break;
}

if(skipRepeatedProbe) {
DLog(@"Skipping another logical track from failed source: %@", url);
} else {
// Logical tracks with different fragments share one decoder source. If
// that source cannot be opened, probing every remaining fragment repeats
// the same expensive failure while this method blocks the main thread.
failedFragmentResource = [[url fragment] length] ? streamURLWithoutFragment(url) : nil;
}
[self setError:YES forTrack:userInfo];
bufferChain = nil;

[self requestNextStream:userInfo];
Expand All @@ -122,11 +157,15 @@ - (void)play:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)r

userInfo = nextStreamUserInfo;
rgi = nextStreamRGInfo;

[self notifyStreamChanged:userInfo];
advancedPastFailedStream = YES;

bufferChain = [[BufferChain alloc] initWithController:self];
}
if(advancedPastFailedStream) {
// Do not present every failed fallback as the current track. Publish only
// the first stream that actually opened.
[self notifyStreamChanged:userInfo];
}

if(resumeInterval || time > 0.0) {
[output seek:time];
Expand Down Expand Up @@ -686,8 +725,11 @@ - (void)sustainHDCD {
// [self sendDelegateMethod:@selector(audioPlayer:sustainHDCD:) withObject:[bufferChain userInfo] waitUntilDone:NO];
}

- (void)setError:(BOOL)status {
[self sendDelegateMethod:@selector(audioPlayer:setError:toTrack:) withObject:@(status) withObject:[bufferChain userInfo] waitUntilDone:NO];
- (void)setError:(BOOL)status forTrack:(id)userInfo {
// Buffer chains overlap while tracks are preloaded and switched. Preserve
// the chain's own track here; consulting the current global bufferChain when
// this asynchronous callback runs can apply a late error to the next track.
[self sendDelegateMethod:@selector(audioPlayer:setError:toTrack:) withObject:@(status) withObject:userInfo waitUntilDone:NO];
}

- (void)setPlaybackStatus:(int)status {
Expand Down
3 changes: 2 additions & 1 deletion Audio/Chain/BufferChain.m
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ - (void)pushInfo:(NSDictionary *)info {
}

- (void)setError:(BOOL)status {
[controller setError:status];
AudioPlayer *audioPlayer = controller;
[audioPlayer setError:status forTrack:userInfo];
}

@end
15 changes: 11 additions & 4 deletions Audio/Chain/InputNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,10 @@ - (void)process {
DLog(@"Seeked! Resetting Buffer");
initialBufferFilled = NO;

if(seekError) {
[controller setError:YES];
}

signalReset = YES;

if([self hasSeekRequestAfterID:activeSeekRequestID]) {
seekError = NO;
continue;
}
}
Expand All @@ -229,6 +226,11 @@ - (void)process {
}

if(chunk && [chunk frameCount]) {
// Some decoders can recover from an inexact or rejected seek and
// immediately return valid audio (cue-sheet fragments do this by
// seeking to their own start). Do not persist a track error when the
// requested playback is still producing audio.
seekError = NO;
@autoreleasepool {
if(signalReset) {
chunk.resetForward = YES;
Expand All @@ -241,6 +243,11 @@ - (void)process {
if([self shouldContinue] == NO) {
break;
}
if(seekError) {
ALog(@"Decoder seek failed and produced no audio");
[controller setError:YES];
seekError = NO;
}
if(chunk) {
@autoreleasepool {
chunk = nil;
Expand Down