8aa689549bafff4e12e52963880bb0cd89700572
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
= "http://live2.fsf.org";
51 app
.icecastApiUrl
= "//live2.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
);
120 app
.withVideo = function(id
, callback
) {
122 var video
= document
.getElementById(id
);
133 speakerMount
: "/stream-123.webm",
134 desktopMount
: "/slides-123.webm",
135 speakerSmallMount
: "/stream-123-480p.webm",
136 ircChannel
: "#libreplanet_room123"
139 speakerMount
: "/stream-141.webm",
140 desktopMount
: "/slides-141.webm",
141 speakerSmallMount
: "/stream-141-480p.webm",
142 ircChannel
: "#libreplanet_room141"
145 speakerMount
: "/stream-144.webm",
146 desktopMount
: "/slides-144.webm",
147 speakerSmallMount
: "/stream-144-480p.webm",
148 ircChannel
: "#libreplanet_room144"
151 speakerMount
: "/stream-155.webm",
152 desktopMount
: "/slides-155.webm",
153 speakerSmallMount
: "/stream-155-480p.webm",
154 ircChannel
: "#libreplanet_room155"
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);
163 // Check stats every 10 seconds.
164 app
.scheduleEvery(10000, this.updateStats
.bind(this));
167 app
.controller
.prototype.updateStats = function() {
168 this.stats
= app
.streamStats(this.stream().speakerMount
);
171 app
.view = function(ctrl
) {
172 var stream
= ctrl
.stream();
173 var stats
= ctrl
.stats() || app
.nullStats
;
174 var showDesktop
= ctrl
.showDesktop();
176 function renderSpeakerStream() {
177 return m("video.lp-video", {
181 // Sync desktop stream state as best we can.
182 onpause
: app
.withVideo("desktop-video", function(video
) {
185 onplay
: app
.withVideo("desktop-video", function(video
) {
190 src
: app
.mountToStreamUrl(stream
.speakerMount
)
194 "Your browser does not support the HTML5 video tag, ",
195 m("a", { href
: "TODO" }, "[ please download ]"),
201 function renderDesktopStream() {
202 return m("video.lp-video", {
206 m("source", { src
: app
.mountToStreamUrl(stream
.desktopMount
) }),
209 "Your browser does not support the HTML5 video tag, ",
210 m("a", { href
: "TODO" }, "[ please download ]"),
216 function renderToggleDesktopStream() {
217 var action
= showDesktop
? "Hide desktop stream" : "Show desktop stream";
220 m(".col-sm-offset-4.col-sm-4",
221 m("button.btn.btn-block.btn-default", {
222 onclick: function() {
223 ctrl
.showDesktop(!showDesktop
);
230 function renderRoomSelector() {
232 m(".col-sm-offset-1.col-sm-10",
233 m("ol.breadcrumb.text-center", app
.streams
.map(function(s
) {
235 class: s
=== stream
? "active" : null,
236 onclick: function() {
237 var speakerVideo
= document
.getElementById("speaker-video");
238 var desktopVideo
= document
.getElementById("desktop-video");
240 app
.changeVideoMount(speakerVideo
, s
.speakerMount
);
242 // Video element doesn't exist when the user
243 // hasn't elected to show it.
245 app
.changeVideoMount(desktopVideo
, s
.desktopMount
);
253 }, m("a.alt-a", { href
: "#" }, s
.name
));
257 function renderStats() {
260 if(stats
=== app
.nullStats
) {
261 info
= m("i", "not broadcasting");
263 info
= m("i", "live");
265 // else if(app.validStreamInfo(stats)) {
267 // m("strong", stats.server_name),
269 // m("i", stats.server_description)
276 m(".col-sm-8", info
),
277 m(".col-sm-4.text-right", [
278 m("strong", stats
.listeners
),
285 renderRoomSelector(),
286 m("h2", stream
.name
),
288 renderSpeakerStream(),
289 showDesktop
? renderDesktopStream() : null,
290 renderToggleDesktopStream(),
291 m("p", [ m("a", { href
: app
.icecastUrl
+ stream
.speakerSmallMount
}, "Low res stream" ) ] ),
293 m("p", "Join the discussion online!"),
296 "Conference-wide Freenode IRC channel: ",
297 m("strong", "#libreplanet")
300 "Freenode IRC channel for ",
303 m("strong", stream
.ircChannel
)
306 "Conference hashtag for ",
307 m("a", { href
: "https://fsf.org/twitter" }, "microblogging"),
309 m("strong", "#libreplanet")
315 m
.module(document
.getElementById("stream"), app
);