دمج البث في تطبيق iOS

يوضّح دليل المطوّرين هذا كيفية إضافة ميزة التوافق مع Google Cast إلى تطبيق iOS المُرسِل باستخدام حزمة تطوير البرامج (SDK) لتطبيق المُرسِل على iOS.

الجهاز الجوّال أو الكمبيوتر المحمول هو المُرسِل الذي يتحكّم في التشغيل، وجهاز Google Cast هو المُستلِم الذي يعرض المحتوى على التلفزيون.

يشير إطار عمل المُرسِل إلى ملفّ ثنائي لـ Cast Class Library والموارد المرتبطِين المتوفّرين أثناء التشغيل على المُرسِل. يشير تطبيق المُرسِل أو تطبيق البث إلى تطبيق يعمل أيضًا على جهاز المُرسِل. يشير تطبيق Web Receiver إلى تطبيق HTML الذي يعمل على Web Receiver.

يستخدم إطار عمل المُرسِل تصميمًا للرجوع غير المتزامن لإعلام تطبيق المُرسِل بالأحداث وللانتقال بين الحالات المختلفة لدورة حياة تطبيق Cast.

مسار المستخدم في التطبيق

توضّح الخطوات التالية خطوات التنفيذ العالية المستوى المعتادة لمرسل تطبيق iOS:

  • يبدأ إطار عمل Cast في تنفيذ GCKDiscoveryManager استنادًا إلى السمات المقدَّمة في GCKCastOptions لبدء البحث عن الأجهزة.
  • عندما ينقر المستخدم على زرّ البث، يعرض إطار العمل مربّع حوار البث الذي يتضمّن قائمة بأجهزة البث التي تم رصدها.
  • عندما يختار المستخدم جهاز بث، يحاول إطار العمل تشغيل تطبيق Web Receiver على جهاز البث.
  • يستدعي إطار العمل عمليات الاستدعاء في تطبيق المُرسِل لتأكيد أنّه تم تشغيل تطبيق Web Receiver.
  • ينشئ إطار العمل قناة اتصال بين تطبيق المُرسِل وتطبيق Web Receiver.
  • يستخدم الإطار قناة الاتصال لتحميل وسائط التشغيل والتحكّم فيها على Web Receiver.
  • يُزامن إطار العمل حالة تشغيل الوسائط بين المُرسِل و مُستلِم الويب: عندما ينفِّذ المستخدم إجراءات واجهة مستخدِم المُرسِل، يُرسِل إطار العمل طلبات التحكّم في الوسائط إلى مُستلِم الويب، وعندما يُرسِل مُستلِم الويب تعديلات حالة الوسائط، يعدِّل إطار العمل حالة واجهة مستخدِم المُرسِل.
  • عندما ينقر المستخدم على زر البثّ لقطع الاتصال بجهاز البثّ، سيقطع إطار العمل اتصال تطبيق المُرسِل بجهاز الاستقبال على الويب.

لتحديد مشاكل المُرسِل وحلّها، عليك تفعيل التسجّل.

للحصول على قائمة شاملة بجميع الصفوف والطُرق والأحداث في إطار عمل Google Cast iOS، يُرجى الاطّلاع على مرجع Google Cast iOS API. تتناول الأقسام التالية الخطوات اللازمة لدمج Cast في تطبيقك المتوافق مع نظام التشغيل iOS.

استدعاء الطرق من سلسلة التعليمات الرئيسية

بدء سياق البث

يحتوي إطار عمل Cast على عنصر فردي عالمي، وهو GCKCastContext، الذي ينسق جميع أنشطة الإطار. يجب بدء تشغيل هذا العنصر في وقت مبكر من دورة حياة التطبيق، عادةً في -[application:didFinishLaunchingWithOptions:] طريقة مفوّض التطبيق، لكي تتمكّن من إعادة بدء الجلسة تلقائيًا عند إعادة تشغيل تطبيق المُرسِل بشكل صحيح.

يجب تقديم عنصر GCKCastOptions عند إعداد GCKCastContext. تحتوي هذه الفئة على خيارات تؤثّر في سلوك الإطار. وأهم هذه العناصر هو معرّف تطبيق Web Receiver، الذي يُستخدَم لفلترة نتائج الاكتشاف وتشغيل تطبيق Web Receiver عند بدء جلسة بث.

