09e63d1c4e65dcc2c78018483b0dccde5335b160
2 * @licstart The following is the entire license notice for the JavaScript code in this page.
4 * IceCast Stream Monitor
5 * Copyright © 2015 David Thompson <davet@gnu.org>
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.
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.
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/>.
21 * @licend The above is the entire license notice for the JavaScript code in this page
24 if (!Array
.prototype.find
) {
25 Array
.prototype.find = function(predicate
) {
27 throw new TypeError('Array.prototype.find called on null or undefined');
29 if (typeof predicate
!== 'function') {
30 throw new TypeError('predicate must be a function');
32 var list
= Object(this);
33 var length
= list
.length
>>> 0;
34 var thisArg
= arguments
[1];
37 for (var i
= 0; i
< length
; i
++) {
39 if (predicate
.call(thisArg
, value
, i
, list
)) {
49 app
.icecastUrl
= "https://live.fsf.org";
51 app
.icecastApiUrl
= "//live.fsf.org";
53 app
.scheduleEvery = function(duration
, thunk
) {
55 setTimeout(function() {
56 app
.scheduleEvery(duration
, thunk
);
63 server_description
: null
66 app
.publicApi = function(xhr
) {
67 xhr
.withCredentials
= false;
70 app
.streamStats = function(mount
) {
71 var statsUrl
= app
.icecastApiUrl
.concat('/status-json.xsl');
77 }).then(function(data
) {
78 // Match the end of the listen URL for the mount point.
79 var regexp
= new RegExp(mount
.concat('$'));
81 if(!data
.icestats
.source
) {
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
];
91 var stats
= data
.icestats
.source
.find(function(source
) {
92 return regexp
.test(source
.listenurl
);
95 return stats
|| app
.nullStats
;
99 app
.validStreamInfo = function(stats
) {
100 var name
= stats
.server_name
;
101 var desc
= stats
.server_description
;
103 return name
&& desc
&& name
!== "Unspecified name" &&
104 desc
!== "Unspecified description";
107 app
.mountToStreamUrl = function(mount
) {
108 return app
.icecastUrl
.concat(mount
);
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);
120 app
.withVideo = function(id
, callback
) {
122 var video
= document
.getElementById(id
);
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"
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"
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"
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);
157 // Check stats every 10 seconds.
158 app
.scheduleEvery(10000, this.updateStats
.bind(this));
161 app
.controller
.prototype.updateStats = function() {
162 //this.stats = app.streamStats(this.stream().speakerMount);
165 app
.view = function(ctrl
) {
166 var stream
= ctrl
.stream();
167 var stats
= ctrl
.stats() || app
.nullStats
;
168 var showDesktop
= ctrl
.showDesktop();
170 function renderSpeakerStream() {
171 return m("video.lp-video", {
175 // Sync desktop stream state as best we can.
176 onpause
: app
.withVideo("desktop-video", function(video
) {
179 onplay
: app
.withVideo("desktop-video", function(video
) {
182 // onended: app.withVideo("desktop-video", function(video) {
183 // setTimeout(function() {
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
189 // // we also probably need to re-try loading the video if it fails to
198 src
: app
.mountToStreamUrl(stream
.speakerMount
) + "?t=" + (new Date() * 1)
202 "Your browser does not support the HTML5 video tag, ",
203 m("a", { href
: "TODO" }, "[ please download ]"),
209 function renderDesktopStream() {
210 return m("video.lp-video", {
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
220 // // we also probably need to re-try loading the video if it fails to
227 m("source", { src
: app
.mountToStreamUrl(stream
.desktopMount
) + "?t=" + (new Date() * 1) }),
230 "Your browser does not support the HTML5 video tag, ",
231 m("a", { href
: "TODO" }, "[ please download ]"),
238 function renderToggleDesktopStream() {
239 var action = showDesktop ? "Hide slides stream" : "Show slides stream";
242 m(".col-sm-offset-4.col-sm-4",
243 m("button.btn.btn-block.btn-default", {
244 onclick: function() {
245 ctrl.showDesktop(!showDesktop);
253 function renderRoomSelector() {
255 m(".col-sm-offset-1.col-sm-10",
256 m("ol.breadcrumb.text-center", app
.streams
.map(function(s
) {
258 class: s
=== stream
? "active" : null,
259 onclick: function() {
260 var speakerVideo
= document
.getElementById("speaker-video");
261 var desktopVideo
= document
.getElementById("desktop-video");
263 app
.changeVideoMount(speakerVideo
, s
.speakerMount
);
265 // Video element doesn't exist when the user
266 // hasn't elected to show it.
268 app
.changeVideoMount(desktopVideo
, s
.desktopMount
);
276 }, m("a.alt-a", { href
: "#" }, s
.name
));
280 function renderStats() {
283 if(stats
=== app
.nullStats
) {
284 info
= m("i", "not broadcasting");
286 info
= m("i", "live");
288 // else if(app.validStreamInfo(stats)) {
290 // m("strong", stats.server_name),
292 // m("i", stats.server_description)
299 // m(".col-sm-8", info),
300 // m(".col-sm-4.text-right", [
301 // m("strong", stats.listeners),
309 renderRoomSelector(),
310 m("h2", stream
.name
),
312 renderSpeakerStream(),
313 showDesktop
? renderDesktopStream() : null,
314 // renderToggleDesktopStream(),
315 m("p", "Join the discussion online!"),
318 "Conference-wide Freenode IRC channel: ",
319 m("strong", "/join #libreplanet")
322 "Freenode IRC channel for ",
325 m("strong", ["/join ", stream
.ircChannel
])
328 // "Conference-wide Mumble (voice chat) server: ",
329 // m("strong", "mumble.fsf.org")
332 "Conference hashtag for ",
333 m("a", { href
: "https://fsf.org/twitter" }, "microblogging"),
335 m("strong", "#libreplanet")
342 m
.module(document
.getElementById("stream"), app
);