بث الصوت في iOS باستخدام AVAssetResourceLoader

بث الصوت في iOS باستخدام AVAssetResourceLoader

Artem Meleshko
Artem Meleshko Co-Founder & iOS Engineer at Everappz

ملخص سريع: استخدم AVAssetResourceLoaderDelegate مع مخطط URL مخصص لاعتراض تحميل الموارد في AVPlayer. يتيح لك هذا إضافة رؤوس تفويض مخصصة لخدمات السحابة، وتخزين الصوت مؤقتًا على القرص، والتحكم في سلوك البث – وكل ذلك دون الحاجة إلى كتابة وكيل HTTP محلي. الكود المصدري الكامل متاح على GitHub.


لماذا يحتاج AVPlayer إلى محمّل موارد مخصص

يشغّل AVPlayer الصوت من الملفات المحلية وعناوين URL البعيدة. بالنسبة لمعظم الخدمات السحابية (Dropbox وGoogle Drive وBox)، يمكنك تمرير رابط تنزيل مباشر وستعمل إمكانية التشغيل فورًا.

غير أن بعض الخدمات مثل Yandex.Disk وWebDAV تتطلب رؤوس تفويض مخصصة في طلبات GET. لا يوفر AVPlayer أي طريقة مدمجة لحقن هذه الرؤوس.

الحل: استخدم خاصية resourceLoader في AVURLAsset. تعترض هذه الواجهة البرمجية طلبات تحميل الموارد، وتعمل كوكيل 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، بما في ذلك الفيديو. ينطبق نفس نهج المخطط المخصص.
هل يدعم هذا تشغيل الصوت في الخلفية؟
نعم، طالما تفعّل وضع الخلفية “Audio, AirPlay, and Picture in Picture” في إمكانيات تطبيقك وتضبط AVAudioSession بشكل صحيح.
Last updated on