Потоковое воспроизведение аудио в iOS с AVAssetResourceLoader

Потоковое воспроизведение аудио в iOS с AVAssetResourceLoader

Anna Kosenko
Anna Kosenko Co-Founder & Director at Everappz

TL;DR: Используйте AVAssetResourceLoaderDelegate с пользовательской схемой URL для перехвата загрузки ресурсов AVPlayer. Это позволяет добавлять пользовательские заголовки авторизации для облачных сервисов, кэшировать аудио на диск и управлять поведением потоковой передачи — без написания локального HTTP-прокси. Полный исходный код доступен на GitHub.


Зачем AVPlayer нужен пользовательский загрузчик ресурсов

AVPlayer воспроизводит аудио из локальных файлов и удалённых URL. Для большинства облачных сервисов (Dropbox, Google Drive, Box) можно передать прямую ссылку для скачивания, и воспроизведение заработает сразу.

Однако некоторые сервисы, например Yandex.Disk и WebDAV, требуют пользовательских заголовков авторизации в GET-запросах. AVPlayer не предоставляет встроенного способа для их добавления.

Решение: используйте свойство resourceLoader класса AVURLAsset. Этот API перехватывает запросы на загрузку ресурсов, работая как локальный HTTP-прокси без сопутствующей сложности.

Как это работает

AVPlayer использует resourceLoader, когда не распознаёт схему URL. Заменив https:// на пользовательскую схему (например, customscheme://), вы заставляете AVPlayer делегировать всю загрузку вашему приложению.

Необходимо реализовать два метода AVAssetResourceLoaderDelegate:

  1. resourceLoader:shouldWaitForLoadingOfRequestedResource: – вызывается, когда AVPlayer нуждается в данных. Сохраните AVAssetResourceLoadingRequest и запустите операцию загрузки данных.
  2. resourceLoader:didCancelLoadingRequest: – вызывается, когда запрос отменяется или заменяется.

Как создать пользовательский AVPlayer

Настройте AVPlayer с пользовательской схемой URL:

NSURL *url = [NSURL URLWithString:@"customscheme://host/audio.mp3"];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
[asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
[self addObserversForPlayerItem:item];
self.player = [AVPlayer playerWithPlayerItem:item];
[self addObserversForPlayer];

Этот код:

  • Определяет URL с вашей пользовательской схемой
  • Создаёт AVURLAsset с делегатом на главной очереди
  • Создаёт AVPlayerItem из ассета
  • Инициализирует AVPlayer

Реализация делегата загрузчика ресурсов

Создайте класс LSFilePlayerResourceLoader для обработки получения данных с сервера и их передачи обратно в AVURLAsset. Храните экземпляры загрузчика в словаре с ключом по URL ресурса.

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    NSURL *resourceURL = [loadingRequest.request URL];
    if ([resourceURL.scheme isEqualToString:@"customscheme"]) {
        LSFilePlayerResourceLoader *loader = 
        [self resourceLoaderForRequest:loadingRequest];
        if (!loader) {
            loader = [[LSFilePlayerResourceLoader alloc] initWithResourceURL:resourceURL session:self.session];
            loader.delegate = self;
            [self.resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]];
        }
        [loader addRequest:loadingRequest];
        return YES;
    }
    return NO;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
    LSFilePlayerResourceLoader *loader = [self resourceLoaderForRequest:loadingRequest];
    [loader removeRequest:loadingRequest];
}

Эти методы проверяют схему URL, создают или получают загрузчик и добавляют запрос в очередь загрузчика. Нераспознанные схемы возвращают NO.

Интерфейс LSFilePlayerResourceLoader

@interface LSFilePlayerResourceLoader : NSObject

@property (nonatomic, readonly, strong) NSURL *resourceURL;
@property (nonatomic, readonly) NSArray *requests;
@property (nonatomic, readonly, strong) YDSession *session;
@property (nonatomic, readonly, assign) BOOL isCancelled;
@property (nonatomic, weak) id<LSFilePlayerResourceLoaderDelegate> delegate;

- (instancetype)initWithResourceURL:(NSURL *)url session:(YDSession *)session;
- (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
- (void)removeRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
- (void)cancel;

@end

@protocol LSFilePlayerResourceLoaderDelegate <NSObject>

@optional
- (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader didFailWithError:(NSError *)error;
- (void)filePlayerResourceLoader:(LSFilePlayerResourceLoader *)resourceLoader didLoadResource:(NSURL *)resourceURL;

@end

Загрузка данных: двухэтапный процесс

Когда запрос на загрузку поступает в очередь, последовательно выполняются две операции:

  • contentInfoOperation – запрашивает длину контента, MIME-тип и поддержку диапазонов байт
  • dataOperation – получает данные файла начиная с requestedOffset

Стратегия кэширования на диске

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

  • Снижает использование полосы пропускания
  • Обеспечивает почти мгновенное повторное воспроизведение
  • Поддерживает операции перемотки в пределах кэшированных диапазонов

Обработка ожидающих запросов

Метод processPendingRequests заполняет contentInformationRequest каждого запроса метаданными и передаёт кэшированные диапазоны байт. Завершённые запросы удаляются из очереди.

Исходный код и дальнейшие шаги

Это руководство даёт общий обзор реализации AVAssetResourceLoaderDelegate для потокового воспроизведения аудио из облака с пользовательскими заголовками авторизации. Полный исходный код доступен на GitHub.

Данный подход лежит в основе механизма потоковой передачи аудио в Evermusic, который транслирует музыку из Dropbox, Google Drive, OneDrive, Yandex.Disk и других облачных сервисов на iOS и macOS.

Часто задаваемые вопросы

Когда следует использовать AVAssetResourceLoaderDelegate вместо прямого URL?
Используйте его, когда облачный сервис требует пользовательских заголовков авторизации, когда вам нужно кэширование на диске для потокового аудио или когда вы хотите детально управлять тем, как данные загружаются и буферизуются.
Этот подход работает со Swift?
Да. Протокол AVAssetResourceLoaderDelegate работает точно так же в Swift. Примеры на Objective-C переносятся напрямую.
Можно ли использовать это для потоковой передачи видео?
Да. AVAssetResourceLoaderDelegate работает с любым типом медиа, поддерживаемым AVPlayer, включая видео. Тот же подход с пользовательской схемой применим.
Поддерживается ли фоновое воспроизведение аудио?
Да, при условии что вы включите фоновый режим «Аудио, AirPlay и Картинка в картинке» в возможностях вашего приложения и правильно настроите AVAudioSession.
Последнее обновление