經過一個多月的研究,終於將iOS DLNA搞定。記錄一下。
關於DLNA開發,目前有兩個框架。一個Cyberlink,一個platinumkit。Cyberlink的好處就是提供了一套OC的api供你調用,很簡單方便。但是此框架有很多問題,且功能不全。platinumkit框架底層為c++,若要用此套框架,就得進行oc和c++的混編,之前我沒做過oc和c++的混編,所以去看platinumkit的源碼時,覺得頭疼無比,浪費了很多時間,但是使用cyberlink框架又有很多功能無法解決,並且框架經常出問題。最后完成DMC部分的時候還是用了cyberlink,有問題怎么辦,只能硬着頭皮改源碼了。好在最后還是搞定了。
1.DMS
首先讓手機自己具有DMS的功能,此處用的Platinumkit框架。github上有個demo :https://github.com/wangshuaidavid/DLNA_iOS_Platinum,當時主要就是依賴於這個demo完成了手機dms部分的功能。關於platinumkit庫的使用,網上有教程,大家可以參考,就是編譯一個.a的文件出來,然后拉進自己的項目。.a的文件分模擬器和真機兩個,可以通過終端合成一個。關於DMS部分沒什么好說的,主要看那個demo就好了。
2.DMC,DMR
使用cyberlink庫完成dmc功能,github上有個demo:https://github.com/FuruyamaTakeshi/DLNA。此demo完成了基本的dmc功能,可以搜索同一局域網內的dms,dmr。
CGUpnpAvController* avCtrl = [[CGUpnpAvController alloc] init]; avCtrl.delegate = self; [avCtrl search]; self.avController = avCtrl;
這個CGupnpAVController類就是關於搜索dms,dmr的類。通過avCtrl可以獲取到對應的dms,然后對dms發送瀏覽文件的action,返回來文件內容,最后一直拿到AVitem,即資源文件的地址。然后dmr獲取到這個資源文件地址以后,通過setAVTransportURI去設置真實的dmr設備的請求地址。設置成功后play就可以播放了。可能看到這里會覺得很簡單,但是cyberlink的dmr只提供了play,stop,pause,next,previous,seek方法,並且next,previous等方法是沒用的。如果你想完成多一些功能,要么就去用platinumkit框架,要么就自己動手給dmr添加這些功能。我選了后者。
要想改源碼首先要搞懂整個DLNA的工作原理。詳細的工作原理我也講不明白,我只是在研究這兩個框架的時候,自己慢慢的明白了一些。
首先無論是dms,還是dmr,都是一個device,通過代碼也能看出這點。CGUpnpAVServer,CGUpnpAvRender都是繼承自CGUpnpDevice的。CGUpnpDevice包含一個CgUpnpDevice的結構體。而這個CGUpnpDevice類就是我們去構造DMS,DMR的關鍵。CGUpnpDevice這個類有一個方法,initWithXMLDescription。即通過一個xml來創建一個device。當你使用dms的標准xml去創建device時,它就是dms。當你使用dmr的標准xml去創建device時,它就是dmr。兩個創建device的xml分別如下
- (NSString *)getDMSDespritionXMl1000000000:(NSString *)uuid { return [NSString stringWithFormat:@"<?xml version='1.0' encoding='utf-8' standalone='yes'?><root xmlns='urn:schemas-upnp-org:device-1-0'><specVersion><major>1</major><minor>0</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType><manufacturer>Plutinosoft LLC</manufacturer><manufacturerURL>http://www.plutinosoft.com</manufacturerURL><modelDescription>Plutinosoft AV Media Server Device</modelDescription><modelName>AV Media Server Device</modelName><modelURL>http://www.plutinosoft.com/platinum</modelURL><UDN>uuid:%@</UDN><dlna:X_DLNADOC xmlns:dlna='urn:schemas-dlna-org:device-1-0'>DMS-1.50</dlna:X_DLNADOC><serviceList><service><serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType><serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId><SCPDURL>/X_MS_MediaReceiverRegistrar/%@/scpd.xml</SCPDURL><controlURL>/X_MS_MediaReceiverRegistrar/%@/control.xml</controlURL><eventSubURL>/X_MS_MediaReceiverRegistrar/%@/event.xml</eventSubURL></service><service><serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType><serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId><SCPDURL>/ContentDirectory/%@/scpd.xml</SCPDURL><controlURL>/ContentDirectory/%@/control.xml</controlURL><eventSubURL>/ContentDirectory/%@/event.xml</eventSubURL></service><service><serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType><serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId><SCPDURL>/ConnectionManager/%@/scpd.xml</SCPDURL><controlURL>/ConnectionManager/%@/control.xml</controlURL><eventSubURL>/ConnectionManager/%@/event.xml</eventSubURL></service></serviceList></device></root>",uuid,uuid,uuid,uuid,uuid,uuid,uuid,uuid,uuid,uuid]; }
- (NSString *)getDMRDescriptionXml11111:(NSString *)uuid { return [NSString stringWithFormat:@"<?xml version='1.0' encoding='UTF-8'?><root xmlns='urn:schemas-upnp-org:device-1-0' xmlns:dlna='urn:schemas-dlna-org:device-1-0'><specVersion><major>1</major><minor>0</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType><friendlyName>hitv_dmr</friendlyName><manufacturer>Plutinosoft LLC</manufacturer><manufacturerURL>http://www.plutinosoft.com</manufacturerURL><modelDescription>Plutinosoft AV Media Renderer Device</modelDescription><modelName>AV Renderer Device</modelName><modelURL>http://www.plutinosoft.com/platinum</modelURL><serialNumber></serialNumber><UDN>uuid:%@</UDN><dlna:X_DLNADOC xmlns:dlna='urn:schemas-dlna-org:device-1-0'>DMR-1.50</dlna:X_DLNADOC><serviceList><service><serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType><serviceId>urn:upnp-org:serviceId:AVTransport</serviceId><SCPDURL>/AVTransport/%@/scpd.xml</SCPDURL><controlURL>/AVTransport/%@/control.xml</controlURL><eventSubURL>/AVTransport/%@/event.xml</eventSubURL></service><service><serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType><serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId><SCPDURL>/ConnectionManager/%@/scpd.xml</SCPDURL><controlURL>/ConnectionManager/%@/control.xml</controlURL><eventSubURL>/ConnectionManager/%@/event.xml</eventSubURL></service><service><serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType><serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId><SCPDURL>/RenderingControl/%@/scpd.xml</SCPDURL><controlURL>/RenderingControl/%@/control.xml</controlURL><eventSubURL>/RenderingControl/%@/event.xml</eventSubURL></service></serviceList></device></root>",uuid,uuid,uuid,uuid,uuid,uuid,uuid,uuid,uuid,uuid]; }
此處的uuid都未設備的uuid。通過xml創建好device后,還需要給device設置friendlyName和loactionurl。在cyberlink庫里是沒有提供設置locationurl的方法,所以得自己加一個。
- (void)setLocationURL:(NSString *)url { if (!cObject) { return ; } cg_upnp_device_setlocationfromssdppacket(cObject, (char *)[url UTF8String]); }
這個cg_upnp_device_setlocationfromssdppacket方法也是自己加到庫里去的。看起來比較麻煩,其實很簡單,就是通過device的sspdPacket去修改loactionurl。改好這些以后,你的device就創創建好了,這時候通過dms或dmr對應的initWithcObject方法就可以創建好dms或者dmr了。為什么要自己去創建dms或者dmr呢。這是因為我自己開發的需要,我這邊不能使用CGupnpAvController這個類去搜索dms或者dmr,所以只能自己去主動創建了,如果你的需求是做正常的搜索功能,不需要這么去做,但是我說這些是為了講我對於dms,dmr的一些理解。
現在我們來看,dmr是怎么播放的。
if ([self.renderer setAVTransportWithItem:self.avItem]) { [self.renderer playWithUrl:[self.avItem.resourceUrl description]]; self.isPlay = [self.renderer isPlaying]; }
就是這幾行代碼,dmr就播放了對應的dms的資源文件。跟進去這個setAVTransportWithItem方法,發現它主要是為了獲取一個aciton,再跟進去,發現先獲取一個service。這個service和我們的dms是不一樣的。這個service是一個device的service。我們再可以去看創建device的xml。xml里包含了一些device的基本信息,同時包含了一個servicelist!就是這個servicelist。
<serviceList><service><serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType><serviceId>urn:upnp-org:serviceId:AVTransport</serviceId><SCPDURL>/AVTransport/%@/scpd.xml</SCPDURL><controlURL>/AVTransport/%@/control.xml</controlURL><eventSubURL>/AVTransport/%@/event.xml</eventSubURL></service><service><serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType><serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId><SCPDURL>/ConnectionManager/%@/scpd.xml</SCPDURL><controlURL>/ConnectionManager/%@/control.xml</controlURL><eventSubURL>/ConnectionManager/%@/event.xml</eventSubURL></service><service><serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType><serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId><SCPDURL>/RenderingControl/%@/scpd.xml</SCPDURL><controlURL>/RenderingControl/%@/control.xml</controlURL><eventSubURL>/RenderingControl/%@/event.xml</eventSubURL></service></serviceList>
這下應該明白了,每個device里都有一個servicelist,通過名稱獲取到對應的service,然后再通過service獲取到你想要的action,然后再把action post出去,我們的真實設備就可以在局域網里收到這個action,響應動作。包括dms,也是一樣的工作原理,通過post action,去獲取dms里的文件目錄。
那么我們要如何給dmr添加我們想要的快進,快退,調節音量等功能呢? 你仔細看這個servicelist,里面有幾個xml的路徑,你的locationurl地址在加上xml路徑,然后在瀏覽器里打開(在火狐瀏覽器打開)。其中一個就有actionlist,actionlist里包含各種aciton,然后你就可以自己去主動獲取這些action,然后post出去,去給自己的dmr加自己想要的功能。
ps:cyberlink里有一些坑,我也記得不太清楚了,能補充多少是多少。
1:setAVTRansportURI這個方法里面沒有去區分item的類別,比如是音樂還是圖片還是視頻,需要自己去改一下。我改成了setAVTransportWithItem,把AVItem傳進去,然后去判斷item的類型
- (BOOL)setAVTransportWithItem:(CGUpnpAvItem *)item { CGUpnpAction *action = [self actionOfTransportServiceForName:@"SetAVTransportURI"]; if (!action) return NO; [action setArgumentValue:@"0" forName:@"InstanceID"]; [action setArgumentValue:[item.resourceUrl description] forName:@"CurrentURI"]; NSString *classStr = [NSString string]; if ([item isAudioClass]) classStr = @"audioItem"; } else if([item isVideoClass]) { classStr = @"videoItem"; } else if([item isImageClass]) { classStr = @"imageItem"; } [action setArgumentValue:[self getCurrentURIMetaData:[item.resourceUrl description] With:classStr] forName:@"CurrentURIMetaData"]; if (![action post]) return NO; return YES; }
2:在創建device的時候,其實你的servicelist沒有被初始化,具體的原因是什么我也不清楚,可能是框架本身的缺陷。所以需要自己主動的去初始化一次。
首先你去獲取service,然后從service里獲取action時,你會發現service里的actionlist要么是空,要么只有一個action,跟進去getActionForName方法,把
#ifdef CG_OPTIMIZED_CP_MODE 這行注掉,這個時候它就會去判斷是否service被初始化過,沒有就初始化一次。當然這樣做還是不保險,最好能自己在創建device后主動初始化一次servicelist。所以我在CGUpnpDevice里添加了一個初始化servicelist的方法
- (void)parisedSCPDUrl { if (!cObject) { return ; } NSArray *servise = self.services; for (int i = 0; i < servise.count; i++) { CGUpnpService *ser = servise[i]; cg_upnp_controlpoint_parsescservicescpd(ser.cObject); } }
這樣就沒問題了。這樣每次當你去獲取action的時候,里面servicelist都已經被初始化了,不會有action取不到的情況。
3:很多關於網絡請求的地方都是沒有用多線程的,比如去初始化這個servicelist,最好能自己在加一個異步的操作,避免阻塞主線程。
目前能想到的就這么多了,確實cyberlink坑比較多,一開始完成正常的dms,dmc等功能其實挺快的,但是我們的需求不一樣,不允許讓dms,dmr暴露在局域網里,所以只能去主動構建,為了這個需求走了好多彎路,一直研究了一個多月才搞定,寫篇博文保留一下當時的研究過程。。。。