שילוב של Cast עם אפליקציית Android

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

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

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

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

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

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

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

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

הגדרת קובץ המניפסט של Android

בקובץ AndroidManifest.xml של האפליקציה, צריך להגדיר את הרכיבים הבאים ל-Cast SDK:

uses-sdk

מגדירים את רמות ה-API המינימליות ורמות היעד של Android שנתמכות ב-Cast SDK. נכון לעכשיו, רמת ה-API המינימלית היא 24 ורמת היעד היא 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

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

האפליקציה צריכה להטמיע את הממשק OptionsProvider כדי לספק את האפשרויות הנדרשות להפעלת ה-singleton CastContext. ‫OptionsProvider מספק מכונה של CastOptions שמכילה אפשרויות שמשפיעות על ההתנהגות של ה-framework. החשוב ביותר מביניהם הוא מזהה האפליקציה של Web Receiver, שמשמש לסינון תוצאות החיפוש ולהפעלת אפליקציית Web Receiver כשמתחילים סשן Cast.

Kotlin
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
    }
}
Java
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().

Kotlin
class MyActivity : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        val castContext = CastContext.getSharedInstance(this)
    }
}
Java
public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        CastContext castContext = CastContext.getSharedInstance(this);
    }
}

ווידג'טים של חוויית המשתמש ב-Cast

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

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

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

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

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

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

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

במדריך הבא מוסבר איך להוסיף את הווידג'טים האלה לאפליקציה.

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

ממשקי ה-API של Android MediaRouter מיועדים לאפשר הצגה והפעלה של מדיה במכשירים משניים. אפליקציות ל-Android שמשתמשות ב-API של MediaRouter צריכות לכלול כפתור להפעלת 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" />
Kotlin
// 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
}
Java
// 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>
Kotlin
// 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)
}
Java
// 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 תיסגר או כשאפליקציית השליחה תעבור לרקע.

Kotlin
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
    }
}
Java
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". כשמגדירים סשן 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.

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

ה-framework יפעיל את השירות כשיש סשן מדיה, ויפסיק אותו כשהסשן מסתיים.

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

ב-Cast Framework, המחלקה RemoteMediaPlayer מ-Cast 2.x הוצאה משימוש והוחלפה במחלקה החדשה RemoteMediaClient, שמספקת את אותה פונקציונליות בקבוצה של ממשקי API נוחים יותר, ולא צריך להעביר אליה GoogleApiClient.

כשהאפליקציה יוצרת CastSession עם אפליקציית Web Receiver שתומכת במרחב השמות של המדיה, המערכת יוצרת באופן אוטומטי מופע של RemoteMediaClient. האפליקציה יכולה לגשת אליו באמצעות קריאה ל-method‏ getRemoteMediaClient() במופע של CastSession.

כל השיטות של RemoteMediaClient ששולחות בקשות ל-Web Receiver יחזירו אובייקט PendingResult שאפשר להשתמש בו כדי לעקוב אחרי הבקשה הזו.

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

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

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

Kotlin
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))))
Java
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.

Kotlin
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())
Java
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.

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

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

Kotlin
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()
}
Java
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 כדי להציג את הפעילות החדשה כשהמדיה המרוחקת נטענת:

Kotlin
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()
    )
}
Java
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 מציג באופן אוטומטי לחצן הפעלה/עצירה במקום לחצן ההפעלה/השהיה בבקר המורחב.

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

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

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

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

ב-Android, אפשר להשתמש בלחצנים הפיזיים במכשיר השולח כדי לשנות את עוצמת הקול של סשן ההעברה ב-Web Receiver כברירת מחדל בכל מכשיר עם Jelly Bean או גרסה חדשה יותר.

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

כדי להשתמש בכפתורי עוצמת הקול הפיזיים כדי לשלוט בעוצמת הקול של מכשיר Web Receiver במכשירי Android שקדמו ל-Jelly Bean, אפליקציית השולח צריכה לשנות את dispatchKeyEvent בפעילויות שלה, ולקרוא ל-CastContext.onDispatchVolumeKeyEventBeforeJellyBean():

Kotlin
class MyActivity : FragmentActivity() {
    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
        return (CastContext.getSharedInstance(this)
            .onDispatchVolumeKeyEventBeforeJellyBean(event)
                || super.dispatchKeyEvent(event))
    }
}
Java
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.

Kotlin
// 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()
Java
// 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. נכון לעכשיו, התכונה של מסך הנעילה מופעלת כל עוד ההתראות מופעלות.

Kotlin
// ... 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()
Java
// ... 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 תבקש באופן אוטומטי את מיקוד האודיו בשבילכם.

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

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