媒体路由器(MediaRouter)

当用户使用无线技术连接电视,家庭影院和音乐播放器时,他们希望Android apps的内容可以在这些大屏幕的设备上播放。启用这种方式的播放,能把一台设备一个用户模式
变为一个乐于分享的、令人激动和鼓舞的多用户模式。
android媒体路由接口被设计成可以在二级设备上呈现和播放媒体。通过使用这些APIs来播放内容有两种实现方式可供使用:

  • 远程播放:当用户手里的一个android设备作为一个远程控制端时,这种方法就利用接收设备来处理检索到的内容数据、解码、和播放。Android apps需要支持Google Cast才能使用该方法。

  • 二级输出:这种方法中,你的app检索、渲染和输出视频或者音频知道到接收设备。Android需要支持无线显示输出才能使用该方法

这篇指导阐明了如何使用这两种方式分别实现你的app传送媒体到二级播放设备。

概述

媒体路由接口支持许多种媒体输出与播放设备通过无线或有线的方式连接android设备。为了使用这些连接,媒体路由框架为android设备的音频和视频输出抽象了逻辑路径。这种架构允许你的app快速地把媒体内容引导至连接的提供android媒体路由支持的播放设备上,如家庭影院和音频系统。

为了在你的app上使用该框架,你必须得到MediaRouter框架对象的一个实例,并且关联一个MediaRouter.Callback对象来监听可用媒体路由的事件。通过媒体路由被导入的内容传递到和路由相关的MediaRouterProvider(尤其在几种特殊的情况下,例如蓝牙输出设备)。下图提供了类的高级视图,你的app可以利用该类通过媒体路由框架来播放内容。

apps使用的关键媒体路由类的概述

那些不支持媒体路由框架的媒体播放器硬件制造商可以为他们的硬件加添支持包,通过实现MediaRouterProvider对象且作为一个应用程序。关于更多实现一个媒体路由提供者的信息,请看MediaRouterProvider参考文档和v7-媒体路由支持库如<sdk> /extras/android/compatibility/v7/mediarouter.

媒体路由包

android支持库版本18或者更高版本中,媒体路由应用程序接口是v7媒体路由支持库的一部分。尤其,你应该使用android.support.v7.media中的媒体路由功能类。这些应用程序接口可以在android 2.1(应用程序接口水平为7)或者更高版本上兼容。

注意:在android.media中提供了媒体路由应用程序接口的另外一个集合,这个集合已被v7媒体路由支持库取代。你不应该再使用android.media类来实现媒体路由的功能。

为了使用android.support.v7.media媒体类库,你应该添加v7-mediarouter 支持库包到你的应用程序开发工程中。

实现用户接口

实现了媒体路由应用程序接口的android apps包含一个按钮用来作为他们的用户接口的一部分,从而允许用户选择媒体路由来在二级输出设备上播放媒体。媒体路由框架为按钮提供了标准的接口,在你的app中你应该使用它来帮助用户识别和使用这个特性。图2举例说明了按钮该如何呈现在你的app上。

图2 一个按钮显示在动态栏的右边

警告:当实现一个提供媒体路由接口的活动页面时,你必须继承android支持库中的ActionBarActivity或者FragmentActivity,即使你的android:minSdkVersion 是API 11或者更高。

按钮

推荐实现用户接口按钮的方法是继承ActionBarAcitivity类,利用onCreateOptionsMenu()方法来添加一个选择菜单。按钮必须使用MediaRouteActionProvider类作为它的行动。


<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="android.support.v7.app.MediaRouteActionProvider"
        app:showAsAction="always"
    />
</menu>

更多关于在app中实现动态栏的信息请看Action Bar开发指南。

一旦你已经添加按钮到你的用户接口,你必须关联一个媒体路由选择器对象。下面一部分将讨论如何创建一个选择器。

如果你不想在你的动作栏创建一个菜单,也可以使用MediaRouteButton来添加一个按钮在你的app中。如果你选择该方法,你应该根据Google Cast Design CheckList添加一个按钮到状态栏中。而且也必须通过setRouteSelector()方法关联一个媒体路由选择器到这个按钮。

关于如何在你的应用程序中加入一个按钮的指导,请回顾Google Cast Design CheckList。

