DLNA设备、服务的注册及响应

DLNA设备、服务的注册及发现(依赖开源库cling)

本文是跟踪代码的记录,因为wifi网络不太好,不能debug跟踪,后面在能够但不跟踪时,会理一下,设备之间的连接过程,及音视频数据的传递过程。

 

DLNA中设备的注册、发现主要基于UPNP协议实现,这是微软推行的一个标准。Upnp最大的愿景是希望任何设备只要一接入网络,所有网上的设备马上就能知道有新设备加入,这些设备之间就可以彼此通信。

 

以下代码分析是基于Cling这个开源库。

 

首先看设备注册:

  • DLNA设备、服务的注册

设备跟服务的注册流程是一样的,以Device的注册为例,注册的过程实际就是通知router这个设备已经存在了,所以会有一个alive类型的notify到多播地址:239.255.255.250:1900

 

  1. 设备实例的创建

LocalDevice

第一个参数是设备ID,其中的UDN是全球唯一的标识符,无论是根设备还是其中的嵌入式设备,而且要保持不变,即使设备重启。这个UND将在SSDP中被使用,有统一格式,前缀是uuid:,后面是Upnp厂商指定的UUID后缀。

如:uuid:b7c7c900-6983-f00b-0000-0000264ce182,其中后面的数字实际是一段hashcode,根据自定义的名字加设备标识生成的hashcode。

 

第二个参数是设备类型,设备类型有固定的命名空间schemas-upnp-org,然后才是具体的类型,如果是渲染端为MediaRenderer,最后是版本号。

完整的设备类型是命名空间+设备类型+版本号。

如:urn:schemas-upnp-org:device:MediaRenderer:1

 

第三个参数是设备详情,其中friendlyname是给设备起的一个比较友好的显示名字,另外一个列表DLNACaps,标识DLNA的能力,通常是"av-upload", "image-upload", "audio-upload"。

 

最后一个参数是服务列表,就是这个设备需要支持哪些服务,比如渲染设备要支持:

ConnectionManagerService,AVTransportService,AudioRenderingControl。

 

  1. 获取UpnpService实例,

Android环境下对应的是AndroidUpnpService接口类型的。这也是Android Upnp应用服务组件的接口。所以UpnpService是以后台服务的形式运行,这样即使应用的Activity退出,后台的服务依然可以继续工作。

 

应用以bindservice的形式启动运行UpnpService的服务,具体是AndroidUpnpServiceImpl实例。这里提供了一个带有android配置的Upnp堆栈,任何想要访问Upnp stack的activity都要绑定和解绑定这个AndroidUpnpServiceImpl。

这个service的默认实现需要配置一些权限,需要指出的是其中的:<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>

获取wifi多播锁的权限,按照开源库的规范api,设备需要获取多播锁,才能正常发送搜索的多播消息,及宣告存在的多播消息。但是咨询了wifi同事,wifi在工作时默认所有数据包都会发送,不会过滤组播包,所以这里对多播锁的获取不是必须的。另外,app本身不需要明确去获取组播锁,这个操作在AndroidRouter工作时已经做了。(测试时,把组播锁相关的代码注释掉,也是可以正常使用的)

 

Android环境下需要重写对Upnp stack的配置数据,通过AndroidUpnpServiceConfiguration来实现,使用jetty构建streamClient,streamServer。

 

通过getRegistryMaintenanceIntervalMillis()可以重写registry 维护线程周期间隔,这个维护线程会周期性的更新设备列表,这个函数可以设置间隔多久去更新一次,实际就是每隔多长时间让registry线程运行一次。

 

通过getExclusiveServiceTypes()过滤掉你不感兴趣的message。

 

 

创建AndroidUpnpServiceImpl实例的过程中,会构建RegistryImpl实例,设备的添加就是通过RegistryImpl来完成的,这个RegistryImpl会启动一个后台的维护线程,执行移除过期设备,执行pending的操作等。

 

创建AndroidUpnpServiceImpl实例的过程中,会构建RouterImpl 实例,具体有AndroidUpnpServiceImpl 完成创建AndroidRouter实例,会配置流监听端口,xml解析库等(AndroidUpnpServiceConfiguration)。AndroidRouter监听网络状态的变化。

 

