MediaRouter 概览

要在您的应用中使用 MediaRouter 框架,您必须获取一个 MediaRouter 对象的实例,并附加一个 MediaRouter.Callback 对象以监听路由事件。通过媒体路由发送的内容会经过该路由关联的 MediaRouteProvider(少数特殊情况除外,例如蓝牙输出设备)。图 1 提供了用于在设备之间路由内容的类的高级视图。

图 1. 应用使用的关键媒体路由类概览。

注意:如果您的应用要支持 Google Cast 设备,您应该使用 Cast SDK 并将您的应用构建为 Cast 发送器。请遵循 Cast 文档中的说明,而不是直接使用 MediaRouter 框架。

媒体路由按钮

Android 应用应使用媒体路由按钮来控制媒体路由。MediaRouter 框架为该按钮提供了一个标准界面,有助于用户在可用时识别和使用路由功能。媒体路由按钮通常位于应用操作栏的右侧,如图 2 所示。

图 2. 操作栏中的媒体路由按钮。

当用户按下媒体路由按钮时,可用的媒体路由会以列表形式显示,如图 3 所示。

图 3. 按下媒体路由按钮后显示的可用媒体路由列表。

按照以下步骤创建媒体路由按钮

  1. 使用 AppCompatActivity
  2. 定义媒体路由按钮菜单项
  3. 创建 MediaRouteSelector
  4. 将媒体路由按钮添加到操作栏
  5. 在 activity 的生命周期中创建和管理 MediaRouter.Callback 方法

本节描述前四个步骤。下一节描述回调方法。

使用 AppCompatActivity

当您在 activity 中使用媒体路由框架时,您应该从 AppCompatActivity 扩展 activity 并导入包 androidx.appcompat.app。您必须将 androidx.appcompat:appcompatandroidx.mediarouter:mediarouter 支持库添加到您的应用开发项目中。有关向项目添加支持库的更多信息,请参阅 Android Jetpack 入门

注意:请务必使用媒体路由框架的 androidx 实现。请勿使用旧版 android.media 包。

创建一个 XML 文件,为媒体路由按钮定义一个菜单项。该项的操作应为 MediaRouteActionProvider 类。这是一个示例文件

// myMediaRouteButtonMenuItem.xml
<?xml version="1.0" encoding="utf-8"?>
<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>

创建 MediaRouteSelector

媒体路由按钮菜单中显示的路由由 MediaRouteSelector 确定。从 AppCompatActivity 扩展您的 activity,并在 activity 创建时从 onCreate() 方法调用 MediaRouteSelector.Builder 来构建选择器,如以下代码示例所示。请注意,选择器保存在类变量中,并且允许的路由类型通过添加 MediaControlIntent 对象来指定

Kotlin

class MediaRouterPlaybackActivity : AppCompatActivity() {

    private var mSelector: MediaRouteSelector? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Create a route selector for the type of routes your app supports.
        mSelector = MediaRouteSelector.Builder()
                // These are the framework-supported intents
                .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
                .build()
    }
}

Java

public class MediaRouterPlaybackActivity extends AppCompatActivity {
    private MediaRouteSelector mSelector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Create a route selector for the type of routes your app supports.
        mSelector = new MediaRouteSelector.Builder()
                // These are the framework-supported intents
                .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
                .build();
    }
}

对于大多数应用,唯一需要的路由类型是 CATEGORY_REMOTE_PLAYBACK。此路由类型将运行您应用的设备视为遥控器。连接的接收设备处理所有内容数据检索、解码和播放。支持 Google Cast 的应用(如 Chromecast)就是这样工作的。

少数制造商支持一种名为“第二输出”的特殊路由选项。通过此路由,您的媒体应用可以直接将视频或音乐检索、渲染和流式传输到所选远程接收设备上的屏幕和/或扬声器。使用第二输出将内容发送到支持无线功能的音乐系统或视频显示器。要启用这些设备的发现和选择,您需要将 CATEGORY_LIVE_AUDIOCATEGORY_LIVE_VIDEO 控制类别添加到 MediaRouteSelector。您还需要创建并处理自己的 Presentation 对话框。

将媒体路由按钮添加到操作栏

定义媒体路由菜单和 MediaRouteSelector 后,您现在可以将媒体路由按钮添加到 activity。为每个 activity 覆盖 onCreateOptionsMenu() 方法以添加选项菜单。

