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