概述:
有时用户通过无线技术连接他们的电视机, 家庭影院系统和音乐播放器, 他们可能会希望这样将播放内容从Android app到那些更大屏幕更大音量的设备上. 启用这种功能可以让我们的app从单用户单设备扩展为多个用户共享, 有很高的实用价值. Android媒体路由API被设计出来以便让媒体可以在其它的设备上播放. 实现这个功能有两种主要的方法:
1. 远程播放– 该功能使用接收设备来处理获取到的数据内容, 解码并播放, 这时用户手中的Android可以用做远程控制设备.
2. 辅助输出– 使用这种方法我们的APP负责获取, 渲染和发送视频或者音频流到接收设备. 这种方法用于支持在Android上无线显示输出. Android设备要负责处理媒体, 包括解码.
下面的文字介绍了我们的APP如何使用上面的方法提供媒体给辅助设备播放.
媒体路由API广泛的支持各种连接到Android的有线和无线的设备. 要启用这些连接, 媒体路由框架为Android设备的音视频输出抽象出了逻辑路径. 这些架构允许我们的APP快速的建立与播放设备的链接, 比如家庭影院和音响系统等可以支持Android媒体路由的设备.
想要在APP中使用该框架, 我们必须获取一个MediaRouter框架的对象并且关联一个MediaRouter.Callback对象来监听可用的媒体路由事件. 媒体内容通过与媒体路由关联的MediaRouteProvider来传递(除了一些特别的场景, 比如一个蓝牙输出设备). 下图提供了一个媒体路由框架实现播放内容的高层视角:
媒体路由框架不支持的硬件制造商可以通过实现一个MediaRouteProvider并作为一款APP发布来为它们的设备添加支持. 更多的关于实现一个媒体路由provider的信息可以参考MediaRouteProviderreference和v7-MediaRouter支持库的栗子<sdk>/extras/android/compatibility/v7/mediarouter.
媒体路由包:
媒体路由API作为Android Support Library 18或更高的版本的一部分提供, 我们可以在v7-mediaroutersupport library中找到. 特别的, 我们应该使用android.support.v7.media中的类来实现媒体路由功能. 这些API可以兼容Android2.1 或者跟高的版本. 注意还有一组媒体路由的API, 由android.media提供, 但是目前它已经由v7-mediarouter支持库所代替, 所以官方不建议我们使用android.media类来实现媒体路由功能.
为了使用android.support.v7.media媒体路由类, 我们必须向app工程中添加v7-mediaroutersupport library package. 更多关于向工程中添加支持库的信息可以参考这里.
实现用户界面:
实现媒体路由API的Android APP应该包含一个Cast button作为他们用户接口的一部分, 让用户可以通过它选择媒体路由让辅助输出设备来播放媒体. 媒体路由框架为该按钮提供了一个标准的接口, 我们可以使用它来帮助用户意识到并使用这个功能. 下图展示了一个Cast button应该如何显示在app中:
图中action bar右侧的图标就是cast button. 注意, 当实现一个提供了媒体路由接口的activity的时候, 我们必须从android support library中扩展ActionBarActivity或者FragmentActivity,即使android:minSdkVersion的API是大于等于11的.
Castbutton:
推荐的实现Cast button用户接口的方式是从ActionBarActivity继承来一个activity, 并且使用onCreateOptionsMenu()方法来添加一个options menu. Cast button必须使用MediaRouteActionProvider类作为它的action:
<?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="android.support.v7.app.MediaRouteActionProvider"
app:showAsAction="always"
/>
</menu>
一旦增加了Cast button在用户接口上, 那么我们就必须让它关联一个媒体路由选择器对象. 创建一个选择器的步骤将会在下文介绍. 如果想要在action bar中实现一个菜单, 则必须要将Cast button用MediaRouteButton添加到app中. 如果选择这种方法的话, 我们必须根据GoogleCast Design Checklist(链接似乎失效了…)来将这个button添加到我们的app action bar中. 还得使用setRouteSelector()方法关联一个媒体选择器.
媒体路由选择器:
当用户点击一个Cast button的时候, 媒体路由框架将会查找可用的媒体路由并展示一个列表给用户, 让他选择, 如下图:
在列表中展示的媒体路由的类型– 远程播放, 辅助输出或者别的什么– 由app自己定义. 我们可以通过MediaRouteSelector来创建这些类型, 它接收一个framework提供的MediaControlIntent对象和其它我们或者别的开发者创建的媒体路由provider. Framework提供的路由类型有以下几种:
CATEGORY_LIVE_AUDIO – 输出音频到一个辅助设备, 比如支持无线的音乐播放器.
CATEGORY_LIVE_VIDEO – 输出视频到一个辅助设备, 比如无线显示设备.
CATEGORY_REMOTE_PLAYBACK – 在指定的支持获取, 解码, 播放的设备上播放音频或者视频, 比如一个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(); } }
媒体路由框架使用选择器对象来提供一个选择媒体路由的接口. 一旦我们定义了这个选择器, 就可以关联它到Cast menu中的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必须获得MediaRouter框架对象, 然后关联到一个MediaRouter.Callback独享. 这个对象会接收从媒体路由框架发来的消息, 比如当一个路由被选中, 修改, 或者断开连接的时候. 想要获取一个MediaRouter框架对象的实例, 需要在一个可以支持媒体路由API的activity的onCreate()方法中调用MediaRouter.getInstance()来获得.
MediaRouter对象是由framework维护的实例. 一旦我们的APP得到了它, 就必须保留该实例直到APP终止, 以防止它被回收.
创建一个MediaRouter回调:
媒体路由框架通过一个关联到MediaRouter框架对象的回调对象来与APP沟通. 一个使用媒体路由框架的APP必须扩展MediaRouter.Callback对象来接收媒体路由链接的消息. 在callback中有一些我们可以重写的方法, 用来接收关于媒体路由事件的信息. 我们至少应该重写这些方法:
onRouteSelected() – 当用户链接到一个媒体路由输出设备的时候调用.
onRouteUnselected() – 当用户关闭一个与媒体路由输出设备的链接的时候调用.
onRoutePresentationDisplayChanged() – 当要显示内容的属性改变的时候. 比如从分辨率720pixel到1080pixel.
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); } } }
关联一个Callback到MediaRouter:
由于媒体路由共享一个接口, 所以我们的APP启动和关闭的时候也必须连接和断开MediaRouter.Callback对象. 为了实现这个则必须在activity的生命周期中从媒体路由框架里添加和移除Callback对象. 这样可以使得其他的app在我们的app在后台或者不运行的时候占用么提路由输出. 如果要写一个可以在后台播放音乐的app, 那么必须创建一个service用来播放, 并且将媒体路由框架和service的生命周期绑定在一起.
下面的栗子演示了如何使用生命周期方法来合适的添加和删除app中的媒体路由Callback:
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()和onStop()方法中添加和移除媒体路由回调, 而不是在onResume()和onPause()中这么做. 媒体路由框架还提供了一个MediaRouteDiscoveryFragment类, 它可以为activity处理添加和删除回调.
现在当我们运行自己的app的时候应该可以看到一个Cast button出现在了activity中. 当它被点击之后, 一个路由选择框将会出现, 让用户可以选择一个可用的媒体路由.
远程播放:
远程播放功能发送控制命令给一个备用设备让它初始化播放并且控制播放进度(暂停快进,声音大小等). 通过这种方法, 接收设备(比如一个Chromecast)负责接收和播放内容. 当我们的APP想要支持这种媒体路由的时候, 必须用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类提供了一些额外的方法用来管理内容播放. 这是其中一些关键的方法:
Play(): 播放一个指定的媒体文件, 通过URI指定.
Pause(): 暂停当前的媒体播放.
Resume(): 暂停之后恢复播放.
Seek(): 移动到一个指定的位置.
Release(): 关闭到远程设备的链接.
这些方法中的大部分除了控制播放之外还可以支持一个Callback对象让我们可以监听播放任务的进度. RemotePlaybackClient类还支持多媒体查询和管理管理查询. 更多例子可以查询<sdk>/extras/android/compatibility/v7/mediarouter
.
辅助输出:
辅助输出功能发送准备好的媒体内容到一个连接的辅助设备播放. 辅助设备可以包括电视或者无线音响系统和通过可以无线或者有线协议连接的设备, 比如HDMI. 通过这种方法, 我们的app负责处理媒体内容(下载, 解码, 同步音视频等), 辅助设备则只负责输出内容. 通过媒体路由框架使用辅助设备需要Android4.2及以上版本, 特别是Presentation类.
创建一个presentation对象:
当使用媒体路由框架向一个辅助输出设备输出的时候, 我们必须创建一个Presentation对象, 它包含我们想要显示的内容. Presentation类从Dialog类继承而来, 所以可以向Presentation添加layout和view.
Presentation对象有它自己的context和resources, 独立于创建该对象的activity.拥有一个辅助的context是很有必要的, 因为Presentation的内容需要被绘制在一个独立的显示设备上. 特别的, 这里辅助显示需要一个独立的context, 因为它可能需要加载自己制定的屏幕尺寸的资源. 下面的代码演示了一个Presentation对象的最小实现, 包括一个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)); } }
创建一个Presentation控制器:
想要显示一个Presentation对象, 我们应该写一个控制器层来处理MediaRouter.Callback对象收到的消息和管理Presentation对象的创建和删除. 控制器层还应该处理关联Presentation对象到一个选中的Display对象, 它代表用户选择的指定的物理显示设备. 控制层可以简单的是种简单的在activity中支持辅助显示的方法.
下面的栗子演示了单个方法实现的控制器层, 该方法在Display未选中或者失去连接到时候将Presentation无效化, 在设备连接的时候创建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; } } }
当用户连接到一个无线显示器, 媒体路由框架会自动提供一个提示, 它会在连接的设备上显示屏幕内容.
参考: https://developer.android.com/guide/topics/media/mediarouter.html