-
2019-05-31 15:05:43
下面就以 Android 平台创建 UPnP 服务并调用相关的控制方法介绍 Cling 的基本使用。
(1) 定义自己的 UpnpService 类,继承自 AndroidUpnpServiceImpl
(2) 启动该 Service
(3) 从 UpnpService 中获取 ControlPoint,并搜索设备upnpService.getControlPoint().search(new STAllHeader());
搜索注册在多播地址的所有设备,也可根据需要使用不同条件搜索。
(4) 获取所有类型为 MediaRenderer 的设备
upnpService.getRegistry().getDevices(new UDADeviceType("MediaRenderer"));
(5) 向 Device 发送指令
从查找到的结果中获取一个 Device,并向其发送 Play 指令Device device = SystemManager.getInstance().getSelectedDevice(); if (device == null) { return; } Service avtService = device.findService(new UDAServiceType("AVTransport")); if (avtService != null) { ControlPoint cp = SystemManager.getInstance().getControlPoint(); cp.execute(new Play(avtService) { @Override public void success(ActionInvocation invocation) { Log.i(TAG, "Play success."); } @Override public void failure(ActionInvocation arg0, UpnpResponse arg1, String arg2) { Log.e(TAG, "Play failed"); } }); }
上述即为一个基本的发现、控制流程,通过 ControlPoint 发送指令并处理 callback。
更多相关内容 -
cling:交互式C ++解释器Cling
2021-04-01 06:09:09Cling-交互式C ++解释器 主要存储库位于 概述 Cling是基于Clang和LLVM编译器基础结构的交互式C ++解释器。 Cling实现了概念,以利用快速的应用程序开发。 解释器实现为LLVM和Clang的一个小扩展,重用了它们的优点,... -
xeus-cling:适用于C ++编程语言的Jupyter内核
2021-02-26 10:13:28xeus-cling是Jupyter内核C ++基于C ++解释和本机实现Jupyter协议的 。 安装 xeus-cling已为Linux和OS X平台上的conda软件包管理器打包。 目前,我们不提供Windows平台的软件包。 为确保安装正常进行,最好将xeus-... -
cling-core-2.1.2.jar和cling-support-2.1.2.jar
2021-07-30 08:29:54cling-core-2.1.2.jar和cling-support-2.1.2.jar 下载 -
simpledlna:基于cling实现的Android投屏方案
2021-05-10 17:50:25simpledlna 基于cling实现的Android投屏方案 源码解析参考 -
cling-core-2.0.1及三个依赖包,用于Java对UPnP操作的实现
2020-03-12 15:55:41cling-core-2.0.1及三个依赖包,用于Java对UPnP操作的实现 cling-core-2.0.1.jar seamless-http-1.1.1.jar seamless-util-1.1.1.jar seamless-xml-1.1.1.jar -
c#源码转java源码的-cling:坚持的C++解释器
2021-05-19 13:16:32Cling的树有一个用Python编写的用户友好的命令行实用程序,称为Cling包装工具(CPT),它可以从源代码构建Cling并生成适用于多种平台的安装程序包。 CPT需要Python 3或更高版本。 如果您在本地克隆了Cling的源代码,... -
flutter_cling
2021-03-15 22:02:49flutter_cling 一个新的Flutter插件。 入门 该项目是Flutter的起点,Flutter是一个特殊的程序包,其中包括针对Android和/或iOS的平台特定的实现代码。 要获得Flutter入门方面的帮助,请查看我们的,其中提供了教程... -
xeus-cling-binaries:https:github.comjupyter-xeusxeus-cling的每周构建
2021-02-27 20:25:01xeus-cling Binaries 每周构建 。 安装 二进制文件包( .tar.gz ,针对GNU libc构建)每周生成一次,并上传到。 您可以像这样安装它们: # Fetch the xeus-cling binary package for your architecture (x86_64 ... -
docker-cling:用于构建C ++解释器CLING的Dockerfile
2021-05-22 10:50:42这是用于构建C ++解释器CLING的Dockerfile。 您可以使用以下命令运行C ++解释器: docker run -i -t tmbdev/cling-local cling 您可能会设置一些挂载并更改UID,以使其对运行C ++脚本更加有用。 -
docker-cling:基于clang的dockerized c++解释器
2021-06-19 09:29:35CERN 的 cling 是一个即时编译器,它使您能够编写和执行 C++14 脚本。 #如何使用容器: ##交互模式 docker run -it --rm nikhedonia/cling cling ##使用文件 cat myfile.cpp | docker run -it --rm nikhedonia/... -
Android代码-Android-Cling
2019-08-06 04:42:59Android-Cling Example of use is available in Solid Launcher What is Cling library Cling is a library that allows you to create overlays that will allow the user to be informed of how your ... -
AndroidSimpleUpnp:使用 Cling 的简化 Android Upnp 库
2021-07-12 18:13:01Android Upnp 库 查看示例项目以获取快速入门。 如何使用? 包括位于项目“out”目录中的 jar 文件。 编辑您的清单以提供适当的权限和服务描述(参见示例) 然后,您可以对 AndroidUpnpProvider 对象执行方法调用... -
cling实现Render例子
2017-09-27 18:15:29利用cling实现Render播放器端框架简单例子,利用qq音乐app可以搜索到此例子 利用cling实现Render播放器端框架简单例子,利用qq音乐app可以搜索到此例子 -
Cling:书签按钮「Cling: Bookmark Button」「Cling Button」-crx插件
2021-03-22 00:26:28一个全新的书签体验正在等待Cling。 通过我们的浏览器扩展程序方便地收集书签。只需单击一下,有趣的链接就可以直接从浏览器移动到Cling。除了成为出色的可视书签管理器之外,Cling的工作还不只是管理书签。将您的... -
Cling Button-crx插件
2021-04-03 00:18:03只需单击一下,有趣的链接就可以直接从浏览器移动到Cling。 除了成为出色的可视书签管理器之外,Cling的工作还不只是管理书签。 将您的书签与注释,待办事项,照片和文件结合在一起,以确保忠实地传达含义和上下文。... -
Cling 2.0 本地视频、音频、图片投屏DEMO
2018-04-28 11:11:37自己整合资料并总结实现的DEMO,界面借用网上它人代码(只实现了网络URL播放),后经过改造核心功能实现 本地视频、音频、图片投屏功能 -
关于cling库的DLNA开发
2014-05-29 10:43:35关于cling库的DLNA开发,对cling库有比较详细的解说,以及代码 -
cling-dlna
2015-12-04 16:02:31cling-dlna非常好的开发软件,支持java开发,基础dlna开发包 -
cling-master.zip
2017-11-16 10:07:15cling-master.zip -
cling.zip
2022-06-04 20:37:35参考资料和源代码 -
cling-website:Cling网站
2021-02-10 22:31:50cling-website:Cling网站 -
cling源码的demo程序
2015-08-11 12:59:07基于cling库的demo示例程序,将所有的包已经包含,可正常运行 -
Android投屏方案(基于cling)
2019-07-22 10:14:49是在73.0.3683.90版本的chrome源码上修改而来,涉及到抓取网页里视频的播放地址、播放视频、视频投屏、视频下载、网页内广告屏蔽等方面,了解到ijkplayer、GSYVideoPlayer、ffmpeg、乐播投屏、cling、NanoHttp、...一 、前言
最近做了一个浏览器&视频播放的项目,是在73.0.3683.90版本的chrome源码上修改而来,涉及到抓取网页里视频的播放地址、播放视频、视频投屏、视频下载、网页内广告屏蔽等方面,了解到ijkplayer、GSYVideoPlayer、ffmpeg、乐播投屏、cling、NanoHttp、adblock等相关技术,现在就准备花点时间把一些技术相关的内容整理一下,分享给大家。
为什么先写的是投屏相关的技术呢?刚开始投屏用的乐播的sdk,乐播的效果肯定是很好的,支持的协议更多,更稳定,但是乐播有一个限制,个人开发者不能获取到APPID和SDK资源,最开始是帮别人做的项目,他们提供了相关的资源,所以就没有去研究过投屏的其他方案。但是后来又有了个新项目,新项目也有一个需求是投屏,但是他们没法提供相关的APPID和SDK,所以我就只能找新的方案,它就是cling。
android相关的投屏方案封装不止cling一个,只是恰巧看到了,并且有人说cling算是封装的比较好的了,所以就直接选择了cling开始做。截止目前,我做的这个项目基本上能正常的投屏图片、音频、视频等资源了,至于控制功能暂时还未尝试,但是相关的方法是有的,只是没有尝试调用。因为需求不同,所以目前我只研究了发送端的功能,至于接收端,我给的参考链接的最后两个链接里是有代码可以参考的。
本来说到投屏技术,一般都会讲到DLNA、AirPlay、UPNP协议等相关基础,但是这方面的介绍文献实在是多如牛毛,我就不在这里浪费时间去复制粘贴别人的劳动成果了,我给出几个当时我找资料时参考的几篇文章,供大家参考:
本着大家都是着重于“取而用之”的实际需求,这里先附上本次项目的源码
二 、实现的过程
我这个人呢,有个特别不好的习惯,不是十分喜欢直接抄袭别人的东西,又喜欢重复造轮子,但是呢,能力又有限,所以写出来的东西会和参考的东西有所区别,但是不一定比别人的好,请大家不要见怪。但这次重复造轮子的原因,主要是因为那个demo里的代码我没办法直接用,以及要解决cling2.2.0版本在9.0系统上出现无法解析描述文件的问题。
整个工程的目录结构如下图所示
[外链图片转存失败(img-znXFPZXt-1563761574281)(https://raw.githubusercontent.com/ykbjson/ykbjson.github.io/master/blogimage/simpledlna/simpledlna_code_structure.png)]
2.1源码浅析前的说明
webserver这个module就是基于NanoHttp实现的本地http服务器的代码。
simplepermission整个module是一个权限请求的库,因为整个工程基于androidx,没花时间去找适配androidx的权限库,就自己改吧改吧了一下原来用的一个权限库来用,因为要实现投屏,必须要一些权限,参见screening module的manifest文件。
sereening module是整个项目的核心,有三个地方要先提出来说清楚,一个是log包下的AndroidLoggingHandler,这个类是为了解决cling包里的logger不输出日志的问题,具体的请看
另一个是xml包下的几个类,主要是重写了cling里解析设备交互报文的SAX解析器,cling原来的代码,在生成解析器的时候抛了异常,导致设备交互的报文无法被解析,后续流程就中断了,以至于无法发现可以投屏的设备。说到这里,不得不说,大神们写的代码,设计的真的非常强大,扩展性考虑的很好,我本以为只能clone cling的源码下来自己改,没想到这个解析器可以自定义,为作者手动点赞!
最后一个地方呢,就是DLNABrowserService,里面只是重载了AndroidUpnpServiceImpl的一个方法,返回DLNAUDA10ServiceDescriptorBinderSAXImpl,以便于替换cling自带的无法在android9.0上面正常工作的UDA10ServiceDescriptorBinderSAXImpl。所以,在使用这个库的时候,在app module的manifest里声明的就不是AndroidUpnpServiceImpl而是DLNABrowserService,这一点要注意。
至于bean包下的两个类,DeviceInfo是对支持投屏的设备——Device 的一个封装;MediaInfo是为了方便传递要投屏的多媒体信息做的封装。
2.2部分源码浅析
接下来我们从listener包开始讲解整个项目的源码,里面有四个回调接口,其实我感觉有些是多余的,但是呢,因为一些操作是异步的,感觉有一个回调接口能更好的控制使用这个库的逻辑,避免出现一些错误。
###初始化DLNAManager回调接口——DLNAStateCallback
public interface DLNAStateCallback { void onConnected(); void onDisconnected(); }
这个其实应该叫DLNAManagerInitCallback,初始化DLNAManager的时候传递的,可以为null,只要你能保证你后续代码时在DLNAManager初始化之后调用的。
###注册设备列表和状态回调接口——DLNARegistryListener
public abstract class DLNARegistryListener implements RegistryListener { private final DeviceType DMR_DEVICE_TYPE = new UDADeviceType("MediaRenderer"); public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { } public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) { } /** * Calls the {@link #onDeviceChanged(List)} method. * * @param registry The Cling registry of all devices and services know to the local UPnP stack. * @param device A validated and hydrated device metadata graph, with complete service metadata. */ public void remoteDeviceAdded(Registry registry, RemoteDevice device) { onDeviceChanged(build(registry.getDevices())); onDeviceAdded(registry, device); } public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { } /** * Calls the {@link #onDeviceChanged(List)} method. * * @param registry The Cling registry of all devices and services know to the local UPnP stack. * @param device A validated and hydrated device metadata graph, with complete service metadata. */ public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { onDeviceChanged(build(registry.getDevices())); onDeviceRemoved(registry, device); } /** * Calls the {@link #onDeviceChanged(List)} method. * * @param registry The Cling registry of all devices and services know to the local UPnP stack. * @param device The local device added to the {@link org.fourthline.cling.registry.Registry}. */ public void localDeviceAdded(Registry registry, LocalDevice device) { onDeviceChanged(build(registry.getDevices())); onDeviceAdded(registry, device); } /** * Calls the {@link #onDeviceChanged(List)} method. * * @param registry The Cling registry of all devices and services know to the local UPnP stack. * @param device The local device removed from the {@link org.fourthline.cling.registry.Registry}. */ public void localDeviceRemoved(Registry registry, LocalDevice device) { onDeviceChanged(build(registry.getDevices())); onDeviceRemoved(registry, device); } public void beforeShutdown(Registry registry) { } public void afterShutdown() { } public void onDeviceChanged(Collection<Device> deviceInfoList) { onDeviceChanged(build(deviceInfoList)); } public abstract void onDeviceChanged(List<DeviceInfo> deviceInfoList); public void onDeviceAdded(Registry registry, Device device) { } public void onDeviceRemoved(Registry registry, Device device) { } private List<DeviceInfo> build(Collection<Device> deviceList) { final List<DeviceInfo> deviceInfoList = new ArrayList<>(); for (Device device : deviceList) { //过滤不支持投屏渲染的设备 if (null == device.findDevices(DMR_DEVICE_TYPE)) { continue; } final DeviceInfo deviceInfo = new DeviceInfo(device, getDeviceName(device)); deviceInfoList.add(deviceInfo); } return deviceInfoList; } private String getDeviceName(Device device) { String name = ""; if (device.getDetails() != null && device.getDetails().getFriendlyName() != null) { name = device.getDetails().getFriendlyName(); } else { name = device.getDisplayString(); } return name; } }
这个类只是对RegistryListener的封装,因为我当时想着这个类主要是回调当前发现的设备的列表信息,所以就简单封装了一下,每次设备数量改变的时候就把新的设备数量通过一个回调方法传递出去,忽略一些不关注的方法。
###连接设备回调接口——DLNADeviceConnectListener
public interface DLNADeviceConnectListener { int TYPE_DLNA = 1; int TYPE_IM = 2; int TYPE_NEW_LELINK = 3; int CONNECT_INFO_CONNECT_SUCCESS = 100000; int CONNECT_INFO_CONNECT_FAILURE = 100001; int CONNECT_INFO_DISCONNECT = 212000; int CONNECT_INFO_DISCONNECT_SUCCESS = 212001; int CONNECT_ERROR_FAILED = 212010; int CONNECT_ERROR_IO = 212011; int CONNECT_ERROR_IM_WAITTING = 212012; int CONNECT_ERROR_IM_REJECT = 212013; int CONNECT_ERROR_IM_TIMEOUT = 212014; int CONNECT_ERROR_IM_BLACKLIST = 212015; void onConnect(DeviceInfo deviceInfo, int errorCode); void onDisconnect(DeviceInfo deviceInfo,int type,int errorCode); }
这个类是给DLNAPlayer连接设备时用的。说到这个所谓的连接设备,其实感觉也不需要这个步骤,cling本身可能已经做好了设备之间的连接,回调回来的设备列表里的设备都是连接过了的,直接可以通信。但是我发现乐播的sdk里就有一个连接设备的方法,必须先调用连接设备的这个方法,在回调里才能继续后续操作,所以我这里也设计了一个连接设备的步骤,我怕万一是cling有专门连接设备的接口,只是我还没发现而已,后面发现了就来改写这个连接设备的方法。
###控制设备回调接口——DLNAControlCallback
public interface DLNAControlCallback { int ERROR_CODE_NO_ERROR = 0; int ERROR_CODE_RE_PLAY = 1; int ERROR_CODE_RE_PAUSE = 2; int ERROR_CODE_RE_STOP = 3; int ERROR_CODE_DLNA_ERROR = 4; int ERROR_CODE_SERVICE_ERROR = 5; int ERROR_CODE_NOT_READY = 6; void onSuccess(@Nullable ActionInvocation invocation); void onReceived(@Nullable ActionInvocation invocation,@Nullable Object ... extra); void onFailure(@Nullable ActionInvocation invocation, @IntRange(from = ERROR_CODE_NO_ERROR, to = ERROR_CODE_NOT_READY) int errorCode, @Nullable String errorMsg); }
顾名思义,这个类就是发送端在控制接收端做出一系列动作时的回调接口,包括播放、暂停、结束、静音开闭、音量调整、播放进度获取等等。播放、暂停、结束、静音开闭、音量调整等方法只会回调onSuccess和onFailure方法;获取播放进度这种需要获取结果的方法会在onReceived方法里返回结果。
看完这几个类之后,我们应该大致知道这个库整个工作的流程了:初始化DLNAManager -> 注册设备列表回调接口 -> 连接一个设备 -> 控制这个设备。只不过呢,我把连接设备和控制设备部分功能封装到了DLNAPlayer里面,不然DLNAManager会有点臃肿,不便于维护。这里说到了整个库的工作流程,那么接下来我们就从DLNAManager开始接着分析。
###整个库的入口——DLNAManager
public final class DLNAManager { private static final String TAG = "DLNAManager"; private static final String LOCAL_HTTP_SERVER_PORT = "9090"; private static boolean isDebugMode = false; private Context mContext; private AndroidUpnpService mUpnpService; private ServiceConnection mServiceConnection; private DLNAStateCallback mStateCallback; private RegistryListener mRegistryListener; private List<DLNARegistryListener> registryListenerList; private Handler mHandler; private BroadcastReceiver mBroadcastReceiver; private DLNAManager() { AndroidLoggingHandler.injectJavaLogger(); mHandler = new Handler(Looper.getMainLooper()); registryListenerList = new ArrayList<>(); mRegistryListener = new RegistryListener() { @Override public void remoteDeviceDiscoveryStarted(final Registry registry, final RemoteDevice device) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.remoteDeviceDiscoveryStarted(registry, device); } } }); } @Override public void remoteDeviceDiscoveryFailed(final Registry registry, final RemoteDevice device, final Exception ex) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.remoteDeviceDiscoveryFailed(registry, device, ex); } } }); } @Override public void remoteDeviceAdded(final Registry registry, final RemoteDevice device) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.remoteDeviceAdded(registry, device); } } }); } @Override public void remoteDeviceUpdated(final Registry registry, final RemoteDevice device) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.remoteDeviceUpdated(registry, device); } } }); } @Override public void remoteDeviceRemoved(final Registry registry, final RemoteDevice device) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.remoteDeviceRemoved(registry, device); } } }); } @Override public void localDeviceAdded(final Registry registry, final LocalDevice device) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.localDeviceAdded(registry, device); } } }); } @Override public void localDeviceRemoved(final Registry registry, final LocalDevice device) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.localDeviceRemoved(registry, device); } } }); } @Override public void beforeShutdown(final Registry registry) { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.beforeShutdown(registry); } } }); } @Override public void afterShutdown() { mHandler.post(() -> { synchronized (DLNAManager.class) { for (DLNARegistryListener listener : registryListenerList) { listener.afterShutdown(); } } }); } }; mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (null != intent && TextUtils.equals(intent.getAction(), ConnectivityManager.CONNECTIVITY_ACTION)) { final NetworkInfo networkInfo = getNetworkInfo(context); if (null == networkInfo) { return; } if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { initLocalMediaServer(); } } } }; } private static class DLNAManagerCreator { private static DLNAManager manager = new DLNAManager(); } public static DLNAManager getInstance() { return DLNAManagerCreator.manager; } public void init(@NonNull Context context) { init(context, null); } public void init(@NonNull Context context, @Nullable DLNAStateCallback stateCallback) { if (null != mContext) { logW("ReInit DLNAManager"); return; } if (context instanceof ContextThemeWrapper || context instanceof android.view.ContextThemeWrapper) { mContext = context.getApplicationContext(); } else { mContext = context; } mStateCallback = stateCallback; initLocalMediaServer(); initConnection(); registerBroadcastReceiver(); } private void initConnection() { mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mUpnpService = (AndroidUpnpService) service; mUpnpService.getRegistry().addListener(mRegistryListener); mUpnpService.getControlPoint().search(); if (null != mStateCallback) { mStateCallback.onConnected(); } logD("onServiceConnected"); } @Override public void onServiceDisconnected(ComponentName name) { mUpnpService = null; if (null != mStateCallback) { mStateCallback.onDisconnected(); } logD("onServiceDisconnected"); } }; mContext.bindService(new Intent(mContext, DLNABrowserService.class), mServiceConnection, Context.BIND_AUTO_CREATE); } /** * 本地视频和图片也可以直接投屏,根目录为sd卡根目录 */ private void initLocalMediaServer() { checkConfig(); try { final PipedOutputStream pipedOutputStream = new PipedOutputStream(); System.setIn(new PipedInputStream(pipedOutputStream)); new Thread(() -> { final String localIpAddress = getLocalIpStr(mContext); final String localMediaRootPath = Environment.getExternalStorageDirectory().getAbsolutePath(); String[] args = { "--host", localIpAddress,/*局域网ip地址*/ "--port", LOCAL_HTTP_SERVER_PORT,/*局域网端口*/ "--dir", localMediaRootPath/*下载视频根目录*/ }; SimpleWebServer.startServer(args); logD("initLocalLinkService success,localIpAddress : " + localIpAddress + ",localVideoRootPath : " + localMediaRootPath); }).start(); } catch (IOException e) { e.printStackTrace(); logE("initLocalLinkService failure", e); } } private void registerBroadcastReceiver() { checkConfig(); mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } private void unregisterBroadcastReceiver() { checkConfig(); mContext.unregisterReceiver(mBroadcastReceiver); } public void registerListener(DLNARegistryListener listener) { checkConfig(); checkPrepared(); if (null == listener) { return; } registryListenerList.add(listener); listener.onDeviceChanged(mUpnpService.getRegistry().getDevices()); } public void unregisterListener(DLNARegistryListener listener) { checkConfig(); checkPrepared(); if (null == listener) { return; } mUpnpService.getRegistry().removeListener(listener); registryListenerList.remove(listener); } public void startBrowser() { checkConfig(); checkPrepared(); mUpnpService.getRegistry().addListener(mRegistryListener); mUpnpService.getControlPoint().search(); } public void stopBrowser() { checkConfig(); checkPrepared(); mUpnpService.getRegistry().removeListener(mRegistryListener); } public void destroy() { checkConfig(); registryListenerList.clear(); unregisterBroadcastReceiver(); SimpleWebServer.stopServer(); stopBrowser(); if (null != mUpnpService) { mUpnpService.getRegistry().removeListener(mRegistryListener); mUpnpService.getRegistry().shutdown(); } if (null != mServiceConnection) { mContext.unbindService(mServiceConnection); mServiceConnection = null; } if (null != mHandler) { mHandler.removeCallbacksAndMessages(null); mHandler = null; } registryListenerList = null; mRegistryListener = null; mBroadcastReceiver = null; mStateCallback = null; mContext = null; } private void checkConfig() { if (null == mContext) { throw new IllegalStateException("Must call init(Context context) at first"); } } private void checkPrepared() { if (null == mUpnpService) { throw new IllegalStateException("Invalid AndroidUpnpService"); } } //------------------------------------------------------静态方法----------------------------------------------- /** * 获取ip地址 * * @param context * @return */ public static String getLocalIpStr(@NonNull Context context) { WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); if (null == wifiInfo) { return ""; } return intToIpAddress(wifiInfo.getIpAddress()); } /** * int类型的ip转换成标准ip地址 * * @param ip * @return */ public static String intToIpAddress(int ip) { return (ip & 0xff) + "." + ((ip >> 8) & 0xff) + "." + ((ip >> 16) & 0xff) + "." + ((ip >> 24) & 0xff); } public static NetworkInfo getNetworkInfo(@NonNull Context context) { final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); return null == connectivityManager ? null : connectivityManager.getActiveNetworkInfo(); } static String tryTransformLocalMediaAddressToLocalHttpServerAddress(@NonNull Context context, String sourceUrl) { logD("tryTransformLocalMediaAddressToLocalHttpServerAddress ,sourceUrl : " + sourceUrl); if (TextUtils.isEmpty(sourceUrl)) { return sourceUrl; } if (!isLocalMediaAddress(sourceUrl)) { return sourceUrl; } String newSourceUrl = getLocalHttpServerAddress(context) + sourceUrl.replace(Environment.getExternalStorageDirectory().getAbsolutePath(), ""); logD("tryTransformLocalMediaAddressToLocalHttpServerAddress ,newSourceUrl : " + newSourceUrl); try { final String[] urlSplits = newSourceUrl.split("/"); final String originFileName = urlSplits[urlSplits.length - 1]; String fileName = originFileName; fileName = URLEncoder.encode(fileName, "UTF-8"); fileName = fileName.replaceAll("\\+", "%20"); newSourceUrl = newSourceUrl.replace(originFileName, fileName); logD("tryTransformLocalMediaAddressToLocalHttpServerAddress ,encodeNewSourceUrl : " + newSourceUrl); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return newSourceUrl; } private static boolean isLocalMediaAddress(String sourceUrl) { return !TextUtils.isEmpty(sourceUrl) && !sourceUrl.startsWith("http://") && !sourceUrl.startsWith("https://") && sourceUrl.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath()); } /** * 获取本地http服务器地址 * * @param context * @return */ public static String getLocalHttpServerAddress(Context context) { return "http://" + getLocalIpStr(context) + ":" + LOCAL_HTTP_SERVER_PORT; } public static void setIsDebugMode(boolean isDebugMode) { DLNAManager.isDebugMode = isDebugMode; } static void logV(String content) { logV(TAG, content); } public static void logV(String tag, String content) { if (!isDebugMode) { return; } Log.v(tag, content); } static void logD(String content) { logD(TAG, content); } public static void logD(String tag, String content) { if (!isDebugMode) { return; } Log.d(tag, content); } static void logI(String content) { logI(TAG, content); } public static void logI(String tag, String content) { if (!isDebugMode) { return; } Log.i(tag, content); } static void logW(String content) { logW(TAG, content); } public static void logW(String tag, String content) { if (!isDebugMode) { return; } Log.w(tag, content); } static void logE(String content) { logE(TAG, content); } public static void logE(String tag, String content) { logE(tag, content, null); } static void logE(String content, Throwable throwable) { logE(TAG, content, throwable); } public static void logE(String tag, String content, Throwable throwable) { if (!isDebugMode) { return; } if (null != throwable) { Log.e(tag, content, throwable); } else { Log.e(tag, content); } } }
这个类有点长,但是要关注的方法就那么几个。init方法里干了几件事:
1.初始化本地投屏服务——initLocalMediaServer,投屏本地视频
2.连接AndroidUpnpService——initConnection,获取控制点和投屏服务
3.注册了一个网络连接变化的广播——registerBroadcastReceiver,网络变化时重启LocalMediaServer,保证本地资源投屏成功的几率
还有就是发起搜索设备的动作、停止搜索设备的动作、注册RegistryListener、移除RegistryListener等方法。剩下一些就是可以封装到工具类里的方法,懒得在添加类了,索性就写到了里面。
这个类还有一个作用就是维护了一个RegistryListener,统一的分发局域网内设备数量、设备状态、设备服务状态变化的回调事件。当你初始化完DLNAManager,并向这个类注册了DLNARegistryListener,然后调用startBrowser发起搜索,如果局域网内有可以接受投屏的设备,你就可以在DLNARegistryListener的onDeviceChanged方法里收到当前局域网内可以投屏的设备列表了。有了可用的设备列表,接下来,我们就可以开始连接接收端设备发送投屏数据以及控制他了。
连接和控制接收端设备——DLNAPlayer
public class DLNAPlayer { private static final String DIDL_LITE_FOOTER = "</DIDL-Lite>"; private static final String DIDL_LITE_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>" + "<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 static final int UNKNOWN = -1; /** * 已连接状态 */ public static final int CONNECTED = 0; /** * 播放状态 */ public static final int PLAY = 1; /** * 暂停状态 */ public static final int PAUSE = 2; /** * 停止状态 */ public static final int STOP = 3; /** * 转菊花状态 */ public static final int BUFFER = 4; /** * 投放失败 */ public static final int ERROR = 5; /** * 已断开状态 */ public static final int DISCONNECTED = 6; private int currentState = UNKNOWN; private DeviceInfo mDeviceInfo; private Device mDevice; private MediaInfo mMediaInfo; private Context mContext;//鉴权预留 private ServiceConnection mServiceConnection; private AndroidUpnpService mUpnpService; private DLNADeviceConnectListener connectListener; /** * 连接、控制服务 */ private ServiceType AV_TRANSPORT_SERVICE; private ServiceType RENDERING_CONTROL_SERVICE; public DLNAPlayer(@NonNull Context context) { mContext = context; AV_TRANSPORT_SERVICE = new UDAServiceType("AVTransport"); RENDERING_CONTROL_SERVICE = new UDAServiceType("RenderingControl"); initConnection(); } public void setConnectListener(DLNADeviceConnectListener connectListener) { this.connectListener = connectListener; } private void initConnection() { mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mUpnpService = (AndroidUpnpService) service; currentState = CONNECTED; if (null != mDeviceInfo) { mDeviceInfo.setState(CONNECTED); mDeviceInfo.setConnected(true); } if (null != connectListener) { connectListener.onConnect(mDeviceInfo, DLNADeviceConnectListener.CONNECT_INFO_CONNECT_SUCCESS); } } @Override public void onServiceDisconnected(ComponentName name) { currentState = DISCONNECTED; if (null != mDeviceInfo) { mDeviceInfo.setState(DISCONNECTED); mDeviceInfo.setConnected(false); } if (null != connectListener) { connectListener.onDisconnect(mDeviceInfo, DLNADeviceConnectListener.TYPE_DLNA, DLNADeviceConnectListener.CONNECT_INFO_DISCONNECT_SUCCESS); } mUpnpService = null; connectListener = null; mDeviceInfo = null; mDevice = null; mMediaInfo = null; AV_TRANSPORT_SERVICE = null; RENDERING_CONTROL_SERVICE = null; mServiceConnection = null; mContext = null; } }; } public void connect(@NonNull DeviceInfo deviceInfo) { checkConfig(); mDeviceInfo = deviceInfo; mDevice = mDeviceInfo.getDevice(); if (null != mUpnpService) { currentState = CONNECTED; if (null != connectListener) { connectListener.onConnect(mDeviceInfo, DLNADeviceConnectListener.CONNECT_INFO_CONNECT_SUCCESS); } return; } mContext.bindService(new Intent(mContext, DLNABrowserService.class), mServiceConnection, Context.BIND_AUTO_CREATE); } public void disconnect() { checkConfig(); try { mContext.unbindService(mServiceConnection); } catch (Exception e) { DLNAManager.logE("DLNAPlayer disconnect error.", e); } } private void checkPrepared() { if (null == mUpnpService) { throw new IllegalStateException("Invalid AndroidUpnpService"); } } private void checkConfig() { if (null == mContext) { throw new IllegalStateException("Invalid context"); } } private void execute(@NonNull ActionCallback actionCallback) { checkPrepared(); mUpnpService.getControlPoint().execute(actionCallback); } private void execute(@NonNull SubscriptionCallback subscriptionCallback) { checkPrepared(); mUpnpService.getControlPoint().execute(subscriptionCallback); } public void play(@NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (checkErrorBeforeExecute(PLAY, avtService, callback)) { return; } execute(new Play(avtService) { @Override public void success(ActionInvocation invocation) { super.success(invocation); currentState = PLAY; callback.onSuccess(invocation); mDeviceInfo.setState(PLAY); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }); } public void pause(@NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (checkErrorBeforeExecute(PAUSE, avtService, callback)) { return; } execute(new Pause(avtService) { @Override public void success(ActionInvocation invocation) { super.success(invocation); currentState = PAUSE; callback.onSuccess(invocation); mDeviceInfo.setState(PAUSE); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }); } public void stop(@NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (checkErrorBeforeExecute(STOP, avtService, callback)) { return; } execute(new Stop(avtService) { @Override public void success(ActionInvocation invocation) { super.success(invocation); currentState = STOP; callback.onSuccess(invocation); mDeviceInfo.setState(STOP); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }); } public void seekTo(String time, @NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (checkErrorBeforeExecute(avtService, callback)) { return; } execute(new Seek(avtService, time) { @Override public void success(ActionInvocation invocation) { super.success(invocation); callback.onSuccess(invocation); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }); } public void setVolume(long volume, @NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(RENDERING_CONTROL_SERVICE); if (checkErrorBeforeExecute(avtService, callback)) { return; } execute(new SetVolume(avtService, volume) { @Override public void success(ActionInvocation invocation) { super.success(invocation); callback.onSuccess(invocation); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }); } public void mute(boolean desiredMute, @NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(RENDERING_CONTROL_SERVICE); if (checkErrorBeforeExecute(avtService, callback)) { return; } execute(new SetMute(avtService, desiredMute) { @Override public void success(ActionInvocation invocation) { super.success(invocation); callback.onSuccess(invocation); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }); } public void getPositionInfo(@NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (checkErrorBeforeExecute(avtService, callback)) { return; } final GetPositionInfo getPositionInfo = new GetPositionInfo(avtService) { @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } @Override public void success(ActionInvocation invocation) { super.success(invocation); callback.onSuccess(invocation); } @Override public void received(ActionInvocation invocation, PositionInfo info) { callback.onReceived(invocation, info); } }; execute(getPositionInfo); } public void getVolume(@NonNull DLNAControlCallback callback) { final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (checkErrorBeforeExecute(avtService, callback)) { return; } final GetVolume getVolume = new GetVolume(avtService) { @Override public void success(ActionInvocation invocation) { super.success(invocation); callback.onSuccess(invocation); } @Override public void received(ActionInvocation invocation, int currentVolume) { callback.onReceived(invocation, currentVolume); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { currentState = ERROR; callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); mDeviceInfo.setState(ERROR); } }; execute(getVolume); } public void setDataSource(@NonNull MediaInfo mediaInfo) { mMediaInfo = mediaInfo; //尝试变换本地播放地址 mMediaInfo.setUri(DLNAManager.tryTransformLocalMediaAddressToLocalHttpServerAddress(mContext, mMediaInfo.getUri())); } public void start(final @NonNull DLNAControlCallback callback) { mDeviceInfo.setMediaID(mMediaInfo.getMediaId()); String metadata = pushMediaToRender(mMediaInfo); final Service avtService = mDevice.findService(AV_TRANSPORT_SERVICE); if (null == avtService) { callback.onFailure(null, DLNAControlCallback.ERROR_CODE_SERVICE_ERROR, null); return; } execute(new SetAVTransportURI(avtService, mMediaInfo.getUri(), metadata) { @Override public void success(ActionInvocation invocation) { super.success(invocation); play(callback); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { DLNAManager.logE("play error:" + defaultMsg); currentState = ERROR; mDeviceInfo.setState(ERROR); callback.onFailure(invocation, DLNAControlCallback.ERROR_CODE_DLNA_ERROR, defaultMsg); } }); } private String pushMediaToRender(@NonNull MediaInfo mediaInfo) { return pushMediaToRender(mediaInfo.getUri(), mediaInfo.getMediaId(), mediaInfo.getMediaName(), mediaInfo.getMediaType()); } private String pushMediaToRender(String url, String id, String name, int ItemType) { final long size = 0; final Res res = new Res(new MimeType(ProtocolInfo.WILDCARD, ProtocolInfo.WILDCARD), size, url); final String creator = "unknow"; final String parentId = "0"; final String metadata; switch (ItemType) { case MediaInfo.TYPE_IMAGE: ImageItem imageItem = new ImageItem(id, parentId, name, creator, res); metadata = createItemMetadata(imageItem); break; case MediaInfo.TYPE_VIDEO: VideoItem videoItem = new VideoItem(id, parentId, name, creator, res); metadata = createItemMetadata(videoItem); break; case MediaInfo.TYPE_AUDIO: AudioItem audioItem = new AudioItem(id, parentId, name, creator, res); metadata = createItemMetadata(audioItem); break; default: throw new IllegalArgumentException("UNKNOWN MEDIA TYPE"); } DLNAManager.logE("metadata: " + metadata); return metadata; } /** * 创建投屏的参数 * * @param item * @return */ private 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())); DateFormat 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)); 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()); } DLNAManager.logE("protocolinfo: " + protocolinfo); // 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 boolean checkErrorBeforeExecute(int expectState, Service avtService, @NonNull DLNAControlCallback callback) { if (currentState == expectState) { callback.onSuccess(null); return true; } return checkErrorBeforeExecute(avtService, callback); } private boolean checkErrorBeforeExecute(Service avtService, @NonNull DLNAControlCallback callback) { if (currentState == UNKNOWN) { callback.onFailure(null, DLNAControlCallback.ERROR_CODE_NOT_READY, null); return true; } if (null == avtService) { callback.onFailure(null, DLNAControlCallback.ERROR_CODE_SERVICE_ERROR, null); return true; } return false; } }
这个类也很长,因为干事情的就是他,所以他的方法比较多,设定播放数据、播放、暂停、停止、拖动进度、静音控制、音量控制等等都在这个DLNAPlayer里实现的。cling对设定投屏数据、播放、暂停、停止、拖动进度、静音控制、音量控制等功能都做了封装,我这里只是统一了一个回调接口,这些个方法里,只有设定投屏数据的时候才需要发送upnp协议规定的xml数据,其他方法都不需要。构建xml数据的方法也是在上面给出的链接里复制的,反正就是upnp协议规定好的,需要这中格式的数据,如果你想接收端能比较完整的显示投屏的数据信息,传递的MediaInfo可以详细些,我这里都值传递了多媒体地址信息。
三、结语
唉,终于贴完代码了,贴的时候感觉好无奈,自己也很反感这中方式,但是这只是对cling的一个简单实用实用示例,技术细节都是别人处理好了的,我只是做了点简单的分层,希望大家看了demo能直接使用cling实现投屏功能,也没什么技术分析,所以就只是贴个代码了。
至于使用的方法,我就更懒得贴了,没有任何意义,大家直接看源码的demo就可以了,我只给大家提几个需要注意的地方:
1.app module的build.gradle文件必须要加上一句
//去重复的引用 packagingOptions { exclude 'META-INF/beans.xml' }
这是由于引入jetty引起的文件重复。
2.build.gradle文件里类似如下代码
minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName
里面的ext.minSdkVersion等等,请参见根目录的build.gradle。
3.所有工程的依赖库都基于androidx,所以,如果有需要的童鞋在集成到自己的工程里的时候要慎重,因为androidx库和support库不兼容。
最后,祝大家工作愉快。
-
cling-support-2.1.1.jar和cling-core-2.1.1.jar
2022-02-21 10:13:03cling-support-2.1.1.jar和cling-core-2.1.1.jar电视投屏相关工具 -
Cling UPnP库为Android。关机并重新启动后没有看到设备
2021-06-09 11:16:24我们使用了补丁版本:cling-distribution-2.0.1所以可能最新的lib也已经修复了吗? 祝你好运! 你用2.0.1测试过吗?所以问题是:从库中的代码 成功运行:07-17 17:53:23.205 14826-14826/ I/Data...我已经找到了一些解决方案,但很遗憾有些问题里面的保鲜库。我的一些colluegue做了修复,我无法分享“修补程序库”,但不清楚哪些修改会受到影响。我们使用了补丁版本:cling-distribution-2.0.1
所以可能最新的lib也已经修复了吗? 祝你好运! 你用2.0.1测试过吗?
所以问题是:从库中的代码 成功运行:
07-17 17:53:23.205 14826-14826/ I/DatagramIO﹕ Creating bound socket (for datagram input/output) on: 192.168.1.110/192.168.1.110
07-17 17:53:23.205 14826-14826/ I/StreamServer﹕ Setting executor service on servlet container adapter
07-17 17:53:23.205 14826-14826/ I/StreamServer﹕ Adding connector: 109.46.235.174/109.46.235.174:0
07-17 17:53:23.205 14826-14826/ D/libc-netbsd﹕ [getaddrinfo]: hostname=xxxxx; servname=(null); cache_mode=(null), netid=0; mark=0
07-17 17:53:23.205 14826-14826/ D/libc-netbsd﹕ [getaddrinfo]: ai_addrlen=0; ai_canonname=xxxxx; ai_flags=4; ai_family=0
07-17 17:53:23.205 14826-14826/ I/pnpServiceConfiguration﹕ Thread pool rejected execution of class org.eclipse.jetty.server.AbstractConnector$Acceptor
07-17 17:53:23.205 14826-14826/ W/System.err﹕ 2015-07-17 17:53:23.215:INFO:oejs.AbstractConnector:Started [email protected]:60087
07-17 17:53:23.205 14826-14826/ I/DatagramIO﹕ Creating bound socket (for datagram input/output) on: 109.46.235.174/109.46.235.174
07-17 17:53:23.205 14826-14826/ I/StreamClient﹕ Starting Jetty HttpClient...
07-17 17:53:23.205 14826-14826/ I/Router﹕ WiFi multicast lock acquired
07-17 17:53:23.205 14826-14826/ I/Router﹕ Created WiFi lock, mode: 3
07-17 17:53:23.205 14826-14826/ I/Router﹕ WiFi lock acquired
07-17 17:53:23.215 14826-14826/ I/UpnpServiceImpl﹕ <<< UPnP service started successfully
错误来看是这样的:
07-17 17:53:23.205 14826-14826/ I/StreamClient﹕ Starting Jetty HttpClient...
07-17 17:53:23.205 14826-14826/ I/Router﹕ WiFi multicast lock acquired
07-17 17:53:23.205 14826-14826/ I/Router﹕ Created WiFi lock, mode: 3
07-17 17:53:23.205 14826-14826/ I/Router﹕ WiFi lock acquired
07-17 17:53:23.205 14826-14826/ I/DatagramIO﹕ Creating bound socket (for datagram input/output) on: 192.168.1.110/192.168.1.110
07-17 17:53:23.205 14826-14826/ I/StreamServer﹕ Setting executor service on servlet container adapter
07-17 17:53:23.205 14826-14826/ I/StreamServer﹕ Adding connector: 109.46.235.174/109.46.235.174:0
07-17 17:53:23.205 14826-14826/ D/libc-netbsd﹕ [getaddrinfo]: hostname=xxxxx; servname=(null); cache_mode=(null), netid=0; mark=0
07-17 17:53:23.205 14826-14826/ D/libc-netbsd﹕ [getaddrinfo]: ai_addrlen=0; ai_canonname=xxxxx; ai_flags=4; ai_family=0
07-17 17:53:23.205 14826-14826/ I/pnpServiceConfiguration﹕ Thread pool rejected execution of class org.eclipse.jetty.server.AbstractConnector$Acceptor
07-17 17:53:23.205 14826-14826/ W/System.err﹕ 2015-07-17 17:53:23.215:INFO:oejs.AbstractConnector:Started [email protected]:60087
07-17 17:53:23.205 14826-14826/ I/DatagramIO﹕ Creating bound socket (for datagram input/output) on: 109.46.235.174/109.46.235.174
07-17 17:53:23.215 14826-14826/ I/UpnpServiceImpl﹕ <<< UPnP service started successfully
-
Cling基础教程 - 用户手册(入门)
2019-06-06 17:20:19Cling基础教程 - 用户手册 version:1.0 原文链接:http://4thline.org/projects/cling/support/manual/cling-support-manual.xhtml#chapter.MediaServer 1. 使用互联网网关设备 一个互联网网关设备可以将...Cling使用教程 - 用户手册
版本:1.0 原文链接:http://4thline.org/projects/cling/support/manual/cling-support-manual.xhtml
1. 使用网关设备
网关设备可以将本地局域网连接到广域网上去,并且通过Upnp服务(Universal Plug-n-Play:即插即用服务)来监视和配置局域网和广域网的接口。通常情况下,你可以用这种设备来进行本地端口的映射,比如说:一个本地局域网应用想要获取广域网上主机的连接,那么他必须在本地路由器上创建一个端口用于转发和映射。
1.1 配置本地端口
Cling包含了所有需要用到的功能,通过Cling在本地网络上的路由来映射端口,只需要三行代码即可:
PortMapping desireMapping = new PortMapping( 8123, "192.168.0.123", PortMapping.Protocol.TCP, "My Port Mapping" ); UpnpService upnpService = new UpnpServiceImpl( new PortMappingListener(desireMapping) ); upnpService.getControlPoint().search();
第一行代码配置了一个端口映射,包括内外端口号,内部IP,使用的协议以及功能描述。
第二行代码启动了Upnp服务,并传入一个PortMappingListener。一旦设备被任何其他发现,PortMappingListener将会把端口映射到这些设备上去。
然后你可以立即调用ControlPoint#search方法,这将触发你所在网络上的所有本地路由的响应和搜索,从而激活端口映射。在应用退出时,你可以通过调用UpnpService#shutdown()来关闭Upnp堆栈,PortMappingListener将删除端口映射。如果你忘记关闭Upnp堆栈,那么这个端口映射将继续保留在网关设备上(默认的时间为0)。
如果程序在运行过程中出错,程序将会输出一些有关org.fourthline.cling.support.igd.PortMappingListener的警告日志。当然你也可以通过重写 PortMappingListener#handleFailureMessage(String)方法来处理这些错误。
另外,你也随时可以用如下回调来手动在已经被发现的设备上添加和删除端口映射:
Service service = device.findService(new UDAServiceId("WANIPConnection")); // 执行添加 upnpService.getControlPoint().execute( new PortMappingAdd(service, desiredMapping){ @Override public void success(ActionInvocation invocation){ // All ok } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){ // Something wrong } } ); assertEquals(mapping[0].getInternalClient(), "192.168.0.123"); assertEquals(mapping[0].getInternalPort().getValue().longValue(), 8123); assertEquals(mapping[0].isEnabled()); // 执行删除 upnpService.getControlPoint().execute( new PortMappingDelete(service, desiredMapping){ @Override public void success(ActionInvocation invocation){ // All ok } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){ // Something wrong } } );
1.2 获取连接信息
通过以下回调,可以从广域网的连接服务中检索出当前连接信息,包括状态、正常运行时间和最后一条错误消息:
Service service = device.findSevice(new UDAServiceId("WANIPConnection")); upnpService.getControlPoint().execute( new GetStatusInfo(service){ @Override protected void success(Connection.StattusInfo statusInfo){ assertEquals(statusInfo.getStatus, Connection.Status.Connected); assertEquals(statusInfo.getUptimeSeconds(), 1000); assertEquals(statusInfo.getLastError(), Connection.Error.ERROR_NONE); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){ // Something is wrong } } )
此外,你还可以通过一个回调函数来获取设备的外部连接IP:
Service service = device.findService(new UDAServiceId("WANIPConnection")); upnpService.getControlPoing().execute( new GetExternalIP(service){ @Override protected void success(String externalIPAddress){ assertEquals(externalIPAddress, "123.123.123.123"); } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { // Something is wrong } } )
2. 发送信息给三星电视
许多可联网的三星电视都实现了 samsung. com:MessageBoxService 的功能。这个功能的初始目标可能是当你在家并且你的手机连接上了你房间内的无线网络时,可以让三星手机自动的把通知和提醒发送到电视上进行显示(前提你的电视是开着的并且也连接到了这个无线网络)。
Cling也提供了类似的类可以让你通过Upnp向三星电视发送通知。
你有几种可以使用的消息类型。第一种就是带有发送者/接收者名称,电话号码以及时间戳和文本信息:
MessageSMS msg = new MessageSMS( new DateTime("2010-06-21", "16:34:12"), new NumberName("1234", "The receiver"), new NumberName("5678", "The sender"), "Hello world!" );
这条消息将以“收到新短信”的形式出现在你的电视上,并带有显示所有消息细节的选项。另外,三星电视识可别的其他消息类型还包括来电通知和日历日程提醒:
MessageIncomingCall msg = new MessageIncomingCall( new DateTime("2010-06-21", "16:34:12"), new NumberName("1234", "The Callee"), new NumberName("5678", "The Caller") ); MessageScheduleReminder msg = new MessageScheduleReminder( new DateTime("2010-06-21", "16:34:12"), new NumberName("1234", "The Owner"), "The Subject", new DateTime("2010-06-21", "17:34:12"), "The Location", "Hello World!" );
以下是你如何通过异步的方式来发送信息:
LocalService service = device.findService(new ServiceId("samsung.com", "MessageBosService")); upnpService.getControlPoint.execute(new AddMessage(service, msg)){ @Override public void success(ActionInvocation invocation){ // All OK } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){ // Something is wrong } }
需要注意的是,电视上可能包含一个移除消息的操作描述。Cling也提供了RemoveMessageCallback来移除消息,但是这个与三星电视的实现有所差别,这个动作是通过远程控制来直接在电视上删除该消息。
3.访问和提供媒体服务
标准的Upnp音视频媒体服务终端模板记录了一些最流行的Upnp服务,尽管是命名为媒体服务,但是实际上这些服务并不提供和访问媒体数据,比如音乐,图片亦或是视频文件。这些服务是通过分享元数据,这些元数据包含媒体文件的相关信息,比如它们的名称、格式和大小,以及可以用来获取实际文件的定位器。传输这些媒体文件已经超出这些媒体服务的范畴,通常情况下会使用简单的HTTP服务器和客户端来实现这个传输任务。
一个媒体服务设备至少包括文件目录(ContentDirectory)和连接管理的服务(ConnectionManager)。
3.1 浏览文件目录
文件目录服务提供媒体资源的元数据。这些元数据的格式是XML,内容由DIDL、Dublic Core和UPnP特定元素和属性组合而成。通常情况下,可以通过调用目录文件服务的Browse方法来获取这个XML格式的元数据,然后手动解析它。
如下是Cling所提供的Browse方法回调处理:
new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN) { @Override public void received(ActionInvocation actionInvocation, DIDLContent didl) { // Read the DIDL content either using generic Container and Item types... assertEquals(didl.getItems().size(), 2); Item item1 = didl.getItems().get(0); assertEquals( item1.getTitle(), "All Secrets Known" ); assertEquals( item1.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM.class), "Black Gives Way To Blue" ); assertEquals( item1.getFirstResource().getProtocolInfo().getContentFormatMimeType().toString(), "audio/mpeg" ); assertEquals( item1.getFirstResource().getValue(), "http://10.0.0.1/files/101.mp3" ); // ... or cast it if you are sure about its type ... assert MusicTrack.CLASS.equals(item1); MusicTrack track1 = (MusicTrack) item1; assertEquals(track1.getTitle(), "All Secrets Known"); assertEquals(track1.getAlbum(), "Black Gives Way To Blue"); assertEquals(track1.getFirstArtist().getName(), "Alice In Chains"); assertEquals(track1.getFirstArtist().getRole(), "Performer"); MusicTrack track2 = (MusicTrack) didl.getItems().get(1); assertEquals(track2.getTitle(), "Check My Brain"); // ... which is much nicer for manual parsing, of course! } @Override public void updateStatus(Status status) { // Called before and after loading the DIDL content } @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { // Something wasn't right... } };
第一个回调(received)检索出了所有包含3(容器标识符)的子元素;
在验证和解析DIDL XML内容之后会调用received()方法,因此可以使用类型安全的接口来处理这些元数据。DIDL的内容是由Container和Item构成的,但是此处只关心根目录容器的子元素,而非子目录的。
你可以实现也可以忽略掉updateStatus()方法,这个方法可以很方便的在元数据加载和解析的前后给你提供通知。例如你可以通过此方法来更新你的消息图标的状态。
如下示例向你展示了一个可提供更多操作的复杂回调示例:
ActionCallback complexBrowseAction = new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN, "*", 100l, 50l, new SortCriterion(true, "dc:title"), // Ascending new SortCriterion(false, "dc:creator")) { // Descending // Implementation... };
你可以通过声明一些通配符参数,将结果限制为50个(从100个开始)分页,以及一些排序条件。由目录文件服务来处理这些操作。
3.2 目录文件服务
换个角度,你可以先开始从目录文件的服务端角度来思考。Cling提供了一个简单的目录文件抽象类,你要做的只需要实现browse()方法:
public class MP3ContentDirectory extends AbstractContentDirectoryService { @Override public BrowseResult browse(String objectID, BrowseFlag browseFlag, String filter, long firstResult, long maxResults, SortCriterion[] orderby) throws ContentDirectoryException { try { // This is just an example... you have to create the DIDL content dynamically! DIDLContent didl = new DIDLContent(); String album = ("Black Gives Way To Blue"); String creator = "Alice In Chains"; // Required PersonWithRole artist = new PersonWithRole(creator, "Performer"); MimeType mimeType = new MimeType("audio", "mpeg"); didl.addItem(new MusicTrack( "101", "3", // 101 is the Item ID, 3 is the parent Container ID "All Secrets Known", creator, album, artist, new Res(mimeType, 123456l, "00:03:25", 8192l, "http://10.0.0.1/files/101.mp3") )); didl.addItem(new MusicTrack( "102", "3", "Check My Brain", creator, album, artist, new Res(mimeType, 2222222l, "00:04:11", 8192l, "http://10.0.0.1/files/102.mp3") )); // Create more tracks... // Count and total matches is 2 return new BrowseResult(new DIDLParser().generate(didl), 2, 2); } catch (Exception ex) { throw new ContentDirectoryException( ContentDirectoryErrorCode.CANNOT_PROCESS, ex.toString() ); } } @Override public BrowseResult search(String containerId, String searchCriteria, String filter, long firstResult, long maxResults, SortCriterion[] orderBy) throws ContentDirectoryException { // You can override this method to implement searching! return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy); } }
在这里可以看到新建了一个DIDLContent实例将结果存储起来,在用DIDLParser将其转换成XML字符串,最后用BrowseReuslt返回数据时。如何去构建DIDL的内容需要你自己来决定,通常情况下,需要动态的去通过后端数据库来查询,然后将结果封装到COntainer和Item中去。Cling提供了去多便利的内容模型类来表示多媒体的元数据,正如内容目录中定义的那样(MusicTrack,Movie等),你可以早org.fourthline.cling.support.model包中找到。
DIDLParser不是线程安全的,所以不要在服务端应用程序的多个线程中使用一个单例。
AbstractContentDirectoryService只实现了COntentDirectory中文件浏览和搜索的必须的动作和声明的变量。如果想要去编辑这些元数据,那就需要另外增加方法了。
媒体服务设备同样需要有个连接管理服务。
3.3 HTTP-GET的简单连接管理
如果你的传输协议是基于HTTP的GET请求,也就是说你的媒体播放器将从HTTP服务器上下载文件或者获取文件流,那么你所要为这个媒体服务提供的将是一个非常简单的连接管理。
这个连接管理实际上并不管理任何连接,甚至它根本都不提供任何功能。如下就是你通过Cling提供的ConnectManagerService来如何创建和绑定这个简单的服务。
LocalService<ConnectionManagerService> service = new AnnotationLocalServiceBinder().read(ConnectionManagerService.class); service.setManager( new DefaultServiceManager<>( service, ConnectionManagerService.class ) );
现在可以将这个服务添加到你的媒体服务设备上去,并且它将开始正常工作。
事实上,许多媒体服务器至少提供了一个“数据源”协议列表。这个列表包含了媒体服务器可能具有的所有(MIME)协议类型。接收器(显示器)将会通过这个协议信息来决定是否可以播放来自媒体服务器的资源文件,而不是去浏览并查看每一个资源的类型。
首先,创建一个服务器支持的协议信息的列表:
final ProtocolInfos sourceProtocols = new ProtocolInfos( new ProtocolInfo( Protocol.HTTP_GET, ProtocolInfo.WILDCARD, "audio/mpeg", "DLNA.ORG_PN=MP3;DLNA.ORG_OP=01" ), new ProtocolInfo( Protocol.HTTP_GET, ProtocolInfo.WILDCARD, "video/mpeg", "DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=01;DLNA.ORG_CI=0" ) );
现在你需要自定义连接管理服务,在实例化时将协议列表作为参数进行传递:
service.setManager( new DefaultServiceManager<ConnectionManagerService>(service, null) { @Override protected ConnectionManagerService createServiceInstance() throws Exception { return new ConnectionManagerService(sourceProtocols, null); } } );
如果你传输协议不是HTTP而是其他的,比如RTSP流,那么这个连接管理将不会起任何作用。
3.4 管理对等点的连接
你可能认为既然媒体播放器上通过URL使用HTTP-GET方式去拉取媒体数据,那么连接管理就不是必须的了。但是你需要明白的是Upnp媒体服务器已经提供了URL,如果还需要他提供URL对应的文件,那么显然这已经超出了常见Upnp的系统架构范围了。
再者,当媒体数据源是要将数据推送到播放器或者需要事先为播放器准备连接,那么此时连接管理服务就变得有用了。在这种情况下,两方连接管理需要事先通过PrepareForConnection操作来协商连接 - 具体哪方发起连接由你决定。当媒体结束播放时,一端的连接管理将调用ConnectionComplete操作。每一个连接都具有唯一的标志符以及相关的连接协议信息,对应的连接管理会将该连接作为对等点连接进行处理。
Cling提供了对等点连接服务AbstractPeeringConnectionManagerService,它将帮助你完成所有繁重的任务,你只需要实现创建和关闭连接的操作。尽管我们现在仍在讨论媒体服务器相关的内容,但是对等点的连接协商是需要在媒体渲染/播放端进行实现的。因此如下的例子相关的就是一个对媒体渲染器的连接管理。
首先,实现你想要如何管理连接两端的连接(这只是一边):
public class PeeringConnectionManager extends AbstractPeeringConnectionManagerService { PeeringConnectionManager(ProtocolInfos sourceProtocolInfo, ProtocolInfos sinkProtocolInfo) { super(sourceProtocolInfo, sinkProtocolInfo); } @Override protected ConnectionInfo createConnection(int connectionID, int peerConnectionId, ServiceReference peerConnectionManager, ConnectionInfo.Direction direction, ProtocolInfo protocolInfo) throws ActionException { // Create the connection on "this" side with the given ID now... ConnectionInfo con = new ConnectionInfo( connectionID, 123, // Logical Rendering Control service ID 456, // Logical AV Transport service ID protocolInfo, peerConnectionManager, peerConnectionId, direction, ConnectionInfo.Status.OK ); return con; } @Override protected void closeConnection(ConnectionInfo connectionInfo) { // Close the connection } @Override protected void peerFailure(ActionInvocation invocation, UpnpResponse operation, String defaultFailureMessage) { System.err.println("Error managing connection with peer: " + defaultFailureMessage); } }
在createConnection()方法中,你需要为负责创建连接的服务提供显示控制和音视频传输的标识符。这个连接ID已经为你定义好了,所以你需要做的就是返回带有这些信息的这个连接。
closeConnection()方法是与createConnection对应的方法,此方法你可以实现在关闭连接服务的相关逻辑,如清理无用信息。
peerFailure()方法与前面的两条方法无关。它只由调用操作的连接管理器使用,而不是在接收端使用。
下面让我们在两个连接管理器之间创建一个对等点连接。首先,创建作为数据源的服务(我们假设这是表示媒体数据源的媒体服务器):
PeeringConnectionManager peerOne = new PeeringConnectionManager( new ProtocolInfos("http-get:*:video/mpeg:*,http-get:*:audio/mpeg:*"), null ); LocalService<PeeringConnectionManager> peerOneService = createService(peerOne);
可以看到它提供了几个协议的媒体元数据。接收器(或媒体渲染器)是对等连接管理器:
PeeringConnectionManager peerTwo = new PeeringConnectionManager( null, new ProtocolInfos("http-get:*:video/mpeg:*") ); LocalService<PeeringConnectionManager> peerTwoService = createService(peerTwo);
它只执行一种特定的协议。
createService()方法只是在从(已经提供的)注释中读取服务元数据后,在服务上设置连接管理器实例:
public LocalService<PeeringConnectionManager> createService(final PeeringConnectionManager peer) { LocalService<PeeringConnectionManager> service = new AnnotationLocalServiceBinder().read( AbstractPeeringConnectionManagerService.class ); service.setManager( new DefaultServiceManager<PeeringConnectionManager>(service, null) { @Override protected PeeringConnectionManager createServiceInstance() throws Exception { return peer; } } ); return service; }
现在必须有一个对等点发起连接。它需要创建一个连接标识符,存储这个标识符(“管理”连接),并调用另一个对等点的PrepareForConnection服务。所有这些都被提供并封装在createConnectionWithPeer()方法中:
int peerOneConnectionID = peerOne.createConnectionWithPeer( peerOneService.getReference(), controlPoint, peerTwoService, new ProtocolInfo("http-get:*:video/mpeg:*"), ConnectionInfo.Direction.Input ); if (peerOneConnectionID == -1) { // Connection establishment failed, the peerFailure() // method has been called already. It's up to you // how you'd like to continue at this point. } int peerTwoConnectionID = peerOne.getCurrentConnectionInfo(peerOneConnectionID) .getPeerConnectionID(); int peerTwoAVTransportID = peerOne.getCurrentConnectionInfo(peerOneConnectionID).getAvTransportID();
你需要提供一个对本地服务的引用,一个执行操作的控制点以及用于此连接的协议信息。连接方向(此处我们是输入)是远程对等点应该如何处理这个连接中的数据传输(另外,我们假设这个对等点是数据接收端)。这个方法可以返回新连接的标识符。你可以通过这个标识符来获取连接的一些信息,比如另一个对等点的标识符,AV传输服务标识符。
当你完成连接任务,你可以通过这个方法进行关闭:
peerOne.closeConnectionWithPeer( controlPoint, peerTwoService, peerOneConnectionID );
peerFailure方法将会在调用createConnectionWithPeer()或closeConnectionWithPeer()失败时调用。
4. 访问和提供媒介提供者
MediaRenderer服务的目的是控制远程媒体输出设备。一种实现渲染器的设备,因此具有必要的AVTransport服务,可以像传统红外遥控器一样进行控制。想想用游戏控制器控制Playstation3上的视频回放有多尴尬吧。MediaRenderer就像一个可编程的通用远程API,所以你可以用iPad、Android手机、触摸屏、笔记本电脑或任何其他可以使用Upnp的设备来代替红外线遥控器或Playstation控制器。
(不幸的是,Playstation3没有公开任何MediaRenderer服务。事实上,在电视和机顶盒中,大多数的MediaRenderer实现都是不完整的,或者不兼容的,这是对规范的严格解释。更糟糕的是,没有简化UPnP A/V规范,反而在DLNA指南中添加了更多的规则,从而使得兼容性更加难以实现。一个工作和行为正确的媒体人似乎是个例外,而不是常态。)
这个过程很简单:首先将媒体资源的URL发送给渲染程序。如何获得该资源的URL完全取决于你,可能需要浏览媒体服务器的资源元数据。现在控制渲染器的状态,例如播放、暂停、停止、录制视频等等。你还可以通过媒体渲染器的标准化渲染控制服务控制音频/视频内容的音量和亮度等其他属性。
Cling提供了org.fourthline.clate.Support.avtransport.AbstractAVTransportService类,一个抽象类型,包含所有UPnP操作和状态变量映射。要实现MediaRenderer,你必须创建一个子类并实现所有方法。如果你已经有一个媒体播放器,并且你想要提供一个Upnp的远程控制接口,那么你应该考虑这个策略。
另外,如果你正在编写一个新媒体播放器,Cling甚至可以为你提供状态管理和转换,因此你所要实现的就是媒体数据的实际输出。
4.1 从零创建渲染器
Cling提供了一个可以使你管理当前播放状态的状态机引擎。该特性简化了使用Upnp渲染器编写媒体播放器的过程,包括如下几个步骤:
4.1.1 定义播放状态
首先,定义你定义状态机以及你的播放器可支持的几种状态:
package example.mediarenderer; import org.fourthline.cling.support.avtransport.impl.AVTransportStateMachine; import org.seamless.statemachine.States; @States({ MyRendererNoMediaPrtesent.class, MyRendererStopped.class, MyRenderPlaying.class }) interface MyRendererStateMachine extends AVTransportStateMachine{}
这是一个非常简单的播放器,只有三种状态:没有媒体时的初始状态,以及播放和停止状态。你还可以支持其他状态,比如暂停和记录,但是我们希望这个示例尽可能简单。(同时比较AVTransport:1规范文件第2.5节中的“操作理论”章节和状态图。)
接下来,实现状态和触发从一个状态到另一个状态转换的操作。
初始状态只有一个可能的转换和一个触发该转换的动作:
public class MyRendererNoMediaPresent extends NoMediaPresent{ public MyRendererNoMediaPresetn(AVTransport transport){ super(transport); } @Override public Class<? extends AbstractState> setTransportURI(URI uri, String metaData){ getTransport().setMediaInfo(new MediaInfo(uri.toString(), metaData)); // if you can, you should find and set the duration of the track here! getTransport().setPositionInfo(new PositionInfo(1, metaData, uri.toString())); // it's up to you what "last changes" you want to announce to event listeners getTransport().getLastChange().setEventedValue( getTransport().getInstaceId(), new AVTransportVariable.AVTransportURI(uri), new AVTransportVariable.CurrentTrackURI(uri) ); return MyRendererStopped.class; } }
当客户端为回放设置一个新的URI时,你必须相应地准备你的渲染程序。你通常希望更改AVTransport的MediaInfo以反映新的“当前”跟踪,并且你可能希望公开关于跟踪的信息,比如回放时间。如何做到这一点(例如,你实际上已经可以检索URL后面的文件并分析它)取决于你。
LastChange对象是如何通知控制点状态的任何变化,这里我们告诉控制点有一个新的“AVTransportURI”和一个新的“CurrentTrackURI”。你可以向LastChange添加更多的变量和它们的值,这取决于实际更改的内容——注意,如果你认为几个更改是原子性的,那么你应该在setEventedValue(…)的单个调用中执行此操作。(最后的更改将被轮询并定期发送到后台的控制点,稍后会详细介绍。)
设置URI之后,AVTransport将转换到停止状态。
停止状态有许多可能的转换,从这里一个控制点可以决定播放、查找、跳过到下一个轨道,等等。下面的例子真的没有做多少,你如何实现这些触发器和状态转换完全取决于你的播放引擎的设计-这只是脚手架:
public class MyRendererStopped extends Stopped { public MyRendererStopped(AVTransport transport) { super(transport); } public void onEntry() { super.onEntry(); // Optional: Stop playing, release resources, etc. } public void onExit() { // Optional: Cleanup etc. } @Override public Class<? extends AbstractState> setTransportURI(URI uri, String metaData) { // This operation can be triggered in any state, you should think // about how you'd want your player to react. If we are in Stopped // state nothing much will happen, except that you have to set // the media and position info, just like in MyRendererNoMediaPresent. // However, if this would be the MyRendererPlaying state, would you // prefer stopping first? return MyRendererStopped.class; } @Override public Class<? extends AbstractState> stop() { /// Same here, if you are stopped already and someone calls STOP, well... return MyRendererStopped.class; } @Override public Class<? extends AbstractState> play(String speed) { // It's easier to let this classes' onEntry() method do the work return MyRendererPlaying.class; } @Override public Class<? extends AbstractState> next() { return MyRendererStopped.class; } @Override public Class<? extends AbstractState> previous() { return MyRendererStopped.class; } @Override public Class<? extends AbstractState> seek(SeekMode unit, String target) { // Implement seeking with the stream in stopped state! return MyRendererStopped.class; } }
每个状态都可以有两个神奇的方法:onEntry()和onExit()——它们完全按照名称执行。如果你决定使用超类的方法,不要忘记调用它们!
通常,当调用播放状态的onEntry()方法时,你将开始回放:
public class MyRendererPlaying extends Playing { public MyRendererPlaying(AVTransport transport) { super(transport); } @Override public void onEntry() { super.onEntry(); // Start playing now! } @Override public Class<? extends AbstractState> setTransportURI(URI uri, String metaData) { // Your choice of action here, and what the next state is going to be! return MyRendererStopped.class; } @Override public Class<? extends AbstractState> stop() { // Stop playing! return MyRendererStopped.class; }
到目前为止,编写你的播放器还没有涉及太多的通用即插即用功能——stick只是为你提供了一个状态机,并通过LastEvent接口向客户端发送状态更改的信号。
4.1.2 注册AVTransportService
下一步是将状态机连接到UPnP服务中,这样就可以将该服务添加到设备中,最后添加到粘附注册表。首先,绑定服务并定义服务管理器如何获取玩家实例:
LocalService<AVTransportService> service = new AnnotationLocalServiceBinder().read(AVTransportService.class); // Service's which have "logical" instances are very special, they use the // "LastChange" mechanism for eventing. This requires some extra wrappers. LastChangeParser lastChangeParser = new AVTransportLastChangeParser(); service.setManager( new LastChangeAwareServiceManager<AVTransportService>(service, lastChangeParser) { @Override protected AVTransportService createServiceInstance() throws Exception { return new AVTransportService( MyRendererStateMachine.class, // All states MyRendererNoMediaPresent.class // Initial state ); } } );
构造函数有两个类,一个是状态机定义,另一个是创建后的初始状态。
就是这样——你已经准备好将此服务添加到MediaRenderer设备和控制点将看到它并能够调用操作。
但是,还有一个细节需要考虑:LastChange事件的传播。当任何播放状态或转换向LastChange添加“更改”时,这些数据将被累积。它不会立即或自动发送到GENA订户!如何以及何时将所有累积的更改刷新到控制点由你决定。一种常见的方法是后台线程每秒钟(甚至更频繁地)执行这个操作:
LastChangeAwareServiceManager manager = (LastChangeAwareServiceManager)service.getManager(); manager.fireLastChange();
最后,请注意AVTransport规范还定义了“逻辑”播放器实例。例如,可以同时播放两个uri的呈现程序将有两个AVTransport实例,每个实例都有自己的标识符。保留的标识符“0”是一个呈现器的默认值,该呈现器一次只支持一个URI的回放。在attach中,每个逻辑AVTransport实例由与AVTransport类型的一个实例关联的状态机的一个实例(及其所有状态)表示。所有这些对象都不会共享,而且它们也不是线程安全的。有关此特性的更多信息,请阅读AVTransportService类的文档和代码——默认情况下,它只支持ID为“0”的单个传输实例,你必须重写findInstance()方法来创建和支持多个并行回放实例。
4.2 控制渲染器
Cling支持提供了几个操作回调,简化了为AVTransport服务创建控制点的过程。这是你的播放器的客户端,遥控器。
这是你如何设置URI播放:
ActionCallback setAVTransportURIAction = new SetAVTransportURI(service, "http://10.0.0.1/file.mp3", "NO METADATA") { @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { // Something was wrong } };
这是你如何开始回放:
ActionCallback playAction = new Play(service) { @Override public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { // Something was wrong } };
你的控制点还可以订阅服务并侦听LastChange事件。Cling提供了一个解析器,因此你可以在控制点上获得与服务器上相同的类型和类——这与发送和接收事件数据是一样的。当你在SubscriptionCallback中接收到“last change”字符串时,你可以对其进行转换,例如,当玩家从nomediap状态转换到stop状态时,服务可能已经发送了这个事件:
LastChange lastChange = new LastChange( new AVTransportLastChangeParser(), lastChangeString ); assertEquals( lastChange.getEventedValue( 0, // Instance ID! AVTransportVariable.AVTransportURI.class ).getValue(), URI.create("http://10.0.0.1/file.mp3") ); assertEquals( lastChange.getEventedValue( 0, AVTransportVariable.CurrentTrackURI.class ).getValue(), URI.create("http://10.0.0.1/file.mp3") ); assertEquals( lastChange.getEventedValue( 0, AVTransportVariable.TransportState.class ).getValue(), TransportState.STOPPED );
-
Cling学习
2016-06-02 11:18:10Cling Table Of Contents: 目录 1.Getting Started(开始) 2.A first UPnP service and control point(第一个UPnP服务和控制点) 2.1.The SwitchPower service implementation(开关服务的实施) 2.2.... -
dlna cling wireme
2013-02-26 16:26:11dlna cling wireme 能运行,不错的代码 -
cling source
2012-09-29 14:56:31最近做android端的dlna功能,用到的cling source code