AVPlayer не загружает медиафайлы в фоновом режиме

При работе в фоновом режиме моя реализация AVPlayer не может воспроизводить загруженный звук (например, подкаст), но может воспроизводить песни, которые хранятся локально. Отказ играть в фоновом режиме бывает только тогда, когда телефон отключен от моего компьютера. Если мой телефон напрямую подключен к моему компьютеру / отладчику, любые локальные или загруженные носители воспроизводятся без проблем. На переднем плане также нет проблем с воспроизведением любого типа мультимедиа.

Вот моя реализация:

AVPlayer                    *moviePlayer;               
AVPlayerItem                *playerItem;
NSURL *address = /* the url of either a local song or remote media */

if (moviePlayer != nil) {
    NSLog(@"removing rate, playbackBufferEmpty, playbackLikelyToKeepUp observers before creating new player");
    [moviePlayer removeObserver:self forKeyPath:@"rate"];
    [playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
    [playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
    [playerItem removeObserver:self forKeyPath:@"status"];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:playerItem];
    [self setMoviePlayer:nil];  // release and nilify
}

// The following block of code was an experiment to see if starting a background task would resolve the problem. As implemented, this did not help.
if([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive)
{
    NSLog(@"Experiment. Starting background task to keep iOS awake");
    task = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
    }];
}

 playerItem = [[AVPlayerItem alloc]initWithURL:address];
 moviePlayer = [[AVPlayer alloc]initWithPlayerItem:playerItem];

 // Add a notification to make sure that the player starts playing. This is handled by observeValueForKeyPath
 [moviePlayer addObserver:self
               forKeyPath:@"rate"
                  options:NSKeyValueObservingOptionNew
                  context:nil];

[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

 // The following 2 notifications handle the end of play
 [[NSNotificationCenter defaultCenter] addObserver:self
                                          selector:@selector(moviePlayBackDidFinish:)
                                              name:AVPlayerItemDidPlayToEndTimeNotification
                                            object:playerItem];

 [[NSNotificationCenter defaultCenter] addObserver:self
                                          selector:@selector(moviePlayBackDidFinish:)
                                              name:AVPlayerItemFailedToPlayToEndTimeNotification
                                            object:playerItem];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(moviePlayBackStalled:)
                                             name:AVPlayerItemPlaybackStalledNotification
                                           object:playerItem];

 // Indicate the action the player should take when it finishes playing.
 moviePlayer.actionAtItemEnd = AVPlayerActionAtItemEndPause;

 moviePlayer.automaticallyWaitsToMinimizeStalling = NO;

ОБНОВЛЕНИЕ: В приведенной выше реализации я также показываю экспериментальную попытку запустить фоновую задачу в надежде позволить AVPlayer воспроизводить подкаст в фоновом режиме. Это тоже не помогло, но я включаю его для справки. Не показано, но я также завершаю фоновую задачу после того, как статус AVPlayerItemplayLikelyToKeepUp изменится на TRUE.