Kotlin

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    super.onCreateOptionsMenu(menu)

    // Inflate the menu and configure the media router action provider.
    menuInflater.inflate(R.menu.sample_media_router_menu, menu)

    // Attach the MediaRouteSelector to the menu item
    val mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item)
    val mediaRouteActionProvider =
            MenuItemCompat.getActionProvider(mediaRouteMenuItem) as MediaRouteActionProvider

    // Attach the MediaRouteSelector that you built in onCreate()
    selector?.also(mediaRouteActionProvider::setRouteSelector)

    // Return true to show the menu.
    return true
}

Java

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);

    // Inflate the menu and configure the media router action provider.
    getMenuInflater().inflate(R.menu.sample_media_router_menu, menu);

    // Attach the MediaRouteSelector to the menu item
    MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
    MediaRouteActionProvider mediaRouteActionProvider =
            (MediaRouteActionProvider)MenuItemCompat.getActionProvider(
            mediaRouteMenuItem);
    // Attach the MediaRouteSelector that you built in onCreate()
    mediaRouteActionProvider.setRouteSelector(selector);

    // Return true to show the menu.
    return true;
}

有关在应用中实现操作栏的更多信息,请参阅操作栏开发者指南。

您还可以将媒体路由按钮作为 MediaRouteButton 添加到任何视图中。您必须使用 setRouteSelector() 方法将 MediaRouteSelector 附加到按钮。有关将媒体路由按钮整合到您的应用中的指南,请参阅 Google Cast 设计清单

MediaRouter 回调

在同一设备上运行的所有应用共享一个 MediaRouter 实例及其路由(由应用的 MediaRouteSelector 按应用过滤)。每个 activity 都使用其自己的 MediaRouter.Callback 方法实现与 MediaRouter 通信。每当用户选择、更改或断开路由时,MediaRouter 都会调用回调方法。

回调中有几种方法可以覆盖以接收有关路由事件的信息。您的 MediaRouter.Callback 类实现至少应覆盖 onRouteSelected()onRouteUnselected()

由于 MediaRouter 是共享资源,您的应用需要响应常规 activity 生命周期回调来管理其 MediaRouter 回调

  • 当 activity 创建时 (onCreate(Bundle)),获取 MediaRouter 的指针并在应用的整个生命周期中保持它。
  • 当 activity 可见时 (onStart()),将回调附加到 MediaRouter,并在 activity 隐藏时 (onStop()) 分离它们。

以下代码示例演示了如何创建和保存回调对象,如何获取 MediaRouter 的实例,以及如何管理回调。请注意,在 onStart() 中附加回调时使用了 CALLBACK_FLAG_REQUEST_DISCOVERY 标志。这允许您的 MediaRouteSelector 刷新媒体路由按钮的可用路由列表。

Kotlin

class MediaRouterPlaybackActivity : AppCompatActivity() {

    private var mediaRouter: MediaRouter? = null
    private var mSelector: MediaRouteSelector? = null

    // Variables to hold the currently selected route and its playback client
    private var mRoute: MediaRouter.RouteInfo? = null
    private var remotePlaybackClient: RemotePlaybackClient? = null

    // Define the Callback object and its methods, save the object in a class variable
    private val mediaRouterCallback = object : MediaRouter.Callback() {

        override fun onRouteSelected(router: MediaRouter, route: MediaRouter.RouteInfo) {
            Log.d(TAG, "onRouteSelected: route=$route")
            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
                // Stop local playback (if necessary)
                // ...

                // Save the new route
                mRoute = route

                // Attach a new playback client
                remotePlaybackClient =
                    RemotePlaybackClient(this@MediaRouterPlaybackActivity, mRoute)

                // Start remote playback (if necessary)
                // ...
            }
        }

