opentok 3.1.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +3 -1
- data/README.md +35 -4
- data/lib/opentok/archives.rb +12 -1
- data/lib/opentok/sip.rb +0 -2
- data/lib/opentok/version.rb +1 -1
- data/opentok.gemspec +9 -8
- data/sample/Broadcast/Gemfile +4 -0
- data/sample/Broadcast/README.md +201 -0
- data/sample/Broadcast/broadcast_sample.rb +97 -0
- data/sample/Broadcast/public/css/sample.css +64 -0
- data/sample/Broadcast/public/js/host.js +185 -0
- data/sample/Broadcast/public/js/participant.js +85 -0
- data/sample/Broadcast/views/host.erb +82 -0
- data/sample/Broadcast/views/index.erb +32 -0
- data/sample/Broadcast/views/layout.erb +29 -0
- data/sample/Broadcast/views/participant.erb +27 -0
- data/sample/HelloWorld/public/js/helloworld.js +4 -10
- data/sample/HelloWorld/views/index.erb +1 -3
- data/spec/cassettes/OpenTok_Archives/calls_layout_on_archive_object.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/changes_the_layout_of_an_archive.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/http_client_errors/.yml +34 -0
- data/spec/cassettes/OpenTok_Archives/should_create_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_create_audio_only_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_create_custom_layout_archives.yml +48 -0
- data/spec/cassettes/OpenTok_Archives/should_create_hd_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_create_individual_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_create_named_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_delete_an_archive_by_id.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_find_archives_by_id.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_find_archives_with_unknown_properties.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_find_expired_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_find_paused_archives_by_id.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/should_stop_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/when_many_archives_are_created/should_return_all_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/when_many_archives_are_created/should_return_archives_with_an_offset.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/when_many_archives_are_created/should_return_count_number_of_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/when_many_archives_are_created/should_return_part_of_the_archives_when_using_offset_and_count.yml +1 -1
- data/spec/cassettes/OpenTok_Archives/when_many_archives_are_created/should_return_session_archives.yml +1 -1
- data/spec/cassettes/OpenTok_Broadcasts/calls_layout_on_broadcast_object.yml +1 -1
- data/spec/cassettes/OpenTok_Broadcasts/changes_the_layout_of_a_broadcast.yml +1 -1
- data/spec/cassettes/OpenTok_Broadcasts/fetches_a_hls_broadcast_url.yml +1 -1
- data/spec/cassettes/OpenTok_Broadcasts/finds_a_broadcast.yml +1 -1
- data/spec/cassettes/OpenTok_Broadcasts/starts_a_rtmp_broadcast.yml +1 -1
- data/spec/cassettes/OpenTok_Broadcasts/stops_a_broadcast.yml +1 -1
- data/spec/cassettes/OpenTok_Connections/forces_a_connection_to_be_terminated.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_always_archived_sessions.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_default_sessions.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_relayed_media_sessions.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_relayed_media_sessions_for_invalid_media_modes.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_relayed_media_sessions_with_a_location_hint.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_routed_media_sessions.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_routed_media_sessions_with_a_location_hint.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/_create_session/creates_sessions_with_a_location_hint.yml +1 -1
- data/spec/cassettes/OpenTok_OpenTok/when_initialized_properly/with_an_addendum_to_the_user_agent_string/should_append_the_addendum_to_the_user_agent_header.yml +1 -1
- data/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_a_connection.yml +1 -1
- data/spec/cassettes/OpenTok_Signals/receives_a_valid_response_for_all_connections.yml +1 -1
- data/spec/cassettes/OpenTok_Sip/receives_a_valid_response.yml +1 -1
- data/spec/cassettes/OpenTok_Streams/get_all_streams_information.yml +1 -1
- data/spec/cassettes/OpenTok_Streams/get_specific_stream_information.yml +1 -1
- data/spec/cassettes/OpenTok_Streams/layout_working_on_two_stream_list.yml +1 -1
- data/spec/opentok/archives_spec.rb +11 -1
- data/spec/spec_helper.rb +2 -0
- metadata +33 -20
@@ -0,0 +1,64 @@
|
|
1
|
+
/* Move down content because we have a fixed navbar that is 50px tall */
|
2
|
+
body {
|
3
|
+
padding-top: 50px;
|
4
|
+
padding-bottom: 20px;
|
5
|
+
background-color: #F2F2F2;
|
6
|
+
}
|
7
|
+
|
8
|
+
/* Responsive: Portrait tablets and up */
|
9
|
+
@media screen and (min-width: 768px) {
|
10
|
+
/* Remove padding from wrapping element since we kick in the grid classes here */
|
11
|
+
.body-content {
|
12
|
+
padding: 0;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
#streams {
|
17
|
+
background-color: gray;
|
18
|
+
width: 320px;
|
19
|
+
height: 180px;
|
20
|
+
}
|
21
|
+
|
22
|
+
#streams > div {
|
23
|
+
width: 20%;
|
24
|
+
height: 20%;
|
25
|
+
float: left;
|
26
|
+
position: relative;
|
27
|
+
cursor: pointer;
|
28
|
+
}
|
29
|
+
|
30
|
+
#streams.vertical > div {
|
31
|
+
left: 0px;
|
32
|
+
clear: left;
|
33
|
+
padding: 0px;
|
34
|
+
}
|
35
|
+
|
36
|
+
#streams .focus {
|
37
|
+
position: relative;
|
38
|
+
top: 0;
|
39
|
+
left: 0;
|
40
|
+
margin-top: 0;
|
41
|
+
height: 100%;
|
42
|
+
width: 100%;
|
43
|
+
}
|
44
|
+
|
45
|
+
#streams.vertical .focus {
|
46
|
+
padding: 0;
|
47
|
+
left: 0;
|
48
|
+
margin: 0;
|
49
|
+
left: 20%;
|
50
|
+
height: 100%;
|
51
|
+
width: 80%;
|
52
|
+
}
|
53
|
+
|
54
|
+
.stop {
|
55
|
+
display: none;
|
56
|
+
}
|
57
|
+
|
58
|
+
.bump-me {
|
59
|
+
padding-top: 40px;
|
60
|
+
}
|
61
|
+
|
62
|
+
.help-block {
|
63
|
+
font-weight: bold;
|
64
|
+
}
|
@@ -0,0 +1,185 @@
|
|
1
|
+
/* global OT, apiKey, sessionId, initialBroadcastId, token, $, initialLayout, focusStreamId */
|
2
|
+
/* eslint-disable no-console */
|
3
|
+
|
4
|
+
var session = OT.initSession(apiKey, sessionId);
|
5
|
+
var publisher = OT.initPublisher('publisher', {
|
6
|
+
insertMode: 'append',
|
7
|
+
width: '100%',
|
8
|
+
height: '100%',
|
9
|
+
resolution: '1280x720'
|
10
|
+
});
|
11
|
+
var broadcastId = initialBroadcastId;
|
12
|
+
var layout = initialLayout;
|
13
|
+
|
14
|
+
function disableForm() {
|
15
|
+
$('.broadcast-options-fields').attr('disabled', 'disabled');
|
16
|
+
$('.start').hide();
|
17
|
+
$('.stop').show();
|
18
|
+
}
|
19
|
+
|
20
|
+
function enableForm() {
|
21
|
+
$('.broadcast-options-fields').removeAttr('disabled');
|
22
|
+
$('.start').show();
|
23
|
+
$('.stop').hide();
|
24
|
+
}
|
25
|
+
|
26
|
+
function positionStreams() {
|
27
|
+
var $focusElement;
|
28
|
+
$focusElement = $('.focus');
|
29
|
+
if ($('#streams').hasClass('vertical')) {
|
30
|
+
$('#streams').children().css('top', '0');
|
31
|
+
$focusElement.appendTo('#streams');
|
32
|
+
$focusElement.css('top', (-20 * ($('#streams').children().size() - 1)) + '%');
|
33
|
+
}
|
34
|
+
else {
|
35
|
+
$focusElement.prependTo('#streams');
|
36
|
+
$focusElement.css('top', '0');
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
function setFocus(focusStreamId) {
|
41
|
+
var $focusElement;
|
42
|
+
var otherStreams = $.map($('#streams').children(), function (element) {
|
43
|
+
var streamId = (element.id === 'publisher' && publisher.stream) ? publisher.stream.streamId
|
44
|
+
: element.id;
|
45
|
+
if (streamId !== focusStreamId) {
|
46
|
+
$('#' + element.id).removeClass('focus');
|
47
|
+
return streamId;
|
48
|
+
}
|
49
|
+
return null;
|
50
|
+
});
|
51
|
+
|
52
|
+
$.post('/focus', {
|
53
|
+
focus: focusStreamId,
|
54
|
+
otherStreams: otherStreams
|
55
|
+
}).done(function () {
|
56
|
+
console.log('Focus changed.');
|
57
|
+
}).fail(function (jqXHR) {
|
58
|
+
console.error('Stream class list error:', jqXHR.responseText);
|
59
|
+
});
|
60
|
+
|
61
|
+
$('.focus').removeClass('focus');
|
62
|
+
$focusElement = (publisher.stream && publisher.stream.streamId === focusStreamId) ?
|
63
|
+
$('#publisher') : $('#' + focusStreamId);
|
64
|
+
$focusElement.addClass('focus');
|
65
|
+
session.signal({
|
66
|
+
type: 'focusStream',
|
67
|
+
data: focusStreamId
|
68
|
+
});
|
69
|
+
positionStreams();
|
70
|
+
}
|
71
|
+
|
72
|
+
function createFocusClick(elementId, focusStreamId) {
|
73
|
+
$('#' + elementId).click(function () {
|
74
|
+
setFocus(focusStreamId);
|
75
|
+
});
|
76
|
+
}
|
77
|
+
|
78
|
+
if (initialLayout === 'verticalPresentation') {
|
79
|
+
$('#streams').addClass('vertical');
|
80
|
+
}
|
81
|
+
|
82
|
+
if (initialLayout === 'verticalPresentation') {
|
83
|
+
$('.start').hide();
|
84
|
+
$('.stop').show();
|
85
|
+
}
|
86
|
+
|
87
|
+
session.connect(token, function (err) {
|
88
|
+
if (err) {
|
89
|
+
alert(err.message || err); // eslint-disable-line no-alert
|
90
|
+
}
|
91
|
+
session.publish(publisher);
|
92
|
+
});
|
93
|
+
|
94
|
+
publisher.on('streamCreated', function () {
|
95
|
+
createFocusClick(publisher.id, publisher.stream.streamId);
|
96
|
+
positionStreams();
|
97
|
+
});
|
98
|
+
|
99
|
+
session.on('streamCreated', function (event) {
|
100
|
+
var subscriber;
|
101
|
+
var streamId = event.stream.streamId;
|
102
|
+
var $streamContainer = $('<div></div>');
|
103
|
+
$streamContainer.attr('id', event.stream.id);
|
104
|
+
$('#streams').append($streamContainer);
|
105
|
+
subscriber = session.subscribe(event.stream, streamId, {
|
106
|
+
insertMode: 'append',
|
107
|
+
width: '100%',
|
108
|
+
height: '100%'
|
109
|
+
});
|
110
|
+
|
111
|
+
if (streamId === focusStreamId) {
|
112
|
+
setFocus(streamId);
|
113
|
+
}
|
114
|
+
createFocusClick(subscriber.id, streamId);
|
115
|
+
positionStreams();
|
116
|
+
});
|
117
|
+
|
118
|
+
session.on('streamDestroyed', function (event) {
|
119
|
+
var $streamElem = $('#' + event.stream.id);
|
120
|
+
if ($streamElem.hasClass('focus')) {
|
121
|
+
setFocus(publisher.stream.streamId);
|
122
|
+
}
|
123
|
+
$streamElem.remove();
|
124
|
+
positionStreams();
|
125
|
+
});
|
126
|
+
|
127
|
+
$(document).ready(function () {
|
128
|
+
$('.start').click(function () {
|
129
|
+
var options = {
|
130
|
+
maxDuration: $('input[name=maxDuration]').val() || undefined,
|
131
|
+
resolution: $('input[name=resolution]:checked').val(),
|
132
|
+
layout: {
|
133
|
+
type: layout
|
134
|
+
}
|
135
|
+
};
|
136
|
+
disableForm();
|
137
|
+
$.post('/start', options)
|
138
|
+
.done(function (response) {
|
139
|
+
console.log('start success.');
|
140
|
+
broadcastId = response.id;
|
141
|
+
setFocus(publisher.stream.streamId);
|
142
|
+
})
|
143
|
+
.fail(function (jqXHR) {
|
144
|
+
console.error(jqXHR.responseText);
|
145
|
+
enableForm();
|
146
|
+
});
|
147
|
+
}).prop('disabled', false);
|
148
|
+
$('.stop').click(function () {
|
149
|
+
$.get('stop/' + broadcastId)
|
150
|
+
.done(function () {
|
151
|
+
console.log('stop success.');
|
152
|
+
broadcastId = null;
|
153
|
+
enableForm();
|
154
|
+
})
|
155
|
+
.fail(function (jqXHR) {
|
156
|
+
console.error(jqXHR.responseText);
|
157
|
+
});
|
158
|
+
});
|
159
|
+
$('.toggle-layout').click(function () {
|
160
|
+
if ($('#streams').hasClass('vertical')) {
|
161
|
+
$('#streams').removeClass('vertical');
|
162
|
+
}
|
163
|
+
else {
|
164
|
+
$('#streams').addClass('vertical');
|
165
|
+
}
|
166
|
+
|
167
|
+
positionStreams();
|
168
|
+
|
169
|
+
layout = $('#streams').hasClass('vertical') ? 'verticalPresentation'
|
170
|
+
: 'horizontalPresentation';
|
171
|
+
|
172
|
+
$.post('broadcast/' + broadcastId + '/layout', {
|
173
|
+
type: layout
|
174
|
+
}).done(function () {
|
175
|
+
console.log('Broadcast layout updated.');
|
176
|
+
}).fail(function (jqXHR) {
|
177
|
+
console.error('Broadcast layout error:', jqXHR.responseText);
|
178
|
+
});
|
179
|
+
|
180
|
+
session.signal({
|
181
|
+
type: 'layoutClass',
|
182
|
+
data: layout
|
183
|
+
});
|
184
|
+
});
|
185
|
+
});
|
@@ -0,0 +1,85 @@
|
|
1
|
+
/* global OT, apiKey, sessionId, token, $, layout, focusStreamId */
|
2
|
+
var session = OT.initSession(apiKey, sessionId);
|
3
|
+
var publisher;
|
4
|
+
|
5
|
+
var container = $('<div id = "publisher"></div>');
|
6
|
+
|
7
|
+
if (layout === 'verticalPresentation') {
|
8
|
+
$('#streams').addClass('vertical');
|
9
|
+
}
|
10
|
+
|
11
|
+
container.addClass('focus');
|
12
|
+
$('#streams').append(container);
|
13
|
+
|
14
|
+
publisher = OT.initPublisher('publisher', {
|
15
|
+
insertMode: 'append',
|
16
|
+
width: '100%',
|
17
|
+
height: '100%',
|
18
|
+
resolution: '1280x720'
|
19
|
+
});
|
20
|
+
|
21
|
+
function positionStreams() {
|
22
|
+
var $focusElement = $('.focus');
|
23
|
+
if ($('#streams').hasClass('vertical')) {
|
24
|
+
$focusElement.appendTo('#streams');
|
25
|
+
$('#streams').children().css('top', '0');
|
26
|
+
$focusElement.css('top', (-20 * ($('#streams').children().size() - 1)) + '%');
|
27
|
+
}
|
28
|
+
else {
|
29
|
+
$focusElement.prependTo('#streams');
|
30
|
+
$focusElement.css('top', '0');
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
function focusStream(streamId) {
|
35
|
+
var focusStreamId = streamId;
|
36
|
+
var $focusElement = (publisher.stream && publisher.stream.id === focusStreamId) ? $('#publisher')
|
37
|
+
: $('#' + focusStreamId);
|
38
|
+
$('.focus').removeClass('focus');
|
39
|
+
$focusElement.addClass('focus');
|
40
|
+
positionStreams();
|
41
|
+
}
|
42
|
+
|
43
|
+
session.connect(token, function (err) {
|
44
|
+
if (err) {
|
45
|
+
alert(err.message || err); // eslint-disable-line no-alert
|
46
|
+
}
|
47
|
+
session.publish(publisher);
|
48
|
+
});
|
49
|
+
|
50
|
+
session.on('streamCreated', function (event) {
|
51
|
+
var streamId = event.stream.id;
|
52
|
+
container = document.createElement('div');
|
53
|
+
container.id = streamId;
|
54
|
+
$('#streams').append(container);
|
55
|
+
session.subscribe(event.stream, streamId, {
|
56
|
+
insertMode: 'append',
|
57
|
+
width: '100%',
|
58
|
+
height: '100%'
|
59
|
+
});
|
60
|
+
if (streamId === focusStreamId) {
|
61
|
+
focusStream(streamId);
|
62
|
+
}
|
63
|
+
positionStreams();
|
64
|
+
});
|
65
|
+
|
66
|
+
session.on('streamDestroyed', function (event) {
|
67
|
+
$('#' + event.stream.id).remove();
|
68
|
+
positionStreams();
|
69
|
+
});
|
70
|
+
|
71
|
+
session.on('signal:layoutClass', function (event) {
|
72
|
+
if (event.data === 'horizontalPresentation') {
|
73
|
+
$('#streams').removeClass('vertical');
|
74
|
+
$('.focus').prependTo('#streams');
|
75
|
+
}
|
76
|
+
else {
|
77
|
+
$('#streams').addClass('vertical');
|
78
|
+
$('.focus').appendTo('#streams');
|
79
|
+
}
|
80
|
+
positionStreams();
|
81
|
+
});
|
82
|
+
|
83
|
+
session.on('signal:focusStream', function (event) {
|
84
|
+
focusStream(event.data);
|
85
|
+
});
|
@@ -0,0 +1,82 @@
|
|
1
|
+
|
2
|
+
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
|
3
|
+
|
4
|
+
<div class="container bump-me">
|
5
|
+
|
6
|
+
<div class="body-content">
|
7
|
+
|
8
|
+
<div class="panel panel-default">
|
9
|
+
<div class="panel-heading">
|
10
|
+
<h3 class="panel-title">Host</h3>
|
11
|
+
</div>
|
12
|
+
<div class="panel-body">
|
13
|
+
<div id="streams">
|
14
|
+
<div id="publisher" class="focus"></div>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<div class="panel-footer">
|
18
|
+
<form class="broadcast-options">
|
19
|
+
<fieldset class="broadcast-options-fields">
|
20
|
+
<div class="form-group">
|
21
|
+
<h4>Broadcast Options:</h4>
|
22
|
+
<label>
|
23
|
+
Maximum duration (seconds, minimum 60, maximum 36000):
|
24
|
+
<input type="number" step="30" min="60" max="36000" name="maxDuration">
|
25
|
+
</label>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div class="form-group">
|
29
|
+
<p>
|
30
|
+
<label>Resolution:</label>
|
31
|
+
<label class="help-block">
|
32
|
+
<input type="radio" name="resolution" value="640x480"> 640x480
|
33
|
+
</label>
|
34
|
+
<label class="help-block">
|
35
|
+
<input type="radio" name="resolution" value="1280x720" checked> 1280x720
|
36
|
+
</label>
|
37
|
+
</p>
|
38
|
+
</div>
|
39
|
+
</fieldset>
|
40
|
+
</form>
|
41
|
+
<button class="btn btn-danger start" disabled>Start broadcast</button>
|
42
|
+
<button class="btn btn-success stop">Stop broadcast</button>
|
43
|
+
<button class="btn toggle-layout">Toggle layout</button>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div class="panel panel-default">
|
49
|
+
<div class="panel-heading">
|
50
|
+
<h3 class="panel-title">Instructions</h3>
|
51
|
+
</div>
|
52
|
+
<div class="panel-body">
|
53
|
+
<p>
|
54
|
+
Click <strong>Start broadcast</strong> to start broadcasting this session.
|
55
|
+
All publishers in the session will be included, and all publishers that
|
56
|
+
join the session will be included as well.
|
57
|
+
</p>
|
58
|
+
<p>
|
59
|
+
Click <strong>Stop broadcast</strong> to stop broadcasting this session.
|
60
|
+
</p>
|
61
|
+
<p>
|
62
|
+
Click <strong>Toggle layout</strong> to toggle the layout
|
63
|
+
between a vertical and horizontal presentation. The layout changes in all clients
|
64
|
+
and in the broadcast.
|
65
|
+
<p>
|
66
|
+
Click any stream to set it to be the focus stream in the broadcast layout.
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<script>
|
72
|
+
var sessionId = "<%= sessionId %>";
|
73
|
+
var initialBroadcastId = "<%= initialBroadcastId || '' %>";
|
74
|
+
var apiKey = "<%= apiKey %>";
|
75
|
+
var token = "<%= token %>";
|
76
|
+
var focusStreamId = "<%= focusStreamId || '' %>";
|
77
|
+
var initialLayout = "<%= initialLayout %>";
|
78
|
+
</script>
|
79
|
+
<script src="/js/host.js"></script>
|
80
|
+
|
81
|
+
</div>
|
82
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<div class="container bump-me">
|
2
|
+
|
3
|
+
|
4
|
+
<div class="body-content">
|
5
|
+
|
6
|
+
|
7
|
+
<div class="row">
|
8
|
+
<div class="col-lg-6 col-offset-1">
|
9
|
+
|
10
|
+
<div class="panel panel-default">
|
11
|
+
<div class="panel-heading">Create a broadcast</div>
|
12
|
+
<div class="panel-body">
|
13
|
+
<p>
|
14
|
+
Everyone who joins either the Host View or Participant View
|
15
|
+
joins a single OpenTok session. The Host can click
|
16
|
+
Start Broadcast and Stop Broadcast to control the live streaming
|
17
|
+
broadcast of the entire session.
|
18
|
+
</p>
|
19
|
+
</div>
|
20
|
+
<div class="panel-footer">
|
21
|
+
<a class="btn btn-danger" href="host">Host View</a>
|
22
|
+
<a class="btn btn-danger" href="participant">Participant View</a>
|
23
|
+
<a class="btn btn-danger" href="broadcast">Broadcast URL</a>
|
24
|
+
</div>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
</div>
|
28
|
+
|
29
|
+
</div>
|
30
|
+
|
31
|
+
</div>
|
32
|
+
</div>
|