ผสานรวมการแคสต์ลงในแอป Android ของคุณ

คู่มือนักพัฒนาซอฟต์แวร์นี้จะอธิบายวิธีเพิ่มการสนับสนุน Google Cast ในแอปผู้ส่งของ Android โดยใช้ SDK ผู้ส่งของ Android

อุปกรณ์เคลื่อนที่หรือแล็ปท็อปเป็นผู้ส่งซึ่งควบคุมการเล่น และอุปกรณ์ Google Cast คือตัวรับซึ่งแสดงเนื้อหาบนทีวี

เฟรมเวิร์กผู้ส่งหมายถึงไบนารีไลบรารีคลาสของ Cast และทรัพยากรที่เกี่ยวข้องซึ่งแสดงขึ้นขณะรันไทม์ของผู้ส่ง แอปผู้ส่งหรือแอปแคสต์หมายถึงแอปที่กำลังทำงานกับผู้ส่งด้วยเช่นกัน แอป Web Receiver หมายถึงแอปพลิเคชัน HTML ที่ทำงานในอุปกรณ์ที่พร้อมใช้งาน Cast

เฟรมเวิร์กผู้ส่งใช้การออกแบบ Callback แบบไม่พร้อมกันเพื่อแจ้งให้แอปผู้ส่งทราบถึงเหตุการณ์ต่างๆ และเพื่อสลับระหว่างสถานะต่างๆ ในวงจรของแอปแคสต์

ขั้นตอนของแอป

ขั้นตอนต่อไปนี้จะอธิบายถึงขั้นตอนการดำเนินการระดับสูงโดยทั่วไปสำหรับแอป Android ของผู้ส่ง

  • เฟรมเวิร์กการแคสต์จะเริ่มการค้นพบอุปกรณ์ MediaRouter โดยอัตโนมัติตามวงจรชีวิตของ Activity
  • เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เฟรมเวิร์กจะแสดงกล่องโต้ตอบ "แคสต์" ที่มีรายการอุปกรณ์แคสต์ที่พบ
  • เมื่อผู้ใช้เลือกอุปกรณ์แคสต์ เฟรมเวิร์กจะพยายามเปิดแอปตัวรับเว็บในอุปกรณ์แคสต์
  • เฟรมเวิร์กนี้จะเรียกใช้ Callback ในแอปผู้ส่งเพื่อยืนยันว่ามีการเปิดตัวแอป Web Receiver แล้ว
  • เฟรมเวิร์กจะสร้างช่องทางการสื่อสารระหว่างผู้ส่งกับแอป Web Receiver
  • เฟรมเวิร์กนี้จะใช้ช่องทางการสื่อสารเพื่อโหลดและควบคุมการเล่นสื่อบน Web Receiver
  • เฟรมเวิร์กจะซิงค์สถานะการเล่นสื่อระหว่างผู้ส่งกับตัวรับเว็บ โดยเฟรมเวิร์กจะอัปเดตสถานะ UI ของผู้ส่ง เฟรมเวิร์กจะส่งคำขอตัวควบคุมสื่อดังกล่าวไปยังเว็บรีซีฟเวอร์
  • เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เพื่อยกเลิกการเชื่อมต่อจากอุปกรณ์ Cast เฟรมเวิร์กจะยกเลิกการเชื่อมต่อแอปผู้ส่งจาก Web Receiver

สำหรับรายการคลาส วิธีการ และเหตุการณ์ทั้งหมดใน Google Cast Android SDK โปรดดูที่ข้อมูลอ้างอิง API ผู้ส่งของ Google Cast สำหรับ Android ส่วนต่อไปนี้จะอธิบายถึงขั้นตอนในการเพิ่มแคสต์ไปยังแอป Android

กำหนดค่าไฟล์ Manifest ของ Android

ไฟล์ AndroidManifest.xml ของแอปกำหนดให้คุณต้องกำหนดค่าองค์ประกอบต่อไปนี้สำหรับ Cast SDK