تُعدّ طريقة -[application:didFinishLaunchingWithOptions:] أيضًا مكانًا جيدًا لإعداد مفوّض تسجيل لتلقّي رسائل التسجيل من إطار العمل. يمكن أن تكون هذه الإعدادات مفيدة لتصحيح الأخطاء وتحديد المشاكل وحلّها.

Swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                    initWithApplicationID:kReceiverAppID];
  GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
  [GCKCastContext setSharedInstanceWithOptions:options];

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

التطبيقات المصغّرة لتجربة المستخدم في Cast

توفّر حزمة تطوير البرامج (SDK) لنظام التشغيل iOS تطبيقات مصغّرة متوافقة مع قائمة التحقّق من تصميمات Cast:

  • العنصر المتراكب التمهيدي: تتضمّن فئة GCKCastContext طريقة، presentCastInstructionsViewControllerOnceWithCastButton، يمكن استخدامها لتسليط الضوء على زر البث في المرة الأولى التي يتوفّر فيها "مُستلِم الويب". يمكن لتطبيق المُرسِل تخصيص النص وموضع العنوان النص وزر "إغلاق".

  • زر البث: بدءًا من الإصدار 4.6.0 من حزمة تطوير البرامج (SDK) لمُرسِل البث على نظام التشغيل iOS، يظهر زر البث دائمًا عندما يكون جهاز المُرسِل متصلاً بشبكة Wi-Fi. في المرة الأولى التي ينقر فيها المستخدم على زر البث بعد تشغيل التطبيق لأول مرة، يظهر مربع حوار أذونات ليتمكّن المستخدم من منح التطبيق إذن الوصول إلى الشبكة المحلية للأجهزة على الشبكة. بعد ذلك، عندما ينقر المستخدم على زر البث، يظهر مربع حوار بث يسرد الأجهزة التي تم رصدها. عندما ينقر المستخدم على زر البث أثناء اتصال الجهاز، يتم عرض ملفه الشخصي الوصفي الحالي للوسائط (مثل العنوان واسم استوديو التسجيل وصورة مصغرة) أو يسمح للمستخدم بقطع الاتصال بجهاز البث. عندما يضغط المستخدم على زر البث بدون توفّر أي أجهزة، ستظهر شاشة توفّر للمستخدم معلومات عن سبب عدم العثور على الأجهزة وكيفية تحديد المشاكل وحلّها.

  • جهاز التحكّم المصغّر: عندما يبث المستخدم المحتوى وينتقل بعيدًا عن صفحة المحتوى الحالية أو جهاز التحكّم الموسّع إلى شاشة أخرى في تطبيق المُرسِل، يتم عرض جهاز التحكّم المصغّر في أسفل الشاشة للسماح للمستخدم باطلاعه على البيانات الوصفية للوسائط التي يتم بثها حاليًا والتحكّم في التشغيل.

  • وحدة التحكّم الموسّعة: عندما يبث المستخدم المحتوى، إذا نقر على إشعار الوسائط أو وحدة التحكّم المصغّرة، يتم تشغيل وحدة التحكّم الموسّعة التي تعرِض البيانات الوصفية للوسائط التي يتم تشغيلها حاليًا وتوفّر عدة أزرار للتحكّم في تشغيل الوسائط.

إضافة زر بث

يوفّر إطار العمل مكوّن زرّ البث كصنف فرعي من UIButton. ويمكن إضافته إلى شريط عنوان التطبيق من خلال لفّه في UIBarButtonItem. يمكن لفئة فرعية نموذجية من UIViewController تثبيت زر بث الوسائط على النحو التالي:

Swift
let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
castButton.tintColor = UIColor.gray
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
Objective-C
GCKUICastButton *castButton = [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
castButton.tintColor = [UIColor grayColor];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:castButton];

يؤدي النقر على الزر تلقائيًا إلى فتح مربّع حوار البث الذي يوفّره الإطار.

يمكن أيضًا إضافة GCKUICastButton مباشرةً إلى لوحة القصة.

ضبط ميزة "رصد الأجهزة"

