@@ -56,9 +56,12 @@ get(new FreeMarkerTemplateView("/host") {
56
56
public ModelAndView handle (Request request , Response response ) {
57
57
58
58
String token = null ;
59
+ ArrayList<String > layoutClassList = new ArrayList<String > ();
60
+ layoutClassList. add(" focus" );
59
61
try {
60
62
token = opentok. generateToken(sessionId, new TokenOptions .Builder ()
61
63
.role(Role . MODERATOR )
64
+ .initialLayoutClassList(layoutClassList)
62
65
.build());
63
66
} catch (OpenTokException e) {
64
67
e. printStackTrace();
@@ -68,6 +71,8 @@ get(new FreeMarkerTemplateView("/host") {
68
71
attributes. put(" apiKey" , apiKey);
69
72
attributes. put(" sessionId" , sessionId);
70
73
attributes. put(" token" , token);
74
+ attributes. put(" layout" , layoutType);
75
+ attributes. put(" focusStreamId" , focusStreamId);
71
76
72
77
return new ModelAndView (attributes, " host.ftl" );
73
78
}
@@ -89,16 +94,20 @@ post(new Route("/start") {
89
94
HttpServletRequest req = request. raw();
90
95
boolean hasAudio = req. getParameterMap(). containsKey(" hasAudio" );
91
96
boolean hasVideo = req. getParameterMap(). containsKey(" hasVideo" );
92
- OutputMode outputMode = OutputMode . COMPOSED ;
93
- if (req. getParameter(" outputMode" ). equals(" individual" )) {
94
- outputMode = OutputMode . INDIVIDUAL ;
97
+ OutputMode outputMode = OutputMode . INDIVIDUAL ;
98
+ ArchiveLayout layout = null ;
99
+ if (req. getParameter(" outputMode" ). equals(" composed" )) {
100
+ outputMode = OutputMode . COMPOSED ;
101
+ layout = new ArchiveLayout (ArchiveLayout . Type . HORIZONTAL );
95
102
}
96
103
try {
97
104
ArchiveProperties properties = new ArchiveProperties .Builder ()
98
105
.name(" Java Archiving Sample App" )
99
106
.hasAudio(hasAudio)
100
107
.hasVideo(hasVideo)
101
- .outputMode(outputMode). build();
108
+ .outputMode(outputMode)
109
+ .layout(layout)
110
+ .build();
102
111
archive = opentok. startArchive(sessionId, properties);
103
112
} catch (OpenTokException e) {
104
113
e. printStackTrace();
@@ -114,8 +123,13 @@ for the session that needs to be archived. An ArchiveProperties object is instan
114
123
optional properties for the archive. The ` name ` is stored with the archive and can be read later.
115
124
The ` hasAudio ` , ` hasVideo ` , and ` outputMode ` values are read from the request body; these define
116
125
whether the archive will record audio and video, and whether it will record streams individually or
117
- to a single file composed of all streams. In this case, as in the HelloWorld sample app, there is
118
- only one session created and it is used here and for the participant view. This will trigger the
126
+ to a single file composed of all streams.
127
+
128
+ The ` layout ` setting is used to set the initial archive layout for a composed archive,
129
+ to be discussed later in the [ Changing Archive Layout] ( #changing-archive-layout ) section.
130
+
131
+ In this case, as in the HelloWorld sample app, there is only one session
132
+ created and it is used here and for the participant view. This will trigger the
119
133
recording to begin. The response sent back to the client's XHR request will be the JSON
120
134
representation of the archive, which is returned from the ` toString() ` method. The client is also
121
135
listening for the ` archiveStarted ` event, and uses that event to change the 'Start Archiving' button
@@ -179,6 +193,8 @@ get(new FreeMarkerTemplateView("/participant") {
179
193
attributes. put(" apiKey" , apiKey);
180
194
attributes. put(" sessionId" , sessionId);
181
195
attributes. put(" token" , token);
196
+ attributes. put(" layout" , layoutType);
197
+ attributes. put(" focusStreamId" , focusStreamId);
182
198
183
199
return new ModelAndView (attributes, " participant.ftl" );
184
200
}
@@ -189,6 +205,176 @@ Since this view has no further interactivity with buttons, this is all that is n
189
205
that is participating in an archived session. Once again, much of the functionality is implemented
190
206
in the client, in code that can be found in the ` public/js/participant.js ` file.
191
207
208
+ ### Changing Archive Layout
209
+
210
+ * Note:* Changing archive layout is only available for composed archives, and setting the layout
211
+ is not required. By default, composed archives use the "best fit" layout. For more information,
212
+ see the OpenTok developer guide for [ Customizing the video layout for composed
213
+ archives] ( https://tokbox.com/developer/guides/archiving/layout-control.html ) .
214
+
215
+ When you create a composed archive (when the ` outputMode ` is set to 'composed), we set
216
+ the ` ArchiveLayout ` object to use the ` ArchiveLayout.Type.HORIZONTAL ` layout type
217
+ (corresponding to the ` 'horizontalPresentation' ` predefined layout type). And we pass
218
+ that ` ArchiveLayout ` object into the ` layout() ` method of the ` ArchiveProperties.Builder `
219
+ object used in the call to ` OpenTok.startArchive() ` :
220
+
221
+ ``` java
222
+ OutputMode outputMode = OutputMode . INDIVIDUAL ;
223
+ ArchiveLayout layout = null ;
224
+ if (req. getParameter(" outputMode" ). equals(" composed" )) {
225
+ outputMode = OutputMode . COMPOSED ;
226
+ layout = new ArchiveLayout (ArchiveLayout . Type . HORIZONTAL );
227
+ }
228
+ try {
229
+ ArchiveProperties properties = new ArchiveProperties .Builder ()
230
+ .name(" Java Archiving Sample App" )
231
+ .hasAudio(hasAudio)
232
+ .hasVideo(hasVideo)
233
+ .outputMode(outputMode)
234
+ .layout(layout)
235
+ .build();
236
+ archive = opentok. startArchive(sessionId, properties);
237
+ } catch (OpenTokException e) {
238
+ // ...
239
+ ```
240
+
241
+ This sets the initial layout type of the archive. `' horizontalPresentation' ` is one of
242
+ the predefined layout types for composed archives.
243
+
244
+ For composed archives, you can change the layout dynamically. The host view includes a
245
+ * Toggle layout* button. This toggles the layout of the streams between a horizontal and vertical
246
+ presentation. When you click this button, the host client switches makes an HTTP POST request to
247
+ the ' /archive/:archiveId/layout' endpoint:
248
+
249
+ ```javascript
250
+ post(new Route (" /archive/:archiveId/layout" ) {
251
+ @Override
252
+ public Object handle (Request request , Response response ) {
253
+ try {
254
+ JsonNode rootNode = objectMapper. readTree(request. body());
255
+ layoutType = rootNode. get(" type" ). textValue();
256
+ ArchiveLayout . Type type = ArchiveLayout . Type . HORIZONTAL ;
257
+ if (layoutType. equals(" verticalPresentation" )) {
258
+ type = ArchiveLayout . Type . VERTICAL ;
259
+ }
260
+ ArchiveProperties archiveProperties = new ArchiveProperties .Builder ()
261
+ .layout(new ArchiveLayout (type))
262
+ .build();
263
+ opentok. setArchiveLayout(request. params(" archiveId" ), archiveProperties);
264
+ } catch (Exception e) {
265
+ e. printStackTrace();
266
+ response. status(400 );
267
+ return e. getMessage();
268
+ }
269
+ return layoutType;
270
+ }
271
+ });
272
+ ```
273
+
274
+ This creates an `ArchiveProperties ` object, and calls the `layout()` method of the
275
+ `ArchiveProperties . Builder ` obejct, passing in either `ArchiveLayout . Type . HORIZONTAL `
276
+ or `ArchiveLayout . Type . VERTICAL ` (depending on the `type` set in the POST request’s body).
277
+ We pass the `ArchiveProperties ` object into the call to the `OpenTok . setArchiveLayout()` method.
278
+ The layout type will either be set to `horizontalPresentation` or `verticalPresentation`,
279
+ which are two of the predefined layout types for OpenTok composed archives.
280
+
281
+ Also , in the host view, you can click any stream to set it to be the focus stream in the
282
+ archive layout. (Click outside of the mute audio icon. ) Doing so sends an HTTP POST request
283
+ to the `/ focus` endpoint:
284
+
285
+ ```java
286
+ post(new Route (" /focus" ) {
287
+ @Override
288
+ public Object handle (Request request , Response response ) {
289
+ ArrayList<String > otherStreams = new ArrayList<String > ();
290
+ HttpServletRequest req = request. raw();
291
+ String newFocusStreamId = req. getParameterMap(). get(" focus" )[0 ];
292
+
293
+ if (newFocusStreamId. equals(focusStreamId)) {
294
+ return focusStreamId;
295
+ }
296
+
297
+ if (focusStreamId. isEmpty()) {
298
+ focusStreamId = newFocusStreamId;
299
+ return focusStreamId;
300
+ }
301
+
302
+ try {
303
+ StreamProperties newFocusStreamProperties = new StreamProperties .Builder ()
304
+ .id(newFocusStreamId)
305
+ .addLayoutClass(" focus" )
306
+ .build();
307
+ StreamProperties oldFocusStreamProperties = new StreamProperties .Builder ()
308
+ .id(focusStreamId)
309
+ .build();
310
+ StreamListProperties streamListProperties = new StreamListProperties .Builder ()
311
+ .addStreamProperties(newFocusStreamProperties)
312
+ .addStreamProperties(oldFocusStreamProperties)
313
+ .build();
314
+ opentok. setStreamLayouts(sessionId, streamListProperties);
315
+ focusStreamId = newFocusStreamId;
316
+ } catch (Exception e) {
317
+ e. printStackTrace();
318
+ response. status(400 );
319
+ return e. getMessage();
320
+ }
321
+ return focusStreamId;
322
+ }
323
+ });
324
+ ```
325
+
326
+ The body of the POST request includes the stream ID of the " focus" stream and an array of
327
+ other stream IDs in the session. The server- side method that handles the POST requests creates
328
+ `StreamProperties ` objects for the new focus stream and for the previous focus streams. For the
329
+ new focus stream, it calls the `addLayoutClass()` method of a `StreamProperties . Builder ` object,
330
+ to at the " focus" class to the layout class list for the stream:
331
+
332
+ ```javascript
333
+ StreamProperties newFocusStreamProperties = new StreamProperties.Builder()
334
+ .id(newFocusStreamId)
335
+ .addLayoutClass("focus")
336
+ .build();
337
+ ```
338
+
339
+ For the previous focus stream, it calls the `addLayoutClass()` method of a
340
+ `StreamProperties.Builder` object, without calling the `addLayoutClass` method. This removes
341
+ all layout classes for the stream:
342
+
343
+ ```javascript
344
+ StreamProperties oldFocusStreamProperties = new StreamProperties.Builder()
345
+ .id(focusStreamId)
346
+ .build();
347
+ ```
348
+
349
+ Each `StreamProperties` object is added to a `StreamListProperties` object, using the
350
+ `addStreamProperties()` of the `StreamListProperties.Builder`. And the `StreamListProperties`
351
+ object is passed into the `OpenTok.setStreamLayouts()` method:
352
+
353
+ ```java
354
+ StreamListProperties streamListProperties = new StreamListProperties.Builder()
355
+ .addStreamProperties(newFocusStreamProperties)
356
+ .addStreamProperties(oldFocusStreamProperties)
357
+ .build();
358
+ opentok.setStreamLayouts(sessionId, streamListProperties);
359
+ ```
360
+
361
+ This sets one stream to have the `focus` class, which causes it to be the large stream
362
+ displayed in the composed archive. (This is the behavior of the `horizontalPresentation` and
363
+ `verticalPresentation` layout types.) To see this effect, you should open the host and participant
364
+ pages on different computers (using different cameras). Or, if you have multiple cameras connected
365
+ to your machine, you can use one camera for publishing from the host, and use another for the
366
+ participant. Or, if you are using a laptop with an external monitor, you can load the host page
367
+ with the laptop closed (no camera) and open the participant page with the laptop open.
368
+
369
+ The host client page also uses OpenTok signaling to notify other clients when the layout type and
370
+ focus stream changes, and they then update the local display of streams in the HTML DOM accordingly.
371
+ However, this is not necessary. The layout of the composed archive is unrelated to the layout of
372
+ streams in the web clients.
373
+
374
+ When you playback the composed archive, the layout type and focus stream changes, based on calls
375
+ to the `OpenTok.setArchiveLayout()` and `OpenTok.setStreamLayouts()` methods during
376
+ the recording.
377
+
192
378
### Past Archives
193
379
194
380
Start by visiting the history page at <http:// localhost:4567/history>. You will see a table that
0 commit comments