uses-sdk

ตั้งค่าระดับ API ขั้นต่ำและเป้าหมายที่ Cast SDK รองรับ ปัจจุบัน API ระดับต่ำสุดคือ API ระดับ 21 และเป้าหมายคือ API ระดับ 28

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

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>

เริ่มต้นบริบทการแคสต์

เฟรมเวิร์กมีออบเจ็กต์ CastContext แบบ Singleton ที่ประสานงานการโต้ตอบทั้งหมดของเฟรมเวิร์ก

แอปของคุณต้องใช้อินเทอร์เฟซ OptionsProvider เพื่อระบุตัวเลือกที่จำเป็นในการเริ่มต้น CastContext เดี่ยว OptionsProvider มีอินสแตนซ์ของ CastOptions ซึ่งมีตัวเลือกที่ส่งผลต่อลักษณะการทำงานของเฟรมเวิร์ก สิ่งสำคัญที่สุดคือรหัสแอปพลิเคชัน Web Receiver ซึ่งใช้เพื่อกรองผลการค้นพบและเปิดแอป Web Receiver เมื่อเซสชันการแคสต์เริ่มต้น

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 เริ่มต้นแบบ Lazy Loading เมื่อมีการเรียก 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

กรอบการทำงานของ Cast มีวิดเจ็ตที่เป็นไปตามรายการตรวจสอบของการออกแบบแคสต์:

  • การวางซ้อนบทนำ: เฟรมเวิร์กนี้มีมุมมองแบบกำหนดเอง IntroductoryOverlay ซึ่งจะแสดงให้ผู้ใช้เห็นเพื่อเรียกความสนใจไปยังปุ่ม "แคสต์" ในครั้งแรกที่รีซีฟเวอร์พร้อมให้ใช้งาน แอปผู้ส่งสามารถปรับแต่งข้อความและตำแหน่งของข้อความชื่อ

  • ปุ่ม "แคสต์": ปุ่ม "แคสต์" จะปรากฏขึ้นไม่ว่าอุปกรณ์แคสต์จะพร้อมใช้งานหรือไม่ก็ตาม เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เป็นครั้งแรก กล่องโต้ตอบ "แคสต์" จะปรากฏขึ้น ซึ่งจะแสดงรายการอุปกรณ์ที่ค้นพบ เมื่อผู้ใช้คลิกปุ่ม "แคสต์" ในขณะที่อุปกรณ์เชื่อมต่ออยู่ จะแสดงข้อมูลเมตาของสื่อปัจจุบัน (เช่น ชื่อสตูดิโอบันทึกเสียงและภาพขนาดย่อ) หรืออนุญาตให้ผู้ใช้ยกเลิกการเชื่อมต่อจากอุปกรณ์แคสต์ บางครั้ง "ปุ่ม "แคสต์" เรียกว่า "ไอคอนแคสต์"

  • มินิคอนโทรลเลอร์: เมื่อผู้ใช้กำลังแคสต์เนื้อหาและออกจากหน้าเนื้อหาปัจจุบันหรือออกไปจากหน้าเนื้อหาปัจจุบันหรือขยายตัวควบคุมไปยังหน้าจออื่นในแอปผู้ส่ง มินิคอนโทรลเลอร์จะแสดงที่ด้านล่างของหน้าจอเพื่อให้ผู้ใช้ดูข้อมูลเมตาของสื่อที่กำลังแคสต์อยู่และควบคุมการเล่นได้

  • ตัวควบคุมที่ขยาย: เมื่อผู้ใช้กำลังแคสต์เนื้อหา หากผู้ใช้คลิกที่การแจ้งเตือนสื่อหรือมินิตัวควบคุม ตัวควบคุมที่ขยายแล้วจะเปิดตัว ซึ่งจะแสดงข้อมูลเมตาของสื่อที่เล่นอยู่ในขณะนั้นและมีปุ่มหลายปุ่มสำหรับควบคุมการเล่นสื่อ

  • การแจ้งเตือน: Android เท่านั้น เมื่อผู้ใช้กำลังแคสต์เนื้อหาและออกจากแอปผู้ส่ง ระบบจะแสดงการแจ้งเตือนสื่อซึ่งแสดงข้อมูลเมตาของสื่อที่แคสต์และการควบคุมการเล่นอยู่ในปัจจุบัน

  • หน้าจอล็อก: Android เท่านั้น เมื่อผู้ใช้กำลังแคสต์เนื้อหาและไปยังส่วนต่างๆ (หรืออุปกรณ์หมดเวลา) ไปยังหน้าจอล็อก ตัวควบคุมหน้าจอล็อกของสื่อจะปรากฏขึ้น ซึ่งจะแสดงข้อมูลเมตาของสื่อที่แคสต์และการควบคุมการเล่นอยู่ในปัจจุบัน

