שילוב של Chromecast באפליקציית iOS

במדריך הזה למפתחים מוסבר איך להוסיף תמיכה ב-Google Cast לאפליקציית השליחה ל-iOS באמצעות iOS Sender SDK.

המכשיר הנייד או המחשב הנייד הוא השולח ששולט בהפעלה, ומכשיר Google Cast הוא המקלט שמציג את התוכן בטלוויזיה.

מסגרת השולח מתייחסת לקובץ הבינארי של ספריית המחלקות של Cast ולמשאבים המשויכים שקיימים בזמן הריצה בשולח. אפליקציית השליחה או אפליקציית Cast היא אפליקציה שפועלת גם במכשיר השולח. אפליקציית Web Receiver היא אפליקציית HTML שפועלת ב-Web Receiver.

מסגרת השולח משתמשת בעיצוב של קריאה חוזרת (callback) אסינכרונית כדי להודיע לאפליקציית השולח על אירועים ולעבור בין מצבים שונים של מחזור החיים של אפליקציית Cast.

תהליך באפליקציה

השלבים הבאים מתארים את תהליך הביצוע הכללי של אפליקציה ל-iOS שולחת:

  • מסגרת Cast מפעילה את GCKDiscoveryManager על סמך המאפיינים שצוינו ב-GCKCastOptions כדי להתחיל לסרוק מכשירים.
  • כשהמשתמש לוחץ על הכפתור להפעלת Cast, המסגרת מציגה את תיבת הדו-שיח של Cast עם רשימת מכשירי ה-Cast שזוהו.
  • כשהמשתמש בוחר מכשיר Cast, ה-framework מנסה להפעיל את אפליקציית Web Receiver במכשיר ה-Cast.
  • ה-framework מפעיל קריאות חוזרות (callbacks) באפליקציית השולח כדי לאשר שאפליקציית Web Receiver הופעלה.
  • ה-framework יוצרת ערוץ תקשורת בין אפליקציות השולח לאפליקציות Web Receiver.
  • ה-framework משתמש בערוץ התקשורת כדי לטעון את הפעלת המדיה ב-Web Receiver ולשלוט בה.
  • ה-framework מסנכרן את מצב הפעלת המדיה בין השולח לבין Web Receiver: כשהמשתמש מבצע פעולות בממשק המשתמש של השולח, ה-framework מעביר את הבקשות האלה לשליטה במדיה אל Web Receiver, וכש-Web Receiver שולח עדכונים לגבי סטטוס המדיה, ה-framework מעדכן את המצב של ממשק המשתמש של השולח.
  • כשהמשתמש לוחץ על הכפתור להפעלת Cast כדי להתנתק ממכשיר ה-Cast, המסגרת תנתק את אפליקציית השליחה מהמקלט לאינטרנט.

כדי לפתור בעיות במכשיר השולח, צריך להפעיל רישום ביומן.

רשימה מקיפה של כל המחלקות, השיטות והאירועים ב-framework של Google Cast ל-iOS מופיעה בחומר העזר בנושא Google Cast iOS API. בקטעים הבאים מוסבר איך לשלב את Cast באפליקציה ל-iOS.

שיטות קריאה מה-thread הראשי

הפעלת ההקשר של Cast

ל-Cast framework יש אובייקט סינגלטון גלובלי, GCKCastContext, שמתאם את כל הפעילויות של ה-framework. צריך להפעיל את האובייקט הזה בשלב מוקדם במחזור החיים של האפליקציה, בדרך כלל ב-method‏ -[application:didFinishLaunchingWithOptions:] של נציג האפליקציה, כדי שהחידוש האוטומטי של הסשן בהפעלה מחדש של אפליקציית השולח יוכל לפעול כמו שצריך.

כשמאתחלים את GCKCastContext, צריך לספק אובייקט GCKCastOptions. המחלקה הזו מכילה אפשרויות שמשפיעות על ההתנהגות של ה-framework. החשוב ביותר מביניהם הוא מזהה האפליקציה של Web Receiver, שמשמש לסינון תוצאות החיפוש ולהפעלת אפליקציית Web Receiver כשמתחילים סשן Cast.

השיטה -[application:didFinishLaunchingWithOptions:] היא גם מקום טוב להגדיר נציג רישום ביומן כדי לקבל את הודעות הרישום ביומן מה-framework. הם יכולים לעזור בניפוי באגים ובפתרון בעיות.

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 UX

