View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  package example.mediaserver;
16  
17  import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder;
18  import org.fourthline.cling.controlpoint.ControlPoint;
19  import org.fourthline.cling.model.DefaultServiceManager;
20  import org.fourthline.cling.model.ServiceReference;
21  import org.fourthline.cling.model.ValidationException;
22  import org.fourthline.cling.model.action.ActionException;
23  import org.fourthline.cling.model.action.ActionInvocation;
24  import org.fourthline.cling.model.message.UpnpResponse;
25  import org.fourthline.cling.model.meta.DeviceDetails;
26  import org.fourthline.cling.model.meta.DeviceIdentity;
27  import org.fourthline.cling.model.meta.LocalDevice;
28  import org.fourthline.cling.model.meta.LocalService;
29  import org.fourthline.cling.model.types.UDADeviceType;
30  import org.fourthline.cling.model.types.UDN;
31  import org.fourthline.cling.support.connectionmanager.AbstractPeeringConnectionManagerService;
32  import org.fourthline.cling.support.model.ConnectionInfo;
33  import org.fourthline.cling.support.model.ProtocolInfo;
34  import org.fourthline.cling.support.model.ProtocolInfos;
35  import org.testng.annotations.Test;
36  
37  import java.beans.PropertyChangeEvent;
38  import java.beans.PropertyChangeListener;
39  
40  import static org.testng.Assert.assertEquals;
41  
42  
43  /**
44   * Managing connections between peers
45   * <p>
46   * You'd probably agree that the <em>ConnectionManager</em> is unnecessary when the
47   * media player <strong>pulls</strong> the media data with a HTTP GET request on the provided URL.
48   * Understand that the UPnP <em>MediaServer</em> device provides the URL; if it also serves the
49   * file named in the URL, that is outside of the scope of UPnP although a common system architecture.
50   * </p>
51   * <p>
52   * Then again, when the source of the media data has to <strong>push</strong> the data to
53   * the player, or prepare the connection with the player beforehand, the <em>ConnectionManager</em>
54   * service becomes useful. In this situation two connection managers would first negotiate a
55   * connection with the <code>PrepareForConnection</code> action - which side initiates this is
56   * up to you. Once the media finished playing, one of the connection managers will then
57   * call the <code>ConnectionComplete</code> action. A connection has a unique identifier and
58   * some associated protocol information, the connection managers handle the connection as peers.
59   * </p>
60   * <p>
61   * Cling Support provides an <code>AbstractPeeringConnectionManagerService</code> that will do
62   * all the heavy lifting for you, all you have to do is implement the creation and closing of
63   * connections. Although we are still discussing this in the context of a media server, this
64   * peer negotiation of a connection naturally also has to be implemented on the media renderer/player
65   * side. The following examples are therefore also relevant for the connection manager of
66   * a <em>MediaRenderer</em>.
67   * </p>
68   * <p>
69   * First, implement how you want to manage the connection on both ends of the connection
70   * (this is just one side):
71   * </p>
72   * <a class="citation" href="javadoc://example.mediaserver.ConnectionManagerPeerTest.PeeringConnectionManager" style="read-title: false;"/>
73   * <p>
74   * Let's create a connection between two connection manager peers. First, create the service
75   * acting as the source (let's also assume that this is the media server representing the source
76   * of the media data):
77   * </p>
78   * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC1;"/>
79   * <p>
80   * You can see that it provides media metadata with several protocols. The sink (or
81   * media renderer) is the peer connection manager:
82   * </p>
83   * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC2;" id="conmgr_sink"/>
84   * <p>
85   * It plays only one particular protocol.
86   * </p>
87   * <p>
88   * The <code>createService()</code> method is simply setting the connection manager
89   * instance on the service, after reading the service metadata from (already provided) annotations:
90   * </p>
91   * <a class="citation" href="javacode://this#createService(example.mediaserver.ConnectionManagerPeerTest.PeeringConnectionManager)"/>
92   * <p>
93   * Now one of the peers has to initiate the connection. It has to create a connection identifier, store this
94   * identifier ("managing" the connection), and call the <code>PrepareForConnection</code> service of the
95   * other peer. All of this is provided and encapsulated in the <code>createConnectionWithPeer()</code>
96   * method:
97   * </p>
98   * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC3;" id="conmgr_prepare"/>
99   * <p>
100  * You have to provide a reference to the local service, a <code>ControlPoint</code>
101  * to execute the action, and the protocol information you want to use for this connection. The
102  * direction (<code>Input</code> in this case) is how the remote peer should handle the data
103  * transmitted on this connection (again, we assume the peer is the data sink). The method returns
104  * the identifer of the new connection. You can use this identifier to obtain
105  * more information about the connection, for example the identifier of the connection assigned by
106  * the other peer, or the logical service identifier for the AV Transport service, also assigned
107  * by the remote peer.
108  * </p>
109  * <p>
110  * When you are done with the connection, close it with the peer:
111  * </p>
112  * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC4;" id="conmgr_close"/>
113  * <p>
114  * The <code>peerFailure()</code> method shown earlier will be called when
115  * an invocation of <code>createConnectionWithPeer()</code> or
116  * <code>closeConnectionWithPeer()</code> fails.
117  * </p>
118  */
119 public class ConnectionManagerPeerTest {
120 
121     @Test
122     public void createDestroyConnections() throws Exception {
123 
124         // Ignore this
125         ControlPoint controlPoint = null;
126 
127         CountingListener listener = new CountingListener();
128 
129         PeeringConnectionManager peerOne =                                                      // DOC: INC1
130             new PeeringConnectionManager(
131                     new ProtocolInfos("http-get:*:video/mpeg:*,http-get:*:audio/mpeg:*"),
132                     null
133             );
134         LocalService<PeeringConnectionManager> peerOneService = createService(peerOne);        // DOC: INC1
135 
136         peerOne.getPropertyChangeSupport().addPropertyChangeListener(listener);
137         createDevice("MEDIASERVER-AAA-AAA-AAA", "MediaServer", peerOneService);
138 
139         PeeringConnectionManager peerTwo =                                                     // DOC: INC2
140             new PeeringConnectionManager(
141                     null,
142                     new ProtocolInfos("http-get:*:video/mpeg:*")
143             );
144         LocalService<PeeringConnectionManager> peerTwoService = createService(peerTwo);     // DOC: INC2
145 
146         peerTwo.getPropertyChangeSupport().addPropertyChangeListener(listener);
147         createDevice("MEDIARENDERER-BBB-BBB-BBB", "MediaRenderer", peerTwoService);
148 
149         int peerOneConnectionID = peerOne.createConnectionWithPeer(                                  // DOC: INC3
150             peerOneService.getReference(),
151             controlPoint,
152             peerTwoService,
153             new ProtocolInfo("http-get:*:video/mpeg:*"),
154             ConnectionInfo.Direction.Input
155         );
156 
157         if (peerOneConnectionID == -1) {
158             // Connection establishment failed, the peerFailure()
159             // method has been called already. It's up to you
160             // how you'd like to continue at this point.
161         }
162         
163         int peerTwoConnectionID =
164                 peerOne.getCurrentConnectionInfo(peerOneConnectionID) .getPeerConnectionID();
165 
166         int peerTwoAVTransportID =
167                 peerOne.getCurrentConnectionInfo(peerOneConnectionID).getAvTransportID();           // DOC: INC3
168 
169         assertEquals(peerOne.getCurrentConnectionIDs().size(), 1);
170         assertEquals(peerTwo.getCurrentConnectionIDs().size(), 1);
171 
172         assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getDirection(), ConnectionInfo.Direction.Output);
173         assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getDirection(), ConnectionInfo.Direction.Input);
174 
175         assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getRcsID(), 111);
176         assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getRcsID(), 111);
177         assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getAvTransportID(), 333);
178         assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getAvTransportID(), 333);
179 
180         assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getConnectionStatus(), ConnectionInfo.Status.OK);
181         assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getConnectionStatus(), ConnectionInfo.Status.OK);
182 
183         // Another connection
184         int anotherID = peerOne.createConnectionWithPeer(
185             peerOneService.getReference(),
186             controlPoint,
187             peerTwoService,
188             new ProtocolInfo("http-get:*:video/mpeg:*"),
189             ConnectionInfo.Direction.Input
190         );
191         int anotherPeerID = peerOne.getCurrentConnectionInfo(anotherID).getPeerConnectionID();
192 
193         assertEquals(peerOne.getCurrentConnectionIDs().size(), 2);
194         assertEquals(peerTwo.getCurrentConnectionIDs().size(), 2);
195 
196         assertEquals(peerOne.getCurrentConnectionInfo(anotherID).getRcsID(), 222);
197         assertEquals(peerTwo.getCurrentConnectionInfo(anotherPeerID).getRcsID(), 222);
198         assertEquals(peerOne.getCurrentConnectionInfo(anotherID).getAvTransportID(), 444);
199         assertEquals(peerTwo.getCurrentConnectionInfo(anotherPeerID).getAvTransportID(), 444);
200 
201         // Close one
202         peerOne.closeConnectionWithPeer(                                            // DOC: INC4
203                 controlPoint,
204                 peerTwoService,
205                 peerOneConnectionID
206         );                                                                          // DOC: INC4
207 
208         assertEquals(peerOne.getCurrentConnectionIDs().size(), 1);
209         assertEquals(peerTwo.getCurrentConnectionIDs().size(), 1);
210 
211         // The other is still there
212         assertEquals(peerOne.getCurrentConnectionInfo(anotherID).getPeerConnectionID(), 1);
213 
214         // Should have 2 + 2 + 2 (connect, connect, disconnect on both connectionmanagers) events
215         assertEquals(listener.count, 6);
216     }
217 
218     public LocalDevice createDevice(String udn, String type, LocalService service) throws ValidationException {
219         return new LocalDevice(
220                 new DeviceIdentity(new UDN(udn)),
221                 new UDADeviceType(type),
222                 new DeviceDetails(type),
223                 service
224         );
225     }
226 
227     public LocalService<PeeringConnectionManager> createService(final PeeringConnectionManager peer) {
228 
229         LocalService<PeeringConnectionManager> service =
230                 new AnnotationLocalServiceBinder().read(
231                         AbstractPeeringConnectionManagerService.class
232                 );
233 
234         service.setManager(
235                 new DefaultServiceManager<PeeringConnectionManager>(service, null) {
236                     @Override
237                     protected PeeringConnectionManager createServiceInstance() throws Exception {
238                         return peer;
239                     }
240                 }
241         );
242         return service;
243     }
244 
245     /**
246      * <a class="citation" href="javacode://this" style="exclude: EXC1"/>
247      * <p>
248      * In the <code>createConnection()</code> method you have to provide the identifiers of your
249      * Rendering Control and A/V Transport logical service, responsible for the created connection.
250      * The connection ID has already been stored for you, so all you have to do is return the
251      * connection information with these identifiers.
252      * </p>
253      * <p>
254      * The <code>closeConnection()</code> method is the counterpart, here you would tear down
255      * your logical services for this connection, or do whatever cleanup is necessary.
256      * </p>
257      * <p>
258      * The <code>peerFailure()</code> message is not related to the two previous messages. It is
259      * only used by a connection manager that invokes the actions, not on the receiving side.
260      * </p>
261      */
262     public class PeeringConnectionManager extends AbstractPeeringConnectionManagerService {
263 
264         PeeringConnectionManager(ProtocolInfos sourceProtocolInfo,
265                                  ProtocolInfos sinkProtocolInfo) {
266             super(sourceProtocolInfo, sinkProtocolInfo);
267         }
268 
269         @Override
270         protected ConnectionInfo createConnection(int connectionID,
271                                                   int peerConnectionId,
272                                                   ServiceReference peerConnectionManager,
273                                                   ConnectionInfo.Direction direction,
274                                                   ProtocolInfo protocolInfo)
275                 throws ActionException {
276 
277             // Create the connection on "this" side with the given ID now...
278             ConnectionInfo con = new ConnectionInfo(
279                     connectionID,
280                     123, // Logical Rendering Control service ID
281                     456, // Logical AV Transport service ID
282                     protocolInfo,
283                     peerConnectionManager,
284                     peerConnectionId,
285                     direction,
286                     ConnectionInfo.Status.OK
287             );
288 
289             // DOC: EXC1
290             con = new ConnectionInfo(
291                     connectionID,
292                     connectionID == 0 ? 111 : 222,
293                     connectionID == 0 ? 333 : 444,
294                     protocolInfo,
295                     peerConnectionManager,
296                     peerConnectionId,
297                     direction,
298                     ConnectionInfo.Status.OK
299             );
300             // DOC: EXC1
301             return con;
302         }
303 
304         @Override
305         protected void closeConnection(ConnectionInfo connectionInfo) {
306             // Close the connection
307         }
308 
309         @Override
310         protected void peerFailure(ActionInvocation invocation,
311                                    UpnpResponse operation,
312                                    String defaultFailureMessage) {
313             System.err.println("Error managing connection with peer: " + defaultFailureMessage);
314         }
315     }
316 
317     class CountingListener implements PropertyChangeListener {
318         int count = 0;
319 
320         public void propertyChange(PropertyChangeEvent e) {
321             if (e.getPropertyName().equals("CurrentConnectionIDs")) {
322                 count++;
323             }
324         }
325     }
326 
327 }