คำแนะนำต่อไปนี้มีคำอธิบายวิธีเพิ่มวิดเจ็ตเหล่านี้ลงในแอป

เพิ่มปุ่ม "แคสต์"

API ของ Android MediaRouter ออกแบบมาเพื่อเปิดใช้การแสดงและการเล่นสื่อในอุปกรณ์รอง แอป Android ที่ใช้ MediaRouter API ควรมีปุ่ม "แคสต์" เป็นส่วนหนึ่งของอินเทอร์เฟซผู้ใช้ เพื่อให้ผู้ใช้เลือกเส้นทางสื่อเพื่อเล่นสื่อในอุปกรณ์รอง เช่น อุปกรณ์แคสต์ ได้

เฟรมเวิร์กนี้ทำให้การเพิ่ม MediaRouteButton เป็น Cast button นั้นง่ายมาก คุณควรเพิ่มรายการเมนูหรือ MediaRouteButton ในไฟล์ XML ที่กำหนดเมนูก่อน แล้วใช้ CastButtonFactory เพื่อเชื่อมโยงกับเฟรมเวิร์ก

// To add a Cast button, add the following snippet.
// menu.xml
<item
    android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
    app:showAsAction="always" />
โกตลิน
// Then override the onCreateOptionMenu() for each of your activities.
// MyActivity.kt
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)
    menuInflater.inflate(R.menu.main, menu)
    CastButtonFactory.setUpMediaRouteButton(
        applicationContext,
        menu,
        R.id.media_route_menu_item
    )
    return true
}
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);
}

หากต้องการตั้งค่ารูปลักษณ์ของปุ่ม "แคสต์" โดยใช้ธีม โปรดดูปรับแต่งปุ่ม "แคสต์"

กำหนดค่าการค้นพบอุปกรณ์

CastContext เป็นผู้จัดการการค้นหาอุปกรณ์โดยสมบูรณ์ เมื่อเริ่ม CastContext แอปผู้ส่งจะระบุรหัสแอปพลิเคชัน Web Receiver และเลือกที่จะขอกรองเนมสเปซได้โดยตั้งค่า supportedNamespaces ใน CastOptions CastContext มีการอ้างอิง MediaRouter เป็นการภายใน และจะเริ่มกระบวนการค้นหาภายใต้เงื่อนไขต่อไปนี้

  • บางครั้งการค้นพบอุปกรณ์จะเริ่มต้นโดยอัตโนมัติเมื่อแอปผู้ส่งเข้าสู่เบื้องหน้า โดยขึ้นอยู่กับอัลกอริทึมที่ออกแบบมาเพื่อรักษาสมดุลระหว่างเวลาในการตอบสนองการค้นพบอุปกรณ์กับการใช้งานแบตเตอรี่
  • กล่องโต้ตอบการแคสต์เปิดอยู่
  • Cast SDK กำลังพยายามกู้คืนเซสชันการแคสต์