في إطار العمل، يتم اكتشاف الأجهزة تلقائيًا. ليس عليك بدء عملية الاكتشاف أو إيقافها صراحةً ما لم تنفِّذ واجهة مستخدم مخصّصة.

تتم إدارة التصفّح في إطار العمل من خلال الفئة GCKDiscoveryManager، التي هي سمة GCKCastContext. يقدّم الإطار المرجعي عنصر مربّع حوار تلقائيًا لبث المحتوى من أجل اختيار الجهاز والتحكّم فيه. يتم ترتيب قائمة الأجهزة أبجديًا حسب الاسم السهل للجهاز.

آلية عمل إدارة الجلسات

تقدّم حزمة تطوير البرامج (SDK) لبث الوسائط مفهوم جلسة البث، ويجمع تأسيسها خطوات الاتصال بجهاز وتشغيل تطبيق Web Receiver (أو الانضمام إليه) والاتصال بهذا التطبيق وبدء قناة التحكّم في الوسائط. اطّلِع على دليل دورة حياة التطبيق لمزيد من المعلومات عن جلسات البث ودورة حياة Web Receiver.

تتم إدارة الجلسات من خلال الصف GCKSessionManager، وهو خاصية GCKCastContext. يتم تمثيل الجلسات الفردية من خلال فئات فرعية من الفئة GCKSession: على سبيل المثال، يمثّل GCKCastSession الجلسات التي تتضمّن أجهزة بث. يمكنك الوصول إلى جلسة البث النشطة حاليًا (إن توفّرت) بصفتك currentCastSession في GCKSessionManager.

يمكن استخدام واجهة GCKSessionManagerListener لمراقبة أحداث الجلسة، مثل إنشاء الجلسة والتعليق والاستئناف والإيقاف. يوقف الإطار العمل جلسات البث تلقائيًا عند انتقال التطبيق المُرسِل إلى الخلفية ويحاول استئنافها عند عودة التطبيق إلى المقدّمة (أو عند إعادة تشغيله بعد إنهاء التطبيق بشكل غير طبيعي أو مفاجئ أثناء نشاط الجلسة).

في حال استخدام مربّع حوار البث، يتم إنشاء الجلسات وإغلاقها تلقائيًا استجابةً لإيماءات المستخدم. بخلاف ذلك، يمكن للتطبيق بدء الجلسات وإنهائها صراحةً من خلال الطرق في GCKSessionManager.

إذا كان التطبيق بحاجة إلى إجراء معالجة خاصة استجابةً لأحداث دورة حياة الجلسة، يمكنه تسجيل مثيل واحد أو أكثر من GCKSessionManagerListener باستخدام GCKSessionManager. GCKSessionManagerListener هو بروتوكول يحدِّد callbacks لأحداث مثل بدء الجلسة وانتهائها وما إلى ذلك.

إعادة توجيه البث

إنّ الحفاظ على حالة الجلسة هو أساس نقل البث، حيث يمكن للمستخدمين نقل أحداث البث الصوتي والفيديوي الحالية على جميع الأجهزة باستخدام الطلبات الصوتية أو تطبيق Google Home أو الشاشات الذكية. يتوقف تشغيل الوسائط على جهاز واحد (المصدر) ويستمر على جهاز آخر (المقصود). يمكن لأي جهاز بث مزوّد بأحدث البرامج الثابتة أن يكون مصدرًا أو وجهة في عملية نقل البث.

للحصول على الجهاز الوجهة الجديد أثناء نقل البث، استخدِم السمة GCKCastSession#device أثناء معالجة المرجع المرسَل [sessionManager:didResumeCastSession:].

اطّلِع على نقل البث على Web Receiver للحصول على مزيد من المعلومات.

إعادة الاتصال التلقائي

يضيف إطار عمل Cast منطقًا لإعادة الربط من أجل معالجة إعادة الربط تلقائيًا في العديد من الحالات الدقيقة، مثل:

  • استرداد البيانات بعد فقدان الاتصال بشبكة Wi-Fi مؤقتًا
  • استعادة البيانات بعد تنشيط الجهاز من وضع السكون
  • استعادة التطبيق من وضع "التشغيل في الخلفية"
  • استرداد البيانات في حال تعطُّل التطبيق

