根据工作需要,暂时翻译部分,后面会详细修改整理



Cling Support
User Manual
Authors:
Christian Bauer
Table Of Contents:
1.Working with InternetGatewayDevices
1.1.Mapping a NAT port
1.2.Getting connection information
2.Sending messages to Samsung TVs
3.Accessing and providing MediaServers
3.1.Browsing a ContentDirectory
3.2.The ContentDirectory service
3.3.A simple ConnectionManager for HTTP-GET
3.4.Managing connections between peers
4.Accessing and providing MediaRenderers
4.1.Creating a renderer from scratch
4.1.1.Defining the states of the player
4.1.2.Registering the AVTransportService
4.2.Controlling a renderer
1. Working with InternetGatewayDevices


An InternetGatewayDevice connects a LAN to a WAN, and through UPnP supports the monitoring and configuration of LAN and WAN interfaces. Typically this functionality is used for NAT port mapping: A client application on the LAN wants to receive network connections from a WAN host, so it has to create a port forwarding and mapping on the LAN router.
1.1. Mapping a NAT port


Cling Support contains all the neccessary functionality, creating a port mapping on all NAT routers on a network requires only three lines of code:


PortMapping desiredMapping =
        new PortMapping(
                8123,
                "192.168.0.123",
                PortMapping.Protocol.TCP,
                "My Port Mapping"
        );


UpnpService upnpService =
        new UpnpServiceImpl(
                new PortMappingListener(desiredMapping)
        );


upnpService.getControlPoint().search();


The first line creates a port mapping configuration with the external/internal port, an internal host IP, the protocol and an optional description.


The second line starts the UPnP service with a special listener. This listener will add the port mapping on any InternetGatewayDevice with a WANIPConnection or a WANPPPConnection service as soon as it is discovered. You should immediately start a ControlPoint#search() for all devices on your network, this triggers a response and discovery of all NAT routers, activating the port mapping.


The listener will also delete the port mapping when you stop the UPnP stack through UpnpService#shutdown(), usually before your application quits. If you forget to shutdown the stack the port mapping will remain on the InternetGatewayDevice - the default lease duration is 0!


If anything goes wrong, log messages with WARNING level will be created on the category org.fourthline.cling.support.igd.PortMappingListener. You can override the PortMappingListener#handleFailureMessage(String) method to customize this behavior.


Alternatively, you can manually add and delete port mappings on an already discovered device with the following ready-to-use action callbacks:


Service service = device.findService(new UDAServiceId("WANIPConnection"));


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 is wrong
        }
    }
);


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 is wrong
        }
    }
);


1.2. Getting connection information


The current connection information, including status, uptime, and last error message can be retrieved from a WAN*Connection service with the following callback:


Service service = device.findService(new UDAServiceId("WANIPConnection"));


upnpService.getControlPoint().execute(
    new GetStatusInfo(service) {


        @Override
        protected void success(Connection.StatusInfo 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
        }
    }
);


Additionally, a callback for obtaining the external IP address of a connection is available:


Service service = device.findService(new UDAServiceId("WANIPConnection"));


upnpService.getControlPoint().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. Sending messages to Samsung TVs


Many network-enabled Samsung TVs implement the proprietary samsung.com:MessageBoxService:1. The original purpose of this service was most likely connectivity with Samsung mobile phones; notification messages and alerts would appear on your TV when you are at home and your cellphone is connected to your local WiFi network (and your TV is turned on).


Cling Support delivers client classes for sending notifications to your Samsung TV via UPnP. (See this page for more information about the reverse-engineered raw message format.)
Sending messages from an Android handset
The XML parsing of messages requires Android 2.2, it won't work on any older version.


There are several message types available. The first is an SMS with a sender and receiver names and phone numbers, as well as a timestamp and message text:


MessageSMS msg = new MessageSMS(
        new DateTime("2010-06-21", "16:34:12"),
        new NumberName("1234", "The Receiver"),
        new NumberName("5678", "The Sender"),
        "Hello World!"
);


This message will appear as a "New SMS Received!" notification on your TV, with the option to reveal all message details. The other message types recognized by the TV are incoming call notification as well as calendar schedule reminder:


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!"
);


This is how you send a message asynchronously:


LocalService service = device.findService(new ServiceId("samsung.com", "MessageBoxService"));


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
        }
    }
);