ขั้นตอนการค้นหาจะหยุดลงเมื่อปิดกล่องโต้ตอบการแคสต์หรือเมื่อแอปผู้ส่งเข้าสู่พื้นหลัง

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 นำเสนอแนวคิดของเซสชันการแคสต์ ซึ่งเป็นการสร้างขั้นตอนรวมขั้นตอนการเชื่อมต่อกับอุปกรณ์ การเปิด (หรือการเข้าร่วม) แอปตัวรับเว็บ การเชื่อมต่อกับแอปดังกล่าว และการเริ่มต้นช่องทางควบคุมสื่อ ดูข้อมูลเพิ่มเติมเกี่ยวกับเซสชันของการแคสต์และวงจรการใช้งานตัวรับเว็บได้ในคู่มือวงจรของแอปพลิเคชันในฝั่งตัวรับเว็บ

เซสชันจะได้รับการจัดการโดยชั้นเรียน SessionManager ซึ่งแอปของคุณเข้าถึงได้ผ่าน CastContext.getSessionManager() แต่ละเซสชันจะแสดงด้วยคลาสย่อยของชั้นเรียน Session ตัวอย่างเช่น CastSession แสดงถึงเซสชันที่มีอุปกรณ์แคสต์ แอปของคุณสามารถเข้าถึงเซสชันการแคสต์ ที่ใช้งานอยู่ในปัจจุบันได้ผ่าน SessionManager.getCurrentCastSession()

แอปของคุณสามารถใช้คลาส SessionManagerListener เพื่อตรวจสอบเหตุการณ์เซสชัน เช่น การสร้าง การระงับ การกลับมาทำงานอีกครั้ง และการสิ้นสุดได้ เฟรมเวิร์กจะพยายามกลับมาทำงานอีกครั้งโดยอัตโนมัติจากการสิ้นสุดที่ผิดปกติ/ฉับพลันขณะที่เซสชันทำงานอยู่

ระบบจะสร้างและแยกเซสชันออกโดยอัตโนมัติตามท่าทางสัมผัสของผู้ใช้จากกล่องโต้ตอบ MediaRouter

แอปสามารถใช้ 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
    }

    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.Listener โดยใช้ CastSession#addCastListener จากนั้นโทร CastSession#getCastDevice() ระหว่างการติดต่อกลับ onDeviceNameChanged

ดูข้อมูลเพิ่มเติมได้ที่การโอนสตรีมบน Web Receiver

การเชื่อมต่ออีกครั้งอัตโนมัติ

เฟรมเวิร์กนี้จะมี ReconnectionService ที่แอปผู้ส่งเปิดใช้เพื่อจัดการการเชื่อมต่ออีกครั้งในกรณีเล็กๆ น้อยๆ ได้หลายกรณี เช่น

  • กู้คืนจากสัญญาณ Wi-Fi ที่หายไปชั่วคราว
  • กู้คืนจากโหมดสลีปของอุปกรณ์
  • กู้คืนจากการเล่นขณะล็อกหน้าจอหรือขณะใช้แอปอื่น
  • กู้คืนหากแอปขัดข้อง

บริการนี้เปิดอยู่โดยค่าเริ่มต้น และจะปิดได้ในCastOptions.Builder

ระบบจะรวมบริการนี้เป็นไฟล์ Manifest ของแอปโดยอัตโนมัติหากเปิดใช้การผสานอัตโนมัติในไฟล์ Gradle

เฟรมเวิร์กจะเริ่มบริการเมื่อมีเซสชันสื่อ และหยุดบริการเมื่อเซสชันสื่อสิ้นสุดลง

วิธีการทำงานของการควบคุมสื่อ

เฟรมเวิร์กของ Cast จะเลิกใช้คลาส RemoteMediaPlayer จาก Cast 2.x เพื่อใช้คลาสใหม่ RemoteMediaClient ซึ่งมีฟังก์ชันการทำงานเดียวกันในชุด API ที่สะดวกยิ่งขึ้น และหลีกเลี่ยงการส่งผ่าน GoogleApiClient

