Android基於Cling開發DLNA應用


# Android基於Cling開發DLNA應用

## DLNA

DLNA,Digital Living Network Alliance的簡稱,即數字生活網絡聯盟。其由消費性電子、移動電話以及電腦廠商組成。目標在於創建一套可以使得各廠商的產品互相連接,互相適應的工業標準,從而爲消費者實現數字化生活。

更多資料:

1. About DLNA
2. 維基百科
3. 百度百科

## Cling

UPnP/DLNA library for Java and Android。

GitHub最多關注,當前仍在維護,許可協議爲LGPL或CDDL。

用戶手冊:Cling on Android

以下爲中文譯文:

  1. ## 5. Android上的Cling 
  2. Cling Core爲Android應用提供了UPnP棧。由於如今大部分Android系統都是小型手持設備,所以通常你需要寫控制端應用。然而你也可以寫Android上的UPnP服務應用,其所有特性Cling Core都支持。 
  3.  
  4. ``` 
  5. Android模擬器上的Cling 
  6. 在寫此時,Android模擬器還不支持接收UDP組播。不過,可以發送UDP組播。你能夠發送一個組播UPnP搜尋,並接收UDP單播迴應,繼而發現正運行的設備。你發現不了在搜尋後新開啓的設備,並且在設備關閉時也收不到消息。另外,其他在你網絡的控制端應用,則不能發現你本地的Android設備或服務。在你測試應用時,所有這些情況都會使你感到困惑,所以除非你真得理解哪些有作用、哪些沒有,不然你應當使用一個真正的設備。 
  7. 這章闡述了你如何整合Cling到你的Android應用,使其成爲一個共享的部件。 
  8. ``` 
  9.  
  10. ### 5.1. 配置應用服務 
  11. 你可以在Android應用主activity中實例化Cling UpnpService。另一方面,如果你好些activities都要要求訪問UPnP棧,那麼最好採用後臺服務,android.app.Service。之後,任何想要訪問UPnP棧的activity,都能夠在需要時綁定或解綁該服務。 
  12.  
  13. 該服務組件的接口是org.teleal.cling.android.AndroidUpnpService: 
  14.  
  15. ``` 
  16. public interface AndroidUpnpService { 
  17.     public UpnpService get(); 
  18.     public UpnpServiceConfiguration getConfiguration(); 
  19.     public Registry getRegistry(); 
  20.     public ControlPoint getControlPoint(); 
  21. ``` 
  22.  
  23. activity通常訪問已知UPnP設備的註冊表,或者通過ControlPoint查詢和控制UPnP設備。 
  24. 你必須在AndroidManifest.xml內配置內建的服務實現: 
  25.  
  26. ``` 
  27. <manifest ...> 
  28.  
  29.     <uses-permission android:name="android.permission.INTERNET"/> 
  30.     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 
  31.     <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/> 
  32.     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 
  33.  
  34.     <application ...> 
  35.  
  36.         <activity ...> 
  37.             ... 
  38.         </activity> 
  39.  
  40.         <service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl"/> 
  41.  
  42.     </application> 
  43.  
  44. </manifest> 
  45. ``` 
  46.  
  47. 此Cling UPnP服務要求設備WiFi接口的訪問權限,事實上其也只將會綁定網絡接口。 
  48. 此服務將會自動檢測WiFi接口的關閉,並優雅地處理這種情形:任何客戶端操作都會導致"no response from server"狀態,而你的代碼必須預料並處理該狀態。 
  49.  
  50. 當服務組件創建或銷燬時,會相應開啓和關閉UPnP系統。這依賴於在你的activities裏是如何訪問此組件的。 
  51.  
  52. ### 5.2 activity如何訪問服務 
  53. service的生命週期在Android中很好的被定義了。如果service還沒啓動的話,第一個綁定服務的activity將會啓動它。當不再有activity綁定到service上時,操作系統將會銷燬此service。 
  54.  
  55. 讓我們寫一個簡單的UPnP瀏覽activity。它用於將所有你網絡內的設備顯示在一個列表內,並有一個菜單選項來觸發搜尋。activity連接UPnP服務之後,會一直監聽註冊表內設備的增加和刪除,所以顯示的設備列表會實時更新。 
  56.  
  57. 以下是activity類的骨架: 
  58.  
  59. ``` 
  60. import android.app.ListActivity; 
  61. import android.content.ComponentName; 
  62. import android.content.Context; 
  63. import android.content.Intent; 
  64. import android.content.ServiceConnection; 
  65. import android.os.Bundle; 
  66. import android.os.IBinder; 
  67. import android.view.Menu; 
  68. import android.view.MenuItem; 
  69. import android.widget.ArrayAdapter; 
  70. import android.widget.Toast; 
  71. import org.teleal.cling.android.AndroidUpnpService; 
  72. import org.teleal.cling.android.AndroidUpnpServiceImpl; 
  73. import org.teleal.cling.model.meta.Device; 
  74. import org.teleal.cling.model.meta.LocalDevice; 
  75. import org.teleal.cling.model.meta.RemoteDevice; 
  76. import org.teleal.cling.registry.DefaultRegistryListener; 
  77. import org.teleal.cling.registry.Registry; 
  78.  
  79. public class UpnpBrowser extends ListActivity { 
  80.  
  81.     private ArrayAdapter<DeviceDisplay> listAdapter; 
  82.  
  83.     private AndroidUpnpService upnpService; 
  84.  
  85.     private ServiceConnection serviceConnection = ... 
  86.  
  87.     private RegistryListener registryListener = new BrowseRegistryListener(); 
  88.  
  89.     @Override 
  90.     public void onCreate(Bundle savedInstanceState) { 
  91.         super.onCreate(savedInstanceState); 
  92.  
  93.         listAdapter = 
  94.             new ArrayAdapter( 
  95.                 this, 
  96.                 android.R.layout.simple_list_item_1 
  97.             ); 
  98.         setListAdapter(listAdapter); 
  99.  
  100.         getApplicationContext().bindService( 
  101.             new Intent(this, AndroidUpnpServiceImpl.class), 
  102.             serviceConnection, 
  103.             Context.BIND_AUTO_CREATE 
  104.         ); 
  105.     } 
  106.  
  107.     @Override 
  108.     protected void onDestroy() { 
  109.         super.onDestroy(); 
  110.         if (upnpService != null) { 
  111.             upnpService.getRegistry().removeListener(registryListener); 
  112.         } 
  113.         getApplicationContext().unbindService(serviceConnection); 
  114.     } 
  115.  
  116.     ... 
  117.  
  118. ``` 
  119.  
  120. 我們採用Android運行時默認提供的佈局和ListActivity父類。注意這個類可以是你應用的主activity,或者進一步上升進任務的堆棧。listAdapter黏合了Cling Registry上設備的增加移除事件與展示在用戶界面的列表項目。 
  121.  
  122. 當沒有後臺服務綁定到該activity時,upnpService變量爲null。綁定和解綁發生在onCreate()和onDestroy()回調,所以activity綁定服務和它的生存週期一樣長。 
  123.  
  124. ``` 
  125. 暫停後臺的UPnP服務 
  126. 當一個activity不再活動時(停止或暫停狀態),它仍會綁定着UPnP服務。UPnP服務將會持續運行,即使應用不再可見。由於UPnP服務的註冊表一定會定期維護髮現的設備、刷新本地設備的通告、刪除過期的GENA事件訂閱等,將會消耗你設備的CPU和電量。當activity onPause()或onStop()方法被調用時,你可以調用Registry#pause()來通知UPnP服務不再維護註冊表。之後,你可以通過Registry#resume()來恢復後臺服務,或同時用Registry#isPaused()檢查狀態。請閱讀這些方法的Javadoc瞭解詳細信息,以及暫停註冊表維護對於設備、服務和GENA訂閱的意義。 
  127. ``` 
  128.  
  129. 以下是使用ServiceConnection處理綁定和解綁服務: 
  130.  
  131. ``` 
  132. private ServiceConnection serviceConnection = new ServiceConnection() { 
  133.  
  134.     public void onServiceConnected(ComponentName className, IBinder service) { 
  135.         upnpService = (AndroidUpnpService) service; 
  136.  
  137.         // Refresh the list with all known devices 
  138.         listAdapter.clear(); 
  139.         for (Device device : upnpService.getRegistry().getDevices()) { 
  140.             registryListener.deviceAdded(device); 
  141.         } 
  142.  
  143.         // Getting ready for future device advertisements 
  144.         upnpService.getRegistry().addListener(registryListener); 
  145.  
  146.         // Search asynchronously for all devices 
  147.         upnpService.getControlPoint().search(); 
  148.     } 
  149.  
  150.     public void onServiceDisconnected(ComponentName className) { 
  151.         upnpService = null
  152.     } 
  153. }; 
  154. ``` 
  155.  
  156. 首先,所有已知的UPnP設備能夠被查詢和顯示(如果UPnP服務剛開啓且到到目前還沒有設備通告它的存在)。 
  157.  
  158. 然後,給UPnP服務的註冊表增加一個監聽者。該監聽者將會處理在你網絡上發現的設備的增加和移除,並在更新在用戶界面列表內顯示的項目。當activity銷燬時,BrowseRegistryListener會被移除。 
  159.  
  160. 最後,通過發送一個搜尋消息給所有UPnP設備,你會開啓異步搜索,此時這些設備將通告它們的存在。注意這個搜尋消息不是每次連接服務都需要的。這隻需一次,在當主activity和應用啓動時,其會將已知設備寫入註冊表。 
  161.  
  162. 以下是BrowseRegistryListener,他的任務就是更新列表項的顯示: 
  163.  
  164. ``` 
  165. class BrowseRegistryListener extends DefaultRegistryListener { 
  166.  
  167.     @Override 
  168.     public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { 
  169.         deviceAdded(device); 
  170.     } 
  171.  
  172.     @Override 
  173.     public void remoteDeviceDiscoveryFailed(Registry registry, final RemoteDevice device, final Exception ex) { 
  174.         runOnUiThread(new Runnable() { 
  175.             public void run() { 
  176.                 Toast.makeText( 
  177.                         BrowseActivity.this, 
  178.                         "Discovery failed of '" + device.getDisplayString() + "': " + 
  179.                                 (ex != null ? ex.toString() : "Couldn't retrieve device/service descriptors"), 
  180.                         Toast.LENGTH_LONG 
  181.                 ).show(); 
  182.             } 
  183.         }); 
  184.         deviceRemoved(device); 
  185.     } 
  186.  
  187.     @Override 
  188.     public void remoteDeviceAdded(Registry registry, RemoteDevice device) { 
  189.         deviceAdded(device); 
  190.     } 
  191.  
  192.     @Override 
  193.     public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { 
  194.         deviceRemoved(device); 
  195.     } 
  196.  
  197.     @Override 
  198.     public void localDeviceAdded(Registry registry, LocalDevice device) { 
  199.         deviceAdded(device); 
  200.     } 
  201.  
  202.     @Override 
  203.     public void localDeviceRemoved(Registry registry, LocalDevice device) { 
  204.         deviceRemoved(device); 
  205.     } 
  206.  
  207.     public void deviceAdded(final Device device) { 
  208.         runOnUiThread(new Runnable() { 
  209.             public void run() { 
  210.                 DeviceDisplay d = new DeviceDisplay(device); 
  211.                 int position = listAdapter.getPosition(d); 
  212.                 if (position >= 0) { 
  213.                     // Device already in the list, re-set new value at same position 
  214.                     listAdapter.remove(d); 
  215.                     listAdapter.insert(d, position); 
  216.                 } else { 
  217.                     listAdapter.add(d); 
  218.                 } 
  219.             } 
  220.         }); 
  221.     } 
  222.  
  223.     public void deviceRemoved(final Device device) { 
  224.         runOnUiThread(new Runnable() { 
  225.             public void run() { 
  226.                 listAdapter.remove(new DeviceDisplay(device)); 
  227.             } 
  228.         }); 
  229.     } 
  230. ``` 
  231.  
  232. 鑑於性能的原因,當發現設備時,我們會直到一個完整的hydrated(所有設備被檢索和驗證)設備元數據模型可用時才執行等待。我們響應儘可能得快,同時只當remoteDeviceAdded()方法被調用時纔去等待。甚至當搜索仍在運行時,我們仍舊顯示所有設備。在臺式電腦上你通常不需要關心這個,不過,Android手持設備效率慢,並且UPnP使用好些臃腫的XML描述符來交換關於設備和服務的元數據。有時,在設備和它的服務完全可用前,這可能會花費數秒鐘。而remoteDeviceDiscoveryStarted()和remoteDeviceDiscoveryFailed()方法在搜索處理時會盡快被調用。順便說一句,如果設備有相同的UDN就表示相等的(a.equal(b)),但它們可能不會完全一致(a==b)。 
  233.  
  234. 注意註冊表將會在分開的線程中調用監聽者方法。你必須在UI線程中更新顯示列表數據。 
  235.  
  236. activity中以下兩個方法增加了用來搜尋的菜單,如此用戶才能手動的刷新列表: 
  237.  
  238. ``` 
  239. @Override 
  240. public boolean onCreateOptionsMenu(Menu menu) { 
  241.     menu.add(0, 0, 0, R.string.search_lan) 
  242.         .setIcon(android.R.drawable.ic_menu_search); 
  243.     return true
  244.  
  245. @Override 
  246. public boolean onOptionsItemSelected(MenuItem item) { 
  247.     if (item.getItemId() == 0 && upnpService != null) { 
  248.         upnpService.getRegistry().removeAllRemoteDevices(); 
  249.         upnpService.getControlPoint().search(); 
  250.     } 
  251.     return false
  252. ``` 
  253.  
  254. 最後,DeviceDisplay類是一個非常簡單的JavaBean,只提供一個toString()方法來呈現列表信息。通過修改此方法,你能夠顯示任何關於UPnP設備的信息: 
  255.  
  256. ``` 
  257. class DeviceDisplay { 
  258.     Device device; 
  259.  
  260.     public DeviceDisplay(Device device) { 
  261.         this.device = device; 
  262.     } 
  263.  
  264.     public Device getDevice() { 
  265.         return device; 
  266.     } 
  267.  
  268.     @Override 
  269.     public boolean equals(Object o) { 
  270.         if (this == o) return true
  271.         if (o == null || getClass() != o.getClass()) return false
  272.         DeviceDisplay that = (DeviceDisplay) o; 
  273.         return device.equals(that.device); 
  274.     } 
  275.  
  276.     @Override 
  277.     public int hashCode() { 
  278.         return device.hashCode(); 
  279.     } 
  280.  
  281.     @Override 
  282.     public String toString() { 
  283.         // Display a little star while the device is being loaded 
  284.         return device.isFullyHydrated() ? device.getDisplayString() : device.getDisplayString() + " *"
  285.     } 
  286. ``` 
  287.  
  288. 還有我們必須覆蓋相等操作,這樣我們纔可以用DeviceDisplay實例作爲便捷的處理,從列表中手動地移除和增加設備。 
  289.  
  290. ### 5.3. 優化服務行爲 
  291. UPnP服務運行時會消耗內存和CPU。儘管通常在一個正常的機器上沒有什麼問題,但在Android手持設備上就可能會有了。如果你禁用Cling UPnP服務的某些功能,或者設置暫停且在合適時恢復它,你可以留有更多的內存和電量。 
  292.  
  293. #### 5.3.1. 調整註冊表維護 
  294. 當服務運行時,後臺有好些東西在執行。首先,有一個服務的註冊表和其維護線程。如果你寫一個控制端,後臺註冊表維護者將會定期從遠程服務更新你對外的GENA訂閱。當沒有通知斷開網絡時,它也會到期並移除任何遠程服務。如果你正提供服務,你的設備通告將被註冊表維護者刷新,並在GENA訂閱沒及時更新時移除它。註冊表維護者爲了有效得防止UPnP網絡上的過時狀態,所以所有參與者會實時更新其他參與者的視圖等等。 
  295.  
  296. 默認情況下,註冊表維護者會每秒運行並檢查是否有事要做(當然,大多數情況下沒事做)。然而默認的Android配置有5秒的間隔休眠,所以這已經花費了更少的後臺CPU佔用時間 — 不過你的應用可能會暴露稍微過時的信息。在UpnpServiceConfiguration你可以通過覆蓋getRegistryMaintenanceIntervalMillis()進一步的調整設置。在Android上,你必須子類化服務實現來提供一個新的配置。 
  297.  
  298. ``` 
  299. public class MyUpnpService extends AndroidUpnpServiceImpl { 
  300.  
  301.     @Override 
  302.     protected AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) { 
  303.         return new AndroidUpnpServiceConfiguration(wifiManager) { 
  304.  
  305.             @Override 
  306.             public int getRegistryMaintenanceIntervalMillis() { 
  307.                 return 7000; 
  308.             } 
  309.  
  310.         }; 
  311.     } 
  312. ``` 
  313.  
  314. 此時不要忘了在AndroidManifest.xml內配置MyUpnpService,而不是原先的實現。當在你的activities裏綁定服務時,也必須使用該類型。 
  315.  
  316. #### 5.3.2. 暫停和恢復註冊表維護 
  317. 另外一個更有效同時也不是很複雜的優化是,每當你的activites不再需要UPnP服務時,暫停和恢復註冊表。這通常發生在當activity不在前臺(暫停),甚至不再顯示(停止)時。默認情況下,activity狀態改變對UPnP服務沒有影響,除非你在activities生命週期的回調內綁定和解綁服務。 
  318.  
  319. 除了綁定和解綁服務,你也可以在activity onPause()或onStop()方法被調用時,通過調用Registry#pause()來暫停註冊表。之後,你可以通過Registry#resume()來恢復後臺服務,或同時用Registry#isPaused()檢查狀態。 
  320.  
  321. 請閱讀這些方法的Javadoc瞭解詳細信息,以及暫停註冊表維護對於設備、服務和GENA訂閱的意義。根據你的應用要做什麼,否則這種小的優化可能不值得處理這些效果。另一方面,你的應用應當能夠處理失敗的GENA訂閱續期,或者消失的遠程設備。 
  322.  
  323. #### 5.3.3. 配置搜索 
  324. 最有效的優化是UPnP設備有選擇性的搜索。儘管UPnP服務的網絡傳輸層在後臺會保持運行(線程正等待且socket被綁定),這個特性允許你有選擇且快速的丟棄搜索信息。 
  325.  
  326. 舉例來說,如果你正在寫一個控制端,且不通告你想要控制的服務(對其他設備沒興趣),那麼你可以丟棄所有接收的搜索信息。另一方面,如果你只提供設備和服務,所有搜索信息(除了你自身服務的搜索信息)可能都可以被丟棄,你對其他遠程設備和其服務一點都不會有興趣。 
  327.  
  328. 一旦UDP數據包內容可用,該搜索信息就會被Cling選擇並偷偷的丟棄,所以不需要進一步得解析和處理,同時CPU時間和內存消耗顯著得減少,即使當你在Android手持設備上後臺持續運行UPnP服務。 
  329.  
  330. 爲了配置你的控制端應用支持哪些服務,需要覆蓋前面章節展示的服務接口並提供一組ServiceType實例: 
  331.  
  332. ``` 
  333. public class MyUpnpService extends AndroidUpnpServiceImpl { 
  334.  
  335.     @Override 
  336.     protected AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) { 
  337.         return new AndroidUpnpServiceConfiguration(wifiManager) { 
  338.  
  339.             @Override 
  340.             public ServiceType[] getExclusiveServiceTypes() { 
  341.                 return new ServiceType[] { 
  342.                         new UDAServiceType("SwitchPower"
  343.                 }; 
  344.             } 
  345.  
  346.         }; 
  347.     } 
  348. ``` 
  349.  
  350. 這個配置將會忽略所有不通告chemas-upnp-org:SwitchPower:1的任何通告。這是我們控制端要處理的,不需要其他任何東西了。如果你返回一個空的數組(默認行爲),所有服務和設備將會發現以及沒有通告會被丟棄。 
  351.  
  352. 如果你正在寫一個控制端應用而不是服務應用,你可以讓getExclusiveServiceTypes()方法返回null。這將會完全禁用搜索,此時所有設備和服務的通告一接收就會被丟棄。 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章