媒体路由选择器

当用户按下按钮时,媒体路由框架将寻找可用的媒体路由并且呈现一个供用户选择的列表,如图3展示:

图3 按下按钮之后呈现出的可用的媒体路由列表

在列表中呈现的媒体路由类型-------如远程播放、二级输出或者其他,在你的应用程序中被定义出来。通过创建一个MediaRouteSelector对象来定义这些类型,该对象要接受来自框架或其你自己或他开发者创建的他媒体路由提供的MediaControlIntent对象。框架提供的路由范畴包含以下几种:

  • CATEGORY_LIVE_AUDIO 音频输出到二级输出设备,例如支持无线的音乐系统

  • CATEGORY_LIVE_VIDEO 视频输出的二级输出设备,例如无线显示设备

  • CATEGORY_REMOTE_PLAYBACK 在支持Google cast远程控制协议的独立设备如Chromecast上播放视频或音频。

当创建一个MediaRouteSelector对象时,可使用MediaRouteSelector.Builder类来创建对象和设置媒体播放类别(控制类别),如以下代码示例展示:

public class MediaRouterPlaybackActivity extends ActionBarActivity {
    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_LIVE_AUDIO)
                .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
                .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
                .build();
    }
}

媒体路由框架使用这个选择器对象为你的应用程序支持的媒体路由选择列表来提供一个接口,如图3所示。一旦你定义了这个选择器,必须把它关联到与菜单栏相关的MediaRouteActionProvider对象。如以下代码示例所示:

@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);
    mediaRouteActionProvider.setRouteSelector(mSelector);

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

一旦你在app中做了这些变化,你可能期待按钮可以呈现在你的活动页面。唉,不可能(除非你的设备已经和无线显示屏配对)。在多数情况下,你必须也和媒体路由框架相联接,下面一部分将讨论这种情况:

连接到媒体路由

为了连接到一个用户选择的媒体路由,你的app必须包含MediaRouter框架对象,然后附加一个MediaRouter.Callbak对象。当一个路由被用户选择,改变或者断开之后,这个回调对象接受来自媒体路由框架的消息。

为了获得一个MediaRouter框架对象实例,可以在一个支持媒体路由应用程序接口的活动页面的onCreate()方法中调用MediaRouter.getInstance()方法。

注意:MediaRouter对象在框架中是一个单例模式。然而,一旦你的应用程序获取到该对象的实力,你必须保持这个实例不被垃圾回收机制回收直到你的应用程序终止。

创建一个MediaRouter的回调函数

媒体路由框架通过你关联到MediaRouter框架实体的回调对象与一个app通信。使用媒体路由框架的的一个app必须继承MediaRouter.Callback对象来接收当媒体路由连接时的消息和通过这个路由器提供内容到连接好的设备。

重写接收关于媒体路由事件消息的回调函数有几种方法。起码MediaRouter.Callback类的实现应该重写以下方法:

  • onRouteSelected()——当用户连接到一个媒体路由输出设备上时调用。

  • onRouteUnselected()——当用户断开一个媒体路由输出设备时调用。

  • onRoutePresentationDisplayChanged()——当展示的显示器改变现实像素,如从720p变到1080p分辨率。

你的MediaRouter.Callback实现方法是第一次机会去决定连接的的路由类型是远程播放设备例如谷歌电视棒还是二级输出设备例如无线播放设备。如果你的app支持这两种类型,那么你的实现方法应该在这里分开,如下面示例代码所示:

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

        if (route.supportsControlCategory(
            MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)){
            // remote playback device
            updateRemotePlayer(route);
        } else {
            // secondary output device
            updatePresentation(route);
        }
    }

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

        if (route.supportsControlCategory(
            MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)){
            // remote playback device
            updateRemotePlayer(route);
        } else {
            // secondary output device
            updatePresentation(route);
        }
    }

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

        if (route.supportsControlCategory(
            MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)){
            // remote playback device
            updateRemotePlayer(route);
        } else {
            // secondary output device
            updatePresentation(route);
        }
    }
}

定义好你的媒体路由回调对象之后,你还需要把它关联到主媒体路由框架对象上。下面一部分将讨论如何关联媒体路由回调函数的合适的方法。

加载回调函数到MediaRouter

