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

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

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

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

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

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

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

  • מסגרת Cast מתחילה אוטומטית את MediaRouter החיפוש של מכשירים על סמך מחזור החיים של Activity.
  • כשהמשתמש לוחץ על לחצן Cast, המסגרת מציגה את תיבת הדו-שיח Cast עם רשימת מכשירי Cast שהמערכת זיהתה.
  • כשהמשתמש בוחר מכשיר Cast, המסגרת מנסה להפעיל את אפליקציית Web Receiver במכשיר Cast.
  • המסגרת מפעילה קריאות חוזרות באפליקציית השולח כדי לאשר שאפליקציית Web Receiver הופעלה.
  • המסגרת יוצרת ערוץ תקשורת בין השולח לבין אפליקציות Web Receiver.
  • המסגרת משתמשת בערוץ התקשורת כדי לטעון ולשלוט בהפעלת מדיה ב-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 המינימלית היא 23 ורמת ה-API לטירגוט היא 34.

<uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="34" />

android:theme

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

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 UX Widgets

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

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

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

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

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

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

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

אפשר למזג את השירות הזה באופן אוטומטי עם המניפסט של האפליקציה אם האפשרות auto-merge מופעלת בקובץ gradle.

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

איך פועלת בקרת המדיה

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

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

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

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

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

הכיתה MediaMetadata מייצגת את המידע על פריט המדיה שרוצים להפעיל ב-Cast. בדוגמה הבאה נוצר מופע חדש של 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 מספק View מותאם אישית, 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 Design Checklist נדרש שאפליקציית השולח תספק בקר מורחב למדיה שמועברת ב-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();
}

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

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

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

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

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

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

הערה: כדי להציג את אמצעי הבקרה במסך הנעילה במכשירים עם גרסאות Android שקדמו ל-Lollipop, RemoteMediaClient תבקש אוטומטית מיקוד אודיו בשמכם.

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

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