1,概述
DLNA设备、服务的注册及发现(依赖开源库cling),DLNA中设备的注册、发现主要基于UPNP协议实现,这是微软推行的一个标准。Upnp最大的愿景是希望任何设备只要一接入网络,所有网上的设备马上就能知道有新设备加入,这些设备之间就可以彼此通信。
2,render设备注册
2.1 设备实例的创建LocalDevice
类LocalDevice有多个构造函数,主要用于不同参数集的对象生成,它的父类为Device。
public LocalDevice(DeviceIdentity identity, DeviceType type, DeviceDetails details,Icon[] icons, LocalService[] services) throws ValidationException {super(identity, type, details, icons, services);this.deviceDetailsProvider = null;}
第一个参数是设备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
NOTIFY * HTTP/1.1
CACHE-CONTROL: max-age=1800 LOCATION: http://192.168.192.65:37757/upnp/dev/4dc519be-77b4-31f1-908a-95c7b88eb8ab/desc
NT: urn:schemas-upnp-org:device:MediaRenderer:1 HOST: 239.255.255.250:1900 NTS: ssdp:alive
USN: uuid:4dc519be-77b4-31f1-908a-95c7b88eb8ab::urn:schemas-upnp-org:device:MediaRenderer:1
第三个参数是设备详情,其中friendlyname是给设备起的一个比较友好的显示名字,另外一个列表DLNACaps,标识DLNA的能力,通常是"av-upload", "image-upload", "audio-upload"。
public DeviceDetails(String friendlyName, ManufacturerDetails manufacturerDetails,ModelDetails modelDetails, URI presentationURI, DLNADoc[] dlnaDocs, DLNACaps dlnaCaps) {this(null, friendlyName, manufacturerDetails, modelDetails, null, null, presentationURI, dlnaDocs, dlnaCaps);}
最后一个参数是服务列表,就是这个设备需要支持哪些服务,比如渲染设备要支持:
ConnectionManagerService,AVTransportService,AudioRenderingControl。
protected open fun generateLocalServices(): Array<LocalService<*>>
2.2 通过RegistryImpl的addDevice来完成设备注册
localDevice = createRendererDevice(Utils.getHttpBaseUrl(applicationContext))upnpService.registry.addDevice(localDevice)
接着调用LocalItems的add方法继续干活,先是判断是否已经注册过了,如果已经注册过,不重复执行,直接返回。然后进一步完成添加设备的过程。
synchronized public void addDevice(LocalDevice localDevice) {localItems.add(localDevice);}
第一步,添加设备下的资源,根据命名空间/upnp下提供的资源,主要有DeviceDescriptor,ServiceDescriptor,ServiceControl,ServiceEvent等资源。
第二步,生成RegistryItem对象,添加到DeviceItems集合中。
第三步,如果需要通告设备存在,发出存在的通知advertiseAlive()。
第四步,回调监听,告诉RegistryListener有本地localDevice设备添加。监听类通常继承DefaultRegistryListener类,并重写其中的设备添加,设备删除方法,具体是在应用的activity中,upnpService实例构建完成,通过upnpService获取到其中的RegistryImpl来添加监听。
DefaultRegistryListener的实现类,通常需要实现:
localDeviceAdded(),
localDeviceRemoved(),
remoteDeviceAdded(),
remoteDeviceRemoved()等方法。
2.3 advertiseAlive
LocalItems.java中的advertiseAlive(),处理设备存在的通知。
这里是通过异步的方式提交一个通知任务,通常任务先睡眠100毫米后在执行,避免对网络造成拥塞。接着通过RegistryImpl中创建的ProtocolFactoryImpl实例生成一个通知消息。具体是SendingNotificationAlive实例。这个通知的类型是NotificationSubtype.ALIVE,也就是ALIVE("ssdp:alive"),SendingNotification这个类,实际是一个Runnable实例,是为注册的本地设备发送一个通知消息。
最后,看下这个通知存在的消息发给了谁?
SendingNotification#Execute()
第一步,获取本机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/a2eeed03-2d36-3176-9b16-31410da11b4c/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(互联网数字分配组织)保留的多播地址。