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 方法

本部分介绍了前四个步骤。下一部分介绍了 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 是共享资源,因此您的应用需要管理其 MediaRouter 回调,以响应常规的 activity 生命周期回调:

  • 创建 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,并从服务的生命周期回调中调用媒体路由器框架。

控制远程播放路由

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

RemotePlaybackClient 类提供了其他用于管理内容播放的方法。以下是一些来自 RemotePlaybackClient 类的主要播放方法:

  • play() - 播放由 Uri 指定的特定媒体文件。
  • pause() - 暂停当前正在播放的媒体轨道。
  • resume() - 执行暂停命令后继续播放当前曲目。
  • seek() - 移至当前轨道中的特定位置。
  • release() - 断开应用与远程播放设备的连接。

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

RemotePlaybackClient 类还支持将多个媒体项加入队列,以便播放和管理媒体队列。

示例代码

Android BasicMediaRouterMediaRouter 示例进一步演示了如何使用 MediaRouter API。