Note that although your TV's service descriptor most likely contains a RemoveMessage action and Cling Support also ships with a RemoveMessageCallback, this action doesn't seem to be implemented by any Samsung TVs. Messages can only be deleted directly on the TV, with the remote control.
3. Accessing and providing MediaServers


The standardized UPnP AV MediaServer:1 device template describes some of the the most popular UPnP services. Despite the name, these services are not about serving and accessing media data such as music, picture, or video files. They are for sharing metadata: The data about media files such as their name, format, and size, and a locator that can be used to obtain the actual file. Transmission of the media file is outside of the scope of these specifications; most of the time that is the job of a simple HTTP server and client.
ContentDirectory server or client on Android
The XML parsing of DIDL content requires Android 2.2, it won't work on any older version.


A MediaServer:1 device has at least a ContentDirectory:1 and a ConnectionManager:1 service.
3.1. Browsing a ContentDirectory
//浏览内容目录
A ContentDirectory:1 service provides media resource metadata. The content format for this metadata is XML and the schema is a mixture of DIDL, Dublic Core, and UPnP specific elements and attributes. Usually you'd have to call the Browse action of the content directory service to get this XML metadata and then parse it manually.
//内容目录服务提供媒体资源的元数据。这些元数据的内容格式是XML,这些概要信息是DIDL,Dublic Core和UPnP特定元素和属性。通常你不得不调用内容目录服务的Browser动作来得到这个XML数据,然后手动分析这些数据。
The Browse action callback in Cling Support handles all of this for you:
你需要处理的内容在Browser动作的回调方法中。
#----------------------------------------------code start here-----------------------------------------------------------
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...
    }
};
#----------------------------------------------code end here-----------------------------------------------------------


The first callback retrieves all the children of container 3 (container identifier).
在回调中首先取得所有的容器3的子目录。(why is 3?)
The root container identifier
根容器的id?
You can not copy/paste the shown example code! It will most likely not return any items! You need to use a different container ID! The shown container ID '3' is just an example. Your server does not have a container with identifier '3'! If you want to browse the "root" container of the ContentDirectory, use the identifier '0': Browse(service, "0", BrowseFlag.DIRECT_CHILDREN). Although not standardized many media servers consider the ID '0' to be the root container's identifier. If it's not, ask your media server vendor. By listing all the children of the root container you can get the identifiers of sub-containers and so on, recursively.
//你不能复制黏贴上面的示例代码。因为大部分事件,它可能不返回任何内容。你需要使用不同的容器ID。例子中的容器ID 3仅仅是一个例子。你的服务器可能不包含ID为3的容器。如果你想浏览ContentDirectory的根容器,使用ID “0”:Browse(service, "0", BrowseFlag.DIRECT_CHILDREN)。尽管没有被标准化,很多服务器都把ID "0"当做根容器的ID。如果不是,你需要问你的媒体服务器供应商。通过罗列所有的跟容器的子目录,你可以得到子容器的ID?(怎么得到的算法呢?)


The received() method is called after the DIDL XML content has been validated and parsed, so you can use a type-safe API to work with the metadata. DIDL content is a composite structure of Container and Item elements, here we are interested in the items of the container, ignoring any sub-containers it might or might not have.
//在分析DIDL XML内容之后,确定它是有效地,received()方法将被调用。这样你可以使用一个类型安全的API和元数据一起工作(什么意思?这句没有看懂)。


You can implement or ignore the updateStatus() method, it's convenient to be notified before the metadata is loaded, and after it has been parsed. You can use this event to update a status message/icon of your user interface, for example.
//你可以实施或者忽略updateStatus()方法,当元数据被重新加载,然后它被分析过了,这个是个方便的通知。例如,你可以使用这个事件来更新你的用户状态信息/图标。


This more complex callback instantiation shows some of the available options:
//更复杂的回调实例显示更多的可选项。
#----------------------------------------------code start here-----------------------------------------------------------
ActionCallback complexBrowseAction =
        new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN,
                   "*",
                   100l, 50l,
                   new SortCriterion(true, "dc:title"),        // Ascending
                   new SortCriterion(false, "dc:creator")) {   // Descending


            // Implementation...


        };
#----------------------------------------------code end here-----------------------------------------------------------
The arguments declare filtering with a wildcard, limiting the result to 50 items starting at item 100 (pagination), and some sort criteria. It's up to the content directory provider to handle these options.
//参数声明一个通配符过滤器,限制从100开始的50个内容。和一些规则。这些选项如何处理由目录提供者完成。


