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

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:
resourceLoader:shouldWaitForLoadingOfRequestedResource:– вызывается, когда AVPlayer нуждается в данных. СохранитеAVAssetResourceLoadingRequestи запустите операцию загрузки данных.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, включая видео. Тот же подход с пользовательской схемой применим.
Поддерживается ли фоновое воспроизведение аудио?
AVAudioSession.