6ca5bddc |
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 | { |
132 | name: "Back Bay Grand Room", |
133 | speakerMount: "/stream-room-grand.webm", |
134 | desktopMount: "/slides-room-grand.webm", |
135 | speakerSmallMount: "/stream-room-grand-480p.webm", |
136 | ircChannel: "#libreplanet_room_grand" |
137 | }, { |
138 | name: "Freedom Room", |
139 | speakerMount: "/stream-room-freedom.webm", |
140 | desktopMount: "/slides-room-freedom.webm", |
141 | speakerSmallMount: "/stream-room-freedom-480p.webm", |
142 | ircChannel: "#libreplanet_room_freedom" |
143 | }, { |
144 | name: "Patriot Room", |
145 | speakerMount: "/stream-room-patriot.webm", |
146 | desktopMount: "/slides-room-patriot.webm", |
147 | speakerSmallMount: "/stream-room-patriot-480p.webm", |
148 | ircChannel: "#libreplanet_room_patriot" |
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 | |
308 | return [ |
309 | renderRoomSelector(), |
310 | m("h2", stream.name), |
311 | renderStats(), |
312 | renderSpeakerStream(), |
313 | showDesktop ? renderDesktopStream() : null, |
314 | // renderToggleDesktopStream(), |
315 | m("p", "Join the discussion online!"), |
316 | m("ul", [ |
317 | m("li", [ |
318 | "Conference-wide Freenode IRC channel: ", |
319 | m("strong", "/join #libreplanet") |
320 | ]), |
321 | m("li", [ |
322 | "Freenode IRC channel for ", |
323 | stream.name, |
324 | ": ", |
325 | m("strong", ["/join ", stream.ircChannel]) |
326 | ]), |
327 | // m("li", [ |
328 | // "Conference-wide Mumble (voice chat) server: ", |
329 | // m("strong", "mumble.fsf.org") |
330 | // ]), |
331 | m("li", [ |
332 | "Conference hashtag for ", |
333 | m("a", { href: "https://fsf.org/twitter" }, "microblogging"), |
334 | ": ", |
335 | m("strong", "#libreplanet") |
336 | ]) |
337 | ]), |
338 | m("h2", "IRC") |
339 | ]; |
340 | }; |
341 | |
342 | m.module(document.getElementById("stream"), app); |