آلية عمل عناصر التحكّم في الوسائط

في حال إنشاء جلسة بث باستخدام تطبيق Web Receiver متوافق مع مساحة имен الوسائط، سينشئ إطار العمل تلقائيًا مثيلًا لمحاولة GCKRemoteMediaClient ، ويمكن الوصول إليه على أنّه سمة remoteMediaClient لمثيل GCKCastSession.

ستُعرِض كل الطرق في GCKRemoteMediaClient التي تُرسل طلبات إلى Web Receiver كائنًا GCKRequest يمكن استخدامه لتتبُّع هذا الطلب. يمكن تعيين GCKRequestDelegate لهذا العنصر لتلقّي إشعارات بشأن نتيجة العملية النهائية.

من المتوقّع أن تتم مشاركة مثيل GCKRemoteMediaClient من خلال أجزاء متعدّدة من التطبيق، وبالفعل تشارك بعض المكوّنات الداخلية للإطار العمل، مثل مربّع حوار البث وعناصر التحكّم في الوسائط المصغّرة، المثيل. ولهذا الغرض، تتيح GCKRemoteMediaClient تسجيل عدة نطاقات GCKRemoteMediaClientListener.

ضبط البيانات الوصفية للوسائط

تمثّل فئة GCKMediaMetadata معلومات عن عنصر وسائط تريد بثّه. في المثال التالي، يتم إنشاء مثيل جديد من GCKMediaMetadata لفيلم مع ضبط العنوان والترجمة والشرح واسم استوديو التسجيل وصورتَين.

Swift
let metadata = GCKMediaMetadata()
metadata.setString("Big Buck Bunny (2008)", forKey: kGCKMetadataKeyTitle)
metadata.setString("Big Buck Bunny tells the story of a giant rabbit with a heart bigger than " +
  "himself. When one sunny day three rodents rudely harass him, something " +
  "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon " +
  "tradition he prepares the nasty rodents a comical revenge.",
                   forKey: kGCKMetadataKeySubtitle)
metadata.addImage(GCKImage(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!,
                           width: 480,
                           height: 360))
Objective-C
GCKMediaMetadata *metadata = [[GCKMediaMetadata alloc]
                                initWithMetadataType:GCKMediaMetadataTypeMovie];
[metadata setString:@"Big Buck Bunny (2008)" forKey:kGCKMetadataKeyTitle];
[metadata setString:@"Big Buck Bunny tells the story of a giant rabbit with a heart bigger than "
 "himself. When one sunny day three rodents rudely harass him, something "
 "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon "
 "tradition he prepares the nasty rodents a comical revenge."
             forKey:kGCKMetadataKeySubtitle];
[metadata addImage:[[GCKImage alloc]
                    initWithURL:[[NSURL alloc] initWithString:@"https://commondatastorage.googleapis.com/"
                                 "gtv-videos-bucket/sample/images/BigBuckBunny.jpg"]
                    width:480
                    height:360]];

راجِع قسم اختيار الصور وتخزينها للاطّلاع على كيفية استخدام الصور مع البيانات الوصفية للوسائط.

تحميل الوسائط

لتحميل عنصر وسائط، أنشئ مثيلًا GCKMediaInformation باستخدام البيانات الوصفية للوسائط. بعد ذلك، احصل على القيمة الحالية لـ GCKCastSession واستخدِم GCKRemoteMediaClient لتحميل الوسائط على تطبيق المُستلِم. يمكنك بعد ذلك استخدام GCKRemoteMediaClient للتحكّم في تطبيق مشغّل وسائط يعمل على المُستلِم، مثل التشغيل والإيقاف المؤقت والإيقاف.

