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

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

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

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

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

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

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

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

רשימה מקיפה של כל הכיתות, השיטות והאירועים ב-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

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

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

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

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

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

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

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

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

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

הוספת לחצן 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) מוסבר איך להגדיר את המראה של לחצן ההעברה באמצעות עיצוב.

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

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

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

הסשנים מנוהלים על ידי הכיתה 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.

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

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

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

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

כל השיטות של RemoteMediaClient שמנפקות בקשות למקלט האינטרנט יחזירו אובייקט 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).

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

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

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

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

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

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

בקרת עוצמת קול באמצעות לחצן פיזי לפני Jelly Bean

כדי להשתמש במקשות עוצמת הקול הפיזיים כדי לשלוט בעוצמת הקול של מכשיר Web Receiver במכשירי Android ישנים יותר מגרסה 4.3, אפליקציית השולח צריכה לשנות את 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 Design, אפליקציית השולח צריכה להטמיע אמצעי בקרה על מדיה בהתראה ובמסך הנעילה, כשהשולח מבצע העברה (cast) אבל האפליקציה של השולח לא נמצאת במוקד. המסגרת מספקת את הרכיבים MediaNotificationService ו-MediaIntentReceiver כדי לעזור לאפליקציית השולח ליצור לחצני מדיה בהתראה ובמסך הנעילה.

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

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

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