Commit | Line | Data |
---|---|---|
fcf95e6d | 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 | ||
d7001c48 | 49 | app.icecastUrl = "http://live2.fsf.org"; |
fcf95e6d | 50 | |
d7001c48 | 51 | app.icecastApiUrl = "//live2.fsf.org"; |
fcf95e6d | 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. | |
d901c86f | 115 | video.src = app.mountToStreamUrl(mount) + "?t=" + (new Date() * 1); |
fcf95e6d | 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", | |
4e4421e6 | 135 | speakerSmallMount: "/stream-123-480p.webm", |
fcf95e6d | 136 | ircChannel: "#libreplanet_room123" |
137 | }, { | |
138 | name: "Room 141", | |
139 | speakerMount: "/stream-141.webm", | |
140 | desktopMount: "/slides-141.webm", | |
4e4421e6 | 141 | speakerSmallMount: "/stream-141-480p.webm", |
fcf95e6d | 142 | ircChannel: "#libreplanet_room141" |
7bcaa6f6 AE |
143 | }, { |
144 | name: "Room 144", | |
145 | speakerMount: "/stream-144.webm", | |
146 | desktopMount: "/slides-144.webm", | |
4e4421e6 | 147 | speakerSmallMount: "/stream-144-480p.webm", |
7bcaa6f6 | 148 | ircChannel: "#libreplanet_room144" |
fcf95e6d | 149 | }, { |
150 | name: "Room 155", | |
151 | speakerMount: "/stream-155.webm", | |
152 | desktopMount: "/slides-155.webm", | |
4e4421e6 | 153 | speakerSmallMount: "/stream-155-480p.webm", |
fcf95e6d | 154 | ircChannel: "#libreplanet_room155" |
155 | } | |
156 | ]; | |
157 | ||
158 | app.controller = function() { | |
159 | this.stream = m.prop(app.streams[0]); | |
160 | this.stats = m.prop(app.nullStats); | |
161 | this.showDesktop = m.prop(false); | |
162 | ||
163 | // Check stats every 10 seconds. | |
164 | app.scheduleEvery(10000, this.updateStats.bind(this)); | |
165 | }; | |
166 | ||
167 | app.controller.prototype.updateStats = function() { | |
168 | this.stats = app.streamStats(this.stream().speakerMount); | |
169 | }; | |
170 | ||
171 | app.view = function(ctrl) { | |
172 | var stream = ctrl.stream(); | |
173 | var stats = ctrl.stats() || app.nullStats; | |
174 | var showDesktop = ctrl.showDesktop(); | |
175 | ||
176 | function renderSpeakerStream() { | |
177 | return m("video.lp-video", { | |
178 | id: "speaker-video", | |
179 | controls: true, | |
180 | autoplay: true, | |
181 | // Sync desktop stream state as best we can. | |
182 | onpause: app.withVideo("desktop-video", function(video) { | |
183 | video.pause(); | |
184 | }), | |
185 | onplay: app.withVideo("desktop-video", function(video) { | |
186 | video.play(); | |
187 | }) | |
188 | }, [ | |
189 | m("source", { | |
a2ddd223 | 190 | src: app.mountToStreamUrl(stream.speakerMount) + "?t=" + (new Date() * 1) |
fcf95e6d | 191 | }), |
192 | m("p", | |
193 | m("em", [ | |
194 | "Your browser does not support the HTML5 video tag, ", | |
195 | m("a", { href: "TODO" }, "[ please download ]"), | |
196 | "the video instead" | |
197 | ])) | |
198 | ]); | |
199 | } | |
200 | ||
201 | function renderDesktopStream() { | |
202 | return m("video.lp-video", { | |
203 | id: "desktop-video", | |
204 | autoplay: true | |
205 | }, [ | |
a2ddd223 | 206 | m("source", { src: app.mountToStreamUrl(stream.desktopMount) + "?t=" + (new Date() * 1) }), |
fcf95e6d | 207 | m("p", |
208 | m("em", [ | |
209 | "Your browser does not support the HTML5 video tag, ", | |
210 | m("a", { href: "TODO" }, "[ please download ]"), | |
211 | "the video instead" | |
212 | ])) | |
213 | ]); | |
214 | } | |
215 | ||
216 | function renderToggleDesktopStream() { | |
217 | var action = showDesktop ? "Hide desktop stream" : "Show desktop stream"; | |
218 | ||
219 | return m(".row", [ | |
220 | m(".col-sm-offset-4.col-sm-4", | |
221 | m("button.btn.btn-block.btn-default", { | |
222 | onclick: function() { | |
223 | ctrl.showDesktop(!showDesktop); | |
224 | } | |
225 | }, action) | |
226 | ) | |
227 | ]); | |
228 | } | |
229 | ||
230 | function renderRoomSelector() { | |
231 | return m(".row", | |
232 | m(".col-sm-offset-1.col-sm-10", | |
233 | m("ol.breadcrumb.text-center", app.streams.map(function(s) { | |
234 | return m("li", { | |
235 | class: s === stream ? "active" : null, | |
236 | onclick: function() { | |
237 | var speakerVideo = document.getElementById("speaker-video"); | |
238 | var desktopVideo = document.getElementById("desktop-video"); | |
239 | ||
240 | app.changeVideoMount(speakerVideo, s.speakerMount); | |
241 | ||
242 | // Video element doesn't exist when the user | |
243 | // hasn't elected to show it. | |
244 | if(desktopVideo) { | |
245 | app.changeVideoMount(desktopVideo, s.desktopMount); | |
246 | } | |
247 | ||
248 | ctrl.stream(s); | |
249 | ctrl.updateStats(); | |
250 | ||
251 | return false; | |
252 | } | |
253 | }, m("a.alt-a", { href: "#" }, s.name)); | |
254 | })))); | |
255 | } | |
256 | ||
257 | function renderStats() { | |
258 | var info; | |
259 | ||
260 | if(stats === app.nullStats) { | |
261 | info = m("i", "not broadcasting"); | |
262 | } else { | |
263 | info = m("i", "live"); | |
264 | } | |
265 | // else if(app.validStreamInfo(stats)) { | |
266 | // info = [ | |
267 | // m("strong", stats.server_name), | |
268 | // " — ", | |
269 | // m("i", stats.server_description) | |
270 | // ]; | |
271 | // } else { | |
272 | // info = null; | |
273 | // } | |
274 | ||
a6dc86d5 AE |
275 | return m(".row", [ |
276 | m(".col-sm-8", info), | |
277 | m(".col-sm-4.text-right", [ | |
278 | m("strong", stats.listeners), | |
279 | " watching" | |
280 | ]) | |
281 | ]); | |
fcf95e6d | 282 | } |
283 | ||
284 | return [ | |
285 | renderRoomSelector(), | |
286 | m("h2", stream.name), | |
287 | renderStats(), | |
288 | renderSpeakerStream(), | |
289 | showDesktop ? renderDesktopStream() : null, | |
290 | renderToggleDesktopStream(), | |
4e4421e6 | 291 | m("p", [ m("a", { href: app.icecastUrl + stream.speakerSmallMount }, "Low res stream" ) ] ), |
fcf95e6d | 292 | m("h2", "IRC"), |
293 | m("p", "Join the discussion online!"), | |
294 | m("ul", [ | |
295 | m("li", [ | |
296 | "Conference-wide Freenode IRC channel: ", | |
297 | m("strong", "#libreplanet") | |
298 | ]), | |
299 | m("li", [ | |
300 | "Freenode IRC channel for ", | |
301 | stream.name, | |
302 | ": ", | |
303 | m("strong", stream.ircChannel) | |
304 | ]), | |
305 | m("li", [ | |
306 | "Conference hashtag for ", | |
307 | m("a", { href: "https://fsf.org/twitter" }, "microblogging"), | |
308 | ": ", | |
a2df0d9d | 309 | m("strong", "#libreplanet") |
fcf95e6d | 310 | ]) |
311 | ]) | |
312 | ]; | |
313 | }; | |
314 | ||
315 | m.module(document.getElementById("stream"), app); |