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官方流程图:
4.TIF为开发者提供的接口
1)TvView:负责显示播放的内容。它是一个ViewGroup的子类,它是切台的入口,内置surface用于显示视频播放的内容和通过控制session可以控制音量的大小等。
2)TvInputService:TvInputService是一个重要的类,继承了它并实现一些规范就可以实现一路input信源供其它应用使用。在该service中要实现onCreatSession()方法该方法要返回一个TvInputService.Session对象。这里的service在Manifest中定义时要注意要添加permission和action。添加完之后系统的TvInputManager可以检测到该service是一个TvInputService,也就是一路信源。
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" />
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);
/** * 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>
List<TvInputInfo> list = tvInputManager.getTvInputList(); for(TvInputInfo info:list){ Log.i(TAG, "id:" + info.getId()); }
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是否可用等:/** * 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应用,其他应用是没有权限读取的。