当前位置: 首页 > 工具软件 > Cling > 使用案例 >

Cling基础教程 - 用户手册(入门)

邢思淼
2023-12-01

Cling使用教程 - 用户手册

版本:1.0
原文链接:http://4thline.org/projects/cling/support/manual/cling-support-manual.xhtml

1. 使用网关设备

网关设备可以将本地局域网连接到广域网上去,并且通过Upnp服务(Universal Plug-n-Play:即插即用服务)来监视和配置局域网和广域网的接口。通常情况下,你可以用这种设备来进行本地端口的映射,比如说:一个本地局域网应用想要获取广域网上主机的连接,那么他必须在本地路由器上创建一个端口用于转发和映射。

1.1 配置本地端口

Cling包含了所有需要用到的功能,通过Cling在本地网络上的路由来映射端口,只需要三行代码即可:

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

第一行代码配置了一个端口映射,包括内外端口号,内部IP,使用的协议以及功能描述。
第二行代码启动了Upnp服务,并传入一个PortMappingListener。一旦设备被任何其他发现,PortMappingListener将会把端口映射到这些设备上去。
然后你可以立即调用ControlPoint#search方法,这将触发你所在网络上的所有本地路由的响应和搜索,从而激活端口映射。

在应用退出时,你可以通过调用UpnpService#shutdown()来关闭Upnp堆栈,PortMappingListener将删除端口映射。如果你忘记关闭Upnp堆栈,那么这个端口映射将继续保留在网关设备上(默认的时间为0)。

如果程序在运行过程中出错,程序将会输出一些有关org.fourthline.cling.support.igd.PortMappingListener的警告日志。当然你也可以通过重写 PortMappingListener#handleFailureMessage(String)方法来处理这些错误。

另外,你也随时可以用如下回调来手动在已经被发现的设备上添加和删除端口映射:

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

assertEquals(mapping[0].getInternalClient(), "192.168.0.123");
assertEquals(mapping[0].getInternalPort().getValue().longValue(), 8123);
assertEquals(mapping[0].isEnabled());

// 执行删除
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 获取连接信息

通过以下回调,可以从广域网的连接服务中检索出当前连接信息,包括状态、正常运行时间和最后一条错误消息:

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

此外,你还可以通过一个回调函数来获取设备的外部连接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. 发送信息给三星电视

许多可联网的三星电视都实现了 samsung. com:MessageBoxService 的功能。这个功能的初始目标可能是当你在家并且你的手机连接上了你房间内的无线网络时,可以让三星手机自动的把通知和提醒发送到电视上进行显示(前提你的电视是开着的并且也连接到了这个无线网络)。

Cling也提供了类似的类可以让你通过Upnp向三星电视发送通知。

你有几种可以使用的消息类型。第一种就是带有发送者/接收者名称,电话号码以及时间戳和文本信息:

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

这条消息将以“收到新短信”的形式出现在你的电视上,并带有显示所有消息细节的选项。另外,三星电视识可别的其他消息类型还包括来电通知和日历日程提醒:

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

以下是你如何通过异步的方式来发送信息:

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

需要注意的是,电视上可能包含一个移除消息的操作描述。Cling也提供了RemoveMessageCallback来移除消息,但是这个与三星电视的实现有所差别,这个动作是通过远程控制来直接在电视上删除该消息。

3.访问和提供媒体服务

标准的Upnp音视频媒体服务终端模板记录了一些最流行的Upnp服务,尽管是命名为媒体服务,但是实际上这些服务并不提供和访问媒体数据,比如音乐,图片亦或是视频文件。这些服务是通过分享元数据,这些元数据包含媒体文件的相关信息,比如它们的名称、格式和大小,以及可以用来获取实际文件的定位器。传输这些媒体文件已经超出这些媒体服务的范畴,通常情况下会使用简单的HTTP服务器和客户端来实现这个传输任务。

一个媒体服务设备至少包括文件目录(ContentDirectory)和连接管理的服务(ConnectionManager)。

3.1 浏览文件目录

文件目录服务提供媒体资源的元数据。这些元数据的格式是XML,内容由DIDL、Dublic Core和UPnP特定元素和属性组合而成。通常情况下,可以通过调用目录文件服务的Browse方法来获取这个XML格式的元数据,然后手动解析它。

