1. 程式人生 > >Android TV框架 TIF(Android TV Input Framework)入門實踐

Android TV框架 TIF(Android TV Input Framework)入門實踐

做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

下面說一下專案的大致流程:

第一步:

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的介紹和框架的使用部分結束了,以後若有新的理解再來新增。