Затем у меня есть следующий код для обработки уведомлений keyPath:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"rate"]) {
        NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: rate"];
        DLog(@"%@", debugString);
        debugString = [self appendAvPlayerStatus: debugString];
        [[FileHandler sharedInstance] logDebugString:debugString];
    }
    else if (object == playerItem && [keyPath isEqualToString:@"playbackBufferEmpty"]) {

        NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: playbackBufferEmpty"];
        DLog(@"%@", debugString);
        debugString = [self appendAvPlayerStatus: debugString];
        [[FileHandler sharedInstance] logDebugString:debugString];
    }
    else if (object == playerItem && [keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {

        NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: playbackLikelyToKeepUp"];
        DLog(@"%@", debugString);
        debugString = [self appendAvPlayerStatus: debugString];
        [[FileHandler sharedInstance] logDebugString:debugString];
    }

    else if (object == playerItem && [keyPath isEqualToString:@"status"]) {

        NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: status"];
        DLog(@"%@", debugString);
        debugString = [self appendAvPlayerStatus: debugString];
        [[FileHandler sharedInstance] logDebugString:debugString];
    }
}

И у меня есть следующее для обработки уведомлений notificationCenter:

- (void) moviePlayBackDidFinish:(NSNotification*)notification {

    NSLog(@"moviePlaybackDidFinish. Time to stopMoviePlayerWithMusicPlayIndication");
    [self stopMoviePlayer: YES];  // stop the movie player 
}

- (void) moviePlayBackStalled:(NSNotification*)notification {

    NSString *debugString = [NSString stringWithFormat: @"In moviePlayBackStalled. Restarting player"];
    DLog(@"%@", debugString);
    debugString = [self appendAvPlayerStatus: debugString];
    [[FileHandler sharedInstance] logDebugString:debugString];
    [moviePlayer play];
}

Вот что я наблюдаю, реализовав инструмент ведения журнала для отслеживания выполнения при отключении от компьютера:

При работе в фоновом режиме, отключенном от компьютера, playerItem загружается с URL-адресом, а AVPlayer инициализируется с помощью playerItem. Это приводит к тому, что будет опубликовано уведомление «Наблюдение за значениемФорКэйпас: скорость», но это будет последнее полученное уведомление. Аудио не воспроизводится. Плеер просто зависает. Вывод из моего журнала, показывающий различные флаги moviePlayer и playerItem, выглядит следующим образом:

ready to play movie/podcast/song dedication/Song from url: http://feedproxy.google.com/~r/1019TheMix-EricAndKathy/~5/ZZnF09tuxr0/20170309-1_1718764.mp3
In observeValueForKeyPath: rate - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 1, playbackBufferFull: 0, rate: 1.0

Однако при работе в фоновом режиме при прямом подключении к компьютеру или при работе на переднем плане вы можете видеть из выходных данных журнала ниже, что после загрузки URL-адреса и инициализации AVPlayer с помощью playerItem появляется серия уведомлений. опубликовано для keyPath: rate, проигрываниеBufferEmpty, проигрываниеLikelyToKeepUp и status. Затем начинается воспроизведение звука. Вывод, показывающий различные флаги moviePlayer и playerItem, выглядит следующим образом:

ready to play movie/podcast/song dedication/Song from url: http://feedproxy.google.com/~r/1019TheMix-EricAndKathy/~5/d3w52TBzd88/20170306-1-16_1717980.mp3
In observeValueForKeyPath: rate - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 1, playbackBufferFull: 0, rate: 1.0
In observeValueForKeyPath: playbackBufferEmpty - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0
In observeValueForKeyPath: playbackLikelyToKeepUp - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0
In observeValueForKeyPath: status - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusReadyToPlay, playbackToKeepUp: 0, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0
In observeValueForKeyPath: playbackLikelyToKeepUp - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusReadyToPlay, playbackToKeepUp: 1, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0
In observeValueForKeyPath: rate - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusReadyToPlay, playbackToKeepUp: 1, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0

Таким образом, вы видите выше, что при работе на переднем плане или в фоновом режиме, если он напрямую подключен к компьютеру / отладчику, AVPlayer успешно загружает буфер воспроизведения и воспроизводит звук. Но при работе в фоновом режиме и НЕ подключенном к компьютеру / отладчику, плеер не загружает носитель и просто зависает.

Во всех случаях уведомление AVPlayerItemPlaybackStalledNotification не получено.

Извините за длинное объяснение. Кто-нибудь может увидеть, что может быть причиной зависания плеера в фоновом режиме, когда он не подключен к компьютеру?


person JeffB6688    schedule 09.03.2017    source источник
comment
Вы пробовали позвонить beginBackgroundTask, когда начинаете играть? В документации говорится, что его не следует использовать для работы в фоновом режиме, но также говорится, что это полезно для незавершенных дел: developer.apple.com/reference/uikit/uiapplication/   -  person Rhythmic Fistman    schedule 14.03.2017
comment
@Rhythmic Fistman Да, я пробовал это. Это не помогло. Кроме того, у AVPlayer нет проблем с воспроизведением локальных медиафайлов в фоновом режиме, поэтому не похоже, что iOS усыпляет меня.   -  person JeffB6688    schedule 14.03.2017
comment
Локальные файлы имеют разные временные свойства. Чтобы избежать отмены планирования, фоновое звуковое приложение должно воспроизводить звук. Проблема может заключаться в том, что вашему AVPlayer требуется время для потоковой передачи аудиоданных, прежде чем он сможет начать воспроизведение.   -  person Rhythmic Fistman    schedule 14.03.2017
comment
@RhythmicFistman Хорошо, в этом есть смысл. Можете ли вы сказать мне, является ли мой метод запуска фоновой задачи тем, что вы имеете в виду? Вот что я пробовал: прямо перед тем, как загрузить AVPlayerItem с адресом подкаста и до того, как playerItem будет связан с AVPlayer, я включил следующий код: if ([[UIApplication sharedApplication] applicationState]! = UIApplicationStateActive) {task = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^ (void) {}]; }   -  person JeffB6688    schedule 14.03.2017
comment
Можете ли вы добавить код к вопросу? В комментариях это сложно прочитать.   -  person Rhythmic Fistman    schedule 14.03.2017
comment
@RhythmicFistman Хорошо, я обновил код в формулировке проблемы, чтобы показать свою попытку сохранить работу AVPlayer в фоновом режиме, запустив фоновую задачу непосредственно перед загрузкой AVPlayItem с адресом подкаста. Также, как показано в формулировке проблемы, я указываю, что завершаю эту фоновую задачу, если AVPlayerItem, по оценкам, имеет достаточно буфера для поддержки. Но в том виде, в котором он реализован, это не помогло. Если это не правильный способ сделать это, пожалуйста, дайте мне знать.   -  person JeffB6688    schedule 15.03.2017
comment
Не считая того факта, что вы должны вызвать endBackgroundTask в обработчике истечения срока, это выглядит нормально. Можете ли вы создать небольшой проект, в котором воспроизводится проблема, и связать его с этим вопросом?   -  person Rhythmic Fistman    schedule 15.03.2017
comment
@RhythmicFistman Я сделал чистую сборку. Затем у меня было странное поведение AVPlayer. Иногда он играл в фоновом режиме, а иногда отказывался. Поэтому я снова вставил код, чтобы запустить фоновую задачу, когда загрузил AVPlayerItem с подкастом. Однако на этот раз я изменил точку завершения фоновой задачи. Теперь я завершаю фоновую задачу, когда AVPlayerItem playsToKeepUp становится истинным. Кажется, теперь это работает с таким подходом. Надеюсь, этот подход подходит. Если вы ответите на вопрос об использовании фоновой задачи, я награждаю вас бонусными баллами.   -  person JeffB6688    schedule 17.03.2017
comment
Готово, я расширил то, что написал в этих комментариях. Спасибо!   -  person Rhythmic Fistman    schedule 18.03.2017