เมื่อแอปของคุณสร้าง CastSession ด้วยแอป Web Receiver ที่รองรับเนมสเปซของสื่อ เฟรมเวิร์กจะสร้างอินสแตนซ์ของ RemoteMediaClient โดยอัตโนมัติ แอปของคุณจะเข้าถึงได้ด้วยการเรียกใช้เมธอด getRemoteMediaClient() ในอินสแตนซ์ CastSession

เมธอดทั้งหมดของ RemoteMediaClient ที่ส่งคำขอไปยัง Web Receiver จะแสดงออบเจ็กต์ PendingResult ที่ใช้ติดตามคำขอนั้นได้

คาดว่าหลายส่วนของแอปอาจแชร์อินสแตนซ์ของ RemoteMediaClient รวมถึงคอมโพเนนต์ภายในบางอย่างของเฟรมเวิร์ก เช่น ตัวควบคุมขนาดเล็กถาวรและบริการการแจ้งเตือน ด้วยเหตุนี้ อินสแตนซ์นี้รองรับการลงทะเบียน RemoteMediaClient.Listener หลายอินสแตนซ์

ตั้งค่าข้อมูลเมตาของสื่อ

คลาส MediaMetadata แสดงถึงข้อมูลเกี่ยวกับรายการสื่อที่คุณต้องการแคสต์ ตัวอย่างต่อไปนี้จะสร้างอินสแตนซ์ MediaMetadata ใหม่ของภาพยนตร์และตั้งค่าชื่อ คำบรรยาย และรูปภาพ 2 รูป

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 เพื่อเล่น หยุดชั่วคราว และควบคุมแอปมีเดียเพลเยอร์ที่ทำงานอยู่บนตัวรับเว็บ

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_*

การแจ้งเตือนด้วยรีโมตคอนโทรลไปยังอุปกรณ์หลายเครื่อง

เมื่อผู้ใช้กำลังแคสต์ อุปกรณ์ Android อื่นๆ ในเครือข่ายเดียวกันจะได้รับการแจ้งเตือนที่อนุญาตให้ควบคุมการเล่นด้วย ใครก็ตามที่อุปกรณ์ได้รับการแจ้งเตือนดังกล่าวสามารถปิดการแจ้งเตือนสำหรับอุปกรณ์นั้นในแอปการตั้งค่าที่ Google > Google Cast > แสดงการแจ้งเตือนบนรีโมตคอนโทรล (การแจ้งเตือนจะมีทางลัดไปยังแอปการตั้งค่า) ดูรายละเอียดเพิ่มเติมได้ที่การแจ้งเตือนบนรีโมตคอนโทรลของ Cast

เพิ่มตัวควบคุมขนาดเล็ก

ตามรายการตรวจสอบการออกแบบการแคสต์ แอปของผู้ส่งควรมีการควบคุมถาวรที่เรียกว่าตัวควบคุมขนาดเล็ก ซึ่งควรปรากฏเมื่อผู้ใช้ออกจากหน้าเนื้อหาปัจจุบันไปยังส่วนอื่นของแอปผู้ส่ง ตัวควบคุมขนาดเล็กจะแสดงการแจ้งเตือนแก่ผู้ใช้ในเซสชันการแคสต์ปัจจุบัน เมื่อแตะตัวควบคุมขนาดเล็ก ผู้ใช้จะกลับไปที่มุมมองตัวควบคุมแบบขยายเต็มหน้าจอของ Cast ได้

เฟรมเวิร์กจะมี 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 จะแสดงปุ่มเล่น/หยุดโดยอัตโนมัติแทนปุ่มเล่น/หยุดชั่วคราวในตัวควบคุมขนาดเล็ก

