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

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

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

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

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

תהליך השימוש באפליקציה

השלבים הבאים מתארים את תהליך הביצוע האופייני ברמה גבוהה באפליקציית iOS ששולחת הודעות:

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

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

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

קריאה לשיטות מהשרשור הראשי

איפוס ההקשר של Cast

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

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

השיטה -[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 של Cast ל-iOS מספקת את הווידג'טים הבאים, שתואמים לרשימת המשימות של עיצוב Cast:

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

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

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

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

הוספת לחצן Cast

המסגרת מספקת רכיב של לחצן העברה (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. המסגרת מספקת רכיב ברירת מחדל של תיבת דו-שיח להעברה (cast) לבחירה ולשליטה במכשיר. רשימת המכשירים מסודרת לפי סדר לקסיקלי לפי השם הידידותי של המכשיר.

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

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

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

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

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

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

העברת סטרימינג

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

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

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

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

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

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

איך פועלים פקדי המדיה

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

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

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

במאמר התאמה אישית של ממשק המשתמש של השולח ב-iOS מוסבר איך אפליקציית השולח יכולה להגדיר את המראה של ווידג'טים של העברה (cast).

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

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

גיבוב באמצעות GCKUICastContainerViewController

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

הדרך הראשונה הזו מתבצעת בדרך כלל בשיטה -[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

הטמעה ב-View Controller קיים

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

מגדירים את ה-view controller במתווך האפליקציה:

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 בתור תצוגת משנה:

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

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

הפונקציונליות של התצוגה הזו מיושמת על ידי הכיתה GCKUIExpandedMediaControlsViewController.

קודם כול, צריך להפעיל את הבקר המורחב שמוגדר כברירת מחדל בהקשר ההעברה (cast). משנים את האפליקציה הנציגה כדי להפעיל את הבקר המורחב שמוגדר כברירת מחדל:

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

רישום ביומן

GCKLogger הוא אובייקט יחיד (singleton) שמשמש את המסגרת לרישום ביומן. אפשר להשתמש ב-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

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

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.