到这里UpnpService实例创建完成,后面可以通过UpnpService添加设备。

 

  1. 通过RegistryImpl的addDevice来完成设备注册。

接着调用LocalItems的add方法继续干活,先是判断是否已经注册过了,如果已经注册过,不重复执行,直接返回。然后进一步完成添加设备的过程。

 

没有添加过,一步步完成添加过程:

第一步,添加设备下的资源,根据命名空间/upnp下提供的资源,主要有DeviceDescriptor,ServiceDescriptor,ServiceControl,ServiceEvent等资源。

 

第二步,生成RegistryItem对象,添加到DeviceItems集合中。

 

第三步,如果需要通告设备存在,发出存在的通知advertiseAlive()。

 

第四步,回调监听,告诉RegistryListener有本地localDevice设备添加。监听类通常继承DefaultRegistryListener类,并重写其中的设备添加,设备删除方法,具体是在应用的activity中,upnpService实例构建完成,通过upnpService获取到其中的RegistryImpl来添加监听。

DefaultRegistryListener的实现类,通常需要实现:

localDeviceAdded(),

localDeviceRemoved(),

remoteDeviceAdded(),

remoteDeviceRemoved()等方法。

 

  1. 宣告设备存在

LocalItems.java中的advertiseAlive(),处理设备存在的通知。

这里是通过异步的方式提交一个通知任务,通常任务先睡眠100毫米后在执行,避免对网络造成拥塞。

 

接着通过RegistryImpl中创建的ProtocolFactoryImpl实例生成一个通知消息。具体是SendingNotificationAlive实例。

这个通知的类型是NotificationSubtype.ALIVE,也就是ALIVE("ssdp:alive"),

SendingNotification这个类,实际是一个Runnable实例,是为注册的本地设备发送一个通知消息。

 

最后,看下这个通知存在的消息发给了谁?

 

在确定消息发给谁之前,先看是如何发现本机的网络地址的?也就是本机在局域网中的IP地址。

调用堆栈是:

new UpnpServiceImpl() @ AndroidUpnpServiceImpl.java  >

new AndroidRouter().enable() @UpnpServiceImpl.java  >

enable() @RouterImpl.java  >

new AndroidNetworkAddressFactory() @ AndroidUpnpServiceConfiguration.java

discoverNetworkInterfaces() @NetworkAddressFactoryImpl.java

通过NetworkInterface.getNetworkInterfaces()枚举出当前设备所有的网络接口,包括虚拟的,物理的,从中过滤出本地的IP地址:

tworkAddressFactoryImpl: Discovered usable network interface: wlan0

tworkAddressFactoryImpl: Discovered usable network interface address: 10.21.247.171

 

enable() @ RouterImpl.java

startInterfaceBasedTransports()在本机的IP地址上初始化一个多播消息的接受端MulticastReceiver。

startAddressBasedTransports()在本地的IP地址上初始化一个StreamServer。

 

之所以要分析上面的本机IP地址的获取,是因为发送通知消息时会涉及到。应用程序仅仅将数据包发送给组播地址,然后路由器将确保数据包被发送到组播组中的全部主机。

 

继续看前面的遗留问题,通知存在的消息发给了谁?

 

Execute() @SendingNotification.java

第一步,获取本机IP地址上初始化的streamServer,

      List<NetworkAddress> activeStreamServers =

            getUpnpService().getRouter().getActiveStreamServers(null);

NetworkAddress.java类型,包含了三个属性,本机IP地址,端口,本机物理地址(MAC)。

 

第二步,封装一个带有本地设备(渲染器,非当前手机),本机上streamServer的描述对象Location。

        List<Location> descriptorLocations = new ArrayList();

        for (NetworkAddress activeStreamServer : activeStreamServers) {

            descriptorLocations.add(

                    new Location(

                            activeStreamServer,

                            getUpnpService().getConfiguration().getNamespace().getDescriptorPath(getDevice())

                    )

            );

        }

其中的参数,getDevice(),是最开始创建的LocalDevice 实例。

