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

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

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

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

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

זרימת אפליקציה

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

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

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

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

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

אתחול ההקשר של הפעלת Cast

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

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

כדאי גם להגדיר משתמש עם הרשאה לרישום ביומן לקבל את הודעות הרישום ביומן מה-framework, באמצעות ה-method -[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)
    }
  }
}
יעד ג'

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

  • הלחצן להפעלת Cast: החל מגרסה 4.6.0 של להפעלת Cast iOS, לחצן ההעברה תמיד גלוי כשהמכשיר של השולח מחובר ל-Wi-Fi. בפעם הראשונה שהמשתמש מקיש על הלחצן להפעלת 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)
יעד ג'
GCKUICastButton *castButton = [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
castButton.tintColor = [UIColor grayColor];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:castButton];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

אפשר לצפות שהמכונה של GCKRemoteMediaClient תשותף על ידי מספר חלקים של האפליקציה, ואכן חלק מהרכיבים הפנימיים של ה-framework, כמו תיבת הדו-שיח של Cast ופקדי מיני מדיה, כן משתפים את המכונה. לשם כך, GCKRemoteMediaClient תומך ברישום של מספר נכסי GCKRemoteMediaClientListener.

הגדרת מטא-נתונים של מדיה

המחלקה GCKMediaMetadata מייצגת מידע על פריט מדיה שרוצים להפעיל Cast. הדוגמה הבאה יוצרת מופע 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))
יעד ג'
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
}
יעד ג'
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 באמצעות ערכי enum GCKVideoInfoHDRType.

הוספת מיני-בקרים

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

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

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

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

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

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

גלישה באמצעות ה-GCKUICastContainerViewController

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

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

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

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

  ...
}
יעד ג'
- (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
    }
  }
}
יעד ג'

AppDelegate.h

@interface AppDelegate : UIResponder <UIApplicationDelegate>

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

@end

AppDelegate.m

@implementation AppDelegate

...

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

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

...

@end

הטמעה בבקר תצוגה קיים

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

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

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

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

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

  ...

  return true
}
יעד ג'
- (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 ומוסיפים אותה לבקר של תצוגת הקונטיינר כתצוגת משנה:

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

...
יעד ג'

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()
  }
יעד ג'
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}

הוספה של בקר מורחב

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

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

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

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

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
יעד ג'
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}

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

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

בקרת עוצמת הקול

ה-Cast מנהל באופן אוטומטי את עוצמת הקול של אפליקציית השולח. ה-framework מסתנכרן אוטומטית עם עוצמת הקול של Web Acceptr של הווידג'טים של ממשק המשתמש. כדי לסנכרן פס הזזה שסופק על ידי האפליקציה, משתמשים ב-GCKUIDeviceVolumeController.

בקרת עוצמת הקול של הלחצן הפיזי

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

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

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

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

רישום ביומן

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)
    }
  }
}
יעד ג'

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
יעד ג'
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
יעד ג'
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;

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