做TV开发一段时间了,国内目前关于这方面的资料并不多,这里我来分享一下我对TIF的使用心得。Android TIF(Android TV input Framework)是Google向电视制造商提供了一套标准的API,用于创建Input模块来控制Android电视。这套API的底层实现的原理是aidl和provider,从而进行了跨进程通信。系统或第三方的应用可以通过TIF获得所有输入(input)的信源(输入的模块包括:搜台模块,MDMI模块,网络模块等),然后通过aidl切台输出到屏幕上。
一.在介绍这套框架之前,先说一下电视相关的知识:
HDMI:高清晰度多媒体接口(英文:High Definition Multimedia Interface,HDMI)是一种数字化视频/音频接口技术,是适合影像传输的专用型数字化接口。对应的
IPTV:网络电视,也叫VOD电视,三方比如说某某视频公司提供的视频资源在电视上播放。
DTV:数字电视
ATV:模拟电视
二.TIF的组成部分:
1)TV Provider (com.android.providers.tv.TvProvider):一个包含频道、节目和相关权限的数据库。
2)TV App (com.android.tv.TvActivity):一个和用户交互的系统应用。
3)TV Input Manager (android.media.tv.TvInputManager):一个中间接口层,能够让TV Inputs和TV App进行通讯。
4)TV Input:可以看做是一个代表物理或者虚拟的电视接收器或者输入端口的应用。Input在TIF中可以看做是一个输入源。
5)TV Input HAL (tv_input module):TV Input的硬件抽象层,可以让系统的TV inputs访问TV特有硬件。
6)Parental Control:儿童锁,一种可以锁住某些频道和节目的技术。
7)HDMI-CEC:一种可以通过HDMI在多种设备上进行远程控制的技术。CEC(Consumer Electronics Control消费电子控制)
三.TIF的整理使用流程。
如上图所示,liveTVApp通过turning调用TV Input Manager获得一个session,session里面放的是一路信源的状态。TvInput将获得的Channel和Programs信息写入到/data/data/com.android.providers.tv/databases/tv.db数据库中。liveTVApp通过session以aidl的方式调用TVinputService获得相关的频道和具体的节目信息进行播放。
四.TIF为开发者提供的接口
1)TvView:负责显示播放的内容。它是一个ViewGroup的子类,它是切台的入口,内置surface用于显示视频播放的内容和通过控制session可以控制音量的大小等。
2)TvInputService:TvInputService是一个重要的类,继承了它并实现一些规范就可以实现一路input信源供其它应用使用。在该service中要实现onCreatSession()方法该方法要返回一个TvInputService.Session对象。这里的service在Manifest中定义时要注意要添加permission和action,具体如图2。添加完之后系统的TvInputManager可以检测到该service是一个TvInputService,也就是一路信源。
3)TvInputService.Sssion:该session类TvView通过Tune方法会指定相应的inputId(往往是该service对应的“包名/.类名”)和uri,uri中包含对应的节目id,该tune方法会调用Session的Onturn方法中,在这个方法中解析传过来的id,根据id利用TvProvider去查询数据库的数据,设置给player,这里使用onSetSurface()方法将TvView创建的surface设置给player,然后player就在该surface上显示内容。
4)TvContract:介于TvProvider和TvApp之间的一层封装,它里面封装了一些uri。里面有两个内部类是两个javaBean。他们分别是TvContract.channels(频道表),TvContract.Programs(频道里面的节目单,比如少儿频道里面海贼王第5集,火影忍者第6集等)。
5)TvInputManager:这个是TIF的核心类,它是系统的类,可以监测到在系统的service中注册"android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS"
action的类,并将其设为一路信源。它来管理一些回调,比如video是否可用,video的大小尺寸是否变换。通过下面的代码可以获得一个TvInputManager,TvInputManager tvInputManager =(TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
得到TvInputManager后我们可以遍历拿到系统当前有多少个service是Tv信源。代码如下:
List list = tvInputManager.getTvInputList();
for(TvInputInfo info:list){
Log.i(TAG, "id:" + info.getId());
}
我这里打出的log如下:
01-03 06:58:11.893 29023-29023/com.lenovo.tvviewsimple I/swj: id:com.mediatek.tvinput/.component.ComponentInputService/HW1
01-03 06:58:11.893 29023-29023/com.lenovo.tvviewsimple I/swj: id:com.mediatek.tvinput/.dtv.TunerInputService/HW0
01-03 06:58:11.893 29023-29023/com.lenovo.tvviewsimple I/swj: id:com.mediatek.tvinput/.composite.CompositeInputService/HW2
01-03 06:58:11.893 29023-29023/com.lenovo.tvviewsimple I/swj: id:lenovo.com.ismartvlive/.Ismartvliveservice
可以看出一共有这么多的信源可以使用。我们可以拿到inputId,在TvView的tune方法中设置。这里的信源就是注册了服务并没有开启。在TvView的tune方法调用的时候会开启服务。
6) TvInputInfo:TvInput的信息。包括频道类型,图标,名称等信息。
7)TvInputCallback。这里是TvView的一个内部类,TvInputCallBack可以反馈给TvView一些信息比如连接service是否成功,Video是否可用等。部分代码如下:
tvView.setCallback(new TvView.TvInputCallback() {
@Override
public void onConnectionFailed(String inputId) {
super.onConnectionFailed(inputId);
LogUtil.i(this,"MainActivity.onConnectionFailed:"+inputId);
}
@Override
public void onDisconnected(String inputId) {
super.onDisconnected(inputId);
LogUtil.i(this,"MainActivity.onDisconnected.");
}
@Override
public void onVideoSizeChanged(String inputId, int width, int height) {
super.onVideoSizeChanged(inputId, width, height);
LogUtil.i(this,"MainActivity.onVideoSizeChanged.");
}
@Override
public void onVideoAvailable(String inputId) {
super.onVideoAvailable(inputId);
LogUtil.i(this,"MainActivity.onVideoAvailable.inputId:"+inputId);
}
@Override
public void onVideoUnavailable(String inputId, int reason) {
super.onVideoUnavailable(inputId, reason);
LogUtil.i(this,"MainActivity.onVideoUnavailable.");
}
......
});
五.简单的例子
效果图:
这里我使用了TvInputservice和TvView分开写的方法,这样写更能体现出跨进程的特点。
详见 https://github.com/songwenju/TIFSample,如果对您有帮助,欢迎star和fork。
这个例子使用的视频源是google提供的 https://storage.googleapis.com/android-tv/android_tv_videos_new.json ,里面使用了retrofit+RxJava做数据请求,对这方面不了解的同学可以查阅相关的资料。项目结构如下图:
下面说一下项目的大致流程:
1.在tifService module中有三个功能,一是负责请求网络数据,这里使用的是retrofit+rxjava,并将网络数据使用TvProvider写入tv.db ,二是用来加载提供TvInputService类,这个类是Tif的controler。三是提供播放器负责播放。这里要说明一下,播放器在service中,但是显示在TvView的界面上,原因是TvView在tune的时候传过来一个surface,这里将播放的内容显示在这个surface上。这三个步骤的核心代码分别是:
1)请求数据:
private void addData() {
LogUtil.i(this,"MainActivity.addData.");
mChannelService.getResult()
.subscribeOn(Schedulers.newThread()) //请求数据在子线程
.map(new Func1() {
@Override
public ChannelResult call(ChannelResult channelResult) {
List googlevideos = channelResult.getGooglevideos();
for (GooglevideosBean googlevideosBean : googlevideos) {
for (VideosBean videoBean : googlevideosBean.getVideos()) {
insertChannelsData(mContext, videoBean);
}
}
return channelResult;
}
}).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1() { // 相当于onNext()
@Override
public void call(ChannelResult s) {
LogUtil.i(this, "MainActivity.endCall.");
Toast.makeText(mContext,"数据写入完毕",Toast.LENGTH_SHORT).show();
}
}, new Action1() { // 相当于onError()
@Override
public void call(Throwable throwable) {
throwable.printStackTrace();
}
});}
/** * 写入channel到数据库
* *@param context 上下文
* @param videoBean videoBean
*/
public static void insertChannelsData(Context context, VideosBean videoBean) {
ContentValues value = new ContentValues();
value.put(TvContract.Channels.COLUMN_INPUT_ID, "com.songwenju.tifservice/.TvService");
value.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, videoBean.getSources().get(0)); //url
value.put(TvContract.Channels.COLUMN_DISPLAY_NAME, videoBean.getTitle()); //name
value.put(TvContract.Channels.COLUMN_DESCRIPTION, videoBean.getDescription());
//description context.getContentResolver().insert(TvContract.Channels.CONTENT_URI, value);
}
2)TvInputService
public class TvService extends TvInputService {
private SimpleSessionImpl mSimpleSession;
private Context mContext;
@Nullable
@Override
public Session onCreateSession(String inputId) {
LogUtil.i(this, "TvService.onCreateSession.inputId:" + inputId);
mContext = this;
mSimpleSession = new SimpleSessionImpl(this);
return mSimpleSession;
}
public class SimpleSessionImpl extends Session {
private MediaPlayer mMediaPlayer;
private Surface mSurface;
/**
* Creates a new Session.
*
* @param context The context of the application
*/
public SimpleSessionImpl(Context context) {
super(context);
LogUtil.i(this, "SimpleSessionImpl.SimpleSessionImpl.");
}
@Override
public void onRelease() {
LogUtil.i(this, "SimpleSessionImpl.onRelease.");
}
@Override
public boolean onSetSurface(Surface surface) {
//
LogUtil.i(this, "SimpleSessionImpl.onSetSurface." + surface);
mSurface = surface;
return true;
}
@Override
public void onSetStreamVolume(float volume) {
LogUtil.i(this, "SimpleSessionImpl.onSetStreamVolume.");
}
@Override
public boolean onTune(Uri channelUri) {
LogUtil.i(this, "SimpleSessionImpl.onTune.");
Long channelId = ContentUris.parseId(channelUri);
LogUtil.d(this, "channelId:" + channelId);
return setChannelIdAndPlay(channelId);
}
}
3)播放的逻辑
/**
* 设置ChannelId并播放
*
* @param channelId
* @return
*/
private boolean setChannelIdAndPlay(Long channelId) {
VideosBean dbChannel = getDbChannel(mContext, channelId);
LogUtil.i(this, "SimpleSessionImpl.setChannelIdAndPlay." + dbChannel.toString());
mMediaPlayer = new MediaPlayer();
String playUrl;
try {
playUrl = dbChannel.getSources().get(0); //google的json有时候不能用
if (TextUtils.isEmpty(playUrl)) {
if (channelId == 1) {
//如果google的网连接不上的话,这里设置了一个默认的地址
playUrl = "http://cord.tvxio.com/v1_0/I2/frk/api/live/m3u8/9/5f754b84-ec33-4d62-bb81-3e4de21c8460/medium/";
}else {
playUrl = " http://cord.tvxio.com/v1_0/I2/frk/api/live/m3u8/9/577da15a-9007-4fdd-a9cf-6e19d7a04528/medium/";
}
}
LogUtil.i(this, "SimpleSessionImpl.setChannelIdAndPlay.playUrl=" + playUrl);
mMediaPlayer.reset();
mMediaPlayer.setDataSource(playUrl);
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.setOnErrorListener(new OnErrorListener());
mMediaPlayer.setOnBufferingUpdateListener(new OnBufferingUpdateListener());
mMediaPlayer.setOnInfoListener(new OnInfoListener());
mMediaPlayer.setOnPreparedListener(new OnPreparedListener());
mMediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
2.在app module中,很简单的一个TvView,通过上面的步骤5)获得inputId,将id和Uri,我这里uri使用的是
Uri.parse("content://main/250"),最后一个250就是要解析的id,通过这个id去拿到频道的播放列表。设置给MediaPlayer去播放。
六,下面说一些注意的点
1. 通过uri解析id:
Long channelId = ContentUris.parseId(channelUri);
2.对于状态的回传,在TvView中我们如果想要获取一些播放器的状态,比如buffer状态,在开始播放之前有一个loading的状态,获取节目的size的变换,以及自定义的一些状态。下面依次说明:
1)lodding状态的回传:
在tune方法的时候使用
mSimpleSession.notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
通知Video不可用,原因是tuning
其他对应的状态还有:
TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:未知原因
TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:信号弱
TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:缓冲
TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:仅仅是音频
在视频播放的时候即在onprepared时调用
mSimpleSession.notifyVideoAvailable();
2)buffer状态的回传:在MediaPlayer中Buffer的两种状态,开始缓冲和结束缓冲对应的是701和702两个状态。在MediaPlayer的onInfo方法中收到了701开始调用
mSimpleSession.notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
3)自定义的状态,这个使用make的方式编代码的时候才能引用,因为这个方法用@system api注解了。可以传一个bundle对象。
notifySessionEvent(@NonNull final String eventType, final Bundle eventArgs)
3.在使用TvProvider提供的Program表的时候,我这里遇到了一个问题,发现表的数据会被不定期的清空。测试那边给的也是偶现的。通过断网,切台,重启系统发现programs表总是被清空。对于开发来说找到bug的复现步骤是最好不过的事情了。通过阅读TvProvider的源码可以看到有一个类专门负责清空Programs的数据,代码如下:
在EpgDataCleanupService.java中会去清除当前时间以前的节目信息,在这个字段对应的时间信息COLUMN_END_TIME_UTC_MILLIS,而这个时间是以毫秒为单位的,我们服务器给的数据是以秒为单位的,所以会被清空。修该一下就可以了。
/**
77 * Clear program info that ended before {@code maxEndTimeMillis}.
78 */
79 @VisibleForTesting
80 void clearOldPrograms(long maxEndTimeMillis) {
81 int deleteCount = getContentResolver().delete(
82 Programs.CONTENT_URI,
83 Programs.COLUMN_END_TIME_UTC_MILLIS + "",
84 new String[] { String.valueOf(maxEndTimeMillis) });
85 if (DEBUG && deleteCount > 0) {
86 Log.d(TAG, "Deleted " + deleteCount + " programs"
87 + " (reason: ended before "
88 + DateUtils.getRelativeTimeSpanString(this, maxEndTimeMillis) + ")");
89 }
90 }
例子详见https://github.com/songwenju/TIFSample,如果对您有帮助,欢迎star和fork。
到此关于Android TIF的介绍和框架的使用部分结束了,以后若有新的理解再来添加。
版权声明:本文为博主原创文章,转载请注明出处。