Location实例包含了两个属性,一个是本机当前可用的streamServer,一个是本地设备对应的URI值。Web上可用的每一种资源都有一个通用资源标识符URI进行定位。

本地设备(DMR)对应的

URI:/upnp/dev/b7c7c900-6983-f00b-0000-0000264ce182/desc

 

第三步,发送消息,因为是基于udp协议发送,默认间隔150毫秒,重复发送三次。

 Execute()@SendingNotification.java

for (Location descriptorLocation : descriptorLocations) {

        sendMessages(descriptorLocation);

}

把descriptorLocation添加上localdevice包装成OutgoingNotificationRequest消息类型。

这个过程会添加消息头:

UpnpHeader.Type.NT: (RootDeviceHeader) 'upnp:rootdevice'

UpnpHeader.Type.USN: (USNRootDeviceHeader) 'uuid:b7c7c900-6983-f00b-0000-0000264ce182'

如有物理地址:

UpnpHeader.Type.EXT_IFACE_MAC: (InterfaceMacHeader) '00:0A:F5:06:0F:24'

 

  1. 数据包IO口的初始化(负责数据包的发送)

从上一步转入RouterImpl.java中的send()方法,这里用DatagramIO完成数据包的发送。

 

先看DatagramIO的初始化,因为绑定到一个IP地址上,这个过程是在startAddressBasedTransports()中,获取到本机IP地址后完成的。

DatagramIOImpl是具体的类实现,构造实例时,可以配置发送参数,如跳数,发送数据包的最大值。

数据包内容的读写通过DatagramProcessorImpl.java来完成。

 

接着看DatagramIOImpl的初始化。

 Init() @ DatagramIOImpl.java

在本机IP地址的基础上封装端口:

localAddress = new InetSocketAddress(bindAddress, 0);

封装一个多播端口,以便把数据包发送到多个client端:

socket = new MulticastSocket(localAddress);

 

最后数据包的发送是通过MulticastSocket完成。

send() @ DatagramIOImpl.java

 

发送的数据包会通过DatagramProcessorImpl.java的write方法来构建DatagramPacket对象。

 

数据包发给谁了呢?这要看message中目标地址destinationAddress是谁?

消息报的目标地址,是在OutgoingNotificationRequest.java的构造函数中设置的。

前面说过,要发送的消息会被包装成OutgoingNotificationRequest类型,或者其子类型OutgoingNotificationRequestRootDevice, OutgoingNotificationRequestUDN,或OutgoingNotificationRequestDeviceType,具体是在:

createDeviceMessages()@SendingNotification.java

 

最后看OutgoingNotificationRequest.java的构造函数:

   super(

                new UpnpRequest(UpnpRequest.Method.NOTIFY),

                ModelUtil.getInetAddressByName(Constants.IPV4_UPNP_MULTICAST_GROUP),

                Constants.UPNP_MULTICAST_PORT

        );

其中,消息类型是NOTIFY,

相应的目标地址:IPV4_UPNP_MULTICAST_GROUP = "239.255.255.250";

目标端口:UPNP_MULTICAST_PORT = 1900;

这是IANA(互联网数字分配组织)保留的多播地址。

 

  • DLNA设备的发现

这个过程是从发出search请求开始,到收到匹配设备返回的响应消息,最后,应用程序根据响应消息显示出RemoteDevice信息。

 

1,发送请求

首先,注册监听DefaultRegistryListener.java,可以统一来用添加、删除、更新本地或远端设备。

upnpService.getRegistry().addListener(deviceListRegistryListener);

它的实现类可以被多个线程并发调用,所以他应该是线程安全的。其中监听的方法都是在一个单独的线程被调用的。

如remoteDeviceAdded(),在一个新发现的设备的完整元数据可用时被调用。

 

然后,从应用中的upnpService.getControlPoint().search();开始。通过ControlPointImpl.java发起搜索。

不带参数的search()发起搜索类型是:STAllHeader() > "ssdp:all"。

通过DefaultUpnpServiceConfiguration.java中的,ClingExecutor实例将搜索任务被放入一个线程池。

 

具体搜索任务的类型,通过ProtocolFactoryImpl.java 中的createSendingSearch()来创建。

 

