I'm using an AVAssetReaderOutput
to read samples from an AVAsset
, do some processing on them, and play the result using a RemoteIO AU.
The problem is that after calling AudioOutputUnitStop
to pause the playback, then after going to the background and back to the foreground the audio won't start again after calling AudioOutputUnitStart
. This is due to an error returned from the copyNextSampleBuffer
method of AVAssetReaderOutput
, which is called as part of the rendering pipeline.
The status
property of AVAssetReader
after calling copyNextSampleBuffer
is AVAssetReaderStatusFailed
, and its error
property is Error Domain=AVFoundationErrorDomain Code=-11847 "Operation Interrupted" UserInfo=0x1d8b6100 {NSLocalizedRecoverySuggestion=Stop other operations and try again., NSLocalizedDescription=Operation Interrupted}
I'm looking for a solution which won't force me to reinitialize the entire pipeline after coming back to the foreground - Hoping there is such a solution, that AVAssetReader
s can survive the app going to background and back...
Notes
AVAudioSession
as the active one both on AVAudioSessionDelegate
s endInterruptionWithFlags:
event and whenever the app becomes active. Doesn't make a difference whether I do this or not, getting the same error.Some code:
AudioPlayer
@implementation AudioPlayer
...
// Audio Unit Setup
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
AudioComponent defaultOutput = AudioComponentFindNext(NULL, &desc);
AudioComponentInstanceNew(defaultOutput, &_audioUnit);
AudioStreamBasicDescription audioFormat;
FillOutASBDForLPCM(audioFormat, 44100, 2, 16, 16, false, false);
AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &audioFormat, sizeof(audioFormat));
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = RenderCallback;
callbackStruct.inputProcRefCon = (__bridge void*)self;
AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, kOutputBus, &callbackStruct, sizeof(callbackStruct));
AudioUnitInitialize(self.audioUnit);
AudioReader Setup
@implementation AudioReader
...
NSError* error = nil;
self.reader = [AVAssetReader assetReaderWithAsset:self.asset error:&error];
NSDictionary *outputSettings = ...
self.readerOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:[self.asset.tracks objectAtIndex:0] outputSettings:outputSettings];
[self.reader addOutput:self.readerOutput];
[self.reader startReading];
AudioReader Render Method, called eventually by the RenderCallback function
-(BOOL)readChunkIntoBuffer
{
CMSampleBufferRef sampleBuffer = [self.readerOutput copyNextSampleBuffer];
if ( sampleBuffer == NULL )
{
NSLog(@"Couldn't copy next sample buffer, reader status=%d error=%@, self.reader.status, self.reader.error);
return NO;
}
...
}
The graph and AVReader underpinnings are not connected/linked whatsoever. The confusion perhaps comes from that iOS won't "background" (hibernate) a process if it sees the audio graph running (because then audio data would be unable to be generated). That's why when you stop the audio graph, 2-3 minutes later, iOS will background your process (as of iOS 9). Presumably, iOS looks at what's going on in your process and decides when your process should be forced to background (via beginBackgroundTaskWithName:expirationHandler
).
Apple devs decided for whatever reason to make AVReader stop when put into background mode (my best guess is QA). The good news is that you can detect it and recover when your process exits background mode, but you WILL need to restart your reader and readerOutput again. First, when AVAssetReaderOutput's copyNextSampleBuffer
returns NULL, check AVAssetReader.error.code
for AVErrorOperationInterrupted
.
The way we pull off a gapless recovery here is when we get to that point, we first calculate the exact time we left off at (easy to do -- just maintain a counter of the total samples already output). Then, you WILL need to restart your AVAssetReader
and AVAssetReaderOutput
flow. But that's no problem since your good development practices would have encapsulated that in a single function that has a seek time as an argument. In that function, use AVAssetReader.timeRange
to seek to that time you need to resume at and presto!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With