前言
上周我们完成了发现设备,今天才是最重点的了,完成设备之间的控制。干代码之前,我们先想一下,我们的手机该怎样去控制tv的播放、暂停、停止、音量等操作呢?
“Let me think about it...”
首先两个设备必须在一个网络下,这样才能保证双方能相互通信。既然要相互通信,那么语言要互通,所以它们之间要保持一个协议规范,传输的内容双方要看得懂。
这样一来,手机该如何控制tv呢?首先我们的手机在网络中已经找到了tv,然后我们就可以向tv端发送一些控制指令(例如:播放、暂停...),tv端收到指令做出对应的操作(这些对指令处理是不需要我们开发的,这些是支持dlna设备必备的)
看完这篇文章你可以了解:
- 控制设备步骤
- 控制设备代码实现
- 手机如何控制tv
- tv将自己的信息如何通知手机
关于投屏的协议&概念、发现设备 这篇文章不会涉及,如果想了解 可以找对应的内容翻看
下面是投屏系列目录:
关于 android 投屏技术系列:
一、知识概念
这章主要讲一些基本概念, 那些 DLNA 类库都是基于这些概念来做的,了解这些概念能帮助你理清思路,同时可以提升开发效率,遇到问题也能有个解决问题的清晰思路。
二、手机与tv对接
这部分是通过Cling DLNA类库来实现发现设备的。
内容包括:
- 抽出发现设备所需接口
- 发现设备步骤的实现
- 原理的分析
三、手机与tv通信
这部分也是通过Cling DLNA类库来实现手机对tv的控制。
内容包括:
- 控制设备步骤
- 控制设备代码实现
- 手机如何控制tv
- tv将自己的信息如何通知手机
- 原理的分析
控制设备三步曲
- 获取tv设备控制服务
- 获取控制点
- 执行指定控制命令
所有的控制方法,包括:播放、暂停、停止等,都是通过这三步完成。
首先解释一下这三步的意义
获取tv设备控制服务
何为服务?
是否记得android设备投屏技术:协议&概念这篇文章里提到的 upnp 协议
如上图可知,控制点需要通过服务来控制设备。这里我们用的服务是 AVTransport 服务
具体处理思路:
我们执行控制操作 可以创建一个控制工具类,它包含所有的控制方法,同时也创建一个单例的工具箱,这个工具箱里包括了一些常用的方法(将控制工具类放入工具箱中,当我们要执行控制操作,直接调用工具想中的控制工具类,执行对应的播放、暂停等操作)
如何获取控制点?
控制点包含在 UpnpService 里面,Cling 对 android 做了一个封装层 AndroidUpnpServiceImpl,这个类里面就有 UpnpService,于是我们可以通过获取 UpnpService 之后 获取到控制点。
AndroidUpnpServiceImpl:AndroidUpnpServiceImpl extends Service
我们的 activity 可以绑定这个 Service。当 onServiceConnected
的之后,我们就可以获取到控制点了,具体获取方法:
upnpService.getControlPoint();
如何使用控制点控制设备?
通过执行命令的方式:
ControlPoint.execute(命令)
命令包括:
下面我们来用代码实现:
代码实现部分是针对 实现代码(github地址) 的详解。
这里 我稍微做了一点封装,是为了更方便使用。
下面看一下总体结构:
简单介绍一下:
- control:控制工具类接口及控制实现
- entity:cling的设备实体、控制点实体,因为考虑 Cling 库可能存在危险 bug,为了到时候换库方便,就抽出了接口对象,以 Cling 开头的代表 Cling 的实现对象。
- listener:监听,BrowseRegistryListener 这个是发现设备监听回调。
- service:继承 Service 服务的类,主要是对 Cling Service 的一个封装。
manager:避免对 Service 直接操作,manager 作为一个操作的代理对象。 - ui:这个就是界面
- upnp:这个暂时木有用到啦
- util:工具类
- Config:全局配置信息
- Intents:用于消息处理
下面是 uml 简图
下面是简单根据三步曲的代码实现
- 获取tv设备控制服务
- 获取控制点
- 执行指定控制命令
1、首先我们需要在 Activity 绑定 Service:
public class MainActivity extends AppCompatActivity{
...
// 当 cling service 连接上之后 回调该方法
private ServiceConnection mUpnpServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.e(TAG, "mUpnpServiceConnection onServiceConnected");
ClingUpnpService.LocalBinder binder = (ClingUpnpService.LocalBinder) service;
ClingUpnpService beyondUpnpService = binder.getService();
ClingUpnpServiceManager clingUpnpServiceManager = ClingUpnpServiceManager.getInstance();
// 将服务 attach 到 ClingUpnpServiceManager 中,
// 之后我们可以直接调用 ClingUpnpServiceManager 来控制 service 。
clingUpnpServiceManager.setUpnpService(beyondUpnpService);
// 设置发现设备监听回调
clingUpnpServiceManager.getRegistry().addListener(mBrowseRegistryListener);
//Search on service created.
clingUpnpServiceManager.searchDevices();
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.e(TAG, "mUpnpServiceConnection onServiceDisconnected");
ClingUpnpServiceManager.getInstance().setUpnpService(null);
}
};
// 当 service bind 之后 回调该方法
private ServiceConnection mSystemServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.e(TAG, "mSystemServiceConnection onServiceConnected");
SystemService.LocalBinder systemServiceBinder = (SystemService.LocalBinder) service;
//Set binder to SystemManager
ClingUpnpServiceManager clingUpnpServiceManager = ClingUpnpServiceManager.getInstance();
clingUpnpServiceManager.setSystemService(systemServiceBinder.getService());
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.e(TAG, "mSystemServiceConnection onServiceDisconnected");
ClingUpnpServiceManager.getInstance().setSystemService(null);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Bind UPnP service
Intent upnpServiceIntent = new Intent(MainActivity.this, ClingUpnpService.class);
bindService(upnpServiceIntent, mUpnpServiceConnection, Context.BIND_AUTO_CREATE);
// Bind System service
Intent systemServiceIntent = new Intent(MainActivity.this, SystemService.class);
bindService(systemServiceIntent, mSystemServiceConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Unbind UPnP service
unbindService(mUpnpServiceConnection);
// Unbind System service
unbindService(mSystemServiceConnection);
// UnRegister Receiver
unregisterReceiver(mTransportStateBroadcastReceiver);
// 释放资源
ClingUpnpServiceManager.getInstance().destroy();
ClingDeviceList.getInstance().destroy();
}
...
}
它做了哪些事情呢?
- 绑定了两个 Service (ClingUpnpService、SystemService)
- 在 ServiceConnection 中将这两个 Service 设置到 ClingUpnpServiceManager 中(之后我们只需要通过 ClingUpnpServiceManager 来获取这两个 Service 里的方法了,比如:获取控制点...)
- 定义了一个发现设备监听,并将监听绑定到 ClingUpnpService 中。
下面我们看看这两个 Service 是怎样定义的:
首先是 ClingUpnpService:
public class ClingUpnpService extends AndroidUpnpServiceImpl {
private LocalDevice mLocalDevice = null;
@Override
public void onCreate() {
super.onCreate();
//LocalBinder instead of binder
binder = new LocalBinder();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public LocalDevice getLocalDevice() {
return mLocalDevice;
}
public UpnpServiceConfiguration getConfiguration() {
return upnpService.getConfiguration();
}
public Registry getRegistry() {
return upnpService.getRegistry();
}
public ControlPoint getControlPoint() {
return upnpService.getControlPoint();
}
public class LocalBinder extends Binder {
public ClingUpnpService getService() {
return ClingUpnpService.this;
}
}
}
其实没有什么特别的,就是继承了 AndroidUpnpServiceImpl,自己实现了一些方法。
下面是 SystemService:
public class SystemService extends Service {
private Binder binder = new LocalBinder();
// 已选择的设备
private ClingDevice mSelectedDevice;
private int mDeviceVolume;
// 设备事件
private AVTransportSubscriptionCallback mAVTransportSubscriptionCallback;
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public class LocalBinder extends Binder {
public SystemService getService() {
return SystemService.this;
}
}
@Override
public void onDestroy() {
//End all subscriptions
if (mAVTransportSubscriptionCallback != null)
mAVTransportSubscriptionCallback.end();
super.onDestroy();
}
public IDevice getSelectedDevice() {
return mSelectedDevice;
}
public void setSelectedDevice(IDevice selectedDevice, ControlPoint controlPoint) {
if (selectedDevice == mSelectedDevice) return;
Log.i(TAG, "Change selected device.");
mSelectedDevice = (ClingDevice) selectedDevice;
//End last device's subscriptions
if (mAVTransportSubscriptionCallback != null) {
mAVTransportSubscriptionCallback.end();
}
//Init Subscriptions
mAVTransportSubscriptionCallback = new AVTransportSubscriptionCallback(
mSelectedDevice.getDevice().findService(ClingUpnpServiceManager.AV_TRANSPORT_SERVICE));
controlPoint.execute(mAVTransportSubscriptionCallback);
Intent intent = new Intent(Intents.ACTION_CHANGE_DEVICE);
sendBroadcast(intent);
}
}
它的主要工作就是:
- 设置选择的设备
- 设备事件处理
何为选择设备?
就是我们 tv端设备。因为在局域网中可能存在多个支持投屏的设备,我们必须选择一个设备进行投屏,我们把这个设备存在内存中,这样是为了控制设备时直接取当前选中的设备,这样控制设备更加方便。
何为设备事件处理?
用它来监听 tv端 的状态,比如:我需要知道 tv端是否正在播放视频,如果正在播放 我就要求停止播放。还比如:我可以通过它获取 tv端 当前的音量。
AVTransportSubscriptionCallback extends SubscriptionCallback
SubscriptionCallback 就是 Cling 做的一层事件处理封装
// 在这个方法里 处理对应事件
@Override
protected void eventReceived(GENASubscription subscription) {
...
}
下面我们看一下 UpnpServiceManager 里面有哪些方法:
public interface IUpnpServiceManager {
/**
* 搜索所有的设备
*/
void searchDevices();
/**
* 获取支持 Media 类型的设备
*
* @return 设备列表
*/
Collection<? extends IDevice> getDmrDevices();
/**
* 获取控制点
*
* @return 控制点
*/
IControlPoint getControlPoint();
/**
* 获取选中的设备
*
* @return 选中的设备
*/
IDevice getSelectedDevice();
/**
* 设置选中的设备
* @param device 已选中设备
*/
void setSelectedDevice(IDevice device);
/**
* 销毁
*/
void destroy();
}
UpnpServiceManager 就是把 Service 中的方法抽出来,做了一层代理而已。
所以,我们要获取控制点,直接使用 UpnpServiceManager.getControlPoint()
就好了。
下面是控制设备的方法列表:
/**
* 说明:对视频的控制操作定义
* 作者:zhouzhan
* 日期:17/6/27 17:13
*/
public interface IPlayControl {
/**
* 播放一个新片源
*
* @param url 片源地址
*/
void playNew(String url, @Nullable ControlCallback callback);
/**
* 播放
*/
void play(@Nullable ControlCallback callback);
/**
* 暂停
*/
void pause(@Nullable ControlCallback callback);
/**
* 停止
*/
void stop(@Nullable ControlCallback callback);
/**
* 视频 seek
*
* @param pos seek到的位置(单位:毫秒)
*/
void seek(int pos, @Nullable ControlCallback callback);
/**
* 设置音量
*
* @param pos 音量值,最大为 100,最小为 0
*/
void setVolume(int pos, @Nullable ControlCallback callback);
/**
* 设置静音
*
* @param desiredMute 是否静音
*/
void setMute(boolean desiredMute, @Nullable ControlCallback callback);
}
下面是控制方法具体实现:
public class ClingPlayControl implements IPlayControl{
...
@Override
public void playNew(final String url, final ControlCallback callback) {
stop(new ControlCallback() { // 1、 停止当前播放视频
@Override
public void success(IResponse response) {
setAVTransportURI(url, new ControlCallback() { // 2、设置 url
@Override
public void success(IResponse response) {
play(callback); // 3、播放视频
}
@Override
public void fail(IResponse response) {
if (Utils.isNotNull(callback)){
callback.fail(response);
}
}
});
}
@Override
public void fail(IResponse response) {
if (Utils.isNotNull(callback)){
callback.fail(response);
}
}
});
}
@Override
public void play(final ControlCallback callback) {
final Service avtService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.AV_TRANSPORT_SERVICE);
if (Utils.isNull(avtService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
controlPointImpl.execute(new Play(avtService) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
@Override
public void pause(final ControlCallback callback) {
final Service avtService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.AV_TRANSPORT_SERVICE);
if (Utils.isNull(avtService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
controlPointImpl.execute(new Pause(avtService) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
@Override
public void stop(final ControlCallback callback) {
final Service avtService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.AV_TRANSPORT_SERVICE);
if (Utils.isNull(avtService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
controlPointImpl.execute(new Stop(avtService) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
@Override
public void seek(int pos, final ControlCallback callback) {
final Service avtService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.AV_TRANSPORT_SERVICE);
if (Utils.isNull(avtService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
String time = Utils.stringForTime(pos);
Log.e(TAG, "seek->pos: " + pos + ", time: " + time);
controlPointImpl.execute(new Seek(avtService, time) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
@Override
public void setVolume(int pos, @Nullable final ControlCallback callback) {
final Service rcService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.RENDERING_CONTROL_SERVICE);
if (Utils.isNull(rcService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
controlPointImpl.execute(new SetVolume(rcService, pos) {
@Override
public void success(ActionInvocation invocation) {
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
@Override
public void setMute(boolean desiredMute, @Nullable final ControlCallback callback) {
final Service rcService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.RENDERING_CONTROL_SERVICE);
if (Utils.isNull(rcService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
controlPointImpl.execute(new SetMute(rcService, desiredMute) {
@Override
public void success(ActionInvocation invocation) {
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
/**
* 设置片源,用于首次播放
*
* @param url 片源地址
* @param callback 回调
*/
private void setAVTransportURI(String url, final ControlCallback callback){
if (Utils.isNull(url))
return;
// 这里是为了兼容更多格式
String metadata = pushMediaToRender(url, "id", "name", "0");
final Service avtService = ClingUtils.findServiceFromSelectedDevice(ClingUpnpServiceManager.AV_TRANSPORT_SERVICE);
if (Utils.isNull(avtService))
return;
final ControlPoint controlPointImpl = ClingUtils.getControlPoint();
if (Utils.isNull(controlPointImpl))
return;
controlPointImpl.execute(new SetAVTransportURI(avtService, url, metadata) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
if (Utils.isNotNull(callback)){
callback.success(new ClingResponse(invocation));
}
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
if (Utils.isNotNull(callback)){
callback.fail(new ClingResponse(invocation, operation, defaultMsg));
}
}
});
}
...
}
主要对控制方法进行实现,例如:播放、暂停、停止、进度拖拽、调节音量、设置静音。
需要注意的是:
在 setAVTransportURL
方法中 有一个设置兼容格式的问题。
下面我们聊聊什么是兼容格式
何为兼容格式?
就比如 能播视频,那么能不能播音乐?能不能展示相册?
这个就是兼容。
在 Upnp 协议中,两个设备通信的传输媒介是 xml 。值得注意的是,两个设备需要懂相同的语言,那么这个 xml 肯定是有一个格式规范的,它是什么?
给你看一下样本:
... 忘了存了,好尴尬啊。哈哈
看这个吧-> 别人写的样本
反正就是这个结构咯~
下面是我拼接的格式,这里 我设置的是全部格式(所以 按道理说 音乐、视频都可以的,只是还没试过音乐播放)。
public static final String DIDL_LITE_FOOTER = "</DIDL-Lite>";
public static final String DIDL_LITE_HEADER = "<?xml version=\"1.0\"?>"
+ "<DIDL-Lite "
+ "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" "
+ "xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
+ "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
+ "xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\">";
public String pushMediaToRender(String url, String id, String name, String duration) {
long size = 0;
long bitrate = 0;
Res res = new Res(new MimeType(ProtocolInfo.WILDCARD,
ProtocolInfo.WILDCARD), size, url);
String creator = "unknow";
String resolution = "unknow";
VideoItem videoItem = new VideoItem(id, "0", name, creator, res);
String metadata = createItemMetadata(videoItem);
Log.e(TAG, "metadata: " + metadata);
return metadata;
}
public String createItemMetadata(DIDLObject item) {
StringBuilder metadata = new StringBuilder();
metadata.append(DIDL_LITE_HEADER);
metadata.append(String.format(
"<item id=\"%s\" parentID=\"%s\" restricted=\"%s\">", item
.getId(), item.getParentID(), item.isRestricted() ? "1"
: "0"));
metadata.append(String.format("<dc:title>%s</dc:title>",
item.getTitle()));
String creator = item.getCreator();
if (creator != null) {
creator = creator.replaceAll("<", "_");
creator = creator.replaceAll(">", "_");
}
metadata.append(String.format("<upnp:artist>%s</upnp:artist>", creator));
metadata.append(String.format("<upnp:class>%s</upnp:class>", item
.getClazz().getValue()));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date now = new Date();
String time = sdf.format(now);
metadata.append(String.format("<dc:date>%s</dc:date>", time));
// metadata.append(String.format("<upnp:album>%s</upnp:album>",
// item.get);
// <res protocolInfo="http-get:*:audio/mpeg:*"
// resolution="640x478">http://192.168.1.104:8088/Music/07.我醒著做夢.mp3</res>
Res res = item.getFirstResource();
if (res != null) {
// protocol info
String protocolinfo = "";
ProtocolInfo pi = res.getProtocolInfo();
if (pi != null) {
protocolinfo = String.format("protocolInfo=\"%s:%s:%s:%s\"",
pi.getProtocol(), pi.getNetwork(),
pi.getContentFormatMimeType(), pi.getAdditionalInfo());
}
// resolution, extra info, not adding yet
String resolution = "";
if (res.getResolution() != null && res.getResolution().length() > 0) {
resolution = String.format("resolution=\"%s\"",
res.getResolution());
}
// duration
String duration = "";
if (res.getDuration() != null && res.getDuration().length() > 0) {
duration = String.format("duration=\"%s\"", res.getDuration());
}
// res begin
// metadata.append(String.format("<res %s>", protocolinfo)); // no resolution & duration yet
metadata.append(String.format("<res %s %s %s>", protocolinfo, resolution, duration));
// url
String url = res.getValue();
metadata.append(url);
// res end
metadata.append("</res>");
}
metadata.append("</item>");
metadata.append(DIDL_LITE_FOOTER);
return metadata.toString();
}
下面是使用:
// 播放
private void play() {
TransportState currentState = mClingPlayControl.getCurrentState();
/**
* 通过判断状态 来决定 是继续播放 还是重新播放
*/
if (currentState.equals(TransportState.STOPPED)) {
mClingPlayControl.playNew(Config.TEST_URL, new ControlCallback() {
@Override
public void success(IResponse response) {
Log.e(TAG, "play success");
}
@Override
public void fail(IResponse response) {
Log.e(TAG, "play fail");
mHandler.sendEmptyMessage(ERROR_ACTION);
}
});
} else {
mClingPlayControl.play(new ControlCallback() {
@Override
public void success(IResponse response) {
Log.e(TAG, "play success");
}
@Override
public void fail(IResponse response) {
Log.e(TAG, "play fail");
mHandler.sendEmptyMessage(ERROR_ACTION);
}
});
}
}
其它操作也类似了,文章写太长了,所以不一一列举出来了。
下面总结一下,并回复前面文章开头提出的问题:
总结
1、控制设备步骤
控制设备步骤分为三步:
- 获取tv设备控制服务:通过选中的设备执行
device.findService(serviceType);
- 获取控制点:通过执行
UpnpService.getControlPoint()
- 执行指定控制命令:通过执行
ControlPoint.execute(命令)
2、控制设备代码实现
我们通过绑定 Service,然后将 Service 绑定到
ClingUpnpServiceManager
中,ClingUpnpServiceManager
是一个单例对象,因为 Service 里面有获取控制点等方法,绑定到ClingUpnpServiceManager
之后,我们可以直接通过ClingUpnpServiceManager
来获取控制点等。这样更方便。
同时,我们在ClingPlayControl
中封装了控制设备的各种方法(例如:播放、暂停...)
3、手机如何控制tv
这个就是通过控制点 执行对应的控制命令来控制啦。
4、tv将自己的信息如何通知手机
我们创建了一个
AVTransportSubscriptionCallback
的类,它继承SubscriptionCallback
,SubscriptionCallback 是 Cling 做的事件回调。
用于接收 tv端 的通知信息。比如:获取 tv端 的音量... 通过eventReceived
方法来接收