Swift
let url = URL.init(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
guard let mediaURL = url else {
  print("invalid mediaURL")
  return
}

let mediaInfoBuilder = GCKMediaInformationBuilder.init(contentURL: mediaURL)
mediaInfoBuilder.streamType = GCKMediaStreamType.none;
mediaInfoBuilder.contentType = "video/mp4"
mediaInfoBuilder.metadata = metadata;
mediaInformation = mediaInfoBuilder.build()

guard let mediaInfo = mediaInformation else {
  print("invalid mediaInformation")
  return
}

if let request = sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInfo) {
  request.delegate = self
}
Objective-C
GCKMediaInformationBuilder *mediaInfoBuilder =
  [[GCKMediaInformationBuilder alloc] initWithContentURL:
   [NSURL URLWithString:@"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]];
mediaInfoBuilder.streamType = GCKMediaStreamTypeNone;
mediaInfoBuilder.contentType = @"video/mp4";
mediaInfoBuilder.metadata = metadata;
self.mediaInformation = [mediaInfoBuilder build];

GCKRequest *request = [self.sessionManager.currentSession.remoteMediaClient loadMedia:self.mediaInformation];
if (request != nil) {
  request.delegate = self;
}

اطّلِع أيضًا على القسم المتعلق بموضوع استخدام مسارات الوسائط.

تنسيق الفيديو بدقة 4K

لتحديد تنسيق الفيديو الخاص بالوسائط، استخدِم السمة videoInfo في GCKMediaStatus للحصول على العنصر الحالي من GCKVideoInfo. يحتوي هذا المثال على نوع تنسيق التلفزيون بتقنية HDR والارتفاع والعرض بالبكسل. يتم الإشارة إلى صيغ 4K في السمة hdrType باستخدام قيم التعداد GCKVideoInfoHDRType.

إضافة وحدات تحكّم صغيرة

وفقًا لقائمة التحقّق من تصاميم التطبيقات المتوافقة مع Google Cast، يجب أن يقدّم تطبيق المُرسِل عنصر تحكّم دائمًا يُعرف باسم جهاز التحكّم المصغر الذي من المفترض أن يظهر عندما ينتقل المستخدم بعيدًا عن صفحة المحتوى الحالية. يوفر جهاز التحكّم الصغير إمكانية الوصول الفوري إلى جلسة البث الحالية وتذكيرًا مرئيًا بها.

يقدّم إطار عمل Cast شريط تحكّم، GCKUIMiniMediaControlsViewController، يمكن إضافته إلى المشاهد التي تريد عرض وحدة التحكّم المصغّرة فيها.

عندما يشغِّل تطبيق المُرسِل بثًا مباشرًا للفيديو أو الصوت، يعرض حِزمة تطوير البرامج (SDK) تلقائيًا زر تشغيل/إيقاف بدلاً من زر التشغيل/الإيقاف المؤقت في وحدة التحكّم المصغّرة.

اطّلِع على مقالة تخصيص واجهة مستخدم المُرسِل في نظام التشغيل iOS لمعرفة كيفية ضبط تطبيق المُرسِل لمظهر التطبيقات المصغّرة لأجهزة Cast.

هناك طريقتان لإضافة وحدة التحكّم الصغيرة إلى تطبيق المُرسِل:

  • يمكنك السماح لإطار عمل Cast بإدارة تنسيق وحدة التحكّم المصغّرة من خلال لفّ وحدة التحكّم الحالية في العرض باستخدام وحدة تحكّم في العرض الخاصة بها.
  • يمكنك إدارة تنسيق التطبيق المصغّر الخاص بوحدة التحكّم بنفسك من خلال إضافته إلى عنصر التحكّم الحالي في العرض من خلال توفير عرض فرعي في لوحة القصة.

التفاف باستخدام GCKUICastContainerViewController

الطريقة الأولى هي استخدام العنصر GCKUICastContainerViewController الذي يلفّ عنصر تحكّم في عرض آخر ويضيف رمز GCKUIMiniMediaControlsViewController في أسفل الشاشة. يُعدّ هذا النهج محدودًا لأنّه لا يمكنك تخصيص المؤثرات المتحركة ولا يمكنك ضبط سلوك عنصر التحكّم في عرض الحاوية.

يتم تنفيذ هذه الطريقة الأولى عادةً في الأسلوب -[application:didFinishLaunchingWithOptions:] لمفوّض التطبيق:

Swift
func applicationDidFinishLaunching(_ application: UIApplication) {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
  let castContainerVC =
          GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window!.rootViewController = castContainerVC
  window!.makeKeyAndVisible()

  ...
}
Objective-C
- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  UIStoryboard *appStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
          [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC =
          [[GCKCastContext sharedInstance] createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
  ...

}
Swift
var castControlBarsEnabled: Bool {
  set(enabled) {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      castContainerVC.miniMediaControlsItemEnabled = enabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
    }
  }
  get {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      return castContainerVC.miniMediaControlsItemEnabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
      return false
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, assign) BOOL castControlBarsEnabled;

@end

AppDelegate.m

@implementation AppDelegate

...

- (void)setCastControlBarsEnabled:(BOOL)notificationsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  castContainerVC.miniMediaControlsItemEnabled = notificationsEnabled;
}

- (BOOL)castControlBarsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  return castContainerVC.miniMediaControlsItemEnabled;
}

...

@end

تضمين العنصر في وحدة تحكّم في العرض الحالية

الطريقة الثانية هي إضافة وحدة التحكّم الصغيرة مباشرةً إلى وحدة التحكّم في المَشاهد الحالية باستخدام createMiniMediaControlsViewController لإنشاء مثيل GCKUIMiniMediaControlsViewController ثم إضافته إلى وحدة التحكّم في عرض الحاوية كمَشاهد فرعية.

إعداد "عنصر التحكّم في العرض" في عنصر تفويض التطبيق:

Swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  ...

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
  window?.clipsToBounds = true

  let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
  rootContainerVC?.miniMediaControlsViewEnabled = true

  ...

  return true
}
Objective-C
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  self.window.clipsToBounds = YES;

  RootContainerViewController *rootContainerVC;
  rootContainerVC =
      (RootContainerViewController *)self.window.rootViewController;
  rootContainerVC.miniMediaControlsViewEnabled = YES;

  ...

  return YES;
}

