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

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

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

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

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

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

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

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

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

הגדרת המניפסט של Android

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

uses-sdk

מגדירים את רמות ה-API המינימליות והיעד של Android שנתמכות ב-Cast SDK. כרגע המינימום הוא רמת API 21 והיעד הוא רמת API 28.

<uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="28" />

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>

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

ה-framework כולל אובייקט גלובלי מסוג singleton, CastContext, שמרכז את כל האינטראקציות של ה-framework.

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

קוטלין
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().

קוטלין
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

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

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

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

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

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

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

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

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

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

ממשקי ה-API של MediaRouter ל-Android נועדו לאפשר הצגה והפעלה של מדיה במכשירים משניים. אפליקציות ל-Android שמשתמשות ב-API MediaRouter צריכות לכלול לחצן 'העברה' כחלק מממשק המשתמש שלהן, על מנת לאפשר למשתמשים לבחור מסלול מדיה להפעלת מדיה במכשיר משני, כמו מכשיר CAST.

בזכות ה-framework אפשר להוסיף MediaRouteButton בתור Cast button בקלות. קודם צריך להוסיף פריט תפריט או MediaRouteButton בקובץ ה-XML שמגדיר את התפריט, ולהשתמש ב-CastButtonFactory כדי לחבר אותו ל-framework.

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
קוטלין
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
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>
קוטלין
// 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, והוא יתחיל את תהליך הגילוי בתנאים הבאים:

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

תהליך הגילוי יופסק כשתיבת הדו-שיח 'העברה' תיסגר או כשאפליקציית השולח תיכנס לרקע.

קוטלין
class CastOptionsProvider : OptionsProvider {
    companion object {
        const val CUSTOM_NAMESPACE = "urn:x-cast:custom_namespace"
    }

    override fun getCastOptions(appContext: Context): CastOptions {
        val supportedNamespaces: MutableList<String> = ArrayList()
        supportedNamespaces.add(CUSTOM_NAMESPACE)

        return CastOptions.Builder()
            .setReceiverApplicationId(context.getString(R.string.app_id))
            .setSupportedNamespaces(supportedNamespaces)
            .build()
    }

    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
        return null
    }
}
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;
    }
}

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

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

סשנים מנוהלים על ידי המחלקה SessionManager, שאליה האפליקציה יכולה לגשת דרך CastContext.getSessionManager(). סשנים נפרדים מיוצגים על ידי מחלקות משנה של המחלקה Session. לדוגמה, הערך CastSession מייצג סשנים עם מכשירי CAST. האפליקציה יכולה לגשת לסשן ההעברה הפעיל דרך SessionManager.getCurrentCastSession().

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

סשנים נוצרים ומוסרים באופן אוטומטי בהתאם לתנועות של המשתמשים בתיבות הדו-שיח MediaRouter.

כדי להבין טוב יותר את שגיאות ההפעלה של Cast, אפליקציות יכולות להשתמש ב-CastContext#getCastReasonCodeForCastStatusCode(int) כדי להמיר את שגיאת התחלת הסשן ל-CastReasonCodes. שימו לב שחלק מהשגיאות בהתחלת הסשן (למשל CastReasonCodes#CAST_CANCELLED) הן התנהגות מכוונות ואין לתעד אותן כשגיאה.

אם אתם צריכים להיות מודעים לשינויי המצב של הסשן, תוכלו להטמיע SessionManagerListener. הדוגמה הזו מתייחסת לזמינות של CastSession ב-Activity.

קוטלין
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            invalidateOptionsMenu()
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {
            val castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error)
            // Handle error
        }

        override fun onSessionSuspended(session: CastSession?, reason Int) {}

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            invalidateOptionsMenu()
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            finish()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
    }

    override fun onResume() {
        super.onResume()
        mCastSession = mSessionManager.currentCastSession
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onPause() {
        super.onPause()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
        mCastSession = null
    }
}
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();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mCastSession = mSessionManager.getCurrentCastSession();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
    @Override
    protected void onPause() {
        super.onPause();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
        mCastSession = null;
    }
}

העברת השידור

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

כדי לקבל את מכשיר היעד החדש במהלך העברה או הרחבה של שידור, רושמים Cast.Listener באמצעות CastSession#addCastListener. לאחר מכן מבצעים קריאה אל CastSession#getCastDevice() במהלך הקריאה החוזרת של onDeviceNameChanged.

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

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

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

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

השירות הזה פועל כברירת מחדל ואפשר להשבית אותו ב-CastOptions.Builder.

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

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

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

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

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

כל השיטות של RemoteMediaClient שמנפיקות בקשות למקלט האינטרנט יחזירו אובייקט PendingResults שאפשר להשתמש בו כדי לעקוב אחר הבקשה.

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

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

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

קוטלין
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)

movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle())
movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio())
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(0))))
movieMetadata.addImage(WebImage(Uri.parse(mSelectedMedia.getImage(1))))
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 Awareer, וגם כדי לשלוט בה בכל דרך אחרת.

קוטלין
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 TV ואת הגובה והרוחב של התצוגה בפיקסלים. וריאנטים של פורמט 4K מסומנים על ידי קבועים HDR_TYPE_*.

שליחת התראות בכמה מכשירים באמצעות שלט רחוק

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

הוספת מיני שלט רחוק

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

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

הוספת שלט רחוק מורחב

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

ה-SDK של Cast מספק ווידג'ט לשלט הרחוק המורחב שנקרא ExpandedControllerActivity. זו מחלקה מופשטת שצריך לבצע בה מחלקה משנית כדי להוסיף לחצן 'העברה'.

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

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
            android:id="@+id/media_route_menu_item"
            android:title="@string/media_route_menu_title"
            app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
            app:showAsAction="always"/>

</menu>

יצירת כיתה חדשה בהיקף של ExpandedControllerActivity.

קוטלין
class ExpandedControlsActivity : ExpandedControllerActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.expanded_controller, menu)
        CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
        return true
    }
}
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 כדי להגדיר את פעילות היעד לפעילות החדשה:

קוטלין
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:

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

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

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

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

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

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

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

MediaIntentReceiver הוא BroadcastReceiver שמטפל בפעולות של המשתמשים מההתראה.

האפליקציה יכולה להגדיר את השליטה בהתראות ובמדיה ממסך הנעילה באמצעות NotificationOptions. האפליקציה יכולה להגדיר אילו לחצני בקרה יוצגו בהתראה, ואיזה Activity ייפתח כשהמשתמש מקיש על ההתראה. אם הפעולות לא צוינו באופן מפורש, המערכת תשתמש בערכי ברירת המחדל – MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK ו-MediaIntentReceiver.ACTION_STOP_CASTING.

קוטלין
// Example showing 4 buttons: "rewind", "play/pause", "forward" and "stop casting".
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_REWIND)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_FORWARD)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)

// Showing "play/pause" and "stop casting" in the compat view of the notification.
val compatButtonActionsIndices = intArrayOf(1, 3)

// Builds a notification with the above actions. Each tap on the "rewind" and "forward" buttons skips 30 seconds.
// Tapping on the notification opens an Activity with class VideoBrowserActivity.
val notificationOptions = NotificationOptions.Builder()
    .setActions(buttonActions, compatButtonActionsIndices)
    .setSkipStepMs(30 * DateUtils.SECOND_IN_MILLIS)
    .setTargetActivityClassName(VideoBrowserActivity::class.java.name)
    .build()
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. נכון לעכשיו, התכונה של מסך הנעילה פועלת כל עוד ההתראה מופעלת.

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

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

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