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

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

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

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

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

مسار التطبيق

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

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

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

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

طُرق الاتصال من سلسلة المحادثات الرئيسية

إعداد سياق البث

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

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

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

سويفت
@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)
    }
  }
}
الهدف-ج

Appتفويض.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

Appتفويض.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

أدوات "تجربة المستخدم" على Google Cast

توفّر حزمة Cast iOS SDK الأدوات التالية التي تتوافق مع قائمة التحقق من "تصميم البث":

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

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

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

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

إضافة زر بث

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

سويفت
let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
castButton.tintColor = UIColor.gray
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
الهدف-ج
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) للبث مفهوم جلسة البث الذي يجمع خطوات ربط الجهاز بجهاز أو إطلاق تطبيق على الويب (أو الانضمام إليه) والاتصال بهذا التطبيق وإعداد قناة للتحكّم بالوسائط. اطلع على دليل دورة حياة التطبيق للمستلِم على الويب للحصول على مزيد من المعلومات حول جلسات البث ودورة حياة مستقبِل الويب.

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

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

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

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

نقل مجموعة البث

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

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

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

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

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

  • استرداد البيانات من فقدان مؤقت لشبكة Wi-Fi
  • الاسترداد من وضع السكون على الجهاز
  • الاسترداد من خلفية التطبيق
  • استرداد ما إذا كان التطبيق تعطّل

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

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

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

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

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

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

سويفت
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))
الهدف-ج
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 للتحكّم في تطبيق مشغّل الوسائط الذي يتم تشغيله على جهاز الاستقبال، مثل التشغيل والإيقاف المؤقت والإيقاف.

سويفت
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
}
الهدف-ج
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 TV والارتفاع والعرض بالبكسل. وتتم الإشارة إلى الصيغ بتنسيق 4K في السمة hdrType من خلال قيم التعداد GCKVideoInfoHDRType.

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

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

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

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

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

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

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

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

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

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

سويفت
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()

  ...
}
الهدف-ج
- (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
    }
  }
}
الهدف-ج

Appتفويض.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

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

@end

Appتفويض.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 وإضافتها إلى وحدة تحكُّم عرض الحاويات كعرض فرعي.

إعداد وحدة التحكم في العرض في تفويض التطبيق:

سويفت
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
}
الهدف-ج
- (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 وأضِفها إلى وحدة التحكّم في عرض الحاوية كعرض فرعي:

سويفت
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)
    }
  }

...
الهدف-ج

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 وحدة التحكّم في عرض المضيف عند ظهور وحدة التحكّم المصغّرة:

سويفت
  func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController,
                                       shouldAppear _: Bool) {
    updateControlBarsVisibility()
  }
الهدف-ج
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

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

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

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

يتم تنفيذ وظائف طريقة العرض هذه بواسطة فئة GCKUIExpandedMediaControlsViewController.

أول ما يجب فعله هو تفعيل وحدة التحكّم الموسّعة التلقائية في سياق البث. عدِّل مفوَّض التطبيق لتفعيل وحدة التحكُّم التلقائية الموسَّعة:

سويفت
func applicationDidFinishLaunching(_ application: UIApplication) {
  ..

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
الهدف-ج
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

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

سويفت
func playSelectedItemRemotely() {
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()

  ...

  // Load your media
  sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation)
}
الهدف-ج
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

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

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

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

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

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

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

التحكّم في مستوى صوت الزر الفعلي

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

سويفت
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
الهدف-ج
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

التعامل مع الأخطاء

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

التسجيل

GCKLogger هو اللغة المنفردة المستخدَمة للتسجيل في إطار العمل. يمكنك استخدام GCKLoggerDelegate لتخصيص طريقة معالجة رسائل السجلّ.

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

سويفت
@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)
    }
  }
}
الهدف-ج

Appتفويض.h

@interface AppDelegate () <GCKLoggerDelegate>
@end

Appتفويض.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

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

سويفت
let filter = GCKLoggerFilter.init()
filter.minimumLevel = GCKLoggerLevel.verbose
GCKLogger.sharedInstance().filter = filter
الهدف-ج
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setMinimumLevel:GCKLoggerLevelVerbose];
[GCKLogger sharedInstance].filter = filter;

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

سويفت
let filter = GCKLoggerFilter.init()
filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton",
                                                            "GCKUIImageCache",
                                                            "NSMutableDictionary"])
GCKLogger.sharedInstance().filter = filter
الهدف-ج
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

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