คู่มือนักพัฒนาซอฟต์แวร์นี้จะอธิบายวิธีเพิ่มการสนับสนุน Google Cast ไปยังแอปผู้ส่งของ Android โดยใช้ Android Sender SDK
อุปกรณ์เคลื่อนที่หรือแล็ปท็อปคือผู้ส่งที่ควบคุมการเล่น และอุปกรณ์ Google Cast คือตัวรับที่แสดงเนื้อหาบน TV
เฟรมเวิร์กของผู้ส่งหมายถึงไบนารีของไลบรารีคลาส Cast และทรัพยากรที่เกี่ยวข้องซึ่งพบขณะรันไทม์ในตัวผู้ส่ง แอปของผู้ส่งหรือแอป Cast หมายถึงแอปที่ทำงานบนผู้ส่งด้วย แอป Web Receiver หมายถึงแอปพลิเคชัน HTML ที่ทำงานบนอุปกรณ์ที่พร้อมใช้งาน Cast
เฟรมเวิร์กของผู้ส่งใช้การออกแบบโค้ดเรียกกลับแบบไม่พร้อมกันเพื่อแจ้งแก่แอปของผู้ส่งเกี่ยวกับเหตุการณ์และเพื่อสลับไปมาระหว่างสถานะต่างๆ ของวงจรชีวิตของแอป Cast
ขั้นตอนของแอป
ขั้นตอนต่อไปนี้อธิบายกระบวนการดำเนินการระดับสูงโดยทั่วไปสำหรับแอป Android ของผู้ส่ง
- เฟรมเวิร์ก Cast จะเริ่มต้นการค้นหาอุปกรณ์
MediaRouter
โดยอัตโนมัติตามวงจรชีวิตของActivity
- เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เฟรมเวิร์กจะแสดงกล่องโต้ตอบ "แคสต์" พร้อมรายการอุปกรณ์แคสต์ที่พบ
- เมื่อผู้ใช้เลือกอุปกรณ์แคสต์ เฟรมเวิร์กจะพยายามเปิดแอป Web Receiver บนอุปกรณ์แคสต์
- เฟรมเวิร์กจะเรียกใช้โค้ดเรียกกลับในแอปของผู้ส่งเพื่อยืนยันว่ามีการเปิดแอป WebReceiver แล้ว
- เฟรมเวิร์กจะสร้างช่องทางการสื่อสารระหว่างผู้ส่งกับแอป WebReceiver
- โดยเฟรมเวิร์กจะใช้ช่องทางการสื่อสารเพื่อโหลดและควบคุมการเล่นสื่อบนรีซีฟเวอร์
- เฟรมเวิร์กจะซิงค์สถานะการเล่นสื่อระหว่างผู้ส่งกับเว็บรีซีฟเวอร์ เมื่อผู้ใช้สร้างการทำงาน UI ของผู้ส่ง เฟรมเวิร์กจะส่งคำขอการควบคุมสื่อเหล่านั้นไปยังเว็บรีซีฟเวอร์ และเมื่อเว็บรีซีฟเวอร์ส่งการอัปเดตสถานะสื่อ เฟรมเวิร์กจะอัปเดตสถานะของ UI ของผู้ส่ง
- เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เพื่อยกเลิกการเชื่อมต่อจากอุปกรณ์แคสต์ เฟรมเวิร์กจะยกเลิกการเชื่อมต่อแอปผู้ส่งจาก Web Receiver
สำหรับรายการที่ครอบคลุมของคลาส เมธอด และเหตุการณ์ทั้งหมดใน 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
เมื่อกำหนดเป้าหมาย SDK เวอร์ชันขั้นต่ำของ Android ที่เป็น 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 Loading เมื่อมีการเรียกใช้ 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 มอบวิดเจ็ตที่เป็นไปตามรายการตรวจสอบของการออกแบบ การแคสต์
การวางซ้อนบทนำ: เฟรมเวิร์กมอบการแสดงผลที่กำหนดเอง
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 }
// 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
เป็นการภายใน และจะเริ่มต้นกระบวนการค้นหาเมื่อแอปของผู้ส่งเข้าสู่เบื้องหน้า และจะหยุดเมื่อแอปของผู้ส่งเข้าสู่เบื้องหลัง
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 จะแนะนำแนวคิดเกี่ยวกับเซสชันการแคสต์ ซึ่งเป็นการสร้างซึ่งรวมขั้นตอนการเชื่อมต่ออุปกรณ์ การเปิด (หรือเข้าร่วม) แอปเว็บตัวรับสัญญาณ การเชื่อมต่อกับแอปดังกล่าว และการเริ่มต้นช่องทางควบคุมสื่อ ดูข้อมูลเพิ่มเติมเกี่ยวกับเซสชันการแคสต์และวงจรการทำงานของเว็บรีซีฟเวอร์ได้ที่คู่มือวงจรการใช้งานแอปพลิเคชัน
เซสชันจะได้รับการจัดการโดยชั้นเรียน 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 } 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 } }
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
ด้วยแอป Web Receiver ที่รองรับเนมสเปซสื่อ เฟรมเวิร์กจะสร้างอินสแตนซ์ RemoteMediaClient
โดยอัตโนมัติ ซึ่งแอปของคุณจะเข้าถึงได้โดยเรียกใช้เมธอด getRemoteMediaClient()
ในอินสแตนซ์ CastSession
เมธอดทั้งหมดของ RemoteMediaClient
ที่ส่งคำขอไปยัง Web Receiver จะแสดงออบเจ็กต์ Pendingผลลัพธ์ ที่ใช้ติดตามคำขอนั้นได้
เราคาดว่าหลายๆ ส่วนของแอปอาจใช้อินสแตนซ์ของ 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
เพื่อเล่น หยุดชั่วคราว และควบคุมแอปโปรแกรมเล่นสื่อที่ทำงานอยู่บนรีซีฟเวอร์
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 ได้
เฟรมเวิร์กจะมี 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 จะแสดงปุ่มเล่น/หยุดแทนปุ่มเล่น/หยุดชั่วคราวในตัวควบคุมขนาดเล็กโดยอัตโนมัติ
หากต้องการกำหนดลักษณะข้อความของชื่อและคำบรรยายของมุมมองที่กำหนดเองนี้ และการเลือกปุ่ม โปรดดู ปรับแต่งมินิคอนโทรล
เพิ่มตัวควบคุมที่ขยาย
รายการตรวจสอบการออกแบบของ 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
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 จะแสดงปุ่มเล่น/หยุดโดยอัตโนมัติแทนปุ่มเล่น/หยุดชั่วคราวในตัวควบคุมที่ขยาย
หากต้องการตั้งค่าลักษณะที่ปรากฏโดยใช้ธีม ให้เลือกปุ่มที่จะแสดง และเพิ่มปุ่มที่กำหนดเอง โปรดดู ปรับแต่งตัวควบคุมที่ขยาย
การควบคุมระดับเสียง
เฟรมเวิร์กจะจัดการวอลุ่มสำหรับแอปของผู้ส่งโดยอัตโนมัติ โดยเฟรมเวิร์กจะซิงค์แอปของผู้ส่งและเว็บรีซีฟเวอร์โดยอัตโนมัติเพื่อให้ UI ของผู้ส่งรายงานปริมาณที่ Web Receiver ระบุเสมอ
การควบคุมระดับเสียงของปุ่มบนเครื่อง
ใน Android ปุ่มบนอุปกรณ์ผู้ส่งสามารถใช้เพื่อเปลี่ยนระดับเสียงของเซสชันการแคสต์บนเว็บรีซีฟเวอร์โดยค่าเริ่มต้นสำหรับอุปกรณ์เครื่องใดก็ตามที่ใช้ Jelly Bean ขึ้นไป
การควบคุมระดับเสียงของปุ่มบนเครื่องก่อน Jelly Bean
หากต้องการใช้แป้นปรับระดับเสียงเพื่อควบคุมระดับเสียงของอุปกรณ์ Web Receiver บนอุปกรณ์ 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
จะขอโฟกัสเสียงในนามของคุณโดยอัตโนมัติ
จัดการข้อผิดพลาด
แอปของผู้ส่งต้องจัดการกับการเรียกกลับข้อผิดพลาดทั้งหมดและตัดสินใจเลือกการตอบสนองที่ดีที่สุดสำหรับแต่ละขั้นตอนของวงจรการแคสต์ แอปสามารถแสดงกล่องโต้ตอบข้อผิดพลาดแก่ผู้ใช้หรืออาจตัดสินใจตัดการเชื่อมต่อกับเว็บรีซีฟเวอร์