既然媒体路由是一个分享接口,你的app必须关联和分离你的MediaRouter.Callback对象分别在你的app开启和关闭时。为了完成这些,你必须在app的活动页面生命周期中添加和移除app的媒体路由框架中的回调对象。当你的app在后台运行或者停止时,这种方法允许其他的apps可以使用媒体路由输出通道。

注意: 如果你正在写一个音乐播放app,并且想在你的app运行在后台时允许播放音乐,你必须创建一个播放服务进程并且连接进程和他的生命周期到媒体路由框架。

下面代码示例演示了如何使用生命周期方法来适当地添加和移除你的app媒体路由回调对象:

public class MediaRouterPlaybackActivity extends ActionBarActivity {
    private MediaRouter mMediaRouter;
    private MediaRouteSelector mSelector;
    private Callback mMediaRouterCallback;

    // your app works with so the framework can discover them.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

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

    // Add the callback on start to tell the media router what kinds of routes
    // your app works with so the framework can discover them.
    @Override
    public void onStart() {
        mMediaRouter.addCallback(mSelector, mMediaRouterCallback,
                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() {
        mMediaRouter.removeCallback(mMediaRouterCallback);
        super.onStop();
    }
    ...
}

你应该只在onStart()和onStopn()生命周期方法中分别添加和移除媒体路由回调对象。不要在onResume()或者onPause()方法中包含这些调用。

注意:媒体路由框架还提供一个MediaRouteDiscoveryFragment类,它也关心一个活动页面的添加和移除回调对象。

现在当你运行应用程序时,你应该明白一个按钮呈现在你的活动页面中。当你按下这个按钮媒体路由框架,路由选择对话框将会呈现出来如图3所示,它将允许你的用户选择有用的媒体路由。当你在测试这些接口时,务必保证你有一个可用的支持媒体路由的设备在你的本地网络中。

注意:为了无线显示路由出现在媒体路由选择框中,用户必须在app设置中启用这个选项。这个选项在显示分类中,在android4.4(KitKat)或更高版本设备中叫做屏幕,在android4.2(Jelly Bean)设备中叫做无线显示。更多使能这个特性的信息请看Wireless display支持页。

远程播放

远程播放方法发送控制命令到二级设备上来初始化播放或者控制正在进行的播放(暂停、倒回、快进、音量高低)。使用这个方法,接收到命令的设备(如一个谷歌电视棒)有责任获取和呈现内容。

当你的app支持这种类型的媒体路由,你必须使用通过MediaRouter.Callback对象接收到的一个远程播放MediaRouter.RouteInfo对象来创建一个RemotePlaybackClient对象。下面的示例代码展示 创建一个新的远程播放客户端和发送播放的视频的一个控制方法 :

private void updateRemotePlayer(RouteInfo route) {
    // Changed route: tear down previous client
    if (mRoute != null && mRemotePlaybackClient != null) {
        mRemotePlaybackClient.release();
        mRemotePlaybackClient = null;
    }

    // Save new route
    mRoute = route;

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

    // Send file for playback
    mRemotePlaybackClient.play(Uri.parse(
            "http://archive.org/download/Sintel/sintel-2048-stereo_512kb.mp4"),
            "video/mp4", null, 0, null, new ItemActionCallback() {

            @Override
            public void onResult(Bundle data, String sessionId,
                    MediaSessionStatus sessionStatus,
                    String itemId, MediaItemStatus itemStatus) {
                logStatus("play: succeeded for item " + itemId);
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logStatus("play: failed - error:"+ code +" - "+ error);
            }
        });
    }
}

RemotePlaybackClient类提供了管理内容播放的另外一些方法。这里是一些RemotePlaybackClient中的关键播放方法:

  • play()——播放特定的媒体文件,指定的一个Uri。

  • pause()——暂停到当前播放的媒体位置。

  • resume()——继续播放暂停时所在的位置。

  • seek()——移动到指定的当前媒体的位置。