如下是Cling所提供的Browse方法回调处理:

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

第一个回调(received)检索出了所有包含3(容器标识符)的子元素;

在验证和解析DIDL XML内容之后会调用received()方法,因此可以使用类型安全的接口来处理这些元数据。DIDL的内容是由Container和Item构成的,但是此处只关心根目录容器的子元素,而非子目录的。

你可以实现也可以忽略掉updateStatus()方法,这个方法可以很方便的在元数据加载和解析的前后给你提供通知。例如你可以通过此方法来更新你的消息图标的状态。

如下示例向你展示了一个可提供更多操作的复杂回调示例:

ActionCallback complexBrowseAction =
        new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN,
                   "*",
                   100l, 50l,
                   new SortCriterion(true, "dc:title"),        // Ascending
                   new SortCriterion(false, "dc:creator")) {   // Descending

            // Implementation...

        };

你可以通过声明一些通配符参数,将结果限制为50个(从100个开始)分页,以及一些排序条件。由目录文件服务来处理这些操作。

3.2 目录文件服务

换个角度,你可以先开始从目录文件的服务端角度来思考。Cling提供了一个简单的目录文件抽象类,你要做的只需要实现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);
    }
}

在这里可以看到新建了一个DIDLContent实例将结果存储起来,在用DIDLParser将其转换成XML字符串,最后用BrowseReuslt返回数据时。如何去构建DIDL的内容需要你自己来决定,通常情况下,需要动态的去通过后端数据库来查询,然后将结果封装到COntainer和Item中去。Cling提供了去多便利的内容模型类来表示多媒体的元数据,正如内容目录中定义的那样(MusicTrack,Movie等),你可以早org.fourthline.cling.support.model包中找到。

DIDLParser不是线程安全的,所以不要在服务端应用程序的多个线程中使用一个单例。

AbstractContentDirectoryService只实现了COntentDirectory中文件浏览和搜索的必须的动作和声明的变量。如果想要去编辑这些元数据,那就需要另外增加方法了。

媒体服务设备同样需要有个连接管理服务。

3.3 HTTP-GET的简单连接管理

如果你的传输协议是基于HTTP的GET请求,也就是说你的媒体播放器将从HTTP服务器上下载文件或者获取文件流,那么你所要为这个媒体服务提供的将是一个非常简单的连接管理。

这个连接管理实际上并不管理任何连接,甚至它根本都不提供任何功能。如下就是你通过Cling提供的ConnectManagerService来如何创建和绑定这个简单的服务。

LocalService<ConnectionManagerService> service =
        new AnnotationLocalServiceBinder().read(ConnectionManagerService.class);

service.setManager(
        new DefaultServiceManager<>(
                service,
                ConnectionManagerService.class
        )
);

现在可以将这个服务添加到你的媒体服务设备上去,并且它将开始正常工作。

事实上,许多媒体服务器至少提供了一个“数据源”协议列表。这个列表包含了媒体服务器可能具有的所有(MIME)协议类型。接收器(显示器)将会通过这个协议信息来决定是否可以播放来自媒体服务器的资源文件,而不是去浏览并查看每一个资源的类型。

首先,创建一个服务器支持的协议信息的列表:

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

现在你需要自定义连接管理服务,在实例化时将协议列表作为参数进行传递:

service.setManager(
    new DefaultServiceManager<ConnectionManagerService>(service, null) {
        @Override
        protected ConnectionManagerService createServiceInstance() throws Exception {
            return new ConnectionManagerService(sourceProtocols, null);
        }
    }
);

如果你传输协议不是HTTP而是其他的,比如RTSP流,那么这个连接管理将不会起任何作用。

3.4 管理对等点的连接

你可能认为既然媒体播放器上通过URL使用HTTP-GET方式去拉取媒体数据,那么连接管理就不是必须的了。但是你需要明白的是Upnp媒体服务器已经提供了URL,如果还需要他提供URL对应的文件,那么显然这已经超出了常见Upnp的系统架构范围了。

再者,当媒体数据源是要将数据推送到播放器或者需要事先为播放器准备连接,那么此时连接管理服务就变得有用了。在这种情况下,两方连接管理需要事先通过PrepareForConnection操作来协商连接 - 具体哪方发起连接由你决定。当媒体结束播放时,一端的连接管理将调用ConnectionComplete操作。每一个连接都具有唯一的标志符以及相关的连接协议信息,对应的连接管理会将该连接作为对等点连接进行处理。