3.2. The ContentDirectory service


Let's switch perspective and consider the server-side of a ContentDirectory. Bundled in Cling Support is a simple ContentDirectory abstract service class, the only thing you have to do is implement the browse() method。一个简单的内容抽象服务类绑定在Cling Support类,你需要做的就是实施browse()率。
//让我们切换观点来看看目录服务的服务端状况。


#----------------------------------------------code start here-----------------------------------------------------------
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);
    }
}
#----------------------------------------------code end here-----------------------------------------------------------


You need a DIDLContent instance and a DIDLParser that will transform the content into an XML string when the BrowseResult is returned. It's up to you how you construct the DIDL content, typically you'd have a backend database you'd query and then build the Container and Item graph dynamically. Cling provides many convenience content model classes fore representing multimedia metadata, as defined in the ContentDirectory:1 specification (MusicTrack, Movie, etc.), they can all be found in the package org.fourthline.cling.support.model.
//你需要一个DIDLContent内容实例和DIDLParser,这个东西会把内容转化成XML字符串,这写字符串是方法的返回值BrowseResult。如何构建DIDL内容,典型的,你有一个后端数据库查询,接着构建容器和动态内容图像。Cling提供一些方便的内容模型类前端来代表多媒体元数据,定义在ContentDirectory规范中(音乐,电影等)。这些类在org.fourthline.cling.support.model包里面。


The DIDLParser is not thread-safe, so don't share a single instance between all threads of your server application!
//DIDLParser不是线程安全的,所以不能够在所有服务程序的进程中共享一个简单实例。


The AbstractContentDirectoryService only implements the mandatory actions and state variables as defined in ContentDirectory:1 for browsing and searching content. If you want to enable editing of metadata, you have to add additional action methods.
//AbstractContentDirectoryService实施了一些强制性动作和状态参数,这些定义在ContentDirectory中,用来浏览和查找内容。


Your MediaServer:1 device also has to have a ConnectionManager:1 service.
//你的媒体服务设备也必须有一个ConnectionManager。


3.3. A simple ConnectionManager for HTTP-GET
一个简单的HTTP-GET的连接管理(ConnectionManager)
If your transmission protocol is based on GET requests with HTTP - that is, your media player will download or stream the media file from an HTTP server - all you need to provide with your MediaServer:1 is a very simple ConnectionManager:1.
//如果你的传输协议基于HTTP的GET请求,也就是,你的媒体播放器将从HTTP服务器下载或者stream媒体文件。所有你需要提交给媒体服务器就是一个简单的链接管理。


This connection manager doesn't actually manage any connections, in fact, it doesn't have to provide any functionality at all. This is how you can create and bind this simple service with the Cling Support bundled ConnectionManagerService:
//实际上,这个连接管理其实并没有管理连接,他根本不必提供基本功能。这就是你如何生成和绑定这个简单服务到Cling Support bundled ConnectionManagerService。


#----------------------------------------------code start here-----------------------------------------------------------
LocalService<ConnectionManagerService> service =
        new AnnotationLocalServiceBinder().read(ConnectionManagerService.class);


service.setManager(
        new DefaultServiceManager<ConnectionManagerService>(
                service,
                ConnectionManagerService.class
        )
);
#----------------------------------------------code end here-----------------------------------------------------------


You can now add this service to your MediaServer:1 device and everything will work.
//你可以添加这个服务到你的媒体服务器设备,这样就OK了。


Many media servers however provide at least a list of "source" protocols. This list contains all the (MIME) protocol types your media server might potentially have resources for. A sink (renderer) would obtain this protocol information and decide upfront if any resource from your media server can be played at all, without having to browse the content and looking at each resource's type.
//许多媒体服务器必须提供至少一个内容协议列表。这个协议列表列出了你的服务器可能提供资源的MIME协议类型。接收设备端可以决定这些协议内容能不能被播放。不必必须浏览内容,然后查出资源类型。


First, create a list of protocol information that is supported:


#----------------------------------------------code start here-----------------------------------------------------------
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"
                )
        );
#----------------------------------------------code end here-----------------------------------------------------------
You now have to customize the instantiation of the connection manager service, passing the list of procotols as a constructor argument:
你必须定制一个连接管理服务的实例,把这些协议列表作为构造参数穿进去。
#----------------------------------------------code start here-----------------------------------------------------------
service.setManager(
    new DefaultServiceManager<ConnectionManagerService>(service, null) {
        @Override
        protected ConnectionManagerService createServiceInstance() throws Exception {
            return new ConnectionManagerService(sourceProtocols, null);
        }
    }
);
#----------------------------------------------code end here-----------------------------------------------------------


If your transmission protocol is not HTTP but for example RTSP streaming, your connection manager will have to do more work.
//你的传输协议不是HTTP而是RTSP流,那么这个连接必须做一些工作了(做哪些工作呢??)。
3.4. Managing connections between peers
//点对点连接管理。


You'd probably agree that the ConnectionManager is unnecessary when the media player pulls the media data with a HTTP GET request on the provided URL. Understand that the UPnP MediaServer device provides the URL; if it also serves the file named in the URL, that is outside of the scope of UPnP although a common system architecture.
//你可能赞成,如果媒体播放器使用提供的URL,并使用HTTP GET请求拉去数据,那么连接管理就是没有用的。注意到,UPnP媒体服务设备提供URL;如果你知识提供在URL中提供文件名,这就是UPnP范围之外的了,尽管这是一个通用的系统架构。


Then again, when the source of the media data has to push the data to the player, or prepare the connection with the player beforehand, the ConnectionManager service becomes useful. In this situation two connection managers would first negotiate a connection with the PrepareForConnection action - which side initiates this is up to you. Once the media finished playing, one of the connection managers will then call the ConnectionComplete action. A connection has a unique identifier and some associated protocol information, the connection managers handle the connection as peers.
//再次,当媒体数据源不得不推数据给播放器,或者事先准备播放器连接时,连接管理就变得有用了。在这种情况下,两个连接管理器先使用PrepareForConnection动作来谈判连接,到底是哪一端启动这个动作,由你自己决定。一旦媒体完成播放,一个连接管理器将调用连接结束动作。


Cling Support provides an AbstractPeeringConnectionManagerService that will do all the heavy lifting for you, all you have to do is implement the creation and closing of connections. Although we are still discussing this in the context of a media server, this peer negotiation of a connection naturally also has to be implemented on the media renderer/player side. The following examples are therefore also relevant for the connection manager of a MediaRenderer.
//


First, implement how you want to manage the connection on both ends of the connection (this is just one side):
//


#----------------------------------------------code start here-----------------------------------------------------------
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);
    }
}
#----------------------------------------------code end here-----------------------------------------------------------


In the createConnection() method you have to provide the identifiers of your Rendering Control and A/V Transport logical service, responsible for the created connection. The connection ID has already been stored for you, so all you have to do is return the connection information with these identifiers.


The closeConnection() method is the counterpart, here you would tear down your logical services for this connection, or do whatever cleanup is necessary.


The peerFailure() message is not related to the two previous messages. It is only used by a connection manager that invokes the actions, not on the receiving side.


Let's create a connection between two connection manager peers. First, create the service acting as the source (let's also assume that this is the media server representing the source of the media data):


PeeringConnectionManager peerOne =
    new PeeringConnectionManager(
            new ProtocolInfos("http-get:*:video/mpeg:*,http-get:*:audio/mpeg:*"),
            null
    );
LocalService<PeeringConnectionManager> peerOneService = createService(peerOne);


You can see that it provides media metadata with several protocols. The sink (or media renderer) is the peer connection manager:


PeeringConnectionManager peerTwo =
    new PeeringConnectionManager(
            null,
            new ProtocolInfos("http-get:*:video/mpeg:*")
    );
LocalService<PeeringConnectionManager> peerTwoService = createService(peerTwo);


It plays only one particular protocol.


The createService() method is simply setting the connection manager instance on the service, after reading the service metadata from (already provided) annotations:


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;
}


Now one of the peers has to initiate the connection. It has to create a connection identifier, store this identifier ("managing" the connection), and call the PrepareForConnection service of the other peer. All of this is provided and encapsulated in the createConnectionWithPeer() method:


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();


You have to provide a reference to the local service, a ControlPoint to execute the action, and the protocol information you want to use for this connection. The direction (Input in this case) is how the remote peer should handle the data transmitted on this connection (again, we assume the peer is the data sink). The method returns the identifer of the new connection. You can use this identifier to obtain more information about the connection, for example the identifier of the connection assigned by the other peer, or the logical service identifier for the AV Transport service, also assigned by the remote peer.


When you are done with the connection, close it with the peer:


peerOne.closeConnectionWithPeer(
        controlPoint,
        peerTwoService,
        peerOneConnectionID
);


The peerFailure() method shown earlier will be called when an invocation of createConnectionWithPeer() or closeConnectionWithPeer() fails.




4. Accessing and providing MediaRenderers
//访问和提供MediaRenderer


The purpose of the MediaRenderer:1's services is remote control of a media output device. A device that implements a renderer and therefore has the necessary AVTransport:1 service can be controlled just like with a traditional infrared remote. Think about how awkward it is to control video playback on the Playstation3 with the game controller. The MediaRenderer is like a programmable universal remote API, so you could replace your infrared remote control or Playstation controller with an iPad, Android handset, touchscreen panel, laptop computer, or anything else that speaks UPnP.
//MediaRenderer服务的目的就是远程控制一个媒体输出设备。一个实施了Render和AVTransport服务的设备,可以被像传统红外遥控那样控制。使用游戏控制器控制Playstation3上的视频播放是多么笨拙啊。MediaRenderer就像一个可编程的远程API,所以,你可以使用实施UPnP的iPad,安装耳机,触摸屏,笔记本等等设备来替代红外遥控或者是游戏站的控制器。


(Unfortunately, the Playstation3 does not expose any MediaRenderer services. In fact, most MediaRenderer implementations in the wild, in TVs and set-top boxes, are incomplete or incompatible given a to-the-letter interpretation of the specifications. To make matters worse, instead of simplifying the UPnP A/V specifications, more rules were added in DLNA guidelines, thus making compatiblity even more difficult to achieve. A working and correctly behaving MediaRenderer seems to be an exception, not the norm.)
//
The procedure is simple: First you send the URL of a media resource to the renderer. How you obtained the URL of that resource is entirely up to you, probably browsing a media server's resources metadata. Now you control the state of the renderer, for example, playing, pausing, stopping, recording the video, and so on. You can also control other properties such as volume and brightness of the audio/video content through the standardized RenderingControl:1 service of a media renderer.
//这个程序非常简单:首先,你给这个renderer发送媒体资源的URL。如何提供媒体资源的URL完全由你自己决定,例如,你可以通过浏览媒体服务器的metadata来找到。现在你可以控制renderer的状态,例如播放、暂停、停止和录制功能等等,你还可以控制其他属性例如音量亮度,当然,这一切都是通过media render的标准服务RenderingControl实现的。


Cling Support provides the org.fourthline.cling.support.avtransport.AbstractAVTransportService class, an abstract type with all the UPnP actions and state variable mappings already in place. To implement a MediaRenderer you'd have to create a subclass and implement all methods. You should consider this strategy if you already have an existing media player, and you want to provide a UPnP remote control interface.
Cling Support提供类org.fourthline.cling.support.avtransport.AbstractAVTransportService,这个类已经实现uPnP动作和状态参数影射的抽象类型。为了实现MediaRenderer,你不得不生成子类并且实现所有方法。如果你已经有一个播放器,而你想提供这个播放器的远程控制,你应该考虑如何实施这个策略。


Alternatively, if you are writing a new media player, Cling can even provide the state management and transitions for you, so all you have to implement is the actual output of media data.
当然,如果你想写一个新的媒体播放器,Cling甚至给你提供了状态管理和迁移的状态机。这样,你的所有工作就是提供实际的媒体数据输出。
4.1. Creating a renderer from scratch
//从草图生成一个renderer


Cling Support provides a state machine for managing the current state of your playback engine. This feature simplifies writing a media player with a UPnP renderer control interface. There are several steps involved
//Cling Support提供了一个对于你当前播放机的状态管理的状态机。这个特征简化了你实现媒体播放机的控制接口。下面几步可以实现。
4.1.1. Defining the states of the player
//定义播放器的状态机
First, define your state machine and what states are supported by your player:
//首先定义你的状态机,和你的播放器支持那些状态
#----------------------------------------code start here------------------------------------------------------------------
package example.mediarenderer;


import org.fourthline.cling.support.avtransport.impl.AVTransportStateMachine;
import org.seamless.statemachine.States;


@States({
        MyRendererNoMediaPresent.class,
        MyRendererStopped.class,
        MyRendererPlaying.class
})
interface MyRendererStateMachine extends AVTransportStateMachine {}
#----------------------------------------code end here------------------------------------------------------------------


