คู่มือนักพัฒนาซอฟต์แวร์นี้อธิบายวิธีเพิ่มการรองรับ Google Cast ลงในแอปผู้ส่ง Android โดยใช้ Android Sender SDK
อุปกรณ์เคลื่อนที่หรือแล็ปท็อปคือผู้ส่งซึ่งควบคุมการเล่น และอุปกรณ์ Google Cast คือผู้รับซึ่งแสดงเนื้อหาบนทีวี
เฟรมเวิร์กผู้ส่งหมายถึงไบนารีไลบรารีคลาส Cast และทรัพยากรที่เกี่ยวข้องซึ่งมีอยู่ในรันไทม์ของผู้ส่ง แอปผู้ส่งหรือแอปแคสต์หมายถึงแอปที่ทำงานบนผู้ส่งด้วย แอป Web Receiver หมายถึงแอปพลิเคชัน HTML ที่ทำงานบนอุปกรณ์ที่พร้อมใช้งาน Cast
เฟรมเวิร์กผู้ส่งใช้การออกแบบ Callback แบบอะซิงโครนัสเพื่อแจ้งแอปผู้ส่งเกี่ยวกับเหตุการณ์และเปลี่ยนสถานะต่างๆ ของวงจรแอป Cast
ขั้นตอนในแอป
ขั้นตอนต่อไปนี้อธิบายขั้นตอนการดำเนินการระดับสูงโดยทั่วไปสำหรับแอป Android ของผู้ส่ง
- เฟรมเวิร์ก Cast จะเริ่มการค้นหาอุปกรณ์
MediaRouterโดยอัตโนมัติตามวงจรของActivity - เมื่อผู้ใช้คลิกปุ่มแคสต์ เฟรมเวิร์กจะแสดงกล่องโต้ตอบแคสต์พร้อมรายการอุปกรณ์แคสต์ที่ค้นพบ
- เมื่อผู้ใช้เลือกอุปกรณ์แคสต์ เฟรมเวิร์กจะพยายามเปิดแอป Web Receiver ในอุปกรณ์แคสต์
- เฟรมเวิร์กจะเรียกใช้ Callback ในแอปผู้ส่งเพื่อยืนยันว่าแอป Web Receiver เปิดขึ้นแล้ว
- เฟรมเวิร์กจะสร้างช่องทางการสื่อสารระหว่างแอปผู้ส่งและแอป Web Receiver
- เฟรมเวิร์กจะใช้ช่องทางการสื่อสารเพื่อโหลดและควบคุมการเล่นสื่อใน Web Receiver
- เฟรมเวิร์กจะซิงค์สถานะการเล่นสื่อระหว่างผู้ส่งกับ Web Receiver โดยเมื่อผู้ใช้ดำเนินการกับ UI ของผู้ส่ง เฟรมเวิร์กจะส่งคำขอตัวควบคุมสื่อเหล่านั้นไปยัง Web Receiver และเมื่อ Web Receiver ส่งการอัปเดตสถานะสื่อ เฟรมเวิร์กจะอัปเดตสถานะของ UI ของผู้ส่ง
- เมื่อผู้ใช้คลิกปุ่มแคสต์เพื่อยกเลิกการเชื่อมต่อจากอุปกรณ์แคสต์ เฟรมเวิร์กจะยกเลิกการเชื่อมต่อแอปผู้ส่งจาก Web Receiver
ดูรายการคลาส เมธอด และเหตุการณ์ทั้งหมดใน Google Cast Android SDK ได้ที่เอกสารอ้างอิง Google Cast Sender API สำหรับ Android ส่วนต่อไปนี้จะอธิบายขั้นตอนในการเพิ่ม Cast ลงในแอป Android
กำหนดค่าไฟล์ Manifest ของ Android
ไฟล์ AndroidManifest.xml ของแอปกำหนดให้คุณต้องกำหนดค่าองค์ประกอบต่อไปนี้สำหรับ Cast SDK
uses-sdk
ตั้งค่าระดับ API ขั้นต่ำและเป้าหมายของ Android ที่ Cast SDK รองรับ ปัจจุบันระดับ API ขั้นต่ำคือระดับ 24 และเป้าหมายคือ API ระดับ 35
<uses-sdk
android:minSdkVersion="24"
android:targetSdkVersion="35" />
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 เพื่อระบุตัวเลือกที่จำเป็นในการเริ่มต้นซิงเกิลตัน CastContext OptionsProvider มีอินสแตนซ์ของ
CastOptions
ซึ่งมีตัวเลือกที่ส่งผลต่อลักษณะการทำงานของเฟรมเวิร์ก ข้อมูลที่สำคัญที่สุดคือรหัสแอปพลิเคชัน Web Receiver ซึ่งใช้เพื่อกรองผลการค้นหาและเปิดแอป Web Receiver เมื่อเซสชันการแคสต์เริ่มต้นขึ้น
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 } }
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 เมื่อมีการเรียกใช้ CastContext.getSharedInstance()
class MyActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { val castContext = CastContext.getSharedInstance(this) } }
public class MyActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { CastContext castContext = CastContext.getSharedInstance(this); } }
วิดเจ็ต UX ของ Cast
เฟรมเวิร์ก Cast มีวิดเจ็ตที่สอดคล้องกับรายการตรวจสอบการออกแบบ Cast ดังนี้
ภาพซ้อนทับแนะนำ: เฟรมเวิร์กมีมุมมองที่กำหนดเอง
IntroductoryOverlayซึ่งจะแสดงต่อผู้ใช้เพื่อดึงดูดความสนใจไปยังปุ่มแคสต์เมื่อมีตัวรับสัญญาณพร้อมใช้งานเป็นครั้งแรก แอปผู้ส่งสามารถปรับแต่งข้อความและตำแหน่งของข้อความชื่อได้ปุ่มแคสต์: ปุ่มแคสต์จะปรากฏขึ้นไม่ว่าอุปกรณ์แคสต์จะพร้อมใช้งานหรือไม่ก็ตาม เมื่อผู้ใช้คลิกปุ่มแคสต์เป็นครั้งแรก กล่องโต้ตอบแคสต์จะแสดงขึ้นพร้อมรายการอุปกรณ์ที่ค้นพบ เมื่อผู้ใช้คลิกปุ่มแคสต์ขณะที่อุปกรณ์เชื่อมต่ออยู่ ระบบจะแสดงข้อมูลเมตาของสื่อปัจจุบัน (เช่น ชื่อ ชื่อสตูดิโอบันทึกเสียง และภาพปก) หรืออนุญาตให้ผู้ใช้ยกเลิกการเชื่อมต่อจากอุปกรณ์แคสต์ บางครั้ง "ปุ่มแคสต์" จะเรียกว่า "ไอคอนแคสต์"
ตัวควบคุมขนาดเล็ก: เมื่อผู้ใช้กำลังแคสต์เนื้อหาและออกจากหน้าเนื้อหาปัจจุบันหรือตัวควบคุมแบบขยายไปยังหน้าจออื่นในแอปผู้ส่ง ตัวควบคุมขนาดเล็กจะแสดงที่ด้านล่างของหน้าจอเพื่อให้ผู้ใช้เห็นข้อมูลเมตาของสื่อที่กำลังแคสต์และควบคุมการเล่นได้
ตัวควบคุมแบบขยาย: เมื่อผู้ใช้แคสต์เนื้อหา หากคลิกการแจ้งเตือนสื่อหรือตัวควบคุมขนาดเล็ก ตัวควบคุมแบบขยายจะเปิดขึ้น ซึ่งจะแสดงข้อมูลเมตาของสื่อที่กำลังเล่นอยู่และมีปุ่มหลายปุ่มสำหรับควบคุมการเล่นสื่อ
การแจ้งเตือน: Android เท่านั้น เมื่อผู้ใช้แคสต์เนื้อหาและออกจากแอปผู้ส่ง การแจ้งเตือนสื่อจะปรากฏขึ้นเพื่อแสดงข้อมูลเมตาของสื่อที่แคสต์อยู่และตัวควบคุมการเล่น
หน้าจอล็อก: Android เท่านั้น เมื่อผู้ใช้แคสต์เนื้อหาและไปยังหน้าจอล็อก (หรืออุปกรณ์หมดเวลา) ตัวควบคุมหน้าจอล็อกของสื่อจะแสดงขึ้น ซึ่งจะแสดงข้อมูลเมตาของสื่อที่แคสต์อยู่และตัวควบคุมการเล่น
คำแนะนำต่อไปนี้จะอธิบายวิธีเพิ่มวิดเจ็ตเหล่านี้ลงในแอป
เพิ่มปุ่มแคสต์
API MediaRouter ของ Android ออกแบบมาเพื่อเปิดใช้การแสดงผลและการเล่นสื่อในอุปกรณ์รอง
แอป 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 }
// 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) }
// 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 กำลังพยายามกู้คืนเซสชัน Cast
กระบวนการค้นหาจะหยุดลงเมื่อกล่องโต้ตอบ 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 } }
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 นำเสนอแนวคิดของเซสชันการแคสต์ ซึ่งเป็นการสร้างที่รวมขั้นตอนการเชื่อมต่อกับอุปกรณ์ การเปิด (หรือเข้าร่วม) แอป Web Receiver การเชื่อมต่อกับแอปนั้น และการเริ่มต้นช่องทางตัวควบคุมสื่อ ดูข้อมูลเพิ่มเติมเกี่ยวกับเซสชันการแคสต์และวงจรของ Web Receiver ได้ที่คู่มือวงจรของแอปพลิเคชัน Web Receiver
เซสชันจะได้รับการจัดการโดยคลาส
SessionManager
ซึ่งแอปของคุณสามารถเข้าถึงได้ผ่าน
CastContext.getSessionManager()
เซสชันแต่ละรายการจะแสดงโดยคลาสย่อยของคลาส Session
เช่น
CastSession
แสดงเซสชันที่มีอุปกรณ์แคสต์ แอปของคุณสามารถเข้าถึงเซสชันการแคสต์ที่ใช้งานอยู่ในปัจจุบันผ่าน SessionManager.getCurrentCastSession()
แอปของคุณสามารถใช้คลาส SessionManagerListener เพื่อตรวจสอบเหตุการณ์ของเซสชัน เช่น การสร้าง การระงับ การกลับมาทำงานอีกครั้ง และการสิ้นสุด เฟรมเวิร์กจะพยายามกลับมาทำงานต่อโดยอัตโนมัติจากการสิ้นสุดที่ผิดปกติ/กะทันหันขณะที่เซสชันทำงานอยู่
ระบบจะสร้างและปิดเซสชันโดยอัตโนมัติเพื่อตอบสนองต่อท่าทางผู้ใช้จากกล่องโต้ตอบ MediaRouter
เพื่อให้เข้าใจข้อผิดพลาดในการเริ่มแคสต์ได้ดียิ่งขึ้น แอปสามารถใช้ 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 mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java) } override fun onResume() { super.onResume() mCastSession = mSessionManager.currentCastSession } override fun onDestroy() { super.onDestroy() mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java) } }
public class MyActivity extends Activity { private CastContext mCastContext; private CastSession mCastSession; private SessionManager mSessionManager; private SessionManagerListener<CastSession> mSessionManagerListener = new SessionManagerListenerImpl(); private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> { @Override public void onSessionStarting(CastSession session) {} @Override public void onSessionStarted(CastSession session, String sessionId) { invalidateOptionsMenu(); } @Override public void onSessionStartFailed(CastSession session, int error) { int castReasonCode = mCastContext.getCastReasonCodeForCastStatusCode(error); // Handle error } @Override public void onSessionSuspended(CastSession session, int reason) {} @Override public void onSessionResuming(CastSession session, String sessionId) {} @Override public void onSessionResumed(CastSession session, boolean wasSuspended) { invalidateOptionsMenu(); } @Override public void onSessionResumeFailed(CastSession session, int error) {} @Override public void onSessionEnding(CastSession session) {} @Override public void onSessionEnded(CastSession session, int error) { finish(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCastContext = CastContext.getSharedInstance(this); mSessionManager = mCastContext.getSessionManager(); mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class); } @Override protected void onResume() { super.onResume(); mCastSession = mSessionManager.getCurrentCastSession(); } @Override protected void onDestroy() { super.onDestroy(); mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class); } }
การถ่ายโอนสตรีม
การคงสถานะเซสชันเป็นพื้นฐานของการถ่ายโอนสตรีม ซึ่งผู้ใช้สามารถย้ายสตรีมเสียงและวิดีโอที่มีอยู่ไปยังอุปกรณ์ต่างๆ โดยใช้คำสั่งเสียง, แอป Google Home หรือจออัจฉริยะ สื่อจะหยุดเล่นในอุปกรณ์เครื่องหนึ่ง (ต้นทาง) และเล่นต่อในอุปกรณ์อีกเครื่องหนึ่ง (ปลายทาง) อุปกรณ์แคสต์ที่มีเฟิร์มแวร์เวอร์ชันล่าสุดสามารถทำหน้าที่เป็นต้นทางหรือปลายทางในการถ่ายโอนสตรีมได้
หากต้องการรับอุปกรณ์ปลายทางใหม่ระหว่างการถ่ายโอนสตรีมหรือการขยาย ให้ลงทะเบียน Cast.Listener โดยใช้ CastSession#addCastListener
จากนั้นโทรหา
CastSession#getCastDevice()
ระหว่างการโทรกลับ onDeviceNameChanged
ดูข้อมูลเพิ่มเติมได้ที่การถ่ายโอนสตรีมใน Web Receiver
การเชื่อมต่อใหม่อัตโนมัติ
เฟรมเวิร์กมี
ReconnectionService
ซึ่งแอปผู้ส่งสามารถเปิดใช้เพื่อจัดการการเชื่อมต่อใหม่ในกรณีที่ซับซ้อนหลายกรณี เช่น
- กู้คืนจากการสูญเสีย Wi-Fi ชั่วคราว
- กู้คืนจากโหมดสลีปของอุปกรณ์
- กู้คืนจากการทำให้แอปทำงานอยู่เบื้องหลัง
- กู้คืนหากแอปขัดข้อง
บริการนี้จะเปิดอยู่โดยค่าเริ่มต้น และสามารถปิดได้ใน CastOptions.Builder
ระบบจะผสานบริการนี้เข้ากับไฟล์ Manifest ของแอปโดยอัตโนมัติหากเปิดใช้การผสานอัตโนมัติในไฟล์ gradle
เฟรมเวิร์กจะเริ่มบริการเมื่อมีเซสชันสื่อ และหยุดบริการเมื่อเซสชันสื่อสิ้นสุดลง
วิธีการทำงานของตัวควบคุมสื่อ
เฟรมเวิร์ก Cast เลิกใช้งานคลาส RemoteMediaPlayer จาก Cast 2.x เพื่อใช้คลาสใหม่ RemoteMediaClient ซึ่งมีฟังก์ชันการทำงานเดียวกันในชุด API ที่สะดวกกว่า และหลีกเลี่ยงการส่ง GoogleApiClient
เมื่อแอปสร้าง CastSession ด้วยแอปตัวรับสัญญาณบนเว็บที่รองรับเนมสเปซสื่อ เฟรมเวิร์กจะสร้างอินสแตนซ์ของ RemoteMediaClient โดยอัตโนมัติ แอปของคุณจะเข้าถึงอินสแตนซ์ดังกล่าวได้โดยเรียกใช้เมธอด getRemoteMediaClient() ในอินสแตนซ์ CastSession
เมธอดทั้งหมดของ RemoteMediaClient ที่ส่งคำขอไปยังตัวรับสัญญาณบนเว็บจะแสดงผลออบเจ็กต์ PendingResult ที่ใช้ติดตามคำขอนั้นได้
คาดว่าอินสแตนซ์ของ RemoteMediaClient อาจแชร์โดยแอปของคุณหลายส่วน และคอมโพเนนต์ภายในบางส่วนของเฟรมเวิร์ก เช่น มินิคอนโทรลเลอร์แบบถาวรและบริการแจ้งเตือน
ด้วยเหตุนี้ อินสแตนซ์นี้จึงรองรับการลงทะเบียนอินสแตนซ์ RemoteMediaClient.Listener หลายรายการ
ตั้งค่าข้อมูลเมตาของสื่อ
คลาส MediaMetadata แสดงข้อมูลเกี่ยวกับรายการสื่อที่คุณต้องการแคสต์ ตัวอย่างต่อไปนี้จะสร้างอินสแตนซ์ MediaMetadata ใหม่ของภาพยนตร์และตั้งค่าชื่อ คำบรรยาย และรูปภาพ 2 รูป
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))))
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_TITLE, mSelectedMedia.getTitle()); movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, mSelectedMedia.getStudio()); movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(0)))); movieMetadata.addImage(new WebImage(Uri.parse(mSelectedMedia.getImage(1))));
ดูการเลือกรูปภาพเกี่ยวกับการใช้รูปภาพที่มีข้อมูลเมตาของสื่อ
โหลดสื่อ
แอปของคุณสามารถโหลดรายการสื่อได้ ดังที่แสดงในโค้ดต่อไปนี้ ขั้นแรกให้ใช้ MediaInfo.Builder กับข้อมูลเมตาของสื่อเพื่อสร้างอินสแตนซ์ MediaInfo รับ RemoteMediaClient จาก CastSession ปัจจุบัน แล้วโหลด MediaInfo ลงใน RemoteMediaClient นั้น ใช้ RemoteMediaClient เพื่อเล่น หยุดชั่วคราว และควบคุมแอปมีเดียเพลเยอร์ที่ทำงานบน Web Receiver
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())
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 แอปผู้ส่งควรมีการควบคุมแบบถาวรที่เรียกว่าตัวควบคุมขนาดเล็ก ซึ่งควรปรากฏขึ้นเมื่อผู้ใช้ออกจากหน้าเนื้อหาปัจจุบันไปยังส่วนอื่นของแอปผู้ส่ง ตัวควบคุมขนาดเล็กจะช่วยเตือนให้ผู้ใช้เห็นเซสชัน Cast ปัจจุบัน เมื่อแตะตัวควบคุมขนาดเล็ก ผู้ใช้จะกลับไปที่มุมมองตัวควบคุมแบบขยายเต็มหน้าจอของ Cast ได้
เฟรมเวิร์กมีมุมมองที่กำหนดเองชื่อ MiniControllerFragment ซึ่งคุณสามารถเพิ่มที่ด้านล่างของไฟล์เลย์เอาต์ของแต่ละกิจกรรมที่ต้องการแสดงตัวควบคุมขนาดเล็กได้
<fragment
android:id="@+id/castMiniController"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"
class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />
เมื่อแอปผู้ส่งกำลังเล่นไลฟ์สดวิดีโอหรือเสียง SDK จะแสดงปุ่มเล่น/หยุดแทนปุ่มเล่น/หยุดชั่วคราวในตัวควบคุมขนาดเล็กโดยอัตโนมัติ
หากต้องการตั้งค่าลักษณะข้อความของชื่อและคำบรรยายของมุมมองที่กำหนดเองนี้ และเลือกปุ่ม โปรดดูหัวข้อปรับแต่งตัวควบคุมขนาดเล็ก
เพิ่มตัวควบคุมแบบขยาย
รายการตรวจสอบการออกแบบ Google Cast กำหนดให้แอปผู้ส่งต้องมีตัวควบคุมแบบขยายสำหรับสื่อที่แคสต์ ตัวควบคุมแบบขยายคือตัวควบคุมขนาดเล็กเวอร์ชันเต็มหน้าจอ
Cast SDK มีวิดเจ็ตสำหรับตัวควบคุมแบบขยายที่เรียกว่า ExpandedControllerActivity
นี่คือคลาส Abstract ที่คุณต้องสร้างคลาสย่อยเพื่อเพิ่มปุ่มแคสต์
ก่อนอื่น ให้สร้างไฟล์ทรัพยากรเมนูใหม่สำหรับตัวควบคุมแบบขยายเพื่อแสดงปุ่มแคสต์ โดยทำดังนี้
<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 } }
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 เพื่อตั้งค่ากิจกรรมเป้าหมายเป็นกิจกรรมใหม่ ดังนี้
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() }
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() ) }
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 คุณสามารถใช้ปุ่มจริงบนอุปกรณ์ผู้ส่งเพื่อเปลี่ยนระดับเสียงของเซสชันการแคสต์ใน Web Receiver โดยค่าเริ่มต้นสำหรับอุปกรณ์ที่ใช้ Jelly Bean ขึ้นไป
การควบคุมระดับเสียงด้วยปุ่มจริงก่อน Jelly Bean
หากต้องการใช้ปุ่มปรับระดับเสียงเพื่อควบคุมระดับเสียงของอุปกรณ์ตัวรับสัญญาณบนเว็บในอุปกรณ์ Android ที่เก่ากว่า Jelly Bean แอปผู้ส่งควรลบล้าง dispatchKeyEvent ในกิจกรรมของตน และเรียกใช้ CastContext.onDispatchVolumeKeyEventBeforeJellyBean():
class MyActivity : FragmentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { return (CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event)) } }
class MyActivity extends FragmentActivity { @Override public boolean dispatchKeyEvent(KeyEvent event) { return CastContext.getSharedInstance(this) .onDispatchVolumeKeyEventBeforeJellyBean(event) || super.dispatchKeyEvent(event); } }
เพิ่มตัวควบคุมสื่อลงในการแจ้งเตือนและหน้าจอล็อก
ใน Android เท่านั้น รายการตรวจสอบการออกแบบ Google Cast กำหนดให้แอปผู้ส่งต้องใช้ตัวควบคุมสื่อในการแจ้งเตือนและในหน้าจอล็อก โดยที่ผู้ส่งกำลังแคสต์แต่แอปผู้ส่งไม่ได้โฟกัสอยู่ เฟรมเวิร์กมี 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()
// 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()
// ... 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 จะขอโฟกัสเสียงในนามของคุณโดยอัตโนมัติ
จัดการข้อผิดพลาด
แอปผู้ส่งต้องจัดการ Callback ข้อผิดพลาดทั้งหมดและตัดสินใจเลือกการตอบสนองที่ดีที่สุดสำหรับแต่ละระยะของวงจรการใช้งาน Cast แอปสามารถแสดงกล่องโต้ตอบข้อผิดพลาดต่อผู้ใช้ หรือจะเลือกปิดการเชื่อมต่อกับ Web Receiver ก็ได้