Cling提供了对等点连接服务AbstractPeeringConnectionManagerService,它将帮助你完成所有繁重的任务,你只需要实现创建和关闭连接的操作。尽管我们现在仍在讨论媒体服务器相关的内容,但是对等点的连接协商是需要在媒体渲染/播放端进行实现的。因此如下的例子相关的就是一个对媒体渲染器的连接管理。

首先,实现你想要如何管理连接两端的连接(这只是一边):

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()方法中,你需要为负责创建连接的服务提供显示控制和音视频传输的标识符。这个连接ID已经为你定义好了,所以你需要做的就是返回带有这些信息的这个连接。

closeConnection()方法是与createConnection对应的方法,此方法你可以实现在关闭连接服务的相关逻辑,如清理无用信息。

peerFailure()方法与前面的两条方法无关。它只由调用操作的连接管理器使用,而不是在接收端使用。

下面让我们在两个连接管理器之间创建一个对等点连接。首先,创建作为数据源的服务(我们假设这是表示媒体数据源的媒体服务器):

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

可以看到它提供了几个协议的媒体元数据。接收器(或媒体渲染器)是对等连接管理器:

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

它只执行一种特定的协议。

createService()方法只是在从(已经提供的)注释中读取服务元数据后,在服务上设置连接管理器实例:

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

现在必须有一个对等点发起连接。它需要创建一个连接标识符,存储这个标识符(“管理”连接),并调用另一个对等点的PrepareForConnection服务。所有这些都被提供并封装在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();

你需要提供一个对本地服务的引用,一个执行操作的控制点以及用于此连接的协议信息。连接方向(此处我们是输入)是远程对等点应该如何处理这个连接中的数据传输(另外,我们假设这个对等点是数据接收端)。这个方法可以返回新连接的标识符。你可以通过这个标识符来获取连接的一些信息,比如另一个对等点的标识符,AV传输服务标识符。

当你完成连接任务,你可以通过这个方法进行关闭:

peerOne.closeConnectionWithPeer(
        controlPoint,
        peerTwoService,
        peerOneConnectionID
);

peerFailure方法将会在调用createConnectionWithPeer()或closeConnectionWithPeer()失败时调用。

4. 访问和提供媒介提供者

MediaRenderer服务的目的是控制远程媒体输出设备。一种实现渲染器的设备,因此具有必要的AVTransport服务,可以像传统红外遥控器一样进行控制。想想用游戏控制器控制Playstation3上的视频回放有多尴尬吧。MediaRenderer就像一个可编程的通用远程API,所以你可以用iPad、Android手机、触摸屏、笔记本电脑或任何其他可以使用Upnp的设备来代替红外线遥控器或Playstation控制器。

(不幸的是,Playstation3没有公开任何MediaRenderer服务。事实上,在电视和机顶盒中,大多数的MediaRenderer实现都是不完整的,或者不兼容的,这是对规范的严格解释。更糟糕的是,没有简化UPnP A/V规范,反而在DLNA指南中添加了更多的规则,从而使得兼容性更加难以实现。一个工作和行为正确的媒体人似乎是个例外,而不是常态。)

这个过程很简单:首先将媒体资源的URL发送给渲染程序。如何获得该资源的URL完全取决于你,可能需要浏览媒体服务器的资源元数据。现在控制渲染器的状态,例如播放、暂停、停止、录制视频等等。你还可以通过媒体渲染器的标准化渲染控制服务控制音频/视频内容的音量和亮度等其他属性。

Cling提供了org.fourthline.clate.Support.avtransport.AbstractAVTransportService类,一个抽象类型,包含所有UPnP操作和状态变量映射。要实现MediaRenderer,你必须创建一个子类并实现所有方法。如果你已经有一个媒体播放器,并且你想要提供一个Upnp的远程控制接口,那么你应该考虑这个策略。

另外,如果你正在编写一个新媒体播放器,Cling甚至可以为你提供状态管理和转换,因此你所要实现的就是媒体数据的实际输出。

4.1 从零创建渲染器