        override fun onRouteUnselected(
                router: MediaRouter,
                route: MediaRouter.RouteInfo,
                reason: Int
        ) {
            Log.d(TAG, "onRouteUnselected: route=$route")
            if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {

                // Changed route: tear down previous client
                mRoute?.also {
                    remotePlaybackClient?.release()
                    remotePlaybackClient = null
                }

                // Save the new route
                mRoute = route

                when (reason) {
                    MediaRouter.UNSELECT_REASON_ROUTE_CHANGED -> {
                        // Resume local playback (if necessary)
                        // ...
                    }
                }
            }
        }
    }


    // Retain a pointer to the MediaRouter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Get the media router service.
        mediaRouter = MediaRouter.getInstance(this)
        ...
    }

    // Use this callback to run your MediaRouteSelector to generate the
    // list of available media routes
    override fun onStart() {
        mSelector?.also { selector ->
            mediaRouter?.addCallback(selector, mediaRouterCallback,
                    MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY)
        }
        super.onStart()
    }

    // Remove the selector on stop to tell the media router that it no longer
    // needs to discover routes for your app.
    override fun onStop() {
        mediaRouter?.removeCallback(mediaRouterCallback)
        super.onStop()
    }
    ...
}

Java

public class MediaRouterPlaybackActivity extends AppCompatActivity {
    private MediaRouter mediaRouter;
    private MediaRouteSelector mSelector;

    // Variables to hold the currently selected route and its playback client
    private MediaRouter.RouteInfo mRoute;
    private RemotePlaybackClient remotePlaybackClient;

    // Define the Callback object and its methods, save the object in a class variable
    private final MediaRouter.Callback mediaRouterCallback =
            new MediaRouter.Callback() {

        @Override
        public void onRouteSelected(MediaRouter router, RouteInfo route) {
            Log.d(TAG, "onRouteSelected: route=" + route);

            if (route.supportsControlCategory(
                MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)){
                // Stop local playback (if necessary)
                // ...

                // Save the new route
                mRoute = route;

                // Attach a new playback client
                remotePlaybackClient = new RemotePlaybackClient(this, mRoute);

                // Start remote playback (if necessary)
                // ...
            }
        }

        @Override
        public void onRouteUnselected(MediaRouter router, RouteInfo route, int reason) {
            Log.d(TAG, "onRouteUnselected: route=" + route);

            if (route.supportsControlCategory(
                MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)){

                // Changed route: tear down previous client
                if (mRoute != null && remotePlaybackClient != null) {
                    remotePlaybackClient.release();
                    remotePlaybackClient = null;
                }

                // Save the new route
                mRoute = route;

                if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
                    // Resume local playback  (if necessary)
                    // ...
                }
            }
        }
    }


    // Retain a pointer to the MediaRouter
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Get the media router service.
        mediaRouter = MediaRouter.getInstance(this);
        ...
    }

    // Use this callback to run your MediaRouteSelector to generate the list of available media routes
    @Override
    public void onStart() {
        mediaRouter.addCallback(mSelector, mediaRouterCallback,
                MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
        super.onStart();
    }

    // Remove the selector on stop to tell the media router that it no longer
    // needs to discover routes for your app.
    @Override
    public void onStop() {
        mediaRouter.removeCallback(mediaRouterCallback);
        super.onStop();
    }
    ...
}

媒体路由框架还提供了一个 MediaRouteDiscoveryFragment 类,它负责为 activity 添加和移除回调。

注意:如果您正在编写一个音乐播放应用并希望该应用在后台播放音乐,则必须为播放构建一个 Service,并从 Service 的生命周期回调中调用媒体路由框架。

控制远程播放路由

当您选择远程播放路由时,您的应用将充当遥控器。路由另一端的设备处理所有内容数据检索、解码和播放功能。应用界面中的控件使用 RemotePlaybackClient 对象与接收设备通信。

RemotePlaybackClient 类提供了用于管理内容播放的其他方法。以下是 RemotePlaybackClient 类中的几个关键播放方法

  • play() — 播放由 Uri 指定的特定媒体文件。
  • pause() — 暂停当前播放的媒体轨道。
  • resume() — 在暂停命令后继续播放当前轨道。
  • seek() — 移动到当前轨道中的特定位置。
  • release() — 中断您的应用与远程播放设备之间的连接。

您可以使用这些方法将操作附加到您在应用中提供的播放控件。这些方法中的大多数还允许您包含一个回调对象,以便您可以监控播放任务或控制请求的进度。

RemotePlaybackClient 类还支持多个媒体项的播放排队和媒体队列的管理。

示例代码

Android BasicMediaRouterMediaRouter 示例进一步演示了 MediaRouter API 的用法。