Cling使用教程(譯)

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
);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市硝烂,隨后出現(xiàn)的幾起案子箕别,更是在濱河造成了極大的恐慌,老刑警劉巖滞谢,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件串稀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡狮杨,警方通過查閱死者的電腦和手機(jī)母截,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來禾酱,“玉大人微酬,你說我怎么就攤上這事绘趋〔眨” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵陷遮,是天一觀的道長滓走。 經(jīng)常有香客問我,道長帽馋,這世上最難降的妖魔是什么搅方? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮绽族,結(jié)果婚禮上姨涡,老公的妹妹穿的比我還像新娘。我一直安慰自己吧慢,他們只是感情好涛漂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著检诗,像睡著了一般匈仗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逢慌,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天悠轩,我揣著相機(jī)與錄音,去河邊找鬼攻泼。 笑死火架,一個(gè)胖子當(dāng)著我的面吹牛鉴象,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播何鸡,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼炼列,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了音比?” 一聲冷哼從身側(cè)響起俭尖,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎洞翩,沒想到半個(gè)月后稽犁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骚亿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年已亥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片来屠。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虑椎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俱笛,到底是詐尸還是另有隱情捆姜,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布迎膜,位于F島的核電站泥技,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏磕仅。R本人自食惡果不足惜珊豹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榕订。 院中可真熱鬧店茶,春花似錦、人聲如沸劫恒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兼贸。三九已至段直,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間溶诞,已是汗流浹背鸯檬。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留螺垢,地道東北人喧务。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓赖歌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親功茴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子庐冯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容