Cling提供了一个可以使你管理当前播放状态的状态机引擎。该特性简化了使用Upnp渲染器编写媒体播放器的过程,包括如下几个步骤:

4.1.1 定义播放状态

首先,定义你定义状态机以及你的播放器可支持的几种状态:

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

这是一个非常简单的播放器,只有三种状态:没有媒体时的初始状态,以及播放和停止状态。你还可以支持其他状态,比如暂停和记录,但是我们希望这个示例尽可能简单。(同时比较AVTransport:1规范文件第2.5节中的“操作理论”章节和状态图。)

接下来,实现状态和触发从一个状态到另一个状态转换的操作。

初始状态只有一个可能的转换和一个触发该转换的动作:

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

当客户端为回放设置一个新的URI时,你必须相应地准备你的渲染程序。你通常希望更改AVTransport的MediaInfo以反映新的“当前”跟踪,并且你可能希望公开关于跟踪的信息,比如回放时间。如何做到这一点(例如,你实际上已经可以检索URL后面的文件并分析它)取决于你。

LastChange对象是如何通知控制点状态的任何变化,这里我们告诉控制点有一个新的“AVTransportURI”和一个新的“CurrentTrackURI”。你可以向LastChange添加更多的变量和它们的值,这取决于实际更改的内容——注意,如果你认为几个更改是原子性的,那么你应该在setEventedValue(…)的单个调用中执行此操作。(最后的更改将被轮询并定期发送到后台的控制点,稍后会详细介绍。)

设置URI之后,AVTransport将转换到停止状态。

停止状态有许多可能的转换,从这里一个控制点可以决定播放、查找、跳过到下一个轨道,等等。下面的例子真的没有做多少,你如何实现这些触发器和状态转换完全取决于你的播放引擎的设计-这只是脚手架:

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

每个状态都可以有两个神奇的方法:onEntry()和onExit()——它们完全按照名称执行。如果你决定使用超类的方法,不要忘记调用它们!

通常,当调用播放状态的onEntry()方法时,你将开始回放:

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只是为你提供了一个状态机,并通过LastEvent接口向客户端发送状态更改的信号。

4.1.2 注册AVTransportService

下一步是将状态机连接到UPnP服务中,这样就可以将该服务添加到设备中,最后添加到粘附注册表。首先,绑定服务并定义服务管理器如何获取玩家实例:

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

构造函数有两个类,一个是状态机定义,另一个是创建后的初始状态。

就是这样——你已经准备好将此服务添加到MediaRenderer设备和控制点将看到它并能够调用操作。

但是,还有一个细节需要考虑:LastChange事件的传播。当任何播放状态或转换向LastChange添加“更改”时,这些数据将被累积。它不会立即或自动发送到GENA订户!如何以及何时将所有累积的更改刷新到控制点由你决定。一种常见的方法是后台线程每秒钟(甚至更频繁地)执行这个操作:

LastChangeAwareServiceManager manager = (LastChangeAwareServiceManager)service.getManager();
manager.fireLastChange();

最后,请注意AVTransport规范还定义了“逻辑”播放器实例。例如,可以同时播放两个uri的呈现程序将有两个AVTransport实例,每个实例都有自己的标识符。保留的标识符“0”是一个呈现器的默认值,该呈现器一次只支持一个URI的回放。在attach中,每个逻辑AVTransport实例由与AVTransport类型的一个实例关联的状态机的一个实例(及其所有状态)表示。所有这些对象都不会共享,而且它们也不是线程安全的。有关此特性的更多信息,请阅读AVTransportService类的文档和代码——默认情况下,它只支持ID为“0”的单个传输实例,你必须重写findInstance()方法来创建和支持多个并行回放实例。

4.2 控制渲染器

Cling支持提供了几个操作回调,简化了为AVTransport服务创建控制点的过程。这是你的播放器的客户端,遥控器。

这是你如何设置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
            }
        };

你的控制点还可以订阅服务并侦听LastChange事件。Cling提供了一个解析器,因此你可以在控制点上获得与服务器上相同的类型和类——这与发送和接收事件数据是一样的。当你在SubscriptionCallback中接收到“last change”字符串时,你可以对其进行转换,例如,当玩家从nomediap状态转换到stop状态时,服务可能已经发送了这个事件:

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
);
 类似资料: