Android TV框架 TIF(Android TV Input Framework)入门实践

Tamic/CSDN
http://blog.csdn.net/sk719887916/article/details/53645615

做TV开发一段时间了,国内目前关于这方面的资料并不多,这里我来分享一下我对TIF的使用心得。Android TIF(Android TV Input Framework)是Google向电视制造商提供了一套标准的API,用于创建Input模块来控制Android电视。这套API的底层实现的原理AIDL和provider,从而进行了跨进程通信(Bunder)。系统或第三方的应用可以通过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为开发者提供的接口

##TvView
  负责显示播放的内容。它是一个ViewGroup的子类,它是切台的入口,内置surface用于显示视频播放的内容和通过控制session可以控制音量的大小等。
##TvInputService
TvInputService是一个重要的类,继承了它并实现一些规范就可以实现一路input信源供其它应用使用。在该service中要实现onCreatSession()方法该方法要返回一个TvInputService.Session对象。这里的service在Manifest中定义时要注意要添加permission和action,具体如图2。添加完之后系统的TvInputManager可以检测到该service是一个TvInputService,也就是一路信源。

这里写图片描述

##TvInputService.Sssion
该session类TvView通过Tune方法会指定相应的inputId(往往是该service对应的“报名/.类名”)和uri,uri中包含对应的节目id,该tune方法会调用Session的Onturn方法中,在这个方法中解析传过来的id,根据id利用TvProvider去查询数据库的数据,设置给player,这里使用onSetSurface()方法将TvView创建的surface设置给player,然后player就在该surface上显示内容。
 
##TvContract
 介于TvProvider和TvApp之间的一层封装,它里面封装了一些uri。里面有两个内部类是两个javaBean。他们分别是TvContract.channels(频道表),TvContract.Programs(频道里面的节目单,比如少儿频道里面海贼王第5集,火影忍者第6集等)。
##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方法调用的时候会开启服务。

##TvInputInfo
TvInput的信息。包括频道类型,图标,名称等信息。
  
##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.g

这里我使用了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);

##对于状态的回传
在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.Program表
在使用TvProvider提供的Program表的时候,我这里遇到了一个问题,发现表的数据会被不定期的清空。测试那边给的也是偶现的。通过断网,切台,重启系统发现programs表总是被清空。对于开发来说找到bug的复现步骤是最好不过的事情了。通过阅读TvProvider的源码可以看到有一个类专门负责清空Programs的数据,代码如下:
在EpgDataCleanupService.java中会去清除当前时间以前的节目信息,在这个字段对应的时间信息COLUMN_END_TIME_UTC_MILLIS,而这个时间是以毫秒为单位的,我们服务器给的数据是以秒为单位的,所以会被清空。修改一下就可以了。

 /**
  * Clear program info that ended before {@codemaxEndTimeMillis}.
  */
  @VisibleForTesting
 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的介绍和框架的使用部分结束了,以后若有新的理解再来添加。

Tamic: CSDN
地址:http://blog.csdn.net/sk719887916/article/details/53645615

参考原文:http://www.jianshu.com/p/385c92fceb16

作者:Tamic 更多原创关注开发者技术前线

开发者技术前线