This is a very simple player with only three states: The initial state when no media is present, and the Playing and Stopped states. You can also support additional states, such as Paused and Recording but we want to keep this example as simple as possible. (Also compare the "Theory of Operation" chapter and state chart in the AVTransport:1 specification document, section 2.5.)
//这是一个只有三个状态的简单播放器:
没有任何媒体的初始状态、播放状态和停止状态。你也可以增加一些附加状态,例如暂停或者录制,此处,我们只想让例子最简单。


Next, implement the states and the actions that trigger a transition from one state to the other.
//接着,实现状态和动作,来触发从一个状态到另一个状态的跃迁。
The initial state has only one possible transition and an action that triggers this transition:
初始状态可以仅仅只有一个可以转移,也只有一个动作触发这个转移。


#----------------------------------------code start here------------------------------------------------------------------
public class MyRendererNoMediaPresent extends NoMediaPresent {


    public MyRendererNoMediaPresent(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().getInstanceId(),
                new AVTransportVariable.AVTransportURI(uri),
                new AVTransportVariable.CurrentTrackURI(uri)
        );
        
        return MyRendererStopped.class;
    }
}
#----------------------------------------code end here------------------------------------------------------------------


When a client sets a new URI for playback, you have to prepare your renderer accordingly. You typically want to change the MediaInfo of your AVTransport to reflect the new "current" track, and you might want to expose information about the track, such as the playback duration. How you do this (e.g. you could actually already retrieve the file behind the URL and analyze it) is up to you.
//当一个客户端提供一个播放新的URI,你必须相应的提供你的renderer。你典型的像改变你的AVTransport的媒体信息来反应你的新“当前”音轨,你还可能像暴露这个音轨信息,例如播放时长。如何实现,你你来完成的,例如,你从URL取得文件,然后分析它。
The LastChange object is how you notify control points about any changes of state, here we tell the control points that there is a new "AVTransportURI" as well as a new "CurrentTrackURI". You can add more variables and their values to the LastChange, depending on what actually changed - note that you should do this within a single call of setEventedValue(...) if you consider several changes to be atomic. (The LastChange will be polled and send to control points periodically in the background, more about this later.)
//对象LastChange是你用来通知控制点状态编号。这里,我们告诉控制点,一个新的AVTransportURI和新的CurrentTrackURI。你可以添加更多参数和他们的对应值到LastChange,这取决于哪些值实际改变了。注意,如果你认为这几个改变是原子的,这必须在一个单一的调用setEventedValue(...)(LastChange将被轮询,然后周期性的发送事件给控制点)
The AVTransport will transition to the Stopped state after the URI has been set.
//?????设置URI之后,AVTransport将转变到停止状态。
The Stopped state has many possible transitions, from here a control point can decide to play, seek, skip to the next track, and so on. The following example is really not doing much, how you implement these triggers and state transitions is completely dependend on the design of your playback engine - this is only the scaffolding:
停止状态有更多的可能传输,在这里,控制点将决定是否播放,拖放或者跳到下一首。下面的例子实际没有做更多,怎么实施那些状态触发和状态迁移决定于你如何设计你的播放器----这仅仅是一个骨架而已。
#----------------------------------------code start here------------------------------------------------------------------
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;
    }
}
#----------------------------------------code end here------------------------------------------------------------------


Each state can have two magic methods: onEntry() and onExit() - they do exactly what the name says. Don't forget to call the superclass' method if you decide to use them!
//每一个状态有两个魔幻方法:onEntry()和onExit()-----他们就像他们名字说的那样做事情。当你使用他们的时候,不要忘记调用他们的超类相应方法。
Usually you'd start playback when the onEntry() method of the Playing state is called:
//通常,播放状态的onEntry()被调用的时候,实际播放就开始了。
#----------------------------------------code start here------------------------------------------------------------------
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;
    }
#----------------------------------------code end here------------------------------------------------------------------


So far there wasn't much UPnP involved in writing your player - Cling just provided a state machine for you and a way to signal state changes to clients through the LastEvent interface.
一定程度上,写一个播放器没有更多的UPnP方法被调用----Cling仅仅提供一个状态机和一个使用LastEvent来通知客户端状态改变的方法。


