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 }