已标记关键词 清除标记
相关推荐
Android_tv_metro是一款安卓版TV Metro框架和服务器API。API和数据结构专辑和显示项目:Metro风格是由两个元素构成专辑可以包含多张专辑和显示项目显示项目可以被定义为视频,游戏,应用程序,音乐等您可以从显示项目中删除你自己的游戏/应用/视频详细条目主页也被定义为专辑。API风格API描述http://host/v1/ns/type/?id=res_id NS:命名空间,资源类型类型:项目或项目列表ID:后端服务器系统的资源ID详细信息http://host/game(video/app)/item?id=12346 return item list专辑http://host/game(video/app)/album?id=6464 return album类别http://host/game(video/app)/category?id=123456 return album注意:专辑和类别接近同一概念。选项卡“应用程序/游戏”选项卡“视频”选项卡“视频类别”首页JSON定义首页JSON示例服务器API定义请看:https://github.com/XiaoMi/android_tv_metro/raw/master/server/TVMarketAPI.md首页显示数据{    "data": [        {            "items": [display items],            "images": {},            "name": "TAB 1",            "id": "recommend",            "type": "album",            "ns": "video"        },        {            "items": [display item],            "images": {},            "name": "TAB 2",            "id": "recommend",            "type": "album",            "ns": "video"        }    ] }显示项目:{    "target": {        "type": "item"    },    "images": {        "back": {            "url": "",            "ani": {},            "pos": {}        }    },    "name": "Display Name)",    "times": {        "updated": 1409202939,        "created": 1409202939    },    "_ui": {        "layout": {            "y": 2,            "x": 3,            "w": 1,            "h": 1        },        "type": "metro_cell_banner"    },    "id": "987722",    "type": "item",    "ns": "video" }专辑{ "data": [     {         "items": [display items],         "images": { },         "name":"game tab name",         "times": {             "updated": 0,             "created": 0         },         "_ui": {             "type": "metro"         },         "id": "recommend",         "type": "album",         "ns": "game"     },     {         "items": [display items],         "images": { },         "name": "game tab Name",         "times": {             "updated": 0,             "created": 0         },         "_ui": {             "type": "metro"         },         "id": "categories",         "type": "album",         "ns": "game"     },     {         "items": [dispay items],         "images": { },         "name": "video tab name",         "times": {             "updated": 0,             "created": 0         },         "_ui": {             "type": "metro"         },         "id": "recommend",         "type": "album",         "ns": "video"     },     {         "items": [display items],         "images": { },         "name": "video tab name",         "times": {             "updated": 0,             "created": 0         },         "_ui": {             "type": "metro"         },         "id": "categories",         "type": "album",         "ns": "video"     } ], "preload": {     "images": [] }, "update_time": 0 }显示项目{ "target": {     "type": "item" }, "images": {     "text": {         "url": "",         "ani": {},         "pos": {}     },     "icon": {         "url": "",         "ani": {},         "pos": {}     },     "back": {         "url": "http://xxx/fffff.png",         "ani": {},         "pos": {}     },     "spirit": {         "url": "",         "ani": {},         "pos": {}     } }, "name": "name", "times": {     "updated": 1404466152,     "created": 1404454443 }, "_ui": {     "type": "metro_cell_banner",     "layout": {         "y": 1,         "x": 1,         "w": 1,         "h": 2     } }, "id": "180", "type": "item", "ns": "game" }TV Metro库和APIandroid库:提供一个建立sw540dp metro布局的框架。API:服务器API和数据结构。该框架能够帮助您轻松构建一个TV metro UI风格的应用程序。至于具体的业务数据定义,你需要自己处理。android库:用于专辑的RecommendCardView Card浏览GenericSubjectLoader Loader(选项卡是专辑的一个实例)如何集成Android库?你只需要继承MainActivity并执行选项卡装载。请参阅TVMetroSample应用如何运行自己的服务器?1.定义你的主页数据2.执行您的详细资料/列表API下载测试APK下载测试APK,你可以在Android平板或电视运行点击下载设计文档:https://github.com/XiaoMi/android_tv_metro/raw/master/design/app_api.ppt 标签:Android
全易通验厂考勤软件系统的特点: 全易通研制开发的QET系列全易通验厂考勤软件和全易通验厂考勤工资系统是一套功能强大,运行稳定,操作简单方便,用户界面美观,统计数据正确的感应卡及指纹考勤系统软件.历经几年在珠江三角洲几百家外资(港资,台资,日资,美资)和国内上千家企业的成功运行,利用最新的SQL SERVER技术,采用C/S结构,使用树形节点功能,设计开发出这套具有智能排班功能的考勤软件,本套考勤软件让操作人员工作起来更方便,更轻松。 1.支持智能排班功能,能自动识别各种班次,如中午连班、直落班、晚班、保安三班、特殊班 2.大型数据库SQL版,安全性,稳定性高;设置后可实现自行备份数据,解除数据丢失的后顾之忧 3.软件可做A/B两套账,加班工时控制在允许范围内,可随时应付劳动局检查和供应商查厂验厂 4.有丰富的考勤报表如明细表、汇总表(迟到、早退、请假、旷工、加班、放假等)、日报表 5.人事资料无需手工输入,可直接导入到软件中。考勤数据可以自由转入转出到EXCEL表格中 6、支持多帐套功能,例如,A帐为真实数据,B帐是验厂数据,不同客户可设置不同帐套; 7、操作简单、参数设置灵活、应用面广,可扩充功能强大; 8、一般情况下,2000人左右的企业只需十分钟,而且绝对符合验厂规则,省事省力; 9、加班时间完全用户自定义控制 10、支持与考勤机的实时连接通讯,操作界面与功能与真帐完全一样 11、考勤数据中的迟到、早退、请假、签卡等可以自定义设置,系统自动产生; 12、系统依排班数据,自动生成为每一个人的打卡记录卡; 13、打卡范围可设置:提前打卡时间、延后打卡时间等,打卡时间随机生成,没有规律; 14、A帐B帐系统自由切换,并通过相关的密码切换功能,严格控制系统使用和登陆权限; 15、安全性好、隐蔽性强; 16、各种报表齐全、对应,如原始打卡数据、考勤日报表、考勤月报表、各种请假单、各种签卡单等(齐全的报表往往让验厂人员感觉如此多的各种报表,造假的可以性很低,大大提高验厂的通过率)
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页