במדריך הזה למפתחים מוסבר איך להוסיף תמיכה ב-Google Cast לאפליקציית השולח ל-Android באמצעות Android Sender SDK.
המכשיר הנייד או המחשב הנייד הוא השולח ששולט בהפעלה, ומכשיר Google Cast הוא המקלט שמציג את התוכן בטלוויזיה.
המונח sender framework מתייחס לקובץ הבינארי של ספריית המחלקות של Cast ולמשאבים המשויכים שקיימים בזמן הריצה בשולח. אפליקציית השולח או אפליקציית Cast מתייחסות לאפליקציה שפועלת גם במכשיר השולח. אפליקציית Web Receiver מתייחסת לאפליקציית ה-HTML שפועלת במכשיר שתומך ב-Cast.
מסגרת השולח משתמשת בעיצוב של קריאה חוזרת אסינכרונית כדי להודיע לאפליקציית השולח על אירועים ולעבור בין מצבים שונים במחזור החיים של אפליקציית Cast.
מסלול המשתמש באפליקציה
בשלבים הבאים מתואר תהליך הביצוע הטיפוסי ברמה גבוהה באפליקציית שולח באנדרואיד:
- ה-framework של Cast מתחיל אוטומטית את תהליך גילוי המכשירים על סמך מחזור החיים של
MediaRouterActivity. - כשהמשתמש לוחץ על הכפתור להפעלת Cast, המסגרת מציגה את תיבת הדו-שיח Cast עם רשימת מכשירי Cast שהתגלו.
- כשהמשתמש בוחר מכשיר Cast, המסגרת מנסה להפעיל את אפליקציית Web Receiver במכשיר Cast.
- המסגרת מפעילה קריאות חוזרות באפליקציית השולח כדי לאשר שהאפליקציה WebReceiver הופעלה.
- המסגרת יוצרת ערוץ תקשורת בין השולח לבין אפליקציות WebReceiver.
- המסגרת משתמשת בערוץ התקשורת כדי לטעון ולשלוט בהפעלת מדיה ב-Web Receiver.
- המסגרת מסנכרנת את מצב הפעלת המדיה בין השולח לבין Web Receiver: כשהמשתמש מבצע פעולות בממשק המשתמש של השולח, המסגרת מעבירה את בקשות השליטה במדיה אל Web Receiver, וכש-Web Receiver שולח עדכונים לגבי סטטוס המדיה, המסגרת מעדכנת את המצב של ממשק המשתמש של השולח.
- כשהמשתמש לוחץ על הכפתור להפעלת Cast כדי להתנתק ממכשיר Cast, המסגרת מנתקת את אפליקציית השולח מ-Web Receiver.
רשימה מקיפה של כל המחלקות, השיטות והאירועים ב-Google Cast Android SDK זמינה במאמר Google Cast Sender API Reference for Android. בקטעים הבאים מוסבר איך להוסיף Cast לאפליקציית Android.
הגדרת קובץ המניפסט של Android
בקובץ AndroidManifest.xml של האפליקציה, צריך להגדיר את הרכיבים הבאים עבור Cast SDK:
uses-sdk
מגדירים את רמות ה-API המינימליות והמטרה של Android שנתמכות על ידי Cast SDK. נכון לעכשיו, רמת ה-API המינימלית היא 24 ורמת ה-API לטירגוט היא 35.
<uses-sdk
android:minSdkVersion="24"
android:targetSdkVersion="35" />
android:theme
מגדירים את ערכת העיצוב של האפליקציה על סמך גרסת Android SDK המינימלית. לדוגמה, אם אתם לא מטמיעים נושא משלכם, אתם צריכים להשתמש בווריאציה של Theme.AppCompat כשמטרגטים גרסה מינימלית של Android SDK שהיא לפני Lollipop.
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" >
...
</application>
הפעלת Cast Context
למסגרת יש אובייקט סינגלטון גלובלי, CastContext, שמתאם את כל האינטראקציות של המסגרת.
האפליקציה צריכה להטמיע את הממשק
OptionsProvider
כדי לספק את האפשרויות שנדרשות לאתחול של
CastContext
singleton. OptionsProvider מספק מופע של CastOptions שמכיל אפשרויות שמשפיעות על ההתנהגות של המסגרת. הכי חשוב מביניהם הוא מזהה אפליקציית Web Receiver, שמשמש לסינון תוצאות החיפוש ולהפעלת אפליקציית Web Receiver כשמתחיל סשן Cast.
class CastOptionsProvider : OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .build() } override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? { return null } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { CastOptions castOptions = new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .build(); return castOptions; } @Override public List<SessionProvider> getAdditionalSessionProviders(Context context) { return null; } }
צריך להצהיר על השם המוגדר במלואו של OptionsProvider שהוטמע כשדה מטא-נתונים בקובץ AndroidManifest.xml של אפליקציית השולח:
<application>
...
<meta-data
android:name=
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.foo.CastOptionsProvider" />
</application>
מתבצע אתחול עצל של CastContext כשקוראים ל-CastContext.getSharedInstance().
class MyActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val castContext = CastContext.getSharedInstance(this) } }
public class MyActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { CastContext castContext = CastContext.getSharedInstance(this); } }
ווידג'טים של חוויית המשתמש ב-Cast
Cast Framework מספק את הווידג'טים שתואמים לרשימת הבדיקה של עיצוב Cast:
שכבת-על של מבוא: המסגרת מספקת תצוגה מותאמת אישית,
IntroductoryOverlay, שמוצגת למשתמש כדי למשוך את תשומת הלב לכפתור להפעלת Cast בפעם הראשונה שזמין מקלט. באפליקציית השולח אפשר להתאים אישית את הטקסט ואת המיקום של טקסט הכותרת.הכפתור להפעלת Cast: הכפתור להפעלת Cast מוצג בלי קשר לזמינות של מכשירי Cast. כשמשתמש לוחץ על הכפתור להפעלת Cast בפעם הראשונה, מוצגת תיבת דו-שיח של Cast עם רשימה של המכשירים שזוהו. כשהמשתמש לוחץ על הכפתור להפעלת Cast בזמן שהמכשיר מחובר, מוצגים המטא-נתונים הנוכחיים של המדיה (כמו כותרת, שם אולפן ההקלטות ותמונה ממוזערת) או שהמשתמש יכול להתנתק ממכשיר Cast. לפעמים קוראים ל'כפתור להפעלת Cast' גם 'סמל Cast'.
השלט המיני: כשמשתמש מפעיל Cast לתוכן ועובר מדף התוכן הנוכחי או מהשלט המורחב למסך אחר באפליקציית השולח, השלט המיני מוצג בתחתית המסך כדי לאפשר למשתמש לראות את מטא-נתוני המדיה שמופעלת ב-Cast ולשלוט בהפעלה.
מרכז הבקרה המורחב: כשהמשתמש מפעיל Cast של תוכן, אם הוא לוחץ על ההתראה על המדיה או על מרכז הבקרה המצומצם, מרכז הבקרה המורחב מופעל. במרכז הבקרה המורחב מוצגים המטא-נתונים של המדיה שמופעלת כרגע, ויש בו כמה לחצנים לשליטה בהפעלת המדיה.
התראה: Android בלבד. כשהמשתמש מפעיל Cast לתוכן ועובר מאפליקציית השולח, מוצגת התראה על מדיה שכוללת את המטא-נתונים של המדיה שמופעלת ב-Cast ואת אמצעי הבקרה להפעלה.
מסך הנעילה: ל-Android בלבד. כשהמשתמש מפעיל Cast לתוכן ועובר (או שהמכשיר מגיע למצב פסק זמן) למסך הנעילה, מוצג אמצעי בקרה של מדיה במסך הנעילה, שבו מוצגים מטא-נתונים של המדיה שמופעלת ב-Cast ואמצעי בקרה להפעלה.
במדריך הבא מוסבר איך להוסיף את הווידג'טים האלה לאפליקציה.
הוספת לחצן Cast
ממשקי ה-API של Android
MediaRouter
נועדו לאפשר הצגה והפעלה של מדיה במכשירים משניים.
אפליקציות ל-Android שמשתמשות ב-MediaRouter API צריכות לכלול את הכפתור להפעלת Cast כחלק מממשק המשתמש שלהן, כדי לאפשר למשתמשים לבחור נתיב מדיה להפעלת מדיה במכשיר משני, כמו מכשיר Cast.
המסגרת מאפשרת להוסיף MediaRouteButton כCast button בקלות רבה. קודם צריך להוסיף פריט לתפריט או MediaRouteButton בקובץ ה-XML שמגדיר את התפריט, ואז להשתמש ב-CastButtonFactory כדי לקשר אותו למסגרת.
// To add a Cast button, add the following snippet.
// menu.xml
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
// Then override the onCreateOptionMenu() for each of your activities. // MyActivity.kt override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.main, menu) CastButtonFactory.setUpMediaRouteButton( applicationContext, menu, R.id.media_route_menu_item ) return true }
// Then override the onCreateOptionMenu() for each of your activities. // MyActivity.java @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.main, menu); CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, R.id.media_route_menu_item); return true; }
אם Activity יורש מ-FragmentActivity, אפשר להוסיף MediaRouteButton לפריסה.
// activity_layout.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal" >
<androidx.mediarouter.app.MediaRouteButton
android:id="@+id/media_route_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:mediaRouteTypes="user"
android:visibility="gone" />
</LinearLayout>
// MyActivity.kt override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_layout) mMediaRouteButton = findViewById<View>(R.id.media_route_button) as MediaRouteButton CastButtonFactory.setUpMediaRouteButton(applicationContext, mMediaRouteButton) mCastContext = CastContext.getSharedInstance(this) }
// MyActivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_layout); mMediaRouteButton = (MediaRouteButton) findViewById(R.id.media_route_button); CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), mMediaRouteButton); mCastContext = CastContext.getSharedInstance(this); }
כדי להגדיר את המראה של הכפתור להפעלת Cast באמצעות עיצוב, אפשר לעיין במאמר בנושא התאמה אישית של הכפתור להפעלת Cast.
הגדרה של גילוי מכשירים
גילוי המכשירים מנוהל באופן מלא על ידי CastContext.
כשמפעילים את CastContext, אפליקציית השולח מציינת את מזהה אפליקציית Web Receiver, ויכולה גם לבקש סינון של מרחבי שמות על ידי הגדרת supportedNamespaces ב-CastOptions.
CastContext מחזיק הפניה ל-MediaRouter באופן פנימי, ויתחיל את תהליך הגילוי בתנאים הבאים:
- על סמך אלגוריתם שנועד לאזן בין זמן האחזור של גילוי המכשיר לבין השימוש בסוללה, הגילוי יתחיל מדי פעם באופן אוטומטי כשאפליקציית השולח תעבור לחזית.
- תיבת הדו-שיח של Cast פתוחה.
- Cast SDK מנסה לשחזר סשן Cast.
תהליך החיפוש ייפסק כשתיבת הדו-שיח של Cast תיסגר או כשאפליקציית השולח תעבור לרקע.
class CastOptionsProvider : OptionsProvider { companion object { const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace" } override fun getCastOptions(appContext: Context): CastOptions { val supportedNamespaces: MutableList<String> = ArrayList() supportedNamespaces.add(CUSTOM_NAMESPACE) return CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setSupportedNamespaces(supportedNamespaces) .build() } override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? { return null } }
class CastOptionsProvider implements OptionsProvider { public static final String CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"; @Override public CastOptions getCastOptions(Context appContext) { List<String> supportedNamespaces = new ArrayList<>(); supportedNamespaces.add(CUSTOM_NAMESPACE); CastOptions castOptions = new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setSupportedNamespaces(supportedNamespaces) .build(); return castOptions; } @Override public List<SessionProvider> getAdditionalSessionProviders(Context context) { return null; } }
איך פועל ניהול הסשנים
ב-Cast SDK מוצג המושג של סשן Cast, שההגדרה שלו משלבת את השלבים של חיבור למכשיר, הפעלה (או הצטרפות) של אפליקציית Web Receiver, חיבור לאפליקציה הזו ואתחול של ערוץ בקרת מדיה. מידע נוסף על סשנים של Cast ומחזור החיים של Web Receiver זמין במדריך בנושא מחזור החיים של אפליקציית Web Receiver.
הסשנים מנוהלים על ידי הכיתה SessionManager, שהאפליקציה יכולה לגשת אליה דרך CastContext.getSessionManager().
סשנים בודדים מיוצגים על ידי מחלקות משנה של המחלקה Session.
לדוגמה,
CastSession
מייצג סשנים עם מכשירי Cast. האפליקציה יכולה לגשת לסשן Cast הפעיל הנוכחי באמצעות SessionManager.getCurrentCastSession().
האפליקציה יכולה להשתמש במחלקה SessionManagerListener כדי לעקוב אחרי אירועים של סשנים, כמו יצירה, השהיה, חידוש וסיום. המסגרת מנסה באופן אוטומטי להמשיך מנקודה שבה הסשן הסתיים באופן לא תקין או פתאומי בזמן שהסשן היה פעיל.
הפעלות נוצרות ומופסקות אוטומטית בתגובה לתנועות של משתמשים בתיבות הדו-שיח של MediaRouter.
כדי להבין טוב יותר את השגיאות בהפעלת Cast, אפליקציות יכולות להשתמש ב-CastContext#getCastReasonCodeForCastStatusCode(int) כדי להמיר את השגיאה בהפעלת הסשן ל-CastReasonCodes.
חשוב לזכור שחלק מהשגיאות שקשורות להתחלת סשן (למשל, CastReasonCodes#CAST_CANCELLED) הן התנהגות מכוונת ולא צריך לרשום אותן ביומן כשגיאה.
אם אתם רוצים לדעת על שינויים במצב של הסשן, אתם יכולים להטמיע SessionManagerListener. בדוגמה הזו מתבצעת האזנה לזמינות של CastSession ב-Activity.
class MyActivity : Activity() { private var mCastSession: CastSession? = null private lateinit var mCastContext: CastContext private lateinit var mSessionManager: SessionManager private val mSessionManagerListener: SessionManagerListener<CastSession> = SessionManagerListenerImpl() private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> { override fun onSessionStarting(session: CastSession?) {} override fun onSessionStarted(session: CastSession?, sessionId: String) { invalidateOptionsMenu() } override fun onSessionStartFailed(session: CastSession?, error: Int) { val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error) // Handle error } override fun onSessionSuspended(session: CastSession?, reason Int) {} override fun onSessionResuming(session: CastSession?, sessionId: String) {} override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) { invalidateOptionsMenu() } override fun onSessionResumeFailed(session: CastSession?, error: Int) {} override fun onSessionEnding(session: CastSession?) {} override fun onSessionEnded(session: CastSession?, error: Int) { finish() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mCastContext = CastContext.getSharedInstance(this) mSessionManager = mCastContext.sessionManager mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java) } override fun onResume() { super.onResume() mCastSession = mSessionManager.currentCastSession } override fun onDestroy() { super.onDestroy() mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java) } }
public class MyActivity extends Activity { private CastContext mCastContext; private CastSession mCastSession; private SessionManager mSessionManager; private SessionManagerListener<CastSession> mSessionManagerListener = new SessionManagerListenerImpl(); private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> { @Override public void onSessionStarting(CastSession session) {} @Override public void onSessionStarted(CastSession session, String sessionId) { invalidateOptionsMenu(); } @Override public void onSessionStartFailed(CastSession session, int error) { int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error); // Handle error } @Override public void onSessionSuspended(CastSession session, int reason) {} @Override public void onSessionResuming(CastSession session, String sessionId) {} @Override public void onSessionResumed(CastSession session, boolean wasSuspended) { invalidateOptionsMenu(); } @Override public void onSessionResumeFailed(CastSession session, int error) {} @Override public void onSessionEnding(CastSession session) {} @Override public void onSessionEnded(CastSession session, int error) { finish(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCastContext = CastContext.getSharedInstance(this); mSessionManager = mCastContext.getSessionManager(); mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class); } @Override protected void onResume() { super.onResume(); mCastSession = mSessionManager.getCurrentCastSession(); } @Override protected void onDestroy() { super.onDestroy(); mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class); } }
העברת שידור
שמירת מצב הסשן היא הבסיס להעברת סטרימינג, שבה משתמשים יכולים להעביר סטרימינג קיים של אודיו ווידאו בין מכשירים באמצעות פקודות קוליות, אפליקציית Google Home או מסכים חכמים. הפעלת המדיה נעצרת במכשיר אחד (המקור) וממשיכה במכשיר אחר (היעד). כל מכשיר Cast עם הקושחה העדכנית יכול לשמש כמקור או כיעד בהעברת סטרימינג.
כדי לקבל את מכשיר היעד החדש במהלך העברה או הרחבה של שידור, צריך לרשום Cast.Listener באמצעות CastSession#addCastListener.
אחר כך מתקשרים אל
CastSession#getCastDevice()
במהלך הקריאה החוזרת של onDeviceNameChanged.
מידע נוסף זמין במאמר בנושא העברת סטרימינג ב-Web Receiver.
חיבור מחדש אוטומטי
המסגרת מספקת ReconnectionService שאפשר להפעיל באפליקציית השולח כדי לטפל בחיבור מחדש במקרים רבים ומורכבים, כמו:
- שחזור אחרי אובדן זמני של חיבור ה-Wi-Fi
- שחזור ממצב שינה של המכשיר
- שחזור אחרי העברת האפליקציה לרקע
- שחזור אם האפליקציה קרסה
השירות הזה מופעל כברירת מחדל, ואפשר להשבית אותו ב-CastOptions.Builder.
אפשר למזג את השירות הזה אוטומטית עם המניפסט של האפליקציה אם מפעילים מיזוג אוטומטי בקובץ gradle.
המסגרת תפעיל את השירות כשיש סשן מדיה, ותפסיק אותו כשהסשן יסתיים.
איך פועל ממשק השליטה במדיה
הוצאה משימוש של המחלקה RemoteMediaPlayer מ-Cast 2.x לטובת מחלקה חדשה RemoteMediaClient, שמספקת את אותה פונקציונליות באמצעות קבוצה של ממשקי API נוחים יותר, ומונעת את הצורך להעביר GoogleApiClient.
כשבאפליקציה נוצר CastSession עם אפליקציית Web Receiver שתומכת במרחב השמות של המדיה, המערכת יוצרת באופן אוטומטי מופע של RemoteMediaClient. האפליקציה יכולה לגשת אליו באמצעות קריאה לשיטה getRemoteMediaClient() במופע CastSession.
כל השיטות של RemoteMediaClient ששולחות בקשות ל-Web Receiver יחזירו אובייקט PendingResult שאפשר להשתמש בו כדי לעקוב אחרי הבקשה.
צפוי שמופע של RemoteMediaClient ישותף על ידי כמה חלקים באפליקציה, ואכן כמה רכיבים פנימיים של המסגרת, כמו בקרי המיני המתמידים ושירות ההתראות.
לכן, המופע הזה תומך ברישום של כמה מופעים של RemoteMediaClient.Listener.
הגדרת מטא-נתונים של מדיה
המחלקות MediaMetadata מייצגות את המידע על פריט המדיה שרוצים להפעיל ב-Cast. בדוגמה הבאה נוצר מופע חדש של MediaMetadata של סרט, ומוגדרים הכותרת, הכתובית ושתי תמונות.
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle()) movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio()) movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0)))) movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle()); movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio()); movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0)))); movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));
מידע נוסף על שימוש בתמונות עם מטא-נתונים של מדיה זמין במאמר בנושא בחירת תמונות.
טעינת מדיה
האפליקציה יכולה לטעון פריט מדיה, כמו שמוצג בקוד הבא. קודם משתמשים ב-MediaInfo.Builder עם המטא-נתונים של המדיה כדי ליצור מופע של MediaInfo. קבלת RemoteMediaClient מה-CastSession הנוכחי, ואז טוענים את MediaInfo לתוך RemoteMediaClient. משתמשים ב-RemoteMediaClient כדי להפעיל, להשהות ולשלוט באפליקציית נגן מדיה שפועלת ב-Web Receiver.
val mediaInfo = MediaInfo.Builder(mSelectedMedia.getUrl()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType("videos/mp4") .setMetadata(movieMetadata) .setStreamDuration(mSelectedMedia.getDuration() * 1000) .build() val remoteMediaClient = mCastSession.getRemoteMediaClient() remoteMediaClient.load(MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build())
MediaInfo mediaInfo = new MediaInfo.Builder(mSelectedMedia.getUrl()) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType("videos/mp4") .setMetadata(movieMetadata) .setStreamDuration(mSelectedMedia.getDuration() * 1000) .build(); RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient(); remoteMediaClient.load(new MediaLoadRequestData.Builder().setMediaInfo(mediaInfo).build());
אפשר לעיין גם בקטע בנושא שימוש בטראקים של מדיה.
פורמט וידאו באיכות 4K
כדי לבדוק באיזה פורמט וידאו המדיה שלכם, משתמשים ב-getVideoInfo() ב-MediaStatus כדי לקבל את המופע הנוכחי של VideoInfo.
המופע הזה מכיל את סוג הפורמט של הטלוויזיה עם HDR, ואת הגובה והרוחב של המסך בפיקסלים. וריאציות של פורמט 4K מסומנות בקבועים HDR_TYPE_*.
שליטה מרחוק בהתראות בכמה מכשירים
כשמשתמש מפעיל Cast, מכשירי Android אחרים באותה רשת יקבלו הודעה שתאפשר להם לשלוט בהפעלה. כל מי שהמכשיר שלו מקבל התראות כאלה יכול להשבית אותן במכשיר דרך אפליקציית ההגדרות > Google > Google Cast > הצגת התראות של שלט רחוק. (ההתראות כוללות קיצור דרך לאפליקציית ההגדרות). פרטים נוספים מופיעים במאמר בנושא התראות של שלט Cast.
הוספת בקר קטן
לפי רשימת המשימות לעיצוב Cast, אפליקציית השולט צריכה לספק אמצעי בקרה קבוע שנקרא בקר קטן. הוא צריך להופיע כשהמשתמש עובר מדף התוכן הנוכחי לחלק אחר באפליקציית השולט. הבקר הקטן מזכיר למשתמש את סשן Cast הנוכחי. הקשה על בקר המיני מאפשרת למשתמש לחזור לתצוגת הבקר המורחבת במסך מלא של Cast.
ה-framework מספק תצוגה מותאמת אישית, MiniControllerFragment, שאפשר להוסיף אותה לתחתית של קובץ הפריסה של כל פעילות שבה רוצים להציג את בקר המיני.
<fragment
android:id="@+id/castMiniController"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"
class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />
כשמפעילים בשידור חי סרטון או אודיו באפליקציית השולח, ה-SDK מציג באופן אוטומטי לחצן הפעלה/עצירה במקום לחצן ההפעלה/השהיה במיני-בקר.
כדי להגדיר את מראה הטקסט של שם התצוגה המותאמת אישית והכתוביות שלה, ולבחור לחצנים, אפשר לעיין במאמר בנושא התאמה אישית של בקר מיני.
הוספת בקר מורחב
במסגרת רשימת המשימות לבדיקת עיצוב של Google Cast, אפליקציית השולח צריכה לספק בקר מורחב למדיה שמועברת באמצעות Cast. הבקר המורחב הוא גרסה במסך מלא של הבקר המיני.
ערכת Cast SDK מספקת ווידג'ט לבקר המורחב שנקרא ExpandedControllerActivity.
זוהי מחלקה מופשטת שצריך ליצור ממנה מחלקת משנה כדי להוסיף את הכפתור להפעלת Cast.
קודם יוצרים קובץ משאבי תפריט חדש עבור בקר ההפעלה המורחב כדי לספק את הכפתור להפעלת Cast:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always"/>
</menu>
יוצרים מחלקה חדשה שמרחיבה את ExpandedControllerActivity.
class ExpandedControlsActivity : ExpandedControllerActivity() { override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.expanded_controller, menu) CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item) return true } }
public class ExpandedControlsActivity extends ExpandedControllerActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.expanded_controller, menu); CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); return true; } }
עכשיו צריך להצהיר על ה-Activity החדש בקובץ מניפסט של אפליקציה בתוך התג application:
<application>
...
<activity
android:name=".expandedcontrols.ExpandedControlsActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.CastVideosDark"
android:screenOrientation="portrait"
android:parentActivityName="com.google.sample.cast.refplayer.VideoBrowserActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
...
</application>
עורכים את CastOptionsProvider ומשנים את NotificationOptions ואת CastMediaOptions כדי להגדיר את יעד הפעילות לפעילות החדשה:
override fun getCastOptions(context: Context): CastOptions? { val notificationOptions = NotificationOptions.Builder() .setTargetActivityClassName(ExpandedControlsActivity::class.java.name) .build() val mediaOptions = CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name) .build() return CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build() }
public CastOptions getCastOptions(Context context) { NotificationOptions notificationOptions = new NotificationOptions.Builder() .setTargetActivityClassName(ExpandedControlsActivity.class.getName()) .build(); CastMediaOptions mediaOptions = new CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .setExpandedControllerActivityClassName(ExpandedControlsActivity.class.getName()) .build(); return new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build(); }
מעדכנים את ה-method LocalPlayerActivity loadRemoteMedia כדי להציג את הפעילות החדשה כשמדיה מרוחקת נטענת:
private fun loadRemoteMedia(position: Int, autoPlay: Boolean) { val remoteMediaClient = mCastSession?.remoteMediaClient ?: return remoteMediaClient.registerCallback(object : RemoteMediaClient.Callback() { override fun onStatusUpdated() { val intent = Intent(this@LocalPlayerActivity, ExpandedControlsActivity::class.java) startActivity(intent) remoteMediaClient.unregisterCallback(this) } }) remoteMediaClient.load( MediaLoadRequestData.Builder() .setMediaInfo(mSelectedMedia) .setAutoplay(autoPlay) .setCurrentTime(position.toLong()).build() ) }
private void loadRemoteMedia(int position, boolean autoPlay) { if (mCastSession == null) { return; } final RemoteMediaClient remoteMediaClient = mCastSession.getRemoteMediaClient(); if (remoteMediaClient == null) { return; } remoteMediaClient.registerCallback(new RemoteMediaClient.Callback() { @Override public void onStatusUpdated() { Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class); startActivity(intent); remoteMediaClient.unregisterCallback(this); } }); remoteMediaClient.load(new MediaLoadRequestData.Builder() .setMediaInfo(mSelectedMedia) .setAutoplay(autoPlay) .setCurrentTime(position).build()); }
כשמפעילים בשידור חי סרטון או אודיו באפליקציית השולח, ה-SDK מציג באופן אוטומטי לחצן הפעלה/עצירה במקום לחצן ההפעלה/ההשהיה בבקר המורחב.
כדי להגדיר את המראה באמצעות עיצובים, לבחור אילו לחצנים יוצגו ולהוסיף לחצנים בהתאמה אישית, אפשר לעיין במאמר בנושא התאמה אישית של אמצעי הבקרה המורחב.
בקרת עוצמת הקול
המסגרת מנהלת אוטומטית את עוצמת הקול באפליקציית השולח. המסגרת מסנכרנת אוטומטית את אפליקציות השולח ואת Web Receiver, כך שממשק המשתמש של השולח תמיד יציג את עוצמת הקול שצוינה ב-Web Receiver.
שליטה בעוצמת הקול באמצעות כפתור פיזי
ב-Android, אפשר להשתמש בלחצנים הפיזיים במכשיר השולח כדי לשנות את עוצמת הקול של סשן Cast ב-Web Receiver כברירת מחדל בכל מכשיר עם Jelly Bean או גרסה חדשה יותר.
שליטה בעוצמת הקול באמצעות לחצן פיזי בגרסאות שלפני Jelly Bean
כדי להשתמש במקשי הווליום הפיזיים לשליטה בווליום של מכשיר Web Receiver במכשירי Android מדגם ישן יותר מ-Jelly Bean, אפליקציית השולח צריכה לבטל את dispatchKeyEvent בפעילויות שלה, ולקרוא ל-CastContext.onDispatchVolumeKeyEventBeforeJellyBean():
class MyActivity : FragmentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { return (CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event)) } }
class MyActivity extends FragmentActivity { @Override public boolean dispatchKeyEvent(KeyEvent event) { return CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event); } }
הוספת אמצעי בקרה למדיה להתראות ולמסך הנעילה
ב-Android בלבד, ברשימת התיוג של Google Cast נדרש מאפליקציית השולח להטמיע אמצעי בקרה להפעלת מדיה בהתראה ובמסך הנעילה, במקרים שבהם השולח מבצע Cast אבל אפליקציית השולח לא נמצאת בפוקוס. המסגרת מספקת את MediaNotificationService וMediaIntentReceiver כדי לעזור לאפליקציה השולחת ליצור לחצני מדיה בהתראה ובמסך הנעילה.
MediaNotificationService מופעל כששולחים תוכן ל-Cast, ומציג התראה עם תמונה ממוזערת ומידע על הפריט הנוכחי שמופעל ב-Cast, לחצן הפעלה/השהיה ולחצן עצירה.
MediaIntentReceiver הוא BroadcastReceiver שמטפל בפעולות משתמשים מתוך ההתראה.
האפליקציה יכולה להגדיר את ההתראות ואת אמצעי הבקרה של המדיה ממסך הנעילה באמצעות
NotificationOptions.
האפליקציה יכולה להגדיר אילו לחצני בקרה יוצגו בהתראה, ואילו Activity ייפתחו כשהמשתמש יקיש על ההתראה. אם לא מציינים פעולות באופן מפורש, המערכת תשתמש בערכי ברירת המחדל, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK ו-MediaIntentReceiver.ACTION_STOP_CASTING.
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting". val buttonActions: MutableList<String> = ArrayList() buttonActions.add(MediaIntentReceiver.ACTION_REWIND) buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK) buttonActions.add(MediaIntentReceiver.ACTION_FORWARD) buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING) // Showing "play/pause" and "stop casting" in the compat view of the notification. val compatButtonActionsIndices = intArrayOf(1, 3) // Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds. // Tapping on the notification opens an Activity with class VideoBrowserActivity. val notificationOptions = NotificationOptions.Builder() .setActions(buttonActions, compatButtonActionsIndices) .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS) .setTargetActivityClassName(VideoBrowserActivity::class.java.name) .build()
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting". List<String> buttonActions = new ArrayList<>(); buttonActions.add(MediaIntentReceiver.ACTION_REWIND); buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK); buttonActions.add(MediaIntentReceiver.ACTION_FORWARD); buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING); // Showing "play/pause" and "stop casting" in the compat view of the notification. int[] compatButtonActionsIndices = new int[]{1, 3}; // Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds. // Tapping on the notification opens an Activity with class VideoBrowserActivity. NotificationOptions notificationOptions = new NotificationOptions.Builder() .setActions(buttonActions, compatButtonActionsIndices) .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS) .setTargetActivityClassName(VideoBrowserActivity.class.getName()) .build();
ההגדרה 'הצגת אמצעי בקרה של מדיה מההתראה וממסך הנעילה' מופעלת כברירת מחדל, ואפשר להשבית אותה על ידי קריאה ל-setNotificationOptions עם null ב-CastMediaOptions.Builder.
נכון לעכשיו, התכונה 'מסך נעילה' מופעלת כל עוד ההתראות מופעלות.
// ... continue with the NotificationOptions built above val mediaOptions = CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .build() val castOptions: CastOptions = Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build()
// ... continue with the NotificationOptions built above CastMediaOptions mediaOptions = new CastMediaOptions.Builder() .setNotificationOptions(notificationOptions) .build(); CastOptions castOptions = new CastOptions.Builder() .setReceiverApplicationId(context.getString(R.string.app_id)) .setCastMediaOptions(mediaOptions) .build();
כשהאפליקציה השולחת מפעילה שידור חי של סרטון או אודיו, ה-SDK מציג באופן אוטומטי לחצן הפעלה/עצירה במקום לחצן ההפעלה/השהיה בלחצן הבקרה של ההתראה, אבל לא בלחצן הבקרה של מסך הנעילה.
הערה: כדי להציג את אמצעי הבקרה של מסך הנעילה במכשירים עם גרסה ישנה יותר מ-Lollipop,
RemoteMediaClient יבקש אוטומטית מיקוד אודיו בשמכם.
טיפול בשגיאות
חשוב מאוד שאפליקציות השולח יטפלו בכל הקריאות החוזרות של שגיאות ויחליטו מהי התגובה הטובה ביותר לכל שלב במחזור החיים של Cast. האפליקציה יכולה להציג למשתמש תיבות דו-שיח של שגיאות, או להחליט לנתק את החיבור ל-Web Receiver.