  • release()——断开你的app和远程播放设备之间的连接。

你可以在app中加载这些方法到相应的提供的播放控制的动作。这些方法多数也允许你包含回调对象,所以你可以监视播放任务的进程或者控制请求。

RemotePlaybackClient类也支持多媒体任务的排队播放并且管理这个媒体队列。如实现这些特性的一个综合的示例,请看SampleMediaRouterActivity,它涉及到的类包含在v7媒体路由支持库示例/extras/android/compatibility/v7/mediarouter中。

谷歌电视棒使用Google Cast API的更多信息,请看Google Cast开发文档。

二级输出

二级输出方法发送准备好的媒体内容到连接上的二级输出设备上播放。二级设备可包含电视或无线音频系统,并且通过无线或有线协议可以被连接上,例如一个HDMI线。使用这种方法,你的app有责任处理媒体内容播放(下载、解码、同步音频和视频轨迹),而二级设备只输出最终形式的内容。

注意:通过媒体路由框架使用二级输出显示路由需要只能在android4.2(API水平17)或更高版本上可用的类,尤其展示类。如果你正在开发一个app同时支持远程播放和二级输出设备,你必须包含当低于支持的android版本水平时禁用这些代码的判断。

创建一个展示对象

当通过媒体路由框架使用一个二级输出显示器时,你需要创建一个包含想要呈现在显示器上的内容的展示对象。展示对象是继承自对话框类,所以可以添加布局和视图到该对象上。

你应该明白展示对象有它自己的上下文和资源,独立于创建该对象的app的活动页面。这就需要拥有一个二级上下文,因为展示的内容被绘制在显示器上,它独立于本地android设备上的你的app显示器。特别地,二级显示需要独立上下文,因为它可能需要加载基于它本身特有的屏幕像素的资源。

一下代码示例展示了一个展示对象至少需要实现的类,包含一个GLSurfaceView对象。

public class SamplePresentation extends Presentation {  
    public SamplePresentation(Context outerContext, Display display) {  
        super(outerContext, display);  
    }  

    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        // Notice that we get resources from the context of the Presentation  
        Resources resources = getContext().getResources();  

        // Inflate a layout.  
        setContentView(R.layout.presentation_with_media_router_content);  

        // Add presentation content here:  
        // Set up a surface view for visual interest  
        mSurfaceView = (GLSurfaceView)findViewById(R.id.surface_view);  
        mSurfaceView.setRenderer(new CubeRenderer(false));  
    }  
}  

创建一个展示控制

为了显示展示对象,你应该写一个控制层来处理来自MediaRouter.Callback对象接收的消息,并管理该对象的创建和移除。控制层应该也处理关联展示对象到一个选择的显示对象上,该显示对象代表着用户选择的独立的物理显示设备。控制层可以是简单地在活动页面中支持二级显示的一个方法。

下面的代码示例展示了Presentation实现对象的作为一个单独方法的控制层.当一个显示器处理不可选状态或者失去联系时,该方法负责清除无效的展示对象,而在一个显示设备连接时负责创建一个展示对象。

private void updatePresentation(RouteInfo route) {  
    // Get its Display if a valid route has been selected  
    Display selectedDisplay = null;  
    if (route != null) {  
        selectedDisplay = route.getPresentationDisplay();  
    }  

    // Dismiss the current presentation if the display has changed or no new  
    // route has been selected  
    if (mPresentation != null && mPresentation.getDisplay() != selectedDisplay) {  
        mPresentation.dismiss();  
            mPresentation = null;  
    }  

    // Show a new presentation if the previous one has been dismissed and a  
    // route has been selected.  
    if (mPresentation == null && selectedDisplay != null) {  
        // Initialize a new Presentation for the Display  
        mPresentation = new SamplePresentation(this, selectedDisplay);  
        mPresentation.setOnDismissListener(  
                new DialogInterface.OnDismissListener() {  
                    // Listen for presentation dismissal and then remove it  
                    @Override  
                    public void onDismiss(DialogInterface dialog) {  
                        if (dialog == mPresentation) {  
                            mPresentation = null;  
                        }  
                    }  
                });  

        // Try to show the presentation, this might fail if the display has  
        // gone away in the meantime  
        try {  
            mPresentation.show();  
        } catch (WindowManager.InvalidDisplayException ex) {  
            // Couldn't show presentation - display was already removed  
            mPresentation = null;  
        }  
    }  
}  

注意:当用户连接一个无线显示器时,媒体路由框架会自动提供一个通知,它展示了显示器的内容在连接的设备上。

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1