Android TV框架TIF使用心得

做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的整理使用流程。

图1.tv-tif-overview.png

  如上图所示,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,也就是一路信源。

图2.service的定义

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<TvInputInfo> 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.");
    }    
......
});

五.简单的例子
效果图:

show.gif

这里我使用了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<ChannelResult, ChannelResult>() { 
               @Override
                public ChannelResult call(ChannelResult channelResult) {
                    List<GooglevideosBean> 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<ChannelResult>() {    // 相当于onNext()
                @Override
                public void call(ChannelResult s) {
                    LogUtil.i(this, "MainActivity.endCall.");
                    Toast.makeText(mContext,"数据写入完毕",Toast.LENGTH_SHORT).show();
                }
            }, new Action1<Throwable>() {                       // 相当于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的介绍和框架的使用部分结束了,以后若有新的理解再来添加。
版权声明:本文为博主原创文章,转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,418评论 22 663
  • 上次说专门写一下前几天参加行摄沙龙,摄影师老崔给我们分享的一些关于摄影的体会,今天就来聊聊摄影的事。 或许,用“拍...
    土川兄一终身建设阅读 325评论 0 2
  • 什么是观察者模式## 有人这么说 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这...
    IAM四十二阅读 4,702评论 3 14