4.1.2. Registering the AVTransportService
//注册AVTransportService
Your next step is wiring the state machine into the UPnP service, so you can add the service to a device and finally the Cling registry. First, bind the service and define how the service manager will obtain an instance of your player:
//你的下一步工作就是在UPnP服务中写一个状态机,这样,你可以把这个服务添加到你的设备,最后注册到Cling。
//首先,绑定这个服务,还要定义服务管理怎么获得播放器的实例。
#----------------------------------------code start here------------------------------------------------------------------
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
                );
            }
        }
);
#----------------------------------------code end here------------------------------------------------------------------
The constructor takes two classes, one is your state machine definition, the other the initial state of the machine after it has been created.
//构造器有两个类,一个是你的状态机定义,另一个是状态机初始化之后的状态。
That's it - you are ready to add this service to a MediaRenderer:1 device and control points will see it and be able to call actions.
//这就是,你必须给MediaRenderer设备添加服务,和控制点,而且能够调用他的动作。


However, there is one more detail you have to consider: Propagation of LastChange events. Whenever any player state or transition adds a "change" to LastChange, this data will be accumulated. It will not be send to GENA subscribers immediately or automatically! It's up to you how and when you want to flush all accumulated changes to control points. A common approach would be a background thread that executes this operation every second (or even more frequently):
//然而,你必须考虑更多细节:传播LastChange事件。合适添加改变到LastChange到你的播放状态或者改变是被累加的。它不会立即和自动发送给GENA订阅者。由你来决定怎样和合适想flush所有的累加改变到控制点。一个后台的方法线程将每秒钟执行这个操作(或者更频繁)。


#----------------------------------------code start here------------------------------------------------------------------
LastChangeAwareServiceManager manager = (LastChangeAwareServiceManager)service.getManager();
manager.fireLastChange();
#----------------------------------------code end here------------------------------------------------------------------


Finally, note that the AVTransport:1 specification also defines "logical" player instances. For examle, a renderer that can play two URIs simultaneously would have two AVTransport instances, each with its own identifier. The reserved identifier "0" is the default for a renderer that only supports playback of a single URI at a time. In Cling, each logical AVTransport instance is represented by one instance of a state machine (with all its states) associated with one instance of the AVTransport type. All of these objects are never shared, and they are not thread-safe. Read the documentation and code of the AVTransportService class for more information on this feature - by default it supports only a single transport instance with ID "0", you have to override the findInstance() methods to create and support several parallel playback instances.
//最后注意到AVTransport规范也定义了一个逻辑播放器实例。例如,一个renderer可以同时播放两个URI同时的,这个renderer将有两个AVTransport实例,每一个实例有它自己的ID。ID 0保留给缺省的renderer只支持一个单一URI。在cling中,每一个逻辑AVTransport实例被一个相关的AVTransport类型的状态机实例代表。所有的对象都是不可共享的,而且不是线程安全的。关于这个特性的更多信息,请阅读AVTransportService的文档。缺省的,它仅仅支持一个简单的ID 0传输实例。如果你想要更多的平行播放实例,请重载findInstance()方法。
4.2. Controlling a renderer
//控制renderer


Cling Support provides several action callbacks that simplify creating a control point for the AVTransport service. This is the client side of your player, the remote control.
//Cling Support提供几个动作回调,这些回调简单生成AVTransport服务的控制点。这是你的播放器客户端,远程控制端。
This is how you set an URI for playback:
//以下是你设置播放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
            }
        };


This is how you actually start playback:
//下面是你如何启动一个播放
ActionCallback playAction =
        new Play(service) {
            @Override
            public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                // Something was wrong
            }
        };


Explore the package org.fourthline.cling.support.avtransport.callback for more options.
//参考org.fourthline.cling.support.avtransport.callback文档获取更多参数选项。


Your control point can also subscribe with the service and listen for LastChange events. Cling provides a parser so you get the same types and classes on the control point as are available on the server - it's the same for sending and receiving the event data. When you receive the "last change" string in your SubscriptionCallback you can transform it, for example, this event could have been sent by the service after the player transitioned from NoMediaPresent to Stopped state:
//你的控制点可以订约更多的服务来侦听更多的LastChange事件。Cling提供一个分析器,你在控制点可以获得和服务端相同类型和类,发送和接收事件数据方法是相同的。当你的SubscriptionCallback收到"last change"字符串的时候,你可以转化它,例如,在播放器从Stopped状态向NoMediaPresent状态转化之后,这个事件可能被你的服务发送出来。


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
);


This manual has been created with Lemma from tested source code and Javadoc. Try it, you will like it.

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