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. 在您的活动生命周期中创建和管理 MediaRouter.Callback 方法

本部分介绍前四个步骤。下一部分将介绍 Callback 方法。

使用 AppCompatActivity

当您在活动中使用媒体路由框架时,您应该从 AppCompatActivity 扩展活动,并导入包 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 扩展您的活动,并在活动创建时通过从 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 后,您现在可以将媒体路由按钮添加到活动。为您的每个活动覆盖 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 过滤)。每个活动使用其自己的 MediaRouter.Callback 方法实现与 MediaRouter 进行通信。每当用户选择、更改或断开连接路由时,MediaRouter 都会调用回调方法。

回调中包含多个方法,您可以覆盖这些方法以接收有关路由事件的信息。至少,您对 MediaRouter.Callback 类的实现应该覆盖 onRouteSelected()onRouteUnselected()

由于 MediaRouter 是一个共享资源,因此您的应用需要根据通常的活动生命周期回调来管理其 MediaRouter 回调

  • 当活动创建时 (onCreate(Bundle)),获取指向 MediaRouter 的指针,并在应用的整个生命周期内保留它。
  • 当活动变为可见时 (onStart()) 将回调附加到 MediaRouter,当活动隐藏时 (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();
    }
    ...
}

MediaRouter 框架还提供了一个 MediaRouteDiscoveryFragment 类,它负责为活动添加和删除回调。

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

控制远程播放路由

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

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

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

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

RemotePlaybackClient 类还支持将多个媒体项目排队以进行播放,以及管理媒体队列。

示例代码

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