Ответы (1)


Попробуйте позвонить beginBackgroundTask перед тем, как начать играть. В документации говорится, что его не следует использовать для работы в фоновом режиме. , но в нем также говорится, что это полезно для незавершенных дел, что, как мне кажется, описывает вашу ситуацию.

Мои рассуждения таковы:

  1. чтобы ваше приложение работало в фоновом режиме в audio фоновом режиме, вы должны воспроизводить звук, когда iOS обычно отключает вас
  2. при воспроизведении удаленного мультимедиа, естественно, требуется больше времени, чем для локального мультимедиа, между моментом, когда вы сообщаете AVPlayer play и когда звук фактически начинает воспроизводиться, позволяя вашему приложению продолжить работу.

Следовательно, фоновая задача дает вашему приложению немного больше времени (некоторые говорят до 3 минут) для загрузки удаленного носителя. и воспроизвести звук.

Стоит ли проделывать этот трюк? Вероятно, нет - это деталь реализации планировщика AVPlayer / iOS - чтобы планировщик iOS был "честным" в случае фонового звука, он действительно должен распознавать вашу "максимальную" попытку воспроизвести звук, позволяя AVPlayer.play() отметить начало фона аудио. Если воспроизведение не удается по какой-либо причине, тогда снимите расписание. Во всяком случае, если вы уверены в этом, вы можете отправить запрос на добавление функции.

p.s. не забудьте позвонить endBackgroundTask! Как в обработчике истечения срока, так и, чтобы быть хорошим гражданином мобильного устройства, как только звук начал воспроизводиться, обеспечивая непрерывную фоновую работу. Если endBackgroundTask влияет на вашу способность воспроизводить звук в фоновом режиме, вы звоните слишком рано!

person Rhythmic Fistman    schedule 17.03.2017