‫Cast iOS SDK מספק את הווידג'טים הבאים, שתואמים לרשימת המשימות לעיצוב Cast:

  • שכבת-על של היכרות: למחלקה GCKCastContext יש שיטה, presentCastInstructionsViewControllerOnceWithCastButton, שאפשר להשתמש בה כדי להדגיש את הכפתור להפעלת Cast בפעם הראשונה שבה מקלט אינטרנט זמין. האפליקציה השולחת יכולה להתאים אישית את הטקסט, את המיקום של טקסט הכותרת ואת כפתור הסגירה.

  • הכפתור להפעלת Cast: החל מגרסה 4.6.0 של Cast iOS sender SDK, הכפתור להפעלת Cast תמיד גלוי כשמכשיר השולח מחובר ל-Wi-Fi. בפעם הראשונה שהמשתמש לוחץ על הכפתור להפעלת Cast אחרי הפעלת האפליקציה, מופיעה תיבת דו-שיח של הרשאות כדי שהמשתמש יוכל לתת לאפליקציה גישה לרשת המקומית למכשירים ברשת. לאחר מכן, כשהמשתמש ילחץ על הכפתור להפעלת Cast, תוצג תיבת דו-שיח של Cast עם רשימה של המכשירים שזוהו. כשהמשתמש לוחץ על הכפתור להפעלת Cast בזמן שהמכשיר מחובר, מוצגים המטא-נתונים הנוכחיים של המדיה (כמו שם, שם אולפן ההקלטות ותמונה ממוזערת) או שהמשתמש יכול להתנתק מהמכשיר להפעלת Cast. אם המשתמש לוחץ על הכפתור להפעלת Cast כשאין מכשירים זמינים, מוצג מסך עם מידע על הסיבות לכך שלא נמצאו מכשירים ועל הדרכים לפתור את הבעיה.

  • בקר מיני: כשהמשתמש מפעיל Cast של תוכן ועובר מדף התוכן הנוכחי או מהבקר המורחב למסך אחר באפליקציית השולח, בקר המיני מוצג בתחתית המסך כדי לאפשר למשתמש לראות את המטא-נתונים של המדיה שמופעלת כרגע ב-Cast ולשלוט בהפעלה.

  • בקר מורחב: כשהמשתמש מעביר תוכן, אם הוא לוחץ על ההתראה של המדיה או על הבקר המיני, הבקר המורחב מופעל. הוא מציג את המטא-נתונים של המדיה שמופעלת כרגע ומספק כמה לחצנים לשליטה בהפעלת המדיה.

הוספת הכפתור להפעלת Cast

ה-framework מספק רכיב של הכפתור להפעלת Cast כמחלקת משנה של UIButton. כדי להוסיף אותו לסרגל הכותרת של האפליקציה, צריך לעטוף אותו ב-UIBarButtonItem. במחלקה משנית אופיינית של UIViewController אפשר להתקין את הכפתור להפעלת Cast באופן הבא:

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];

כברירת מחדל, לחיצה על הכפתור תפתח את תיבת הדו-שיח של Cast שסופקה על ידי המסגרת.

אפשר גם להוסיף GCKUICastButton ישירות ללוח התסריט.

הגדרת גילוי המכשיר

במסגרת, גילוי המכשירים מתבצע באופן אוטומטי. אין צורך להפעיל או להפסיק באופן מפורש את תהליך הגילוי, אלא אם אתם מטמיעים ממשק משתמש בהתאמה אישית.

הגילוי במסגרת מנוהל על ידי המחלקה GCKDiscoveryManager, שהיא מאפיין של GCKCastContext. ה-framework מספק רכיב ברירת מחדל של תיבת דו-שיח של Cast לבחירת מכשיר ולשליטה בו. רשימת המכשירים מסודרת באופן לקסיקוגרפי לפי השם הידידותי של המכשיר.

איך פועל ניהול הסשנים

ב-Cast SDK יש קונספט שנקרא "סשן Cast". כשמגדירים סשן Cast, משלבים את השלבים של התחברות למכשיר, הפעלה של אפליקציית Web Receiver (או הצטרפות אליה), התחברות לאפליקציה הזו ואתחול של ערוץ ממשק השליטה במדיה. למידע נוסף על סשנים של Cast ועל מחזור החיים של Web Receiver, עיינו במדריך למחזור החיים של אפליקציית Web Receiver.

הסשנים מנוהלים על ידי המחלקה GCKSessionManager, שהיא מאפיין של GCKCastContext. סשנים ספציפיים מיוצגים על ידי מחלקות משנה של המחלקה GCKSession: לדוגמה, GCKCastSession מייצג סשנים עם מכשירי Cast. אתם יכולים לגשת לסשן ה-Cast הפעיל (אם יש כזה) בתור המאפיין currentCastSession של GCKSessionManager.