في عنصر التحكّم في العرض الجذر، أنشئ مثيلًا من GCKUIMiniMediaControlsViewController وأضِفه إلى عنصر التحكّم في عرض الحاوية كعرض فرعي:

Swift
let kCastControlBarsAnimationDuration: TimeInterval = 0.20

@objc(RootContainerViewController)
class RootContainerViewController: UIViewController, GCKUIMiniMediaControlsViewControllerDelegate {
  @IBOutlet weak private var _miniMediaControlsContainerView: UIView!
  @IBOutlet weak private var _miniMediaControlsHeightConstraint: NSLayoutConstraint!
  private var miniMediaControlsViewController: GCKUIMiniMediaControlsViewController!
  var miniMediaControlsViewEnabled = false {
    didSet {
      if self.isViewLoaded {
        self.updateControlBarsVisibility()
      }
    }
  }

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

  override func viewDidLoad() {
    super.viewDidLoad()
    let castContext = GCKCastContext.sharedInstance()
    self.miniMediaControlsViewController = castContext.createMiniMediaControlsViewController()
    self.miniMediaControlsViewController.delegate = self
    self.updateControlBarsVisibility()
    self.installViewController(self.miniMediaControlsViewController,
                               inContainerView: self._miniMediaControlsContainerView)
  }

  func updateControlBarsVisibility() {
    if self.miniMediaControlsViewEnabled && self.miniMediaControlsViewController.active {
      self._miniMediaControlsHeightConstraint.constant = self.miniMediaControlsViewController.minHeight
      self.view.bringSubview(toFront: self._miniMediaControlsContainerView)
    } else {
      self._miniMediaControlsHeightConstraint.constant = 0
    }
    UIView.animate(withDuration: kCastControlBarsAnimationDuration, animations: {() -> Void in
      self.view.layoutIfNeeded()
    })
    self.view.setNeedsLayout()
  }

  func installViewController(_ viewController: UIViewController?, inContainerView containerView: UIView) {
    if let viewController = viewController {
      self.addChildViewController(viewController)
      viewController.view.frame = containerView.bounds
      containerView.addSubview(viewController.view)
      viewController.didMove(toParentViewController: self)
    }
  }

