Android基于Cling开发DLNA应用

DLNA,Digital Living Network Alliance的简称,即数字生活网络联盟。其由消费性电子、移动电话以及电脑厂商组成。目标在于创建一套可以使得各厂商的产品互相连接,互相适应的工业标准,从而为消费者实现数字化生活。

 

UPnP/DLNA library for Java and Android

GitHub最多关注,当前仍在维护,许可协议为LGPL或CDDL。


以下为中文译文:

 

## 5. Android上的Cling
Cling Core为Android应用提供了UPnP栈。由于如今大部分Android系统都是小型手持设备,所以通常你需要写控制端应用。然而你也可以写Android上的UPnP服务应用,其所有特性Cling Core都支持。
 
```
Android模拟器上的Cling
在写此时,Android模拟器还不支持接收UDP组播。不过,可以发送UDP组播。你能够发送一个组播UPnP搜寻,并接收UDP单播回应,继而发现正运行的设备。你发现不了在搜寻后新开启的设备,并且在设备关闭时也收不到消息。另外,其他在你网络的控制端应用,则不能发现你本地的Android设备或服务。在你测试应用时,所有这些情况都会使你感到困惑,所以除非你真得理解哪些有作用、哪些没有,不然你应当使用一个真正的设备。
这章阐述了你如何整合Cling到你的Android应用,使其成为一个共享的部件。
```
 
### 5.1. 配置应用服务
你可以在Android应用主activity中实例化Cling UpnpService。另一方面,如果你好些activities都要要求访问UPnP栈,那么最好采用后台服务,android.app.Service。之后,任何想要访问UPnP栈的activity,都能够在需要时绑定或解绑该服务。
 
该服务组件的接口是org.teleal.cling.android.AndroidUpnpService:

1. public interface AndroidUpnpService { 
2.     public UpnpService get(); 
3.     public UpnpServiceConfiguration getConfiguration(); 
4.     public Registry getRegistry(); 
5.     public ControlPoint getControlPoint(); 
6. }

activity通常访问已知UPnP设备的注册表,或者通过ControlPoint查询和控制UPnP设备。
你必须在AndroidManifest.xml内配置内建的服务实现:

 

01. <manifest ...> 
02.    
03.     <uses-permission android:name="android.permission.INTERNET"/> 
04.     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 
05.     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/> 
06.     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 
07.    
08.     <application ...> 
09.    
10.         <activity ...> 
11.             ... 
12.         </activity
13.    
14.         <service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl"/> 
15.    
16.     </application
17.    
18. </manifest>

此Cling UPnP服务要求设备WiFi接口的访问权限,事实上其也只将会绑定网络接口。
此服务将会自动检测WiFi接口的关闭,并优雅地处理这种情形:任何客户端操作都会导致"no response from server"状态,而你的代码必须预料并处理该状态。
 
当服务组件创建或销毁时,会相应开启和关闭UPnP系统。这依赖于在你的activities里是如何访问此组件的。
 
### 5.2 activity如何访问服务
service的生命周期在Android中很好的被定义了。如果service还没启动的话,第一个绑定服务的activity将会启动它。当不再有activity绑定到service上时,操作系统将会销毁此service。
 
让我们写一个简单的UPnP浏览activity。它用于将所有你网络内的设备显示在一个列表内,并有一个菜单选项来触发搜寻。activity连接UPnP服务之后,会一直监听注册表内设备的增加和删除,所以显示的设备列表会实时更新。
 
以下是activity类的骨架:

 

