คู่มือนักพัฒนาซอฟต์แวร์นี้จะอธิบายวิธีเพิ่มการสนับสนุน Google Cast ในแอปผู้ส่งของ Android โดยใช้ SDK ผู้ส่งของ Android
อุปกรณ์เคลื่อนที่หรือแล็ปท็อปเป็นผู้ส่งซึ่งควบคุมการเล่น และอุปกรณ์ Google Cast คือตัวรับซึ่งแสดงเนื้อหาบนทีวี
เฟรมเวิร์กผู้ส่งหมายถึงไบนารีไลบรารีคลาสของ Cast และทรัพยากรที่เกี่ยวข้องซึ่งแสดงขึ้นขณะรันไทม์ของผู้ส่ง แอปผู้ส่งหรือแอปแคสต์หมายถึงแอปที่กำลังทำงานกับผู้ส่งด้วยเช่นกัน แอป Web Receiver หมายถึงแอปพลิเคชัน HTML ที่ทำงานในอุปกรณ์ที่พร้อมใช้งาน Cast
เฟรมเวิร์กผู้ส่งใช้การออกแบบ Callback แบบไม่พร้อมกันเพื่อแจ้งให้แอปผู้ส่งทราบถึงเหตุการณ์ต่างๆ และเพื่อสลับระหว่างสถานะต่างๆ ในวงจรของแอปแคสต์
ขั้นตอนของแอป
ขั้นตอนต่อไปนี้จะอธิบายถึงขั้นตอนการดำเนินการระดับสูงโดยทั่วไปสำหรับแอป Android ของผู้ส่ง
- เฟรมเวิร์กการแคสต์จะเริ่มการค้นพบอุปกรณ์
MediaRouter
โดยอัตโนมัติตามวงจรชีวิตของActivity
- เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เฟรมเวิร์กจะแสดงกล่องโต้ตอบ "แคสต์" ที่มีรายการอุปกรณ์แคสต์ที่พบ
- เมื่อผู้ใช้เลือกอุปกรณ์แคสต์ เฟรมเวิร์กจะพยายามเปิดแอปตัวรับเว็บในอุปกรณ์แคสต์
- เฟรมเวิร์กนี้จะเรียกใช้ Callback ในแอปผู้ส่งเพื่อยืนยันว่ามีการเปิดตัวแอป Web Receiver แล้ว
- เฟรมเวิร์กจะสร้างช่องทางการสื่อสารระหว่างผู้ส่งกับแอป Web Receiver
- เฟรมเวิร์กนี้จะใช้ช่องทางการสื่อสารเพื่อโหลดและควบคุมการเล่นสื่อบน Web Receiver
- เฟรมเวิร์กจะซิงค์สถานะการเล่นสื่อระหว่างผู้ส่งกับตัวรับเว็บ โดยเฟรมเวิร์กจะอัปเดตสถานะ UI ของผู้ส่ง เฟรมเวิร์กจะส่งคำขอตัวควบคุมสื่อดังกล่าวไปยังเว็บรีซีฟเวอร์
- เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เพื่อยกเลิกการเชื่อมต่อจากอุปกรณ์ Cast เฟรมเวิร์กจะยกเลิกการเชื่อมต่อแอปผู้ส่งจาก Web Receiver
สำหรับรายการคลาส วิธีการ และเหตุการณ์ทั้งหมดใน Google Cast Android SDK โปรดดูที่ข้อมูลอ้างอิง API ผู้ส่งของ Google Cast สำหรับ Android ส่วนต่อไปนี้จะอธิบายถึงขั้นตอนในการเพิ่มแคสต์ไปยังแอป Android
กำหนดค่าไฟล์ Manifest ของ Android
ไฟล์ AndroidManifest.xml ของแอปกำหนดให้คุณต้องกำหนดค่าองค์ประกอบต่อไปนี้สำหรับ Cast SDK
uses-sdk
ตั้งค่าระดับ API ขั้นต่ำและเป้าหมายที่ Cast SDK รองรับ ปัจจุบัน API ระดับต่ำสุดคือ API ระดับ 23 และเป้าหมายคือ API ระดับ 34
<uses-sdk
android:minSdkVersion="23"
android:targetSdkVersion="34" />
android:theme
ตั้งค่าธีมของแอปตามเวอร์ชัน Android SDK ขั้นต่ำ เช่น หากไม่ใช้ธีมของตัวเอง คุณควรใช้ตัวแปร Theme.AppCompat
เมื่อกำหนดเป้าหมาย Android SDK เวอร์ชันก่อนเป็น Lollipop ขั้นต่ำ
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" >
...
</application>
เริ่มต้นบริบทการแคสต์
เฟรมเวิร์กมีออบเจ็กต์ CastContext
แบบ Singleton ที่ประสานงานการโต้ตอบทั้งหมดของเฟรมเวิร์ก
แอปของคุณต้องใช้อินเทอร์เฟซ
OptionsProvider
เพื่อระบุตัวเลือกที่จำเป็นในการเริ่มต้น
CastContext
เดี่ยว OptionsProvider
มีอินสแตนซ์ของ CastOptions
ซึ่งมีตัวเลือกที่ส่งผลต่อลักษณะการทำงานของเฟรมเวิร์ก สิ่งสำคัญที่สุดคือรหัสแอปพลิเคชัน Web Receiver ซึ่งใช้เพื่อกรองผลการค้นพบและเปิดแอป Web Receiver เมื่อเซสชันการแคสต์เริ่มต้น
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); } }
วิดเจ็ต Cast UX
กรอบการทำงานของ Cast มีวิดเจ็ตที่เป็นไปตามรายการตรวจสอบของการออกแบบแคสต์:
การวางซ้อนบทนำ: เฟรมเวิร์กนี้มีมุมมองแบบกำหนดเอง
IntroductoryOverlay
ซึ่งจะแสดงให้ผู้ใช้เห็นเพื่อเรียกความสนใจไปยังปุ่ม "แคสต์" ในครั้งแรกที่รีซีฟเวอร์พร้อมให้ใช้งาน แอปผู้ส่งสามารถปรับแต่งข้อความและตำแหน่งของข้อความชื่อปุ่ม "แคสต์": ปุ่ม "แคสต์" จะปรากฏขึ้นไม่ว่าอุปกรณ์แคสต์จะพร้อมใช้งานหรือไม่ก็ตาม เมื่อผู้ใช้คลิกปุ่ม "แคสต์" เป็นครั้งแรก กล่องโต้ตอบ "แคสต์" จะปรากฏขึ้น ซึ่งจะแสดงรายการอุปกรณ์ที่ค้นพบ เมื่อผู้ใช้คลิกปุ่ม "แคสต์" ในขณะที่อุปกรณ์เชื่อมต่ออยู่ จะแสดงข้อมูลเมตาของสื่อปัจจุบัน (เช่น ชื่อสตูดิโอบันทึกเสียงและภาพขนาดย่อ) หรืออนุญาตให้ผู้ใช้ยกเลิกการเชื่อมต่อจากอุปกรณ์แคสต์ บางครั้ง "ปุ่ม "แคสต์" เรียกว่า "ไอคอนแคสต์"
มินิคอนโทรลเลอร์: เมื่อผู้ใช้กำลังแคสต์เนื้อหาและออกจากหน้าเนื้อหาปัจจุบันหรือออกไปจากหน้าเนื้อหาปัจจุบันหรือขยายตัวควบคุมไปยังหน้าจออื่นในแอปผู้ส่ง มินิคอนโทรลเลอร์จะแสดงที่ด้านล่างของหน้าจอเพื่อให้ผู้ใช้ดูข้อมูลเมตาของสื่อที่กำลังแคสต์อยู่และควบคุมการเล่นได้
ตัวควบคุมที่ขยาย: เมื่อผู้ใช้กำลังแคสต์เนื้อหา หากผู้ใช้คลิกที่การแจ้งเตือนสื่อหรือมินิตัวควบคุม ตัวควบคุมที่ขยายแล้วจะเปิดตัว ซึ่งจะแสดงข้อมูลเมตาของสื่อที่เล่นอยู่ในขณะนั้นและมีปุ่มหลายปุ่มสำหรับควบคุมการเล่นสื่อ
การแจ้งเตือน: Android เท่านั้น เมื่อผู้ใช้กำลังแคสต์เนื้อหาและออกจากแอปผู้ส่ง ระบบจะแสดงการแจ้งเตือนสื่อซึ่งแสดงข้อมูลเมตาของสื่อที่แคสต์และการควบคุมการเล่นอยู่ในปัจจุบัน
หน้าจอล็อก: Android เท่านั้น เมื่อผู้ใช้กำลังแคสต์เนื้อหาและไปยังส่วนต่างๆ (หรืออุปกรณ์หมดเวลา) ไปยังหน้าจอล็อก ตัวควบคุมหน้าจอล็อกของสื่อจะปรากฏขึ้น ซึ่งจะแสดงข้อมูลเมตาของสื่อที่แคสต์และการควบคุมการเล่นอยู่ในปัจจุบัน
คำแนะนำต่อไปนี้มีคำอธิบายวิธีเพิ่มวิดเจ็ตเหล่านี้ลงในแอป
เพิ่มปุ่ม "แคสต์"
API ของ Android
MediaRouter
ออกแบบมาเพื่อเปิดใช้การแสดงและการเล่นสื่อในอุปกรณ์รอง
แอป Android ที่ใช้ MediaRouter
API ควรมีปุ่ม "แคสต์" เป็นส่วนหนึ่งของอินเทอร์เฟซผู้ใช้ เพื่อให้ผู้ใช้เลือกเส้นทางสื่อเพื่อเล่นสื่อในอุปกรณ์รอง เช่น อุปกรณ์แคสต์ ได้
เฟรมเวิร์กนี้ทำให้การเพิ่ม MediaRouteButton
เป็น Cast button
นั้นง่ายมาก คุณควรเพิ่มรายการเมนูหรือ MediaRouteButton
ในไฟล์ XML ที่กำหนดเมนูก่อน แล้วใช้ CastButtonFactory
เพื่อเชื่อมโยงกับเฟรมเวิร์ก
// To add a Cast button, add the following snippet.
// menu.xml
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
// Then override the onCreateOptionMenu() for each of your activities. // MyActivity.kt override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.main, menu) CastButtonFactory.setUpMediaRouteButton( applicationContext, menu, R.id.media_route_menu_item ) return true }
// 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 กำลังพยายามกู้คืนเซสชันการแคสต์
ขั้นตอนการค้นหาจะหยุดลงเมื่อปิดกล่องโต้ตอบการแคสต์หรือเมื่อแอปผู้ส่งเข้าสู่พื้นหลัง
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 หรือจออัจฉริยะ สื่อจะหยุดเล่นบนอุปกรณ์หนึ่ง (ต้นทาง) และจะเล่นต่อในอุปกรณ์อื่น (ปลายทาง) อุปกรณ์แคสต์ทุกเครื่องที่มีเฟิร์มแวร์เวอร์ชันล่าสุดจะใช้เป็นแหล่งที่มาหรือปลายทางในการโอนสตรีมได้
หากต้องการรับอุปกรณ์ปลายทางใหม่ระหว่างการโอนหรือขยายสตรีม ให้ลงทะเบียน Cast.Listener
โดยใช้ CastSession#addCastListener
จากนั้นโทร CastSession#getCastDevice()
ระหว่างการติดต่อกลับ onDeviceNameChanged
ดูข้อมูลเพิ่มเติมได้ที่การโอนสตรีมบน Web Receiver
การเชื่อมต่ออีกครั้งอัตโนมัติ
เฟรมเวิร์กนี้จะมี ReconnectionService
ที่แอปผู้ส่งเปิดใช้เพื่อจัดการการเชื่อมต่ออีกครั้งในกรณีเล็กๆ น้อยๆ ได้หลายกรณี เช่น
- กู้คืนจากสัญญาณ Wi-Fi ที่หายไปชั่วคราว
- กู้คืนจากโหมดสลีปของอุปกรณ์
- กู้คืนจากการเล่นขณะล็อกหน้าจอหรือขณะใช้แอปอื่น
- กู้คืนหากแอปขัดข้อง
บริการนี้เปิดอยู่โดยค่าเริ่มต้น และจะปิดได้ในCastOptions.Builder
ระบบจะรวมบริการนี้เป็นไฟล์ Manifest ของแอปโดยอัตโนมัติหากเปิดใช้การผสานอัตโนมัติในไฟล์ Gradle
เฟรมเวิร์กจะเริ่มบริการเมื่อมีเซสชันสื่อ และหยุดบริการเมื่อเซสชันสื่อสิ้นสุดลง
วิธีการทำงานของการควบคุมสื่อ
เฟรมเวิร์กของ Cast จะเลิกใช้คลาส RemoteMediaPlayer
จาก Cast 2.x เพื่อใช้คลาสใหม่ RemoteMediaClient
ซึ่งมีฟังก์ชันการทำงานเดียวกันในชุด API ที่สะดวกยิ่งขึ้น และหลีกเลี่ยงการส่งผ่าน GoogleApiClient
เมื่อแอปของคุณสร้าง CastSession
ด้วยแอป Web Receiver ที่รองรับเนมสเปซของสื่อ เฟรมเวิร์กจะสร้างอินสแตนซ์ของ RemoteMediaClient
โดยอัตโนมัติ แอปของคุณจะเข้าถึงได้ด้วยการเรียกใช้เมธอด getRemoteMediaClient()
ในอินสแตนซ์ CastSession
เมธอดทั้งหมดของ RemoteMediaClient
ที่ส่งคำขอไปยัง Web Receiver จะแสดงออบเจ็กต์ PendingResult ที่ใช้ติดตามคำขอนั้นได้
คาดว่าหลายส่วนของแอปอาจแชร์อินสแตนซ์ของ RemoteMediaClient
รวมถึงคอมโพเนนต์ภายในบางอย่างของเฟรมเวิร์ก เช่น ตัวควบคุมขนาดเล็กถาวรและบริการการแจ้งเตือน
ด้วยเหตุนี้ อินสแตนซ์นี้รองรับการลงทะเบียน RemoteMediaClient.Listener
หลายอินสแตนซ์
ตั้งค่าข้อมูลเมตาของสื่อ
คลาส MediaMetadata
แสดงถึงข้อมูลเกี่ยวกับรายการสื่อที่คุณต้องการแคสต์ ตัวอย่างต่อไปนี้จะสร้างอินสแตนซ์ MediaMetadata ใหม่ของภาพยนตร์และตั้งค่าชื่อ คำบรรยาย และรูปภาพ 2 รูป
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 ได้
เฟรมเวิร์กจะมี 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
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 คุณสามารถใช้ปุ่มบนอุปกรณ์ผู้ส่งเพื่อเปลี่ยนระดับเสียงของเซสชันการแคสต์บนตัวรับเว็บโดยค่าเริ่มต้นสำหรับอุปกรณ์ใดก็ได้ที่ใช้ 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); } }
เพิ่มตัวควบคุมสื่อลงในการแจ้งเตือนและหน้าจอล็อก
รายการตรวจสอบสำหรับการออกแบบ Google Cast กำหนดให้แอปผู้ส่งต้องใช้การควบคุมสื่อในการแจ้งเตือนและในหน้าจอล็อกบน Android เท่านั้น ซึ่งผู้ส่งกำลังแคสต์แต่แอปผู้ส่งไม่มีโฟกัส เฟรมเวิร์กนี้จะให้ข้อมูล
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 จะแสดงปุ่มเล่น/หยุดโดยอัตโนมัติแทนปุ่มเล่น/หยุดชั่วคราวบนตัวควบคุมการแจ้งเตือน แต่จะไม่แสดงการควบคุมหน้าจอล็อก
หมายเหตุ: หากต้องการแสดงตัวควบคุมหน้าจอล็อกในอุปกรณ์ Pre-Lollipop
RemoteMediaClient
จะขอโฟกัสเสียงในนามของคุณโดยอัตโนมัติ
จัดการข้อผิดพลาด
แอปของผู้ส่งควรจัดการกับการเรียกกลับที่ผิดพลาดทั้งหมดและตัดสินใจเลือกการตอบสนองที่ดีที่สุดสำหรับแต่ละขั้นในวงจรของการแคสต์ แอปสามารถแสดงกล่องโต้ตอบข้อผิดพลาดให้ผู้ใช้เห็นหรือตัดสินใจที่จะยกเลิกการเชื่อมต่อกับเว็บรีซีฟเวอร์