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

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

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

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

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

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

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

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

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

กำหนดค่าไฟล์ Manifest ของ 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

ตั้งค่าธีมของแอปตามเวอร์ชัน Android SDK ขั้นต่ำ เช่น หากคุณไม่ได้นำธีมของตัวเองไปใช้ คุณควรใช้ตัวแปร Theme.AppCompat เมื่อกำหนดเป้าหมาย Android SDK เวอร์ชันขั้นต่ำที่เป็น Lollipop ล่วงหน้า

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat" >
       ...
</application>

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

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

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

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

วิดเจ็ต UX ของ Cast

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

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

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

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

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

  • การแจ้งเตือน: 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" />
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);
}

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

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

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

หากต้องการอุปกรณ์ปลายทางเครื่องใหม่ระหว่างการโอนหรือการขยายสตรีม ให้ลงทะเบียน Cast.Listener โดยใช้ CastSession#addCastListener จากนั้นเรียกใช้ CastSession#getCastDevice() ระหว่างการติดต่อกลับ onDeviceNameChanged

ดูข้อมูลเพิ่มเติมได้ที่การโอนสตรีมบนเว็บรีซีฟเวอร์

เชื่อมต่อใหม่โดยอัตโนมัติ

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

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

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

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

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

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

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

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

เมธอดทั้งหมดของ RemoteMediaClient ที่ส่งคำขอไปยังเว็บรีซีฟเวอร์จะส่งกลับออบเจ็กต์ Pending Results ที่ใช้ติดตามคำขอนั้นได้

คาดว่าจะมีการแชร์อินสแตนซ์ของ 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 TV รวมถึงความสูงและความกว้างของการแสดงผลเป็นพิกเซล รูปแบบต่างๆ ของรูปแบบ 4K จะระบุด้วยค่าคงที่ HDR_TYPE_*

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

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

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

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

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

การควบคุมระดับเสียง

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

การควบคุมระดับเสียงของปุ่มบนเครื่อง

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

ปุ่มควบคุมระดับเสียงก่อน Jelly Bean

หากต้องการใช้แป้นปรับระดับเสียงเพื่อควบคุมระดับเสียงของอุปกรณ์ Web Reportr บนอุปกรณ์ 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 ด้วยค่าว่างใน CastMediaOptions.Builder ขณะนี้ฟีเจอร์หน้าจอล็อกเปิดอยู่ตราบใดที่การแจ้งเตือนเปิดอยู่

Kotlin
// ... continue with the NotificationOptions built above
val mediaOptions = CastMediaOptions.Builder()
    .setNotificationOptions(notificationOptions)
    .build()
val castOptions: CastOptions = Builder()
    .setReceiverApplicationId(context.getString(R.string.app_id))
    .setCastMediaOptions(mediaOptions)
    .build()
Java
// ... continue with the NotificationOptions built above
CastMediaOptions mediaOptions = new CastMediaOptions.Builder()
        .setNotificationOptions(notificationOptions)
        .build();
CastOptions castOptions = new CastOptions.Builder()
        .setReceiverApplicationId(context.getString(R.string.app_id))
        .setCastMediaOptions(mediaOptions)
        .build();

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

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

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

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