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

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

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

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

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

Заметки

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

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

Аудиоплеер

@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 Метод, который в конечном итоге вызывается функцией 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;
     }
 ...
}

2 ответа

Решение

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

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

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

Там первое, что я хотел бы попробовать это:

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

[self.readerOutput copyNextSampleBuffer]

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

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

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

Другие вопросы по тегам