Commit | Line | Data |
---|---|---|
76c2419a GF |
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 | ||
49 | app.icecastUrl = "https://live.fsf.org"; | |
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 | { | |
0851364a AE |
132 | name: "Jupiter Room", |
133 | speakerMount: "/stream-room-jupiter.webm", | |
134 | desktopMount: "/slides-room-jupiter.webm", | |
135 | speakerSmallMount: "/stream-room-jupiter-480p.webm", | |
136 | ircChannel: "#libreplanet_room_jupiter" | |
76c2419a | 137 | }, { |
0851364a AE |
138 | name: "Saturn Room", |
139 | speakerMount: "/stream-room-saturn.webm", | |
140 | desktopMount: "/slides-room-saturn.webm", | |
141 | speakerSmallMount: "/stream-room-saturn-480p.webm", | |
142 | ircChannel: "#libreplanet_room_saturn" | |
76c2419a | 143 | }, { |
0851364a AE |
144 | name: "Neptune Room", |
145 | speakerMount: "/stream-room-neptune.webm", | |
146 | desktopMount: "/slides-room-neptune.webm", | |
147 | speakerSmallMount: "/stream-room-neptune-480p.webm", | |
148 | ircChannel: "#libreplanet_room_neptune" | |
76c2419a GF |
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 | }) | |
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 | // }) | |
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, | |
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 | // }) | |
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 | /* | |
238 | function renderToggleDesktopStream() { | |
239 | var action = showDesktop ? "Hide slides stream" : "Show slides stream"; | |
240 | ||
241 | return m(".row", [ | |
242 | m(".col-sm-offset-4.col-sm-4", | |
243 | m("button.btn.btn-block.btn-default", { | |
244 | onclick: function() { | |
245 | ctrl.showDesktop(!showDesktop); | |
246 | } | |
247 | }, action) | |
248 | ) | |
249 | ]); | |
250 | } | |
251 | */ | |
252 | ||
253 | function renderRoomSelector() { | |
254 | return m(".row", | |
255 | m(".col-sm-offset-1.col-sm-10", | |
256 | m("ol.breadcrumb.text-center", app.streams.map(function(s) { | |
257 | return m("li", { | |
258 | class: s === stream ? "active" : null, | |
259 | onclick: function() { | |
260 | var speakerVideo = document.getElementById("speaker-video"); | |
261 | var desktopVideo = document.getElementById("desktop-video"); | |
262 | ||
263 | app.changeVideoMount(speakerVideo, s.speakerMount); | |
264 | ||
265 | // Video element doesn't exist when the user | |
266 | // hasn't elected to show it. | |
267 | if(desktopVideo) { | |
268 | app.changeVideoMount(desktopVideo, s.desktopMount); | |
269 | } | |
270 | ||
271 | ctrl.stream(s); | |
272 | ctrl.updateStats(); | |
273 | ||
274 | return false; | |
275 | } | |
276 | }, m("a.alt-a", { href: "#" }, s.name)); | |
277 | })))); | |
278 | } | |
279 | ||
280 | function renderStats() { | |
281 | var info; | |
282 | ||
283 | if(stats === app.nullStats) { | |
284 | info = m("i", "not broadcasting"); | |
285 | } else { | |
286 | info = m("i", "live"); | |
287 | } | |
288 | // else if(app.validStreamInfo(stats)) { | |
289 | // info = [ | |
290 | // m("strong", stats.server_name), | |
291 | // " — ", | |
292 | // m("i", stats.server_description) | |
293 | // ]; | |
294 | // } else { | |
295 | // info = null; | |
296 | // } | |
297 | ||
298 | //return m(".row", [ | |
299 | // m(".col-sm-8", info), | |
300 | // m(".col-sm-4.text-right", [ | |
301 | // m("strong", stats.listeners), | |
302 | // " watching" | |
303 | // ]) | |
304 | //]); | |
305 | return m("span"); | |
306 | } | |
307 | ||
94901463 AE |
308 | // show or hide irc info based on page URL |
309 | if (!window.location.pathname.match(RegExp("\/2021\/live.*"))) { | |
310 | var irc_info = [ | |
68afd985 AE |
311 | "This room's channel: ", |
312 | m("strong", ["/join ", stream.ircChannel]) | |
94901463 | 313 | ]; |
c32d5b90 | 314 | // set css to show irc links in sidebar |
68afd985 | 315 | document.querySelector('#irc-links-panel').style.display = "block"; |
94901463 AE |
316 | } else { |
317 | var irc_info = [ | |
12c92727 AE |
318 | m("strong", [ |
319 | m("a", { target: "_blank", href: "https://my.fsf.org/civicrm/event/info?reset=1&id=92" }, "Register"), | |
320 | ]), | |
f2f84858 | 321 | " gratis to access more IRC rooms. ", |
53d4a866 AE |
322 | m("strong", [ |
323 | m("a", { href: "/2021/registered/live/" }, "Already registered?"), | |
324 | ]) | |
94901463 AE |
325 | ]; |
326 | } | |
327 | ||
76c2419a GF |
328 | return [ |
329 | renderRoomSelector(), | |
330 | m("h2", stream.name), | |
331 | renderStats(), | |
332 | renderSpeakerStream(), | |
333 | showDesktop ? renderDesktopStream() : null, | |
334 | // renderToggleDesktopStream(), | |
335 | m("p", "Join the discussion online!"), | |
336 | m("ul", [ | |
337 | m("li", [ | |
338 | "Conference-wide Freenode IRC channel: ", | |
339 | m("strong", "/join #libreplanet") | |
340 | ]), | |
39a9abec AE |
341 | m("li", |
342 | m("div", { class: "special-irc" }, | |
343 | irc_info) | |
344 | ), | |
76c2419a GF |
345 | // m("li", [ |
346 | // "Conference-wide Mumble (voice chat) server: ", | |
347 | // m("strong", "mumble.fsf.org") | |
348 | // ]), | |
349 | m("li", [ | |
350 | "Conference hashtag for ", | |
5b7bc4f3 | 351 | m("a", { target: "_blank", href: "https://www.fsf.org/share" }, "microblogging"), |
76c2419a GF |
352 | ": ", |
353 | m("strong", "#libreplanet") | |
354 | ]) | |
355 | ]), | |
76c2419a GF |
356 | ]; |
357 | }; | |
358 | ||
359 | m.module(document.getElementById("stream"), app); |