Потокова передача та кешування аудіо в 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.