Commit | Line | Data |
---|---|---|
d4eb563b | 1 | /** |
2 | * @licstart The following is the entire license notice for the JavaScript code in this page. | |
3 | * | |
4 | * IceCast Stream Monitor | |
5 | * Copyright © 2015 David Thompson <davet@gnu.org> | |
6 | * | |
7 | * This program is free software: you can redistribute it and/or | |
8 | * modify it under the terms of the GNU Affero General Public License | |
9 | * as published by the Free Software Foundation, either version 3 of | |
10 | * the License, or (at your option) any later version. | |
11 | * | |
12 | * This program is distributed in the hope that it will be useful, but | |
13 | * WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
15 | * Affero General Public License for more details. | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License | |
18 | * along with this program. If not, see | |
19 | * <http://www.gnu.org/licenses/>. | |
20 | * | |
21 | * @licend The above is the entire license notice for the JavaScript code in this page | |
22 | */ | |
23 | ||
24 | if (!Array.prototype.find) { | |
25 | Array.prototype.find = function(predicate) { | |
26 | if (this == null) { | |
27 | throw new TypeError('Array.prototype.find called on null or undefined'); | |
28 | } | |
29 | if (typeof predicate !== 'function') { | |
30 | throw new TypeError('predicate must be a function'); | |
31 | } | |
32 | var list = Object(this); | |
33 | var length = list.length >>> 0; | |
34 | var thisArg = arguments[1]; | |
35 | var value; | |
36 | ||
37 | for (var i = 0; i < length; i++) { | |
38 | value = list[i]; | |
39 | if (predicate.call(thisArg, value, i, list)) { | |
40 | return value; | |
41 | } | |
42 | } | |
43 | return undefined; | |
44 | }; | |
45 | } | |
46 | ||
47 | var app = {}; | |
48 | ||
d44f0f2e | 49 | app.icecastUrl = "https://live.fsf.org"; |
d4eb563b | 50 | |
51 | app.icecastApiUrl = "//live.fsf.org"; | |
52 | ||
53 | app.scheduleEvery = function(duration, thunk) { | |
54 | thunk(); | |
55 | setTimeout(function() { | |
56 | app.scheduleEvery(duration, thunk); | |
57 | }, duration); | |
58 | }; | |
59 | ||
60 | app.nullStats = { | |
61 | listeners: 0, | |
62 | server_name: null, | |
63 | server_description: null | |
64 | }; | |
65 | ||
66 | app.publicApi = function(xhr) { | |
67 | xhr.withCredentials = false; | |
68 | }; | |
69 | ||
70 | app.streamStats = function(mount) { | |
71 | var statsUrl = app.icecastApiUrl.concat('/status-json.xsl'); | |
72 | ||
73 | return m.request({ | |
74 | method: "GET", | |
75 | url: statsUrl, | |
76 | config: app.publicApi | |
77 | }).then(function(data) { | |
78 | // Match the end of the listen URL for the mount point. | |
79 | var regexp = new RegExp(mount.concat('$')); | |
80 | ||
81 | if(!data.icestats.source) { | |
82 | return app.nullStats; | |
83 | } | |
84 | ||
85 | // Due to <https://trac.xiph.org/ticket/2174>, we must | |
86 | // explicitly test if icestats.source is an array. | |
87 | if(!(data.icestats.source instanceof Array)) { | |
88 | data.icestats.source = [data.icestats.source]; | |
89 | } | |
90 | ||
91 | var stats = data.icestats.source.find(function(source) { | |
92 | return regexp.test(source.listenurl); | |
93 | }); | |
94 | ||
95 | return stats || app.nullStats; | |
96 | }); | |
97 | }; | |
98 | ||
99 | app.validStreamInfo = function(stats) { | |
100 | var name = stats.server_name; | |
101 | var desc = stats.server_description; | |
102 | ||
103 | return name && desc && name !== "Unspecified name" && | |
104 | desc !== "Unspecified description"; | |
105 | }; | |
106 | ||
107 | app.mountToStreamUrl = function(mount) { | |
108 | return app.icecastUrl.concat(mount); | |
109 | }; | |
110 | ||
111 | app.changeVideoMount = function(video, mount) { | |
112 | // This is quite hacky and doesn't feel like the Mithril way to do | |
113 | // things, but we need to explicitly reload the video when the | |
114 | // source URL changes. | |
115 | video.src = app.mountToStreamUrl(mount) + "?t=" + (new Date() * 1); | |
116 | video.load(); | |
117 | video.play(); | |
118 | }; | |
119 | ||
120 | app.withVideo = function(id, callback) { | |
121 | return function() { | |
122 | var video = document.getElementById(id); | |
123 | ||
124 | if(video) { | |
125 | callback(video); | |
126 | } | |
127 | }; | |
128 | }; | |
129 | ||
130 | app.streams = [ | |
131 | { | |
132 | name: "Room 123", | |
133 | speakerMount: "/stream-123.webm", | |
134 | desktopMount: "/slides-123.webm", | |
135 | speakerSmallMount: "/stream-123-480p.webm", | |
136 | ircChannel: "#libreplanet_room123" | |
137 | }, { | |
138 | name: "Room 144", | |
139 | speakerMount: "/stream-144.webm", | |
140 | desktopMount: "/slides-144.webm", | |
141 | speakerSmallMount: "/stream-144-480p.webm", | |
142 | ircChannel: "#libreplanet_room144" | |
143 | }, { | |
144 | name: "Room 155", | |
145 | speakerMount: "/stream-155.webm", | |
146 | desktopMount: "/slides-155.webm", | |
147 | speakerSmallMount: "/stream-155-480p.webm", | |
148 | ircChannel: "#libreplanet_room155" | |
d4eb563b | 149 | } |
150 | ]; | |
151 | ||
152 | app.controller = function() { | |
153 | this.stream = m.prop(app.streams[0]); | |
154 | this.stats = m.prop(app.nullStats); | |
155 | this.showDesktop = m.prop(false); | |
156 | ||
157 | // Check stats every 10 seconds. | |
158 | app.scheduleEvery(10000, this.updateStats.bind(this)); | |
159 | }; | |
160 | ||
161 | app.controller.prototype.updateStats = function() { | |
162 | //this.stats = app.streamStats(this.stream().speakerMount); | |
163 | }; | |
164 | ||
165 | app.view = function(ctrl) { | |
166 | var stream = ctrl.stream(); | |
167 | var stats = ctrl.stats() || app.nullStats; | |
168 | var showDesktop = ctrl.showDesktop(); | |
169 | ||
170 | function renderSpeakerStream() { | |
171 | return m("video.lp-video", { | |
172 | id: "speaker-video", | |
173 | controls: true, | |
174 | autoplay: true, | |
175 | // Sync desktop stream state as best we can. | |
176 | onpause: app.withVideo("desktop-video", function(video) { | |
177 | video.pause(); | |
178 | }), | |
179 | onplay: app.withVideo("desktop-video", function(video) { | |
180 | video.play(); | |
181 | }) | |
9a52b53b AE |
182 | // onended: app.withVideo("desktop-video", function(video) { |
183 | // setTimeout(function() { | |
184 | // | |
185 | // // we probably need to update the video stream GET request with the | |
186 | // // date hack so we get a non-cached version of the video when we | |
187 | // // try to resume. | |
188 | // | |
189 | // // we also probably need to re-try loading the video if it fails to | |
190 | // // load | |
191 | // | |
192 | // video.load(); | |
193 | // video.play(); | |
194 | // }, 3000); | |
195 | // }) | |
d4eb563b | 196 | }, [ |
197 | m("source", { | |
198 | src: app.mountToStreamUrl(stream.speakerMount) + "?t=" + (new Date() * 1) | |
199 | }), | |
200 | m("p", | |
201 | m("em", [ | |
202 | "Your browser does not support the HTML5 video tag, ", | |
203 | m("a", { href: "TODO" }, "[ please download ]"), | |
204 | "the video instead" | |
205 | ])) | |
206 | ]); | |
207 | } | |
208 | ||
209 | function renderDesktopStream() { | |
210 | return m("video.lp-video", { | |
211 | id: "desktop-video", | |
212 | controls: true, | |
9a52b53b AE |
213 | autoplay: true |
214 | // onended: app.withVideo("desktop-video", function(video) { | |
215 | // setTimeout(function() { | |
216 | // // we probably need to update the video stream GET request with the | |
217 | // // date hack so we get a non-cached version of the video when we | |
218 | // // try to resume. | |
219 | // | |
220 | // // we also probably need to re-try loading the video if it fails to | |
221 | // // load | |
222 | // video.load(); | |
223 | // video.play(); | |
224 | // }, 3000); | |
225 | // }) | |
d4eb563b | 226 | }, [ |
227 | m("source", { src: app.mountToStreamUrl(stream.desktopMount) + "?t=" + (new Date() * 1) }), | |
228 | m("p", | |
229 | m("em", [ | |
230 | "Your browser does not support the HTML5 video tag, ", | |
231 | m("a", { href: "TODO" }, "[ please download ]"), | |
232 | "the video instead" | |
233 | ])) | |
234 | ]); | |
235 | } | |
236 | ||
237 | function renderToggleDesktopStream() { | |
1afaf466 | 238 | var action = showDesktop ? "Hide slides stream" : "Show slides stream"; |
d4eb563b | 239 | |
240 | return m(".row", [ | |
241 | m(".col-sm-offset-4.col-sm-4", | |
242 | m("button.btn.btn-block.btn-default", { | |
243 | onclick: function() { | |
244 | ctrl.showDesktop(!showDesktop); | |
245 | } | |
246 | }, action) | |
247 | ) | |
248 | ]); | |
249 | } | |
250 | ||
251 | function renderRoomSelector() { | |
252 | return m(".row", | |
253 | m(".col-sm-offset-1.col-sm-10", | |
254 | m("ol.breadcrumb.text-center", app.streams.map(function(s) { | |
255 | return m("li", { | |
256 | class: s === stream ? "active" : null, | |
257 | onclick: function() { | |
258 | var speakerVideo = document.getElementById("speaker-video"); | |
259 | var desktopVideo = document.getElementById("desktop-video"); | |
260 | ||
261 | app.changeVideoMount(speakerVideo, s.speakerMount); | |
262 | ||
263 | // Video element doesn't exist when the user | |
264 | // hasn't elected to show it. | |
265 | if(desktopVideo) { | |
266 | app.changeVideoMount(desktopVideo, s.desktopMount); | |
267 | } | |
268 | ||
269 | ctrl.stream(s); | |
270 | ctrl.updateStats(); | |
271 | ||
272 | return false; | |
273 | } | |
274 | }, m("a.alt-a", { href: "#" }, s.name)); | |
275 | })))); | |
276 | } | |
277 | ||
278 | function renderStats() { | |
279 | var info; | |
280 | ||
281 | if(stats === app.nullStats) { | |
282 | info = m("i", "not broadcasting"); | |
283 | } else { | |
284 | info = m("i", "live"); | |
285 | } | |
286 | // else if(app.validStreamInfo(stats)) { | |
287 | // info = [ | |
288 | // m("strong", stats.server_name), | |
289 | // " — ", | |
290 | // m("i", stats.server_description) | |
291 | // ]; | |
292 | // } else { | |
293 | // info = null; | |
294 | // } | |
295 | ||
296 | //return m(".row", [ | |
297 | // m(".col-sm-8", info), | |
298 | // m(".col-sm-4.text-right", [ | |
299 | // m("strong", stats.listeners), | |
300 | // " watching" | |
301 | // ]) | |
302 | //]); | |
303 | return m("span"); | |
304 | } | |
305 | ||
306 | return [ | |
307 | renderRoomSelector(), | |
308 | m("h2", stream.name), | |
309 | renderStats(), | |
310 | renderSpeakerStream(), | |
311 | showDesktop ? renderDesktopStream() : null, | |
312 | renderToggleDesktopStream(), | |
d4eb563b | 313 | m("p", "Join the discussion online!"), |
314 | m("ul", [ | |
315 | m("li", [ | |
316 | "Conference-wide Freenode IRC channel: ", | |
b08a6669 | 317 | m("strong", "/join #libreplanet") |
d4eb563b | 318 | ]), |
319 | m("li", [ | |
320 | "Freenode IRC channel for ", | |
321 | stream.name, | |
322 | ": ", | |
b08a6669 | 323 | m("strong", ["/join ", stream.ircChannel]) |
d4eb563b | 324 | ]), |
b717d65e AE |
325 | m("li", [ |
326 | "Conference-wide Mumble (voice chat) server: ", | |
692eb59a | 327 | m("strong", "mumble.fsf.org") |
b717d65e | 328 | ]), |
d4eb563b | 329 | m("li", [ |
330 | "Conference hashtag for ", | |
331 | m("a", { href: "https://fsf.org/twitter" }, "microblogging"), | |
332 | ": ", | |
333 | m("strong", "#libreplanet") | |
334 | ]) | |
9d51ccbb AE |
335 | ]), |
336 | m("h2", "IRC") | |
d4eb563b | 337 | ]; |
338 | }; | |
339 | ||
340 | m.module(document.getElementById("stream"), app); |