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

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

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

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

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

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

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

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

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

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

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

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

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

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

בעזרת השיטה -[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)
    }
  }
}
יעד ג'

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

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

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

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

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

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

הוספת לחצן הפעלת Cast

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

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

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

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

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

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

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

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

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

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

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

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

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

העברת השידור

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

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

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

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

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

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

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

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

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

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

הוספת מיני שלטים

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

ה-Cast framework מספק סרגל בקרה, 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;
}

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

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

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

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

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

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
יעד ג'
- (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)
}
יעד ג'
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

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

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

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

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

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

ה-framework מנהל באופן אוטומטי את עוצמת הקול באפליקציית השולח. ה-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];

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

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

רישום ביומן

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;

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