หากต้องการตั้งค่าลักษณะข้อความของชื่อและคำบรรยายของมุมมองที่กำหนดเองนี้ และการเลือกปุ่ม โปรดดูปรับแต่ง Mini Controller

เพิ่มตัวควบคุมที่ขยาย

รายการตรวจสอบของการออกแบบ Google Cast กำหนดให้แอปผู้ส่งมีตัวควบคุมแบบขยายสำหรับสื่อที่กำลังแคสต์อยู่ ตัวควบคุมแบบขยายจะเป็นตัวควบคุมขนาดเล็ก เวอร์ชันเต็มหน้าจอ

Cast SDK มีวิดเจ็ตสำหรับตัวควบคุมแบบขยายที่ชื่อว่า ExpandedControllerActivity นี่คือคลาสนามธรรมที่คุณต้องให้คลาสย่อยเพิ่มปุ่ม "แคสต์"

ก่อนอื่น ให้สร้างไฟล์ทรัพยากรเมนูใหม่สำหรับตัวควบคุมที่ขยายเพื่อให้มีปุ่ม "แคสต์" ดังนี้

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

ตอนนี้ให้ประกาศกิจกรรมใหม่ในไฟล์ Manifest ของแอปภายในแท็ก 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 โดยอัตโนมัติเพื่อให้ UI ของผู้ส่งรายงานวอลุ่มที่ Web Receiver ระบุไว้เสมอ

ตัวควบคุมระดับเสียงของปุ่มจริง

ใน Android คุณสามารถใช้ปุ่มบนอุปกรณ์ผู้ส่งเพื่อเปลี่ยนระดับเสียงของเซสชันการแคสต์บนตัวรับเว็บโดยค่าเริ่มต้นสำหรับอุปกรณ์ใดก็ได้ที่ใช้ 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);
    }
}

เพิ่มตัวควบคุมสื่อลงในการแจ้งเตือนและหน้าจอล็อก

รายการตรวจสอบสำหรับการออกแบบ Google Cast กำหนดให้แอปผู้ส่งต้องใช้การควบคุมสื่อในการแจ้งเตือนและในหน้าจอล็อกบน Android เท่านั้น ซึ่งผู้ส่งกำลังแคสต์แต่แอปผู้ส่งไม่มีโฟกัส เฟรมเวิร์กนี้จะให้ข้อมูล MediaNotificationService และ MediaIntentReceiver เพื่อช่วยให้แอปผู้ส่งสร้างตัวควบคุมสื่อในการแจ้งเตือนและในหน้าจอล็อก

MediaNotificationService จะทำงานเมื่อผู้ส่งกำลังแคสต์ และจะแสดงการแจ้งเตือนพร้อมภาพขนาดย่อของรูปภาพและข้อมูลเกี่ยวกับรายการที่แคสต์ปัจจุบัน ปุ่มเล่น/หยุดชั่วคราว และปุ่มหยุด

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 จะแสดงปุ่มเล่น/หยุดโดยอัตโนมัติแทนปุ่มเล่น/หยุดชั่วคราวบนตัวควบคุมการแจ้งเตือน แต่จะไม่แสดงการควบคุมหน้าจอล็อก

หมายเหตุ: หากต้องการแสดงตัวควบคุมหน้าจอล็อกในอุปกรณ์ Pre-Lollipop RemoteMediaClient จะขอโฟกัสเสียงในนามของคุณโดยอัตโนมัติ

จัดการข้อผิดพลาด

แอปของผู้ส่งควรจัดการกับการเรียกกลับที่ผิดพลาดทั้งหมดและตัดสินใจเลือกการตอบสนองที่ดีที่สุดสำหรับแต่ละขั้นในวงจรของการแคสต์ แอปสามารถแสดงกล่องโต้ตอบข้อผิดพลาดให้ผู้ใช้เห็นหรือตัดสินใจที่จะยกเลิกการเชื่อมต่อกับเว็บรีซีฟเวอร์