01. import android.app.ListActivity; 
02. import android.content.ComponentName; 
03. import android.content.Context; 
04. import android.content.Intent; 
05. import android.content.ServiceConnection; 
06. import android.os.Bundle; 
07. import android.os.IBinder; 
08. import android.view.Menu; 
09. import android.view.MenuItem; 
10. import android.widget.ArrayAdapter; 
11. import android.widget.Toast; 
12. import org.teleal.cling.android.AndroidUpnpService; 
13. import org.teleal.cling.android.AndroidUpnpServiceImpl; 
14. import org.teleal.cling.model.meta.Device; 
15. import org.teleal.cling.model.meta.LocalDevice; 
16. import org.teleal.cling.model.meta.RemoteDevice; 
17. import org.teleal.cling.registry.DefaultRegistryListener; 
18. import org.teleal.cling.registry.Registry; 
19.    
20. public class UpnpBrowser extends ListActivity { 
21.    
22.     private ArrayAdapter<DeviceDisplay> listAdapter; 
23.    
24.     private AndroidUpnpService upnpService; 
25.    
26.     private ServiceConnection serviceConnection = ... 
27.    
28.     private RegistryListener registryListener = new BrowseRegistryListener(); 
29.    
30.     @Override 
31.     public void onCreate(Bundle savedInstanceState) { 
32.         super.onCreate(savedInstanceState); 
33.    
34.         listAdapter = 
35.             new ArrayAdapter( 
36.                 this
37.                 android.R.layout.simple_list_item_1 
38.             ); 
39.         setListAdapter(listAdapter); 
40.    
41.         getApplicationContext().bindService( 
42.             new Intent(this, AndroidUpnpServiceImpl.class), 
43.             serviceConnection, 
44.             Context.BIND_AUTO_CREATE 
45.         ); 
46.     
47.    
48.     @Override 
49.     protected void onDestroy() { 
50.         super.onDestroy(); 
51.         if (upnpService != null) { 
52.             upnpService.getRegistry().removeListener(registryListener); 
53.         
54.         getApplicationContext().unbindService(serviceConnection); 
55.     
56.    
57.     ... 
58.    
59.
60. ```

我们采用Android运行时默认提供的布局和ListActivity父类。注意这个类可以是你应用的主activity,或者进一步上升进任务的堆栈。listAdapter黏合了Cling Registry上设备的增加移除事件与展示在用户界面的列表项目。
 
当没有后台服务绑定到该activity时,upnpService变量为null。绑定和解绑发生在onCreate()和onDestroy()回调,所以activity绑定服务和它的生存周期一样长。
 
```
暂停后台的UPnP服务
当一个activity不再活动时(停止或暂停状态),它仍会绑定着UPnP服务。UPnP服务将会持续运行,即使应用不再可见。由于UPnP服务的注册表一定会定期维护发现的设备、刷新本地设备的通告、删除过期的GENA事件订阅等,将会消耗你设备的CPU和电量。当activity onPause()或onStop()方法被调用时,你可以调用Registry#pause()来通知UPnP服务不再维护注册表。之后,你可以通过Registry#resume()来恢复后台服务,或同时用Registry#isPaused()检查状态。请阅读这些方法的Javadoc了解详细信息,以及暂停注册表维护对于设备、服务和GENA订阅的意义。
```
 
以下是使用ServiceConnection处理绑定和解绑服务:

 

01. private ServiceConnection serviceConnection = new ServiceConnection() { 
02.    
03.     public void onServiceConnected(ComponentName className, IBinder service) { 
04.         upnpService = (AndroidUpnpService) service; 
05.    
06.         // Refresh the list with all known devices 
07.         listAdapter.clear(); 
08.         for (Device device : upnpService.getRegistry().getDevices()) { 
09.             registryListener.deviceAdded(device); 
10.         
11.    
12.         // Getting ready for future device advertisements 
13.         upnpService.getRegistry().addListener(registryListener); 
14.    
15.         // Search asynchronously for all devices 
16.         upnpService.getControlPoint().search(); 
17.     
18.    
19.     public void onServiceDisconnected(ComponentName className) { 
20.         upnpService = null
21.     
22. }; 
23. ```

首先,所有已知的UPnP设备能够被查询和显示(如果UPnP服务刚开启且到到目前还没有设备通告它的存在)。
 
然后,给UPnP服务的注册表增加一个监听者。该监听者将会处理在你网络上发现的设备的增加和移除,并在更新在用户界面列表内显示的项目。当activity销毁时,BrowseRegistryListener会被移除。
 
最后,通过发送一个搜寻消息给所有UPnP设备,你会开启异步搜索,此时这些设备将通告它们的存在。注意这个搜寻消息不是每次连接服务都需要的。这只需一次,在当主activity和应用启动时,其会将已知设备写入注册表。

以下是BrowseRegistryListener,他的任务就是更新列表项的显示:

 

01. class BrowseRegistryListener extends DefaultRegistryListener { 
02.    
03.     @Override 
04.     public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { 
05.         deviceAdded(device); 
06.     
07.    
08.     @Override 
09.     public void remoteDeviceDiscoveryFailed(Registry registry, final RemoteDevice device, final Exception ex) { 
10.         runOnUiThread(new Runnable() { 
11.             public void run() { 
12.                 Toast.makeText( 
13.                         BrowseActivity.this
14.                         "Discovery failed of '" + device.getDisplayString() + "': "
15.                                 (ex != null ? ex.toString() : "Couldn't retrieve device/service descriptors"), 
16.                         Toast.LENGTH_LONG 
17.                 ).show(); 
18.             
19.         }); 
20.         deviceRemoved(device); 
21.     
22.    
23.     @Override 
24.     public void remoteDeviceAdded(Registry registry, RemoteDevice device) { 
25.         deviceAdded(device); 
26.     
27.    
28.     @Override 
29.     public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { 
30.         deviceRemoved(device); 
31.     
32.    
33.     @Override 
34.     public void localDeviceAdded(Registry registry, LocalDevice device) { 
35.         deviceAdded(device); 
36.     
37.    
38.     @Override 
39.     public void localDeviceRemoved(Registry registry, LocalDevice device) { 
40.         deviceRemoved(device); 
41.     
42.    
43.     public void deviceAdded(final Device device) { 
44.         runOnUiThread(new Runnable() { 
45.             public void run() { 
46.                 DeviceDisplay d = new DeviceDisplay(device); 
47.                 int position = listAdapter.getPosition(d); 
48.                 if (position >= 0) { 
49.                     // Device already in the list, re-set new value at same position 
50.                     listAdapter.remove(d); 
51.                     listAdapter.insert(d, position); 
52.                 } else
53.                     listAdapter.add(d); 
54.                 
55.             
56.         }); 
57.     
58.    
59.     public void deviceRemoved(final Device device) { 
60.         runOnUiThread(new Runnable() { 
61.             public void run() { 
62.                 listAdapter.remove(new DeviceDisplay(device)); 
63.             
64.         }); 
65.     
66.
67. ```

鉴于性能的原因,当发现设备时,我们会直到一个完整的hydrated(所有设备被检索和验证)设备元数据模型可用时才执行等待。我们响应尽可能得快,同时只当remoteDeviceAdded()方法被调用时才去等待。甚至当搜索仍在运行时,我们仍旧显示所有设备。在台式电脑上你通常不需要关心这个,不过,Android手持设备效率慢,并且UPnP使用好些臃肿的XML描述符来交换关于设备和服务的元数据。有时,在设备和它的服务完全可用前,这可能会花费数秒钟。而remoteDeviceDiscoveryStarted()和remoteDeviceDiscoveryFailed()方法在搜索处理时会尽快被调用。顺便说一句,如果设备有相同的UDN就表示相等的(a.equal(b)),但它们可能不会完全一致(a==b)。
 
注意注册表将会在分开的线程中调用监听者方法。你必须在UI线程中更新显示列表数据。
 
activity中以下两个方法增加了用来搜寻的菜单,如此用户才能手动的刷新列表:

 

01. ``` 
02. @Override 
03. public boolean onCreateOptionsMenu(Menu menu) { 
04.     menu.add(0, 0, 0, R.string.search_lan) 
05.         .setIcon(android.R.drawable.ic_menu_search); 
06.     return true
07.
08.    
09. @Override 
10. public boolean onOptionsItemSelected(MenuItem item) { 
11.     if (item.getItemId() == 0 && upnpService != null) { 
12.         upnpService.getRegistry().removeAllRemoteDevices(); 
13.         upnpService.getControlPoint().search(); 
14.     
15.     return false
16.
17. ```

最后,DeviceDisplay类是一个非常简单的JavaBean,只提供一个toString()方法来呈现列表信息。通过修改此方法,你能够显示任何关于UPnP设备的信息:

 

01. class DeviceDisplay { 
02.     Device device; 
03.    
04.     public DeviceDisplay(Device device) { 
05.         this.device = device; 
06.     
07.    
08.     public Device getDevice() { 
09.         return device; 
10.     
11.    
12.     @Override 
13.     public boolean equals(Object o) { 
14.         if (this == o) return true
15.         if (o == null || getClass() != o.getClass()) return false
16.         DeviceDisplay that = (DeviceDisplay) o; 
17.         return device.equals(that.device); 
18.     
19.    
20.     @Override 
21.     public int hashCode() { 
22.         return device.hashCode(); 
23.     
24.    
25.     @Override 
26.     public String toString() { 
27.         // Display a little star while the device is being loaded 
28.         return device.isFullyHydrated() ? device.getDisplayString() : device.getDisplayString() + " *"
29.     
30.
31. ```

还有我们必须覆盖相等操作,这样我们才可以用DeviceDisplay实例作为便捷的处理,从列表中手动地移除和增加设备。
 
### 5.3. 优化服务行为
UPnP服务运行时会消耗内存和CPU。尽管通常在一个正常的机器上没有什么问题,但在Android手持设备上就可能会有了。如果你禁用Cling UPnP服务的某些功能,或者设置暂停且在合适时恢复它,你可以留有更多的内存和电量。
 
#### 5.3.1. 调整注册表维护
当服务运行时,后台有好些东西在执行。首先,有一个服务的注册表和其维护线程。如果你写一个控制端,后台注册表维护者将会定期从远程服务更新你对外的GENA订阅。当没有通知断开网络时,它也会到期并移除任何远程服务。如果你正提供服务,你的设备通告将被注册表维护者刷新,并在GENA订阅没及时更新时移除它。注册表维护者为了有效得防止UPnP网络上的过时状态,所以所有参与者会实时更新其他参与者的视图等等。
 
默认情况下,注册表维护者会每秒运行并检查是否有事要做(当然,大多数情况下没事做)。然而默认的Android配置有5秒的间隔休眠,所以这已经花费了更少的后台CPU占用时间 — 不过你的应用可能会暴露稍微过时的信息。在UpnpServiceConfiguration你可以通过覆盖getRegistryMaintenanceIntervalMillis()进一步的调整设置。在Android上,你必须子类化服务实现来提供一个新的配置。

 

01. ``` 
02. public class MyUpnpService extends AndroidUpnpServiceImpl { 
03.    
04.     @Override 
05.     protected AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) { 
06.         return new AndroidUpnpServiceConfiguration(wifiManager) { 
07.    
08.             @Override 
09.             public int getRegistryMaintenanceIntervalMillis() { 
10.                 return 7000
11.             
12.    
13.         }; 
14.     
15.
16. ```

此时不要忘了在AndroidManifest.xml内配置MyUpnpService,而不是原先的实现。当在你的activities里绑定服务时,也必须使用该类型。
 
#### 5.3.2. 暂停和恢复注册表维护
另外一个更有效同时也不是很复杂的优化是,每当你的activites不再需要UPnP服务时,暂停和恢复注册表。这通常发生在当activity不在前台(暂停),甚至不再显示(停止)时。默认情况下,activity状态改变对UPnP服务没有影响,除非你在activities生命周期的回调内绑定和解绑服务。
 
除了绑定和解绑服务,你也可以在activity onPause()或onStop()方法被调用时,通过调用Registry#pause()来暂停注册表。之后,你可以通过Registry#resume()来恢复后台服务,或同时用Registry#isPaused()检查状态。
 
请阅读这些方法的Javadoc了解详细信息,以及暂停注册表维护对于设备、服务和GENA订阅的意义。根据你的应用要做什么,否则这种小的优化可能不值得处理这些效果。另一方面,你的应用应当能够处理失败的GENA订阅续期,或者消失的远程设备。 www.it165.net
 
#### 5.3.3. 配置搜索
最有效的优化是UPnP设备有选择性的搜索。尽管UPnP服务的网络传输层在后台会保持运行(线程正等待且socket被绑定),这个特性允许你有选择且快速的丢弃搜索信息。
 
举例来说,如果你正在写一个控制端,且不通告你想要控制的服务(对其他设备没兴趣),那么你可以丢弃所有接收的搜索信息。另一方面,如果你只提供设备和服务,所有搜索信息(除了你自身服务的搜索信息)可能都可以被丢弃,你对其他远程设备和其服务一点都不会有兴趣。
 
一旦UDP数据包内容可用,该搜索信息就会被Cling选择并偷偷的丢弃,所以不需要进一步得解析和处理,同时CPU时间和内存消耗显著得减少,即使当你在Android手持设备上后台持续运行UPnP服务。
 
为了配置你的控制端应用支持哪些服务,需要覆盖前面章节展示的服务接口并提供一组ServiceType实例:

 

01. ``` 
02. public class MyUpnpService extends AndroidUpnpServiceImpl { 
03.    
04.     @Override 
05.     protected AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) { 
06.         return new AndroidUpnpServiceConfiguration(wifiManager) { 
07.    
08.             @Override 
09.             public ServiceType[] getExclusiveServiceTypes() { 
10.                 return new ServiceType[] { 
11.                         new UDAServiceType("SwitchPower"
12.                 }; 
13.             
14.    
15.         }; 
16.     
17.
18. ```

这个配置将会忽略所有不通告chemas-upnp-org:SwitchPower:1的任何通告。这是我们控制端要处理的,不需要其他任何东西了。如果你返回一个空的数组(默认行为),所有服务和设备将会发现以及没有通告会被丢弃。
 
如果你正在写一个控制端应用而不是服务应用,你可以让getExclusiveServiceTypes()方法返回null。这将会完全禁用搜索,此时所有设备和服务的通告一接收就会被丢弃。
 


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值