Android TV : 电视输入框架(TIF)剖析
Android TIF(Android TV input Framework)是Google向電視制造商提供了一套標準的API,用于創建Input模塊來控制Android電視。這套API的底層實現的原理是aidl和provider,從而進行了跨進程通信。系統或第三方的應用可以通過TIF獲得所有輸入(input)的信源(輸入的模塊包括:搜臺模塊,MDMI模塊,網絡模塊等),然后通過aidl切臺輸出到屏幕上。
1.電視相關的知識:
HDMI:高清晰度多媒體接口(英文:High Definition Multimedia Interface,HDMI)是一種數字化視頻/音頻接口技術,是適合影像傳輸的專用型數字化接口。
IPTV:網絡電視,也叫VOD電視,三方比如說某某視頻公司提供的視頻資源在電視上播放。
DTV:數字電視
ATV:模擬電視
2.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消費電子控制)
3.TIF官方流程圖:
如上圖所示,TVProvider和TV Input Manager就是TIF中的內容,liveTVApp通過turning調用TV Input Manager獲得一個session,session里面放的是一路信源的狀態,TV Input Manager 必須與 TV Input 創建一對一的會話。liveTVApp通過session以aidl的方式調用TVinputService獲得相關的頻道和具體的節目信息進行播放,并提供家長控制功能。TvInput將獲得的Channel和Programs信息寫入到/data/data/com.android.providers.tv/databases/tv.db數據庫中。
4.TIF為開發者提供的接口
1)TvView:負責顯示播放的內容。它是一個ViewGroup的子類,它是切臺的入口,內置surface用于顯示視頻播放的內容和通過控制session可以控制音量的大小等。
2)TvInputService:TvInputService是一個重要的類,繼承了它并實現一些規范就可以實現一路input信源供其它應用使用。在該service中要實現onCreatSession()方法該方法要返回一個TvInputService.Session對象。這里的service在Manifest中定義時要注意要添加permission和action。添加完之后系統的TvInputManager可以檢測到該service是一個TvInputService,也就是一路信源。
下面創建一個自定義的Service,而這個Service要繼承系統的TvInputService,當然為了簡化這個過程我們可以使用android官方提供的TIF 隨播內容庫:
compile 'com.google.android.libraries.tv:companionlibrary:0.2'
public class TvService extends BaseTvInputService {
@Nullable
@Override
public TvInputService.Session onCreateSession(@NonNull String inputId) {
TvInputSessionImpl session = new TvInputSessionImpl(this, inputId);
session.setOverlayViewEnabled(true);
return session;
}
}
這里的BaseTvInputService也是繼承的TvInputService,需要復寫onCreateSession方法,創建自己的Session用于和TvInputManager交互,最后在清單文件中配置如下:
<service android:name=".service.TvService" android:permission="android.permission.BIND_TV_INPUT"> <intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter> <meta-data android:name="android.media.tv.input" android:resource="@xml/richtvinputservice" /></service>
接著在xml/richtvinputservice中配置了兩個activty,這個是提供LiveTv去打開的,比如第一次啟動這個源時,需要啟動到setupActivity所指定的activity,設置時需要啟動到settingsActivity配置的activity.
<?xml version="1.0" encoding="utf-8"?>
<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.xray.tv.input.MainActivity"
android:setupActivity="com.xray.tv.input.MainActivity" />
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,所以我們可以知道這個service其實在system_server進程中,在類TvInputManagerService中實現。由于這個地方是跨進程通信,其實它使用的是aidl的方式,所以我們可以找到TvInputManager在aidl中定義的接口:
/**
* Interface to the TV input manager service.
* @hide
*/
interface ITvInputManager {
List<TvInputInfo> getTvInputList(int userId);
TvInputInfo getTvInputInfo(in String inputId, int userId);
void updateTvInputInfo(in TvInputInfo inputInfo, int userId);
int getTvInputState(in String inputId, int userId);
List<TvContentRatingSystemInfo> getTvContentRatingSystemList(int userId);
void registerCallback(in ITvInputManagerCallback callback, int userId);
void unregisterCallback(in ITvInputManagerCallback callback, int userId);
boolean isParentalControlsEnabled(int userId);
void setParentalControlsEnabled(boolean enabled, int userId);
boolean isRatingBlocked(in String rating, int userId);
List<String> getBlockedRatings(int userId);
void addBlockedRating(in String rating, int userId);
void removeBlockedRating(in String rating, int userId);
void createSession(in ITvInputClient client, in String inputId, boolean isRecordingSession,
int seq, int userId);
void releaseSession(in IBinder sessionToken, int userId);
int getClientPid(in String sessionId);
void setMainSession(in IBinder sessionToken, int userId);
void setSurface(in IBinder sessionToken, in Surface surface, int userId);
void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
int userId);
void setVolume(in IBinder sessionToken, float volume, int userId);
void tune(in IBinder sessionToken, in Uri channelUri, in Bundle params, int userId);
void setCaptionEnabled(in IBinder sessionToken, boolean enabled, int userId);
void selectTrack(in IBinder sessionToken, int type, in String trackId, int userId);
void sendAppPrivateCommand(in IBinder sessionToken, in String action, in Bundle data,
int userId);
void createOverlayView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
int userId);
void relayoutOverlayView(in IBinder sessionToken, in Rect frame, int userId);
void removeOverlayView(in IBinder sessionToken, int userId);
void unblockContent(in IBinder sessionToken, in String unblockedRating, int userId);
void timeShiftPlay(in IBinder sessionToken, in Uri recordedProgramUri, int userId);
void timeShiftPause(in IBinder sessionToken, int userId);
void timeShiftResume(in IBinder sessionToken, int userId);
void timeShiftSeekTo(in IBinder sessionToken, long timeMs, int userId);
void timeShiftSetPlaybackParams(in IBinder sessionToken, in PlaybackParams params, int userId);
void timeShiftEnablePositionTracking(in IBinder sessionToken, boolean enable, int userId);
// For the recording session
void startRecording(in IBinder sessionToken, in Uri programUri, in Bundle params, int userId);
void stopRecording(in IBinder sessionToken, int userId);
// For TV input hardware binding
List<TvInputHardwareInfo> getHardwareList();
ITvInputHardware acquireTvInputHardware(int deviceId, in ITvInputHardwareCallback callback,
in TvInputInfo info, int userId, String tvInputSessionId, int priorityHint);
void releaseTvInputHardware(int deviceId, in ITvInputHardware hardware, int userId);
// For TV input capturing
List<TvStreamConfig> getAvailableTvStreamConfigList(in String inputId, int userId);
boolean captureFrame(in String inputId, in Surface surface, in TvStreamConfig config,
int userId);
boolean isSingleSessionActive(int userId);
// For DVB device binding
List<DvbDeviceInfo> getDvbDeviceList();
ParcelFileDescriptor openDvbDevice(in DvbDeviceInfo info, int device);
// For preview channels and programs
void sendTvInputNotifyIntent(in Intent intent, int userId);
void requestChannelBrowsable(in Uri channelUri, int userId);
// For CTS purpose only. Add/remove a TvInputHardware device
void addHardwareDevice(in int deviceId);
void removeHardwareDevice(in int deviceId);
}
它的實現是在TvInputManagerService的內部類BinderService中:
private final class BinderService extends ITvInputManager.Stub {
@Override
public List<TvInputInfo> getTvInputList(int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getTvInputList");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
List<TvInputInfo> inputList = new ArrayList<>();
for (TvInputState state : userState.inputMap.values()) {
inputList.add(state.info);
}
return inputList;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public TvInputInfo getTvInputInfo(String inputId, int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getTvInputInfo");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
TvInputState state = userState.inputMap.get(inputId);
return state == null ? null : state.info;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
......
}
TvInputManagerService是在SystemServer中啟動的,具體在SystemServer類的startOtherServices方法中:
if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_LIVE_TV)
|| mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
t.traceBegin("StartTvInputManager");
mSystemServiceManager.startService(TvInputManagerService.class);
t.traceEnd();
}
注意上面會判斷系統TV和LEANBACK的特征而決定是否啟動TvInputManagerService,特征可在device/google/atv/permissions/tv_core_hardware.xml中進行配置:
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<permissions>
<!-- These are the hardware components that all television devices must
include. Devices with optional hardware must also include extra hardware
files.
-->
<feature name="android.hardware.audio.output" />
<feature name="android.hardware.location" />
<feature name="android.hardware.location.network" />
<feature name="android.hardware.screen.landscape" />
<feature name="android.hardware.type.television" />
<feature name="android.software.backup" />
<feature name="android.software.leanback" />
<feature name="android.software.leanback_only" />
<feature name="android.software.live_tv" />
<feature name="android.software.picture_in_picture" notLowRam="true" />
<feature name="android.software.activities_on_secondary_displays" notLowRam="true" />
<feature name="android.software.voice_recognizers" notLowRam="true" />
<feature name="android.software.input_methods" />
<feature name="android.software.autofill" />
<feature name="android.software.cts" />
</permissions>
得到TvInputManager后我們可以遍歷拿到系統當前有多少個service是Tv信源:
List<TvInputInfo> list = tvInputManager.getTvInputList();
for(TvInputInfo info:list){
Log.i(TAG, "id:" + info.getId());
}
6) TvInputInfo:TvInput的信息。包括頻道類型,圖標,名稱等信息。
因為TvInput經常是以第三方應用的方式實現的,當TvInput應用安裝時,TvInputManagerService會檢測安裝包中是否包含TvInputService。
private void registerBroadcastReceivers() {
PackageMonitor monitor = new PackageMonitor() {
private void buildTvInputList(String[] packages) {
synchronized (mLock) {
if (mCurrentUserId == getChangingUserId()) {
buildTvInputListLocked(mCurrentUserId, packages);
buildTvContentRatingSystemListLocked(mCurrentUserId);
}
}
}
@Override
public void onPackageUpdateFinished(String packageName, int uid) {
if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
// This callback is invoked when the TV input is reinstalled.
// In this case, isReplacing() always returns true.
buildTvInputList(new String[] { packageName });
}
...
}
當有安裝包安裝時,監測其中是否有TvInputService,并且權限符合則綁定這個Service.
private void buildTvInputListLocked(int userId, String[] updatedPackages) {
UserState userState = getOrCreateUserStateLocked(userId);
userState.packageSet.clear();
if (DEBUG) Slog.d(TAG, "buildTvInputList");
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TvInputService.SERVICE_INTERFACE),
PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
userId);
List<TvInputInfo> inputList = new ArrayList<>();
for (ResolveInfo ri : services) {
ServiceInfo si = ri.serviceInfo;
//檢測是否有android.permission.BIND_TV_INPUT這個權限
if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) {
Slog.w(TAG, "Skipping TV input " + si.name + ": it does not require the permission "
+ android.Manifest.permission.BIND_TV_INPUT);
continue;
}
ComponentName component = new ComponentName(si.packageName, si.name);
if (hasHardwarePermission(pm, component)) {
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState == null) {
// New hardware input found. Create a new ServiceState and connect to the
// service to populate the hardware list.
serviceState = new ServiceState(component, userId);
userState.serviceStateMap.put(component, serviceState);
updateServiceConnectionLocked(component, userId);
} else {
inputList.addAll(serviceState.hardwareInputMap.values());
}
} else {
try {
TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build();
inputList.add(info);
} catch (Exception e) {
Slog.e(TAG, "failed to load TV input " + si.name, e);
continue;
}
}
userState.packageSet.add(si.packageName);
}
Map<String, TvInputState> inputMap = new HashMap<>();
for (TvInputInfo info : inputList) {
if (DEBUG) {
Slog.d(TAG, "add " + info.getId());
}
TvInputState inputState = userState.inputMap.get(info.getId());
if (inputState == null) {
inputState = new TvInputState();
}
inputState.info = info;
inputMap.put(info.getId(), inputState);
}
for (String inputId : inputMap.keySet()) {
if (!userState.inputMap.containsKey(inputId)) {
notifyInputAddedLocked(userState, inputId);
} else if (updatedPackages != null) {
// Notify the package updates
ComponentName component = inputMap.get(inputId).info.getComponent();
for (String updatedPackage : updatedPackages) {
if (component.getPackageName().equals(updatedPackage)) {
//綁定TvInputService
updateServiceConnectionLocked(component, userId);
notifyInputUpdatedLocked(userState, inputId);
break;
}
}
}
}
...
}
綁定第三方自定義的TvInputService:
private void updateServiceConnectionLocked(ComponentName component, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState == null) {
return;
}
if (serviceState.reconnecting) {
if (!serviceState.sessionTokens.isEmpty()) {
// wait until all the sessions are removed.
return;
}
serviceState.reconnecting = false;
}
boolean shouldBind;
if (userId == mCurrentUserId) {
shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware;
} else {
// For a non-current user,
// if sessionTokens is not empty, it contains recording sessions only
// because other sessions must have been removed while switching user
// and non-recording sessions are not created by createSession().
shouldBind = !serviceState.sessionTokens.isEmpty();
}
if (serviceState.service == null && shouldBind) {
// This means that the service is not yet connected but its state indicates that we
// have pending requests. Then, connect the service.
if (serviceState.bound) {
// We have already bound to the service so we don't try to bind again until after we
// unbind later on.
return;
}
if (DEBUG) {
Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
}
//bind 第三方應用自定義的TvInputService
Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
serviceState.bound = mContext.bindServiceAsUser(
i, serviceState.connection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
new UserHandle(userId));
} else if (serviceState.service != null && !shouldBind) {
// This means that the service is already connected but its state indicates that we have
// nothing to do with it. Then, disconnect the service.
if (DEBUG) {
Slog.d(TAG, "unbindService(service=" + component + ")");
}
mContext.unbindService(serviceState.connection);
userState.serviceStateMap.remove(component);
}
}
TvInputService現在綁定了,那么TvInputMangerService和TvInputService交互的邏輯就到了ServiceConnection中,它的實現在InputServiceConnection中:
@Override
public void onServiceConnected(ComponentName component, IBinder service) {
if (DEBUG) {
Slog.d(TAG, "onServiceConnected(component=" + component + ")");
}
synchronized (mLock) {
UserState userState = mUserStates.get(mUserId);
if (userState == null) {
// The user was removed while connecting.
mContext.unbindService(this);
return;
}
ServiceState serviceState = userState.serviceStateMap.get(mComponent);
serviceState.service = ITvInputService.Stub.asInterface(service);
// Register a callback, if we need to.
if (serviceState.isHardware && serviceState.callback == null) {
serviceState.callback = new ServiceCallback(mComponent, mUserId);
try {
serviceState.service.registerCallback(serviceState.callback);
} catch (RemoteException e) {
Slog.e(TAG, "error in registerCallback", e);
}
}
List<IBinder> tokensToBeRemoved = new ArrayList<>();
// And create sessions, if any.
for (IBinder sessionToken : serviceState.sessionTokens) {
if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) {
tokensToBeRemoved.add(sessionToken);
}
}
for (IBinder sessionToken : tokensToBeRemoved) {
removeSessionStateLocked(sessionToken, mUserId);
}
for (TvInputState inputState : userState.inputMap.values()) {
if (inputState.info.getComponent().equals(component)
&& inputState.state != INPUT_STATE_CONNECTED) {
notifyInputStateChangedLocked(userState, inputState.info.getId(),
inputState.state, null);
}
}
if (serviceState.isHardware) {
serviceState.hardwareInputMap.clear();
for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
try {
serviceState.service.notifyHardwareAdded(hardware);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHardwareAdded", e);
}
}
for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
try {
serviceState.service.notifyHdmiDeviceAdded(device);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
}
}
}
}
}
在onServiceConnected成功后,就可以拿到從TvInputService中獲取的Binder對象,和第三方的TvInputService聯通進行交互,它們之間交互需要創建一個Session,也就是TvInputService.Session,這個Session中的交互是通過ITvInputSessionCallback來實現。
7)TvInputCallback:TvView的一個內部類,TvInputCallBack可以反饋給TvView一些信息比如連接service是否成功,Video是否可用等:
ITvInputSessionCallback.aidl
/**
* Helper interface for ITvInputSession to allow the TV input to notify the system service when a
* new session has been created.
* @hide
*/
oneway interface ITvInputSessionCallback {
void onSessionCreated(ITvInputSession session, in IBinder hardwareSessionToken);
void onSessionEvent(in String name, in Bundle args);
void onChannelRetuned(in Uri channelUri);
void onTracksChanged(in List<TvTrackInfo> tracks);
void onTrackSelected(int type, in String trackId);
void onVideoAvailable();
void onVideoUnavailable(int reason);
void onContentAllowed();
void onContentBlocked(in String rating);
void onLayoutSurface(int left, int top, int right, int bottom);
void onTimeShiftStatusChanged(int status);
void onTimeShiftStartPositionChanged(long timeMs);
void onTimeShiftCurrentPositionChanged(long timeMs);
// For the recording session
void onTuned(in Uri channelUri);
void onRecordingStopped(in Uri recordedProgramUri);
void onError(int error);
}
自定義第三方TvInputService時,根據需求實現以上方法:
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.");
}
......
});
至此,TvInputManager和第三方TvInputService的交互就完成了。
8)TvProvider:
LiveTv和TvInput之間交互還有一種方式就是TvProvider, TvInput應用會將自己的頻道和節目數據寫入TvProvider對應的數據庫中,數據庫的位置在:/data/data/com.android.providers.tv/databases/tv.db
這樣LiveTv就可以讀取TvProvider中的數據了。當然這里的數據除了LiveTv和當前的TvInput應用,其他應用是沒有權限讀取的。
總結
以上是生活随笔為你收集整理的Android TV : 电视输入框架(TIF)剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ProGuard 最全混淆规则说明
- 下一篇: Select查询命令