Не удается продолжить чтение из AVAssetReaderOutput после перехода на задний план и обратно на передний план

Я использую AVAssetReaderOutput для чтения образцов из AVAsset, выполняю некоторую обработку на них и воспроизвожу результат с помощью RemoteIO AU.

Проблема заключается в том, что после вызова AudioOutputUnitStop для приостановки воспроизведения, после перехода на задний план и обратно на передний план звук не будет запускаться снова после вызова AudioOutputUnitStart. Это связано с ошибкой, возвращаемой методом copyNextSampleBuffer AVAssetReaderOutput, который называется частью конвейера рендеринга.

Свойство status AVAssetReader после вызова copyNextSampleBuffer равно AVAssetReaderStatusFailed, а его свойство error Ошибка домена = AVFoundationErrorDomain Code = -11847 "Операция прерывается" UserInfo = 0x1d8b6100 {NSLocalizedRecoverySuggestion = Остановите другие операции и повторите попытку. NSLocalizedDescription = Operation Interrupted}

Я ищу решение, которое не заставило бы меня повторно инициализировать весь конвейер после возвращения на передний план. Надеясь, что есть такое решение, AVAssetReader может выжить, когда приложение перейдет на задний план и обратно..

Примечания

  • Приложение имеет право воспроизводить аудио в фоновом режиме.
  • Я обрабатываю прерывания звука. Установка моего AVAudioSession как активного как в событии AVAudioSessionDelegate endInterruptionWithFlags:, так и при каждом включении приложения. Не имеет значения, выполняю ли я это или нет, получая ту же ошибку.

Некоторые коды:

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

@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, вызванный в конечном итоге функцией RenderCallback

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

Ответ 1

Графики и основы AVReader не связаны/связаны вообще. Путаница, возможно, исходит из того, что iOS не будет "фоновым" (спящим) процессом, если видит, что работает звуковой график (потому что тогда аудиоданные не смогут быть сгенерированы). Поэтому, когда вы прекратите звуковой график, через 2-3 минуты, iOS закроет ваш процесс (начиная с iOS 9). Предположительно, iOS смотрит на то, что происходит в вашем процессе, и решает, когда ваш процесс должен быть привязан к фону (через beginBackgroundTaskWithName:expirationHandler).

Apple devs решила по какой-либо причине заставить AVReader останавливаться при вводе в фоновый режим (мое лучшее предположение - QA). Хорошей новостью является то, что вы можете обнаружить ее и восстановить, когда ваш процесс выходит из фонового режима, но вам нужно снова перезапустить ваш ридер и readerOutput. Во-первых, когда AVAssetReaderOutput copyNextSampleBuffer возвращает NULL, проверьте AVAssetReader.error.code на AVErrorOperationInterrupted.

То, как мы снимаем безразличное восстановление здесь, - это когда мы добираемся до этого момента, мы сначала вычисляем точное время, которое мы остановили (легко сделать - просто поддерживаем счетчик уже полученных проб). Затем вам необходимо перезапустить поток AVAssetReader и AVAssetReaderOutput. Но это не проблема, так как ваши хорошие методы развития инкапсулировали бы это в одну функцию, которая имеет время поиска в качестве аргумента. В этой функции используйте AVAssetReader.timeRange, чтобы искать то время, в которое вам нужно возобновить и престо!

Ответ 2

В первую очередь я попробую:

Не вызывать AudioOutputUnitStop? Почему бы вам не оставить график включенным и когда аудио приостановлено просто не вызывать

[self.readerOutput copyNextSampleBuffer]

Вместо этого просто заполните буфер 0 и/или используйте действие рендеринга kAudioUnitRenderAction_OutputIsSilence для буферов. Запуск и остановка графика требует времени и приводит к невосприимчивому графу. Я никогда не останавливаю график во время работы моих приложений. (Если не произойдет какое-то серьезное прерывание) Это может привести к тому, что читатель активов будет рад и счастлив.

Но у меня есть догадка, что, поскольку AVAssetReader напрямую не подключен к звуковому графику (вам просто нужно запросить образцы и поместить их в буфер, предоставленный обратным вызовом визуализации), проблема не связана с аудиографом останавливается и перезапускается вообще. Итак, следующая строка атаки, которую я бы попробовал, - это запросить образцы из AVAsset, поместить приложение в фоновый режим и вернуть его на передний план без использования AUGraph вообще. Это определит, связана ли проблема с AUGraph или приложением, входящим в фоновое состояние.

Если что-то случится, что вынудит вас снести ваш AVAssetReader, вам придется снова подключиться к активу. Так как вам нужно написать этот код, другой вариант, на который я бы посмотрел, - это тот, который вы не хотите делать. Подключите к AVAssetReader, найдите место, в котором вы остановились, и продолжайте движение.