  func uninstallViewController(_ viewController: UIViewController) {
    viewController.willMove(toParentViewController: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "NavigationVCEmbedSegue" {
      self.navigationController = (segue.destination as? UINavigationController)
    }
  }

...
Objective-C

RootContainerViewController.h

static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

@interface RootContainerViewController () <GCKUIMiniMediaControlsViewControllerDelegate> {
  __weak IBOutlet UIView *_miniMediaControlsContainerView;
  __weak IBOutlet NSLayoutConstraint *_miniMediaControlsHeightConstraint;
  GCKUIMiniMediaControlsViewController *_miniMediaControlsViewController;
}

@property(nonatomic, weak, readwrite) UINavigationController *navigationController;

@property(nonatomic, assign, readwrite) BOOL miniMediaControlsViewEnabled;
@property(nonatomic, assign, readwrite) BOOL miniMediaControlsItemEnabled;

@end

RootContainerViewController.m

@implementation RootContainerViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  GCKCastContext *castContext = [GCKCastContext sharedInstance];
  _miniMediaControlsViewController =
      [castContext createMiniMediaControlsViewController];
  _miniMediaControlsViewController.delegate = self;

  [self updateControlBarsVisibility];
  [self installViewController:_miniMediaControlsViewController
              inContainerView:_miniMediaControlsContainerView];
}

- (void)setMiniMediaControlsViewEnabled:(BOOL)miniMediaControlsViewEnabled {
  _miniMediaControlsViewEnabled = miniMediaControlsViewEnabled;
  if (self.isViewLoaded) {
    [self updateControlBarsVisibility];
  }
}

- (void)updateControlBarsVisibility {
  if (self.miniMediaControlsViewEnabled &&
      _miniMediaControlsViewController.active) {
    _miniMediaControlsHeightConstraint.constant =
        _miniMediaControlsViewController.minHeight;
    [self.view bringSubviewToFront:_miniMediaControlsContainerView];
  } else {
    _miniMediaControlsHeightConstraint.constant = 0;
  }
  [UIView animateWithDuration:kCastControlBarsAnimationDuration
                   animations:^{
                     [self.view layoutIfNeeded];
                   }];
  [self.view setNeedsLayout];
}

- (void)installViewController:(UIViewController *)viewController
              inContainerView:(UIView *)containerView {
  if (viewController) {
    [self addChildViewController:viewController];
    viewController.view.frame = containerView.bounds;
    [containerView addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];
  }
}

- (void)uninstallViewController:(UIViewController *)viewController {
  [viewController willMoveToParentViewController:nil];
  [viewController.view removeFromSuperview];
  [viewController removeFromParentViewController];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:@"NavigationVCEmbedSegue"]) {
    self.navigationController =
        (UINavigationController *)segue.destinationViewController;
  }
}

...

@end

يُعلم العنصر GCKUIMiniMediaControlsViewControllerDelegate عنصر التحكّم في عرض المضيف عندما يجب أن يكون عنصر التحكّم المصغّر مرئيًا:

Swift
  func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController,
                                       shouldAppear _: Bool) {
    updateControlBarsVisibility()
  }
Objective-C
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

إضافة وحدة تحكّم موسّعة

تتطلّب قائمة التحقّق من التصميم في Google Cast من تطبيق المُرسِل توفير عناصر تحكّم موسّعة للوسائط التي يتم بثّها. جهاز التحكّم الموسّع هو نسخة ملء الشاشة من جهاز التحكّم المصغّر.

لوحة التحكّم الموسّعة هي طريقة عرض بملء الشاشة تتيح التحكّم الكامل في تشغيل الوسائط عن بُعد. من المفترض أن تسمح هذه الطريقة للتطبيق بإدارة كل جانب قابل للإدارة من جلسة البث، باستثناء التحكّم في مستوى الصوت في Web Receiver ودورة حياة الجلسة (الاتصال/إيقاف البث). ويقدّم أيضًا كل معلومات الحالة حول جلسة الوسائط (العمل الفني والعنوان والترجمة والشرح وما إلى ذلك).

يتم تنفيذ وظائف هذا العرض من خلال فئة GCKUIExpandedMediaControlsViewController.

أول إجراء عليك اتّخاذه هو تفعيل وحدة التحكّم الموسّعة التلقائية في سياق البث. عدِّل مفوّض التطبيق لتفعيل وحدة التحكّم الموسّعة التلقائية:

Swift
func applicationDidFinishLaunching(_ application: UIApplication) {
  ..

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
Objective-C
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

أضِف الرمز التالي إلى وحدة التحكّم في العرض لتحميل وحدة التحكّم الموسّعة عندما يبدأ المستخدم ببث فيديو:

Swift
func playSelectedItemRemotely() {
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()

  ...

  // Load your media
  sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation)
}
Objective-C
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

  // Load your media
  [self.sessionManager.currentSession.remoteMediaClient loadMedia:mediaInformation];
}

