Cling使用教程 - 用戶手冊(cè)
版本:1.0
原文鏈接:http://4thline.org/projects/cling/support/manual/cling-support-manual.xhtml
1. 使用網(wǎng)關(guān)設(shè)備
網(wǎng)關(guān)設(shè)備可以將本地局域網(wǎng)連接到廣域網(wǎng)上去廓译,并且通過Upnp服務(wù)(Universal Plug-n-Play:即插即用服務(wù))來監(jiān)視和配置局域網(wǎng)和廣域網(wǎng)的接口。通常情況下征绸,你可以用這種設(shè)備來進(jìn)行本地端口的映射,比如說:一個(gè)本地局域網(wǎng)應(yīng)用想要獲取廣域網(wǎng)上主機(jī)的連接渤弛,那么他必須在本地路由器上創(chuàng)建一個(gè)端口用于轉(zhuǎn)發(fā)和映射。
1.1 配置本地端口
Cling包含了所有需要用到的功能辕宏,通過Cling在本地網(wǎng)絡(luò)上的路由來映射端口瑞筐,只需要三行代碼即可:
PortMapping desireMapping =
new PortMapping(
8123,
"192.168.0.123",
PortMapping.Protocol.TCP,
"My Port Mapping"
);
UpnpService upnpService =
new UpnpServiceImpl(
new PortMappingListener(desireMapping)
);
upnpService.getControlPoint().search();
第一行代碼配置了一個(gè)端口映射,包括內(nèi)外端口號(hào)膘格,內(nèi)部IP,使用的協(xié)議以及功能描述菜秦。
第二行代碼啟動(dòng)了Upnp服務(wù),并傳入一個(gè)PortMappingListener主慰。一旦設(shè)備被任何其他發(fā)現(xiàn)河哑,PortMappingListener將會(huì)把端口映射到這些設(shè)備上去。
然后你可以立即調(diào)用ControlPoint#search方法佳吞,這將觸發(fā)你所在網(wǎng)絡(luò)上的所有本地路由的響應(yīng)和搜索,從而激活端口映射衷模。
在應(yīng)用退出時(shí)刁憋,你可以通過調(diào)用UpnpService#shutdown()來關(guān)閉Upnp堆棧至耻,PortMappingListener將刪除端口映射。如果你忘記關(guān)閉Upnp堆棧疤苹,那么這個(gè)端口映射將繼續(xù)保留在網(wǎng)關(guān)設(shè)備上(默認(rèn)的時(shí)間為0)。
如果程序在運(yùn)行過程中出錯(cuò)夸溶,程序?qū)?huì)輸出一些有關(guān)org.fourthline.cling.support.igd.PortMappingListener的警告日志缝裁。當(dāng)然你也可以通過重寫 PortMappingListener#handleFailureMessage(String)方法來處理這些錯(cuò)誤。
另外粹污,你也隨時(shí)可以用如下回調(diào)來手動(dòng)在已經(jīng)被發(fā)現(xiàn)的設(shè)備上添加和刪除端口映射:
Service service = device.findService(new UDAServiceId("WANIPConnection"));
// 執(zhí)行添加
upnpService.getControlPoint().execute(
new PortMappingAdd(service, desiredMapping){
@Override
public void success(ActionInvocation invocation){
// All ok
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){
// Something wrong
}
}
);
assertEquals(mapping[0].getInternalClient(), "192.168.0.123");
assertEquals(mapping[0].getInternalPort().getValue().longValue(), 8123);
assertEquals(mapping[0].isEnabled());
// 執(zhí)行刪除
upnpService.getControlPoint().execute(
new PortMappingDelete(service, desiredMapping){
@Override
public void success(ActionInvocation invocation){
// All ok
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){
// Something wrong
}
}
);
1.2 獲取連接信息
通過以下回調(diào)加缘,可以從廣域網(wǎng)的連接服務(wù)中檢索出當(dāng)前連接信息沈贝,包括狀態(tài)宋下、正常運(yùn)行時(shí)間和最后一條錯(cuò)誤消息:
Service service = device.findSevice(new UDAServiceId("WANIPConnection"));
upnpService.getControlPoint().execute(
new GetStatusInfo(service){
@Override
protected void success(Connection.StattusInfo statusInfo){
assertEquals(statusInfo.getStatus, Connection.Status.Connected);
assertEquals(statusInfo.getUptimeSeconds(), 1000);
assertEquals(statusInfo.getLastError(), Connection.Error.ERROR_NONE);
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){
// Something is wrong
}
}
)
此外滤奈,你還可以通過一個(gè)回調(diào)函數(shù)來獲取設(shè)備的外部連接IP:
Service service = device.findService(new UDAServiceId("WANIPConnection"));
upnpService.getControlPoing().execute(
new GetExternalIP(service){
@Override
protected void success(String externalIPAddress){
assertEquals(externalIPAddress, "123.123.123.123");
}
@Override
public void failure(ActionInvocation invocation,
UpnpResponse operation,
String defaultMsg) {
// Something is wrong
}
}
)
2. 發(fā)送信息給三星電視
許多可聯(lián)網(wǎng)的三星電視都實(shí)現(xiàn)了samsung.com:MessageBoxService的功能伺帘。這個(gè)功能的初始目標(biāo)可能是當(dāng)你在家并且你的手機(jī)連接上了你房間內(nèi)的無線網(wǎng)絡(luò)時(shí),可以讓三星手機(jī)自動(dòng)的把通知和提醒發(fā)送到電視上進(jìn)行顯示(前提你的電視是開著的并且也連接到了這個(gè)無線網(wǎng)絡(luò))张咳。
Cling也提供了類似的類可以讓你通過Upnp向三星電視發(fā)送通知脚猾。
你有幾種可以使用的消息類型。第一種就是帶有發(fā)送者/接收者名稱蛛芥,電話號(hào)碼以及時(shí)間戳和文本信息:
MessageSMS msg = new MessageSMS(
new DateTime("2010-06-21", "16:34:12"),
new NumberName("1234", "The receiver"),
new NumberName("5678", "The sender"),
"Hello world!"
);
這條消息將以“收到新短信”的形式出現(xiàn)在你的電視上称勋,并帶有顯示所有消息細(xì)節(jié)的選項(xiàng)赡鲜。另外,三星電視識(shí)可別的其他消息類型還包括來電通知和日歷日程提醒:
MessageIncomingCall msg = new MessageIncomingCall(
new DateTime("2010-06-21", "16:34:12"),
new NumberName("1234", "The Callee"),
new NumberName("5678", "The Caller")
);
MessageScheduleReminder msg = new MessageScheduleReminder(
new DateTime("2010-06-21", "16:34:12"),
new NumberName("1234", "The Owner"),
"The Subject",
new DateTime("2010-06-21", "17:34:12"),
"The Location",
"Hello World!"
);
以下是你如何通過異步的方式來發(fā)送信息:
LocalService service = device.findService(new ServiceId("samsung.com", "MessageBosService"));
upnpService.getControlPoint.execute(new AddMessage(service, msg)){
@Override
public void success(ActionInvocation invocation){
// All OK
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg){
// Something is wrong
}
}
需要注意的是,電視上可能包含一個(gè)移除消息的操作描述盗棵。Cling也提供了RemoveMessageCallback來移除消息,但是這個(gè)與三星電視的實(shí)現(xiàn)有所差別,這個(gè)動(dòng)作是通過遠(yuǎn)程控制來直接在電視上刪除該消息狱庇。
3.訪問和提供媒體服務(wù)
標(biāo)準(zhǔn)的Upnp音視頻媒體服務(wù)終端模板記錄了一些最流行的Upnp服務(wù)密任,盡管是命名為媒體服務(wù),但是實(shí)際上這些服務(wù)并不提供和訪問媒體數(shù)據(jù)淹遵,比如音樂透揣,圖片亦或是視頻文件。這些服務(wù)是通過分享元數(shù)據(jù)拆祈,這些元數(shù)據(jù)包含媒體文件的相關(guān)信息,比如它們的名稱淤年、格式和大小麸粮,以及可以用來獲取實(shí)際文件的定位器。傳輸這些媒體文件已經(jīng)超出這些媒體服務(wù)的范疇齐遵,通常情況下會(huì)使用簡(jiǎn)單的HTTP服務(wù)器和客戶端來實(shí)現(xiàn)這個(gè)傳輸任務(wù)拓哟。
一個(gè)媒體服務(wù)設(shè)備至少包括文件目錄(ContentDirectory)和連接管理的服務(wù)(ConnectionManager)断序。
3.1 瀏覽文件目錄
文件目錄服務(wù)提供媒體資源的元數(shù)據(jù)。這些元數(shù)據(jù)的格式是XML景图,內(nèi)容由DIDL、Dublic Core和UPnP特定元素和屬性組合而成。通常情況下贮尖,可以通過調(diào)用目錄文件服務(wù)的Browse方法來獲取這個(gè)XML格式的元數(shù)據(jù)薪前,然后手動(dòng)解析它。
如下是Cling所提供的Browse方法回調(diào)處理:
new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN) {
@Override
public void received(ActionInvocation actionInvocation, DIDLContent didl) {
// Read the DIDL content either using generic Container and Item types...
assertEquals(didl.getItems().size(), 2);
Item item1 = didl.getItems().get(0);
assertEquals(
item1.getTitle(),
"All Secrets Known"
);
assertEquals(
item1.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM.class),
"Black Gives Way To Blue"
);
assertEquals(
item1.getFirstResource().getProtocolInfo().getContentFormatMimeType().toString(),
"audio/mpeg"
);
assertEquals(
item1.getFirstResource().getValue(),
"http://10.0.0.1/files/101.mp3"
);
// ... or cast it if you are sure about its type ...
assert MusicTrack.CLASS.equals(item1);
MusicTrack track1 = (MusicTrack) item1;
assertEquals(track1.getTitle(), "All Secrets Known");
assertEquals(track1.getAlbum(), "Black Gives Way To Blue");
assertEquals(track1.getFirstArtist().getName(), "Alice In Chains");
assertEquals(track1.getFirstArtist().getRole(), "Performer");
MusicTrack track2 = (MusicTrack) didl.getItems().get(1);
assertEquals(track2.getTitle(), "Check My Brain");
// ... which is much nicer for manual parsing, of course!
}
@Override
public void updateStatus(Status status) {
// Called before and after loading the DIDL content
}
@Override
public void failure(ActionInvocation invocation,
UpnpResponse operation,
String defaultMsg) {
// Something wasn't right...
}
};
第一個(gè)回調(diào)(received)檢索出了所有包含3(容器標(biāo)識(shí)符)的子元素丁稀;
在驗(yàn)證和解析DIDL XML內(nèi)容之后會(huì)調(diào)用received()方法凿可,因此可以使用類型安全的接口來處理這些元數(shù)據(jù)唬复。DIDL的內(nèi)容是由Container和Item構(gòu)成的,但是此處只關(guān)心根目錄容器的子元素休建,而非子目錄的测砂。
你可以實(shí)現(xiàn)也可以忽略掉updateStatus()方法,這個(gè)方法可以很方便的在元數(shù)據(jù)加載和解析的前后給你提供通知存璃。例如你可以通過此方法來更新你的消息圖標(biāo)的狀態(tài)纵东。
如下示例向你展示了一個(gè)可提供更多操作的復(fù)雜回調(diào)示例:
ActionCallback complexBrowseAction =
new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN,
"*",
100l, 50l,
new SortCriterion(true, "dc:title"), // Ascending
new SortCriterion(false, "dc:creator")) { // Descending
// Implementation...
};
你可以通過聲明一些通配符參數(shù)辑甜,將結(jié)果限制為50個(gè)(從100個(gè)開始)分頁衰絮,以及一些排序條件。由目錄文件服務(wù)來處理這些操作磷醋。
3.2 目錄文件服務(wù)
換個(gè)角度岂傲,你可以先開始從目錄文件的服務(wù)端角度來思考。Cling提供了一個(gè)簡(jiǎn)單的目錄文件抽象類子檀,你要做的只需要實(shí)現(xiàn)browse()方法:
public class MP3ContentDirectory extends AbstractContentDirectoryService {
@Override
public BrowseResult browse(String objectID, BrowseFlag browseFlag,
String filter,
long firstResult, long maxResults,
SortCriterion[] orderby) throws ContentDirectoryException {
try {
// This is just an example... you have to create the DIDL content dynamically!
DIDLContent didl = new DIDLContent();
String album = ("Black Gives Way To Blue");
String creator = "Alice In Chains"; // Required
PersonWithRole artist = new PersonWithRole(creator, "Performer");
MimeType mimeType = new MimeType("audio", "mpeg");
didl.addItem(new MusicTrack(
"101", "3", // 101 is the Item ID, 3 is the parent Container ID
"All Secrets Known",
creator, album, artist,
new Res(mimeType, 123456l, "00:03:25", 8192l, "http://10.0.0.1/files/101.mp3")
));
didl.addItem(new MusicTrack(
"102", "3",
"Check My Brain",
creator, album, artist,
new Res(mimeType, 2222222l, "00:04:11", 8192l, "http://10.0.0.1/files/102.mp3")
));
// Create more tracks...
// Count and total matches is 2
return new BrowseResult(new DIDLParser().generate(didl), 2, 2);
} catch (Exception ex) {
throw new ContentDirectoryException(
ContentDirectoryErrorCode.CANNOT_PROCESS,
ex.toString()
);
}
}
@Override
public BrowseResult search(String containerId,
String searchCriteria, String filter,
long firstResult, long maxResults,
SortCriterion[] orderBy) throws ContentDirectoryException {
// You can override this method to implement searching!
return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy);
}
}
在這里可以看到新建了一個(gè)DIDLContent實(shí)例將結(jié)果存儲(chǔ)起來镊掖,在用DIDLParser將其轉(zhuǎn)換成XML字符串缩歪,最后用BrowseReuslt返回?cái)?shù)據(jù)時(shí)。如何去構(gòu)建DIDL的內(nèi)容需要你自己來決定祟身,通常情況下,需要?jiǎng)討B(tài)的去通過后端數(shù)據(jù)庫來查詢,然后將結(jié)果封裝到COntainer和Item中去蛛壳。Cling提供了去多便利的內(nèi)容模型類來表示多媒體的元數(shù)據(jù)斩披,正如內(nèi)容目錄中定義的那樣(MusicTrack,Movie等)这溅,你可以早org.fourthline.cling.support.model包中找到。
DIDLParser不是線程安全的蛋褥,所以不要在服務(wù)端應(yīng)用程序的多個(gè)線程中使用一個(gè)單例。
AbstractContentDirectoryService只實(shí)現(xiàn)了COntentDirectory中文件瀏覽和搜索的必須的動(dòng)作和聲明的變量薄货。如果想要去編輯這些元數(shù)據(jù),那就需要另外增加方法了谤绳。
媒體服務(wù)設(shè)備同樣需要有個(gè)連接管理服務(wù)。
3.3 HTTP-GET的簡(jiǎn)單連接管理
如果你的傳輸協(xié)議是基于HTTP的GET請(qǐng)求断凶,也就是說你的媒體播放器將從HTTP服務(wù)器上下載文件或者獲取文件流窗价,那么你所要為這個(gè)媒體服務(wù)提供的將是一個(gè)非常簡(jiǎn)單的連接管理。
這個(gè)連接管理實(shí)際上并不管理任何連接,甚至它根本都不提供任何功能舞痰。如下就是你通過Cling提供的ConnectManagerService來如何創(chuàng)建和綁定這個(gè)簡(jiǎn)單的服務(wù)给涕。
LocalService<ConnectionManagerService> service =
new AnnotationLocalServiceBinder().read(ConnectionManagerService.class);
service.setManager(
new DefaultServiceManager<>(
service,
ConnectionManagerService.class
)
);
現(xiàn)在可以將這個(gè)服務(wù)添加到你的媒體服務(wù)設(shè)備上去莺掠,并且它將開始正常工作结耀。
事實(shí)上蜂厅,許多媒體服務(wù)器至少提供了一個(gè)“數(shù)據(jù)源”協(xié)議列表。這個(gè)列表包含了媒體服務(wù)器可能具有的所有(MIME)協(xié)議類型。接收器(顯示器)將會(huì)通過這個(gè)協(xié)議信息來決定是否可以播放來自媒體服務(wù)器的資源文件罢杉,而不是去瀏覽并查看每一個(gè)資源的類型而叼。
首先秤朗,創(chuàng)建一個(gè)服務(wù)器支持的協(xié)議信息的列表:
final ProtocolInfos sourceProtocols =
new ProtocolInfos(
new ProtocolInfo(
Protocol.HTTP_GET,
ProtocolInfo.WILDCARD,
"audio/mpeg",
"DLNA.ORG_PN=MP3;DLNA.ORG_OP=01"
),
new ProtocolInfo(
Protocol.HTTP_GET,
ProtocolInfo.WILDCARD,
"video/mpeg",
"DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=01;DLNA.ORG_CI=0"
)
);
現(xiàn)在你需要自定義連接管理服務(wù)姨裸,在實(shí)例化時(shí)將協(xié)議列表作為參數(shù)進(jìn)行傳遞:
service.setManager(
new DefaultServiceManager<ConnectionManagerService>(service, null) {
@Override
protected ConnectionManagerService createServiceInstance() throws Exception {
return new ConnectionManagerService(sourceProtocols, null);
}
}
);
如果你傳輸協(xié)議不是HTTP而是其他的赡艰,比如RTSP流芹血,那么這個(gè)連接管理將不會(huì)起任何作用辆憔。
3.4 管理對(duì)等點(diǎn)的連接
你可能認(rèn)為既然媒體播放器上通過URL使用HTTP-GET方式去拉取媒體數(shù)據(jù)虱咧,那么連接管理就不是必須的了枫甲。但是你需要明白的是Upnp媒體服務(wù)器已經(jīng)提供了URL瑞凑,如果還需要他提供URL對(duì)應(yīng)的文件,那么顯然這已經(jīng)超出了常見Upnp的系統(tǒng)架構(gòu)范圍了嫩挤。
再者血筑,當(dāng)媒體數(shù)據(jù)源是要將數(shù)據(jù)推送到播放器或者需要事先為播放器準(zhǔn)備連接绘沉,那么此時(shí)連接管理服務(wù)就變得有用了。在這種情況下豺总,兩方連接管理需要事先通過PrepareForConnection操作來協(xié)商連接 - 具體哪方發(fā)起連接由你決定车伞。當(dāng)媒體結(jié)束播放時(shí),一端的連接管理將調(diào)用ConnectionComplete操作喻喳。每一個(gè)連接都具有唯一的標(biāo)志符以及相關(guān)的連接協(xié)議信息另玖,對(duì)應(yīng)的連接管理會(huì)將該連接作為對(duì)等點(diǎn)連接進(jìn)行處理。
Cling提供了對(duì)等點(diǎn)連接服務(wù)AbstractPeeringConnectionManagerService,它將幫助你完成所有繁重的任務(wù)日矫,你只需要實(shí)現(xiàn)創(chuàng)建和關(guān)閉連接的操作赂弓。盡管我們現(xiàn)在仍在討論媒體服務(wù)器相關(guān)的內(nèi)容绑榴,但是對(duì)等點(diǎn)的連接協(xié)商是需要在媒體渲染/播放端進(jìn)行實(shí)現(xiàn)的哪轿。因此如下的例子相關(guān)的就是一個(gè)對(duì)媒體渲染器的連接管理。
首先翔怎,實(shí)現(xiàn)你想要如何管理連接兩端的連接(這只是一邊):
public class PeeringConnectionManager extends AbstractPeeringConnectionManagerService {
PeeringConnectionManager(ProtocolInfos sourceProtocolInfo,
ProtocolInfos sinkProtocolInfo) {
super(sourceProtocolInfo, sinkProtocolInfo);
}
@Override
protected ConnectionInfo createConnection(int connectionID,
int peerConnectionId,
ServiceReference peerConnectionManager,
ConnectionInfo.Direction direction,
ProtocolInfo protocolInfo)
throws ActionException {
// Create the connection on "this" side with the given ID now...
ConnectionInfo con = new ConnectionInfo(
connectionID,
123, // Logical Rendering Control service ID
456, // Logical AV Transport service ID
protocolInfo,
peerConnectionManager,
peerConnectionId,
direction,
ConnectionInfo.Status.OK
);
return con;
}
@Override
protected void closeConnection(ConnectionInfo connectionInfo) {
// Close the connection
}
@Override
protected void peerFailure(ActionInvocation invocation,
UpnpResponse operation,
String defaultFailureMessage) {
System.err.println("Error managing connection with peer: " + defaultFailureMessage);
}
}
在createConnection()方法中窃诉,你需要為負(fù)責(zé)創(chuàng)建連接的服務(wù)提供顯示控制和音視頻傳輸?shù)臉?biāo)識(shí)符。這個(gè)連接ID已經(jīng)為你定義好了赤套,所以你需要做的就是返回帶有這些信息的這個(gè)連接飘痛。
closeConnection()方法是與createConnection對(duì)應(yīng)的方法,此方法你可以實(shí)現(xiàn)在關(guān)閉連接服務(wù)的相關(guān)邏輯容握,如清理無用信息宣脉。
peerFailure()方法與前面的兩條方法無關(guān)。它只由調(diào)用操作的連接管理器使用剔氏,而不是在接收端使用塑猖。
下面讓我們?cè)趦蓚€(gè)連接管理器之間創(chuàng)建一個(gè)對(duì)等點(diǎn)連接。首先谈跛,創(chuàng)建作為數(shù)據(jù)源的服務(wù)(我們假設(shè)這是表示媒體數(shù)據(jù)源的媒體服務(wù)器):
PeeringConnectionManager peerOne =
new PeeringConnectionManager(
new ProtocolInfos("http-get:*:video/mpeg:*,http-get:*:audio/mpeg:*"),
null
);
LocalService<PeeringConnectionManager> peerOneService = createService(peerOne);
可以看到它提供了幾個(gè)協(xié)議的媒體元數(shù)據(jù)羊苟。接收器(或媒體渲染器)是對(duì)等連接管理器:
PeeringConnectionManager peerTwo =
new PeeringConnectionManager(
null,
new ProtocolInfos("http-get:*:video/mpeg:*")
);
LocalService<PeeringConnectionManager> peerTwoService = createService(peerTwo);
它只執(zhí)行一種特定的協(xié)議。
createService()方法只是在從(已經(jīng)提供的)注釋中讀取服務(wù)元數(shù)據(jù)后感憾,在服務(wù)上設(shè)置連接管理器實(shí)例:
public LocalService<PeeringConnectionManager> createService(final PeeringConnectionManager peer) {
LocalService<PeeringConnectionManager> service =
new AnnotationLocalServiceBinder().read(
AbstractPeeringConnectionManagerService.class
);
service.setManager(
new DefaultServiceManager<PeeringConnectionManager>(service, null) {
@Override
protected PeeringConnectionManager createServiceInstance() throws Exception {
return peer;
}
}
);
return service;
}
現(xiàn)在必須有一個(gè)對(duì)等點(diǎn)發(fā)起連接蜡励。它需要?jiǎng)?chuàng)建一個(gè)連接標(biāo)識(shí)符,存儲(chǔ)這個(gè)標(biāo)識(shí)符(“管理”連接)阻桅,并調(diào)用另一個(gè)對(duì)等點(diǎn)的PrepareForConnection服務(wù)凉倚。所有這些都被提供并封裝在createConnectionWithPeer()方法中:
int peerOneConnectionID = peerOne.createConnectionWithPeer(
peerOneService.getReference(),
controlPoint,
peerTwoService,
new ProtocolInfo("http-get:*:video/mpeg:*"),
ConnectionInfo.Direction.Input
);
if (peerOneConnectionID == -1) {
// Connection establishment failed, the peerFailure()
// method has been called already. It's up to you
// how you'd like to continue at this point.
}
int peerTwoConnectionID =
peerOne.getCurrentConnectionInfo(peerOneConnectionID) .getPeerConnectionID();
int peerTwoAVTransportID =
peerOne.getCurrentConnectionInfo(peerOneConnectionID).getAvTransportID();
你需要提供一個(gè)對(duì)本地服務(wù)的引用,一個(gè)執(zhí)行操作的控制點(diǎn)以及用于此連接的協(xié)議信息嫂沉。連接方向(此處我們是輸入)是遠(yuǎn)程對(duì)等點(diǎn)應(yīng)該如何處理這個(gè)連接中的數(shù)據(jù)傳輸(另外占遥,我們假設(shè)這個(gè)對(duì)等點(diǎn)是數(shù)據(jù)接收端)。這個(gè)方法可以返回新連接的標(biāo)識(shí)符输瓜。你可以通過這個(gè)標(biāo)識(shí)符來獲取連接的一些信息瓦胎,比如另一個(gè)對(duì)等點(diǎn)的標(biāo)識(shí)符,AV傳輸服務(wù)標(biāo)識(shí)符尤揣。
當(dāng)你完成連接任務(wù)搔啊,你可以通過這個(gè)方法進(jìn)行關(guān)閉:
peerOne.closeConnectionWithPeer(
controlPoint,
peerTwoService,
peerOneConnectionID
);
peerFailure方法將會(huì)在調(diào)用createConnectionWithPeer()或closeConnectionWithPeer()失敗時(shí)調(diào)用。
4. 訪問和提供媒介提供者
MediaRenderer服務(wù)的目的是控制遠(yuǎn)程媒體輸出設(shè)備北戏。一種實(shí)現(xiàn)渲染器的設(shè)備负芋,因此具有必要的AVTransport服務(wù),可以像傳統(tǒng)紅外遙控器一樣進(jìn)行控制。想想用游戲控制器控制Playstation3上的視頻回放有多尷尬吧旧蛾。MediaRenderer就像一個(gè)可編程的通用遠(yuǎn)程API莽龟,所以你可以用iPad、Android手機(jī)锨天、觸摸屏毯盈、筆記本電腦或任何其他可以使用Upnp的設(shè)備來代替紅外線遙控器或Playstation控制器。
(不幸的是病袄,Playstation3沒有公開任何MediaRenderer服務(wù)搂赋。事實(shí)上,在電視和機(jī)頂盒中益缠,大多數(shù)的MediaRenderer實(shí)現(xiàn)都是不完整的脑奠,或者不兼容的,這是對(duì)規(guī)范的嚴(yán)格解釋幅慌。更糟糕的是宋欺,沒有簡(jiǎn)化UPnP A/V規(guī)范,反而在DLNA指南中添加了更多的規(guī)則胰伍,從而使得兼容性更加難以實(shí)現(xiàn)齿诞。一個(gè)工作和行為正確的媒體人似乎是個(gè)例外,而不是常態(tài)喇辽。)
這個(gè)過程很簡(jiǎn)單:首先將媒體資源的URL發(fā)送給渲染程序掌挚。如何獲得該資源的URL完全取決于你,可能需要瀏覽媒體服務(wù)器的資源元數(shù)據(jù)∑凶桑現(xiàn)在控制渲染器的狀態(tài)吠式,例如播放、暫停抽米、停止特占、錄制視頻等等。你還可以通過媒體渲染器的標(biāo)準(zhǔn)化渲染控制服務(wù)控制音頻/視頻內(nèi)容的音量和亮度等其他屬性云茸。
Cling提供了org.fourthline.clate.Support.avtransport.AbstractAVTransportService類是目,一個(gè)抽象類型,包含所有UPnP操作和狀態(tài)變量映射标捺。要實(shí)現(xiàn)MediaRenderer懊纳,你必須創(chuàng)建一個(gè)子類并實(shí)現(xiàn)所有方法。如果你已經(jīng)有一個(gè)媒體播放器亡容,并且你想要提供一個(gè)Upnp的遠(yuǎn)程控制接口嗤疯,那么你應(yīng)該考慮這個(gè)策略。
另外闺兢,如果你正在編寫一個(gè)新媒體播放器茂缚,Cling甚至可以為你提供狀態(tài)管理和轉(zhuǎn)換,因此你所要實(shí)現(xiàn)的就是媒體數(shù)據(jù)的實(shí)際輸出。
4.1 從零創(chuàng)建渲染器
Cling提供了一個(gè)可以使你管理當(dāng)前播放狀態(tài)的狀態(tài)機(jī)引擎脚囊。該特性簡(jiǎn)化了使用Upnp渲染器編寫媒體播放器的過程龟糕,包括如下幾個(gè)步驟:
4.1.1 定義播放狀態(tài)
首先,定義你定義狀態(tài)機(jī)以及你的播放器可支持的幾種狀態(tài):
package example.mediarenderer;
import org.fourthline.cling.support.avtransport.impl.AVTransportStateMachine;
import org.seamless.statemachine.States;
@States({
MyRendererNoMediaPrtesent.class,
MyRendererStopped.class,
MyRenderPlaying.class
})
interface MyRendererStateMachine extends AVTransportStateMachine{}
這是一個(gè)非常簡(jiǎn)單的播放器悔耘,只有三種狀態(tài):沒有媒體時(shí)的初始狀態(tài)讲岁,以及播放和停止?fàn)顟B(tài)。你還可以支持其他狀態(tài)淮逊,比如暫停和記錄催首,但是我們希望這個(gè)示例盡可能簡(jiǎn)單扶踊。(同時(shí)比較AVTransport:1規(guī)范文件第2.5節(jié)中的“操作理論”章節(jié)和狀態(tài)圖泄鹏。)
接下來,實(shí)現(xiàn)狀態(tài)和觸發(fā)從一個(gè)狀態(tài)到另一個(gè)狀態(tài)轉(zhuǎn)換的操作秧耗。
初始狀態(tài)只有一個(gè)可能的轉(zhuǎn)換和一個(gè)觸發(fā)該轉(zhuǎn)換的動(dòng)作:
public class MyRendererNoMediaPresent extends NoMediaPresent{
public MyRendererNoMediaPresetn(AVTransport transport){
super(transport);
}
@Override
public Class<? extends AbstractState> setTransportURI(URI uri, String metaData){
getTransport().setMediaInfo(new MediaInfo(uri.toString(), metaData));
// if you can, you should find and set the duration of the track here!
getTransport().setPositionInfo(new PositionInfo(1, metaData, uri.toString()));
// it's up to you what "last changes" you want to announce to event listeners
getTransport().getLastChange().setEventedValue(
getTransport().getInstaceId(),
new AVTransportVariable.AVTransportURI(uri),
new AVTransportVariable.CurrentTrackURI(uri)
);
return MyRendererStopped.class;
}
}
當(dāng)客戶端為回放設(shè)置一個(gè)新的URI時(shí)备籽,你必須相應(yīng)地準(zhǔn)備你的渲染程序。你通常希望更改AVTransport的MediaInfo以反映新的“當(dāng)前”跟蹤分井,并且你可能希望公開關(guān)于跟蹤的信息车猬,比如回放時(shí)間。如何做到這一點(diǎn)(例如尺锚,你實(shí)際上已經(jīng)可以檢索URL后面的文件并分析它)取決于你珠闰。
LastChange對(duì)象是如何通知控制點(diǎn)狀態(tài)的任何變化,這里我們告訴控制點(diǎn)有一個(gè)新的“AVTransportURI”和一個(gè)新的“CurrentTrackURI”瘫辩。你可以向LastChange添加更多的變量和它們的值伏嗜,這取決于實(shí)際更改的內(nèi)容——注意,如果你認(rèn)為幾個(gè)更改是原子性的伐厌,那么你應(yīng)該在setEventedValue(…)的單個(gè)調(diào)用中執(zhí)行此操作承绸。(最后的更改將被輪詢并定期發(fā)送到后臺(tái)的控制點(diǎn),稍后會(huì)詳細(xì)介紹挣轨。)
設(shè)置URI之后军熏,AVTransport將轉(zhuǎn)換到停止?fàn)顟B(tài)。
停止?fàn)顟B(tài)有許多可能的轉(zhuǎn)換卷扮,從這里一個(gè)控制點(diǎn)可以決定播放荡澎、查找、跳過到下一個(gè)軌道晤锹,等等摩幔。下面的例子真的沒有做多少,你如何實(shí)現(xiàn)這些觸發(fā)器和狀態(tài)轉(zhuǎn)換完全取決于你的播放引擎的設(shè)計(jì)-這只是腳手架:
public class MyRendererStopped extends Stopped {
public MyRendererStopped(AVTransport transport) {
super(transport);
}
public void onEntry() {
super.onEntry();
// Optional: Stop playing, release resources, etc.
}
public void onExit() {
// Optional: Cleanup etc.
}
@Override
public Class<? extends AbstractState> setTransportURI(URI uri, String metaData) {
// This operation can be triggered in any state, you should think
// about how you'd want your player to react. If we are in Stopped
// state nothing much will happen, except that you have to set
// the media and position info, just like in MyRendererNoMediaPresent.
// However, if this would be the MyRendererPlaying state, would you
// prefer stopping first?
return MyRendererStopped.class;
}
@Override
public Class<? extends AbstractState> stop() {
/// Same here, if you are stopped already and someone calls STOP, well...
return MyRendererStopped.class;
}
@Override
public Class<? extends AbstractState> play(String speed) {
// It's easier to let this classes' onEntry() method do the work
return MyRendererPlaying.class;
}
@Override
public Class<? extends AbstractState> next() {
return MyRendererStopped.class;
}
@Override
public Class<? extends AbstractState> previous() {
return MyRendererStopped.class;
}
@Override
public Class<? extends AbstractState> seek(SeekMode unit, String target) {
// Implement seeking with the stream in stopped state!
return MyRendererStopped.class;
}
}
每個(gè)狀態(tài)都可以有兩個(gè)神奇的方法:onEntry()和onExit()——它們完全按照名稱執(zhí)行抖甘。如果你決定使用超類的方法热鞍,不要忘記調(diào)用它們!
通常,當(dāng)調(diào)用播放狀態(tài)的onEntry()方法時(shí),你將開始回放:
public class MyRendererPlaying extends Playing {
public MyRendererPlaying(AVTransport transport) {
super(transport);
}
@Override
public void onEntry() {
super.onEntry();
// Start playing now!
}
@Override
public Class<? extends AbstractState> setTransportURI(URI uri, String metaData) {
// Your choice of action here, and what the next state is going to be!
return MyRendererStopped.class;
}
@Override
public Class<? extends AbstractState> stop() {
// Stop playing!
return MyRendererStopped.class;
}
到目前為止薇宠,編寫你的播放器還沒有涉及太多的通用即插即用功能——stick只是為你提供了一個(gè)狀態(tài)機(jī)偷办,并通過LastEvent接口向客戶端發(fā)送狀態(tài)更改的信號(hào)。
4.1.2 注冊(cè)AVTransportService
下一步是將狀態(tài)機(jī)連接到UPnP服務(wù)中澄港,這樣就可以將該服務(wù)添加到設(shè)備中椒涯,最后添加到粘附注冊(cè)表。首先回梧,綁定服務(wù)并定義服務(wù)管理器如何獲取玩家實(shí)例:
LocalService<AVTransportService> service =
new AnnotationLocalServiceBinder().read(AVTransportService.class);
// Service's which have "logical" instances are very special, they use the
// "LastChange" mechanism for eventing. This requires some extra wrappers.
LastChangeParser lastChangeParser = new AVTransportLastChangeParser();
service.setManager(
new LastChangeAwareServiceManager<AVTransportService>(service, lastChangeParser) {
@Override
protected AVTransportService createServiceInstance() throws Exception {
return new AVTransportService(
MyRendererStateMachine.class, // All states
MyRendererNoMediaPresent.class // Initial state
);
}
}
);
構(gòu)造函數(shù)有兩個(gè)類废岂,一個(gè)是狀態(tài)機(jī)定義,另一個(gè)是創(chuàng)建后的初始狀態(tài)狱意。
就是這樣——你已經(jīng)準(zhǔn)備好將此服務(wù)添加到MediaRenderer設(shè)備和控制點(diǎn)將看到它并能夠調(diào)用操作湖苞。
但是,還有一個(gè)細(xì)節(jié)需要考慮:LastChange事件的傳播详囤。當(dāng)任何播放狀態(tài)或轉(zhuǎn)換向LastChange添加“更改”時(shí)财骨,這些數(shù)據(jù)將被累積。它不會(huì)立即或自動(dòng)發(fā)送到GENA訂戶!如何以及何時(shí)將所有累積的更改刷新到控制點(diǎn)由你決定藏姐。一種常見的方法是后臺(tái)線程每秒鐘(甚至更頻繁地)執(zhí)行這個(gè)操作:
LastChangeAwareServiceManager manager = (LastChangeAwareServiceManager)service.getManager();
manager.fireLastChange();
最后隆箩,請(qǐng)注意AVTransport規(guī)范還定義了“邏輯”播放器實(shí)例。例如羔杨,可以同時(shí)播放兩個(gè)uri的呈現(xiàn)程序?qū)⒂袃蓚€(gè)AVTransport實(shí)例捌臊,每個(gè)實(shí)例都有自己的標(biāo)識(shí)符。保留的標(biāo)識(shí)符“0”是一個(gè)呈現(xiàn)器的默認(rèn)值兜材,該呈現(xiàn)器一次只支持一個(gè)URI的回放理澎。在attach中,每個(gè)邏輯AVTransport實(shí)例由與AVTransport類型的一個(gè)實(shí)例關(guān)聯(lián)的狀態(tài)機(jī)的一個(gè)實(shí)例(及其所有狀態(tài))表示护姆。所有這些對(duì)象都不會(huì)共享矾端,而且它們也不是線程安全的。有關(guān)此特性的更多信息卵皂,請(qǐng)閱讀AVTransportService類的文檔和代碼——默認(rèn)情況下秩铆,它只支持ID為“0”的單個(gè)傳輸實(shí)例,你必須重寫findInstance()方法來創(chuàng)建和支持多個(gè)并行回放實(shí)例灯变。
4.2 控制渲染器
Cling支持提供了幾個(gè)操作回調(diào)殴玛,簡(jiǎn)化了為AVTransport服務(wù)創(chuàng)建控制點(diǎn)的過程。這是你的播放器的客戶端添祸,遙控器滚粟。
這是你如何設(shè)置URI播放:
ActionCallback setAVTransportURIAction =
new SetAVTransportURI(service, "http://10.0.0.1/file.mp3", "NO METADATA") {
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
// Something was wrong
}
};
這是你如何開始回放:
ActionCallback playAction =
new Play(service) {
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
// Something was wrong
}
};
你的控制點(diǎn)還可以訂閱服務(wù)并偵聽LastChange事件。Cling提供了一個(gè)解析器刃泌,因此你可以在控制點(diǎn)上獲得與服務(wù)器上相同的類型和類——這與發(fā)送和接收事件數(shù)據(jù)是一樣的凡壤。當(dāng)你在SubscriptionCallback中接收到“l(fā)ast change”字符串時(shí)署尤,你可以對(duì)其進(jìn)行轉(zhuǎn)換,例如亚侠,當(dāng)玩家從nomediap狀態(tài)轉(zhuǎn)換到stop狀態(tài)時(shí)曹体,服務(wù)可能已經(jīng)發(fā)送了這個(gè)事件:
LastChange lastChange = new LastChange(
new AVTransportLastChangeParser(),
lastChangeString
);
assertEquals(
lastChange.getEventedValue(
0, // Instance ID!
AVTransportVariable.AVTransportURI.class
).getValue(),
URI.create("http://10.0.0.1/file.mp3")
);
assertEquals(
lastChange.getEventedValue(
0,
AVTransportVariable.CurrentTrackURI.class
).getValue(),
URI.create("http://10.0.0.1/file.mp3")
);
assertEquals(
lastChange.getEventedValue(
0,
AVTransportVariable.TransportState.class
).getValue(),
TransportState.STOPPED
);