אפשר להשתמש בממשק GCKSessionManagerListener כדי לעקוב אחרי אירועים בסשן, כמו יצירה, השעיה, המשך וסיום של סשן. ה-framework משהה באופן אוטומטי סשנים כשאפליקציית השולח עוברת לרקע, ומנסה להמשיך אותם כשהאפליקציה חוזרת לחזית (או מופעלת מחדש אחרי סגירה חריגה או פתאומית של האפליקציה בזמן שהסשן היה פעיל).

אם משתמשים בתיבת הדו-שיח של Cast, הסשנים נוצרים ונסגרים באופן אוטומטי בתגובה לתנועות של המשתמש. אחרת, האפליקציה יכולה להתחיל ולסיים סשנים באופן מפורש באמצעות שיטות ב-GCKSessionManager.

אם האפליקציה צריכה לבצע עיבוד מיוחד בתגובה לאירועים במחזור החיים של הסשן, היא יכולה לרשום מופע אחד או יותר של GCKSessionManagerListener באמצעות GCKSessionManager. ‫GCKSessionManagerListener הוא פרוטוקול שמגדיר קריאות חוזרות (callbacks) לאירועים כמו התחלת סשן, סיום סשן וכו'.

החלפת רמקול

שמירת מצב הסשן היא הבסיס להחלפת רמקול, שבה המשתמשים יכולים להעביר שידורי אודיו ווידאו קיימים בין מכשירים באמצעות פקודות קוליות, אפליקציית Google Home או מסכים חכמים. הפעלת המדיה נפסקת במכשיר אחד (המקור) וממשיכה במכשיר אחר (היעד). כל מכשיר Cast עם הקושחה העדכנית ביותר יכול לשמש כמקור או כיעד בהחלפת רמקול.

כדי לקבל את מכשיר היעד החדש במהלך החלפת הרמקול, משתמשים במאפיין GCKCastSession#device במהלך הקריאה החוזרת (callback) של [sessionManager:didResumeCastSession:].

מידע נוסף זמין במאמר העברת סטרימינג ב-Web Receiver.

חיבור מחדש באופן אוטומטי

Cast framework מוסיף לוגיקה של חיבור מחדש כדי לטפל באופן אוטומטי בחיבור מחדש במקרים רבים של מקרי קצה, כמו:

  • התאוששות מניתוק זמני של ה-Wi-Fi
  • התאוששות ממצב שינה של המכשיר
  • שחזור מהעברת האפליקציה לרקע
  • שחזור אם האפליקציה קרסה

איך פועל ממשק השליטה במדיה

אם סשן Cast נוצר באמצעות אפליקציית Web Receiver שתומכת במרחב השמות של המדיה, המערכת תיצור באופן אוטומטי מופע של GCKRemoteMediaClient. אפשר לגשת אליו בתור המאפיין remoteMediaClient של המופע GCKCastSession.

כל השיטות ב-GCKRemoteMediaClient ששולחות בקשות ל-Web Receiver יחזירו אובייקט GCKRequest שאפשר להשתמש בו כדי לעקוב אחרי הבקשה. אפשר להקצות GCKRequestDelegate לאובייקט הזה כדי לקבל התראות על התוצאה הסופית של הפעולה.

יכול להיות שהמופע של GCKRemoteMediaClient ישותף על ידי כמה חלקים של האפליקציה, ואכן כמה רכיבים פנימיים של המסגרת, כמו תיבת הדו-שיח של Cast וממשק השליטה המצומצם במדיה, משתפים את המופע. לשם כך, 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 באמצעות ערכי enum‏ GCKVideoInfoHDRType.

הוספת פקדים קטנים

לפי רשימת המשימות לעיצוב Cast, אפליקציית השולח צריכה לספק פקד קבוע שנקרא מיני-בקר. הפקד הזה אמור להופיע כשהמשתמש עובר מדף התוכן הנוכחי. השלט הקטן מספק גישה מיידית ותזכורת גלויה לסשן ה-Cast הנוכחי.

מסגרת Cast מספקת סרגל בקרה, GCKUIMiniMediaControlsViewController, שאפשר להוסיף לסצנות שבהן רוצים להציג את השלט המיני.

כשאפליקציית השליחה מפעילה שידור חי של וידאו או אודיו, ה-SDK מציג באופן אוטומטי לחצן הפעלה/עצירה במקום לחצן ההפעלה/השהיה בבקר המיני.