سيتم أيضًا تشغيل وحدة التحكّم الموسّعة تلقائيًا عندما يصنّع المستخدِم على وحدة التحكّم المصغّرة.

عندما يشغِّل تطبيق المُرسِل بثًا مباشرًا للفيديو أو الصوت، يعرض حِزمة تطوير البرامج (SDK) تلقائيًا زر تشغيل/إيقاف بدلاً من زر التشغيل/الإيقاف المؤقت في وحدة التحكّم الموسّعة.

اطّلِع على مقالة تطبيق أنماط مخصّصة على تطبيق iOS لمعرفة كيفية ضبط تطبيق المُرسِل لمظهر التطبيقات المصغّرة لأجهزة Cast.

التحكم في مستوى الصوت

يدير إطار عمل Cast مستوى الصوت في تطبيق المُرسِل تلقائيًا. تتم مزامنة الإطار مع مستوى الصوت في Web Receiver تلقائيًا لتطبيقات التطبيقات المصغّرة التي توفّر واجهة مستخدم. لمزامنة شريط تمرير يقدّمه التطبيق، استخدِم GCKUIDeviceVolumeController.

التحكّم في مستوى الصوت باستخدام أزرار خارجية

يمكن استخدام زرَّي التحكّم بمستوى الصوت على جهاز الإرسال لتغيير مستوى صوت جلسة البث على Web Receiver باستخدام العلامة physicalVolumeButtonsWillControlDeviceVolume في GCKCastOptions، التي يتم ضبطها على GCKCastContext.

Swift
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
Objective-C
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

معالجة الأخطاء

من المهم جدًا أن تعالج تطبيقات المُرسِلين جميع عمليات الاستدعاء المتعلّقة بالأخطاء وأن تقرّر أفضل استجابة لكل مرحلة من مراحل دورة حياة Cast. يمكن للتطبيق عرض مربعات حوار تتعلّق بالأخطاء للمستخدم أو يمكنه إنهاء جلسة البث.

التسجيل

GCKLogger هي فئة فردية يستخدمها إطار العمل لتسجيل البيانات. استخدِم الرمز GCKLoggerDelegate لتخصيص كيفية معالجة رسائل السجلّ.

باستخدام GCKLogger، تُنشئ حزمة SDK سجلّاً في شكل رسائل debugging والأخطاء والتحذيرات. تساعد رسائل السجلّ هذه في تصحيح الأخطاء، وهي مفيدة لتحديد المشاكل وحلّها. يتم تلقائيًا كتم GCKLoggerDelegateإخراج السجلّ، ولكن من خلال تحديد GCKLoggerDelegate، يمكن للتطبيق المُرسِل تلقّي هذه الرسائل من حزمة تطوير البرامج (SDK) وتسجيلها في وحدة تحكّم النظام.

Swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    ...

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
Objective-C

AppDelegate.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

AppDelegate.m

@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end

لتفعيل تصحيح الأخطاء والرسائل التفصيلية أيضًا، أضِف هذا السطر إلى الرمز بعد ضبط المفوَّض (الموضَّح سابقًا):

Swift
let filter = GCKLoggerFilter.init()
filter.minimumLevel = GCKLoggerLevel.verbose
GCKLogger.sharedInstance().filter = filter
Objective-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setMinimumLevel:GCKLoggerLevelVerbose];
[GCKLogger sharedInstance].filter = filter;

يمكنك أيضًا فلترة رسائل السجلّ التي أنشأها GCKLogger. اضبط الحد الأدنى لمستوى التسجيل لكل فئة، على سبيل المثال:

Swift
let filter = GCKLoggerFilter.init()
filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton",
                                                            "GCKUIImageCache",
                                                            "NSMutableDictionary"])
GCKLogger.sharedInstance().filter = filter
Objective-C
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

يمكن أن تكون أسماء الفئات إما أسماء حرفية أو أنماط عناوين شاملة، على سبيل المثال، GCKUI\* وGCK\*Session.