接着看SendingSearch.java,搜索数据包的信息,搜索请求消息没间隔500毫秒,发送一次,共发送5次。

 

execute()@SendingSearch.java

OutgoingSearchRequest.java

     super(

                new UpnpRequest(UpnpRequest.Method.MSEARCH),

                ModelUtil.getInetAddressByName(Constants.IPV4_UPNP_MULTICAST_GROUP),

                Constants.UPNP_MULTICAST_PORT

        );

搜索请求,类型:"M-SEARCH"

目标地址:IPV4_UPNP_MULTICAST_GROUP = "239.255.255.250";

目标端口:UPNP_MULTICAST_PORT = 1900;

 

getUpnpService().getRouter().send(msg); 转到RouterImpl.java通过数据包IO接口实例发出消息。

 

 Send()@DatagramIOImpl.java

通过DatagramProcessorImpl.java的write完成发送数据包的填充。

 

最后通过MulticastSocket的send完成发送。

 

2,接受响应

MulticastSocket是绑定了本机IP地址的套接字,能发送单播、多播数据包,也能接受单播数据包。

 

在RouterImpl.java中的startAddressBasedTransports()方法,对DatagramIOImpl初始化后,会将其放入线程池运行,所以直接看DatagramIOImpl的run方法。

run() @DatagramIOImpl.java

这里的socket是MulticastSocket对象,负责监听所有发送到本机IP的UDP数据包。

                byte[] buf = new byte[getConfiguration().getMaxDatagramBytes()];

                DatagramPacket datagram = new DatagramPacket(buf, buf.length);

 

                socket.receive(datagram);

这里的router是RouterImpl.java对象,处理前还是由DatagramProcessorImpl.java中的read方法解读DatagramPacket,转成IncomingDatagramMessage类型对象。

router.received(datagramProcessor.read(localAddress.getAddress(), datagram));

 

然后看RouterImpl.java如何把接收的响应信息,回到到监听处。

根据消息类型,ProtocolFactoryImpl.java中createReceivingAsync方法负责构建搜索响应的实例。

具体是createReceivingSearchResponse(incomingResponse)方法,最终创建的ReceivingSearchResponse实例,这个实例来处理搜索响应消息的接收。

 

同样的,ReceivingSearchResponse实例,也会被添加到线程池执行,具体看其execute方法。

execute()@ReceivingSearchResponse.java

这里获取返回搜索响应的远端设备的描述信息,

RemoteDeviceIdentity rdIdentity = new RemoteDeviceIdentity(getInputMessage());

 

根据远端设备的描述信息,构建RemoteDevice对象。

rd = new RemoteDevice(rdIdentity);

 

因为这个时候并不知道,这个远端设备是根设备,还是嵌入设备,所以还要去解析它的描述符,然后在处理添加。具体是:

new RetrieveRemoteDescriptors(getUpnpService(), rd)

这个runnable也会在线程池被执行。

run()@RetrieveRemoteDescriptors.java

其中describe()的调用非常耗时,需要确保每次的调用都是必须的,且尽量少的调用。

出去描述符文件等的解析外,跟设备添加有关的调用是:

getUpnpService().getRegistry().addDevice(hydratedDevice);

 

addDevice(RemoteDevice remoteDevice) @ RegistryImpl.java

add(final RemoteDevice device) @ RemoteItems.java

如下循环完成远端设备的添加:

        for (final RegistryListener listener : registry.getListeners()) {

            registry.getConfiguration().getRegistryListenerExecutor().execute(

                    new Runnable() {

                        public void run() {

                            listener.remoteDeviceAdded(registry, device);

                        }

                    }

            );

        }

通过RegistryListener的具体实现类,把远端设备添加到应用列表中。如DefaultRegistryListener.java,通常应用程序都会集成这个类,并实现其中的接口:

remoteDeviceAdded()

remoteDeviceUpdated()

等。

https://www.cnblogs.com/guxia/p/8076099.html Jetty使用

https://blog.csdn.net/qq_37878579/article/details/78404931 Jetty使用

https://blog.csdn.net/hknock/article/details/44243675 multicastSocket使用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值