במאמר Customize iOS Sender UI (התאמה אישית של ממשק המשתמש של השולח ב-iOS) מוסבר איך אפשר להגדיר את המראה של הווידג'טים של Cast באפליקציית השולח.

יש שתי דרכים להוסיף את הבקר המיני לאפליקציית השולח:

  • כדי לאפשר ל-Cast framework לנהל את הפריסה של הבקר המיני, צריך לעטוף את בקר התצוגה הקיים בבקר תצוגה משלו.
  • כדי לנהל בעצמכם את הפריסה של הווידג'ט של הבקר המיני, אתם יכולים להוסיף אותו לבקר התצוגה הקיים באמצעות תצוגת משנה ב-storyboard.

עטיפה באמצעות GCKUICastContainerViewController

הדרך הראשונה היא להשתמש ב-GCKUICastContainerViewController, שעוטף בקר תצוגה אחר ומוסיף GCKUIMiniMediaControlsViewController בתחתית. הגישה הזו מוגבלת כי אי אפשר להתאים אישית את האנימציה ואי אפשר להגדיר את ההתנהגות של בקר התצוגה של מאגר התגים.

בדרך כלל, הדרך הראשונה מתבצעת ב-method‏ -[application:didFinishLaunchingWithOptions:] של ה-app delegate:

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

הטמעה ב-view controller קיים

הדרך השנייה היא להוסיף את הבקר המיני ישירות לבקר התצוגה הקיים באמצעות createMiniMediaControlsViewController כדי ליצור מופע של GCKUIMiniMediaControlsViewController, ואז להוסיף אותו לבקר התצוגה של מאגר התגים כתצוגת משנה.

מגדירים את ה-view controller ב-app delegate:

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;
}

ב-root view controller, יוצרים מופע של GCKUIMiniMediaControlsViewController ומוסיפים אותו ל-container view controller בתור subview:

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.

הדבר הראשון שצריך לעשות הוא להפעיל את הבקר המורחב שמוגדר כברירת מחדל בהקשר של Cast. משנים את ה-app delegate כדי להפעיל את בקר ההרחבה שמוגדר כברירת מחדל:

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

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

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

מוסיפים את הקוד הבא ל-view controller כדי לטעון את ה-expanded controller כשהמשתמש מתחיל להפעיל Cast של סרטון:

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 framework מנהל באופן אוטומטי את עוצמת הקול של אפליקציית השולח. ה-framework מסתנכרן באופן אוטומטי עם עוצמת הקול של Web Receiver עבור הווידג'טים של ממשק המשתמש שסופקו. כדי לסנכרן פס הזזה שמופיע באפליקציה, משתמשים ב-GCKUIDeviceVolumeController.

שליטה בעוצמת הקול באמצעות כפתור פיזי

אפשר להשתמש בכפתורי עוצמת הקול הפיזיים במכשיר השולח כדי לשנות את עוצמת הקול של סשן ה-Cast במקלט האינטרנט באמצעות הדגל 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];

טיפול בשגיאות

חשוב מאוד שאפליקציות השולח יטפלו בכל הקריאות החוזרות (callbacks) של השגיאות ויחליטו מהי התגובה הטובה ביותר לכל שלב במחזור החיים של Cast. האפליקציה יכולה להציג למשתמש תיבות דו-שיח של שגיאות או לסיים את הסשן של Cast.

חשוב לדעת שחלק מהשגיאות, כולל GCKErrorCode GCKErrorCodeCancelled, הן התנהגות מכוונת.

אל תנסו להתחבר מחדש אם החיבור נכשל עם GCKErrorCodeCancelled. אם תעשו את זה, יכול להיות שתיתקלו בבעיות לא צפויות.

רישום ביומן

GCKLogger הוא סינגלטון שמשמש לרישום ביומן על ידי ה-framework. כדי להתאים אישית את אופן הטיפול בהודעות ביומן, משתמשים ב-GCKLoggerDelegate.

באמצעות GCKLogger, ה-SDK מפיק פלט של רישום ביומן בצורת הודעות ניפוי באגים, שגיאות ואזהרות. הודעות היומן האלה עוזרות לנפות באגים, לפתור בעיות ולזהות אותן. כברירת מחדל, פלט היומן מוסתר, אבל אם מקצים 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

כדי להפעיל גם הודעות ניפוי באגים והודעות מפורטות, מוסיפים את השורה הזו לקוד אחרי הגדרת ה-delegate (כפי שמוצג למעלה):

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;

שמות המחלקות יכולים להיות שמות מילוליים או תבניות glob, לדוגמה, GCKUI\* ו-GCK\*Session.