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://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
);
133 speakerMount
: "/stream-123.webm",
134 desktopMount
: "/slides-123.webm",
135 speakerSmallMount
: "/stream-123-480p.webm",
136 ircChannel
: "#libreplanet_room123"
139 speakerMount
: "/stream-144.webm",
140 desktopMount
: "/slides-144.webm",
141 speakerSmallMount
: "/stream-144-480p.webm",
142 ircChannel
: "#libreplanet_room144"
145 speakerMount
: "/stream-155.webm",
146 desktopMount
: "/slides-155.webm",
147 speakerSmallMount
: "/stream-155-480p.webm",
148 ircChannel
: "#libreplanet_room155"
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
) {
184 src
: app
.mountToStreamUrl(stream
.speakerMount
) + "?t=" + (new Date() * 1)
188 "Your browser does not support the HTML5 video tag, ",
189 m("a", { href
: "TODO" }, "[ please download ]"),
195 function renderDesktopStream() {
196 return m("video.lp-video", {
201 m("source", { src
: app
.mountToStreamUrl(stream
.desktopMount
) + "?t=" + (new Date() * 1) }),
204 "Your browser does not support the HTML5 video tag, ",
205 m("a", { href
: "TODO" }, "[ please download ]"),
211 function renderToggleDesktopStream() {
212 var action
= showDesktop
? "Hide desktop stream" : "Show desktop stream";
215 m(".col-sm-offset-4.col-sm-4",
216 m("button.btn.btn-block.btn-default", {
217 onclick: function() {
218 ctrl
.showDesktop(!showDesktop
);
225 function renderRoomSelector() {
227 m(".col-sm-offset-1.col-sm-10",
228 m("ol.breadcrumb.text-center", app
.streams
.map(function(s
) {
230 class: s
=== stream
? "active" : null,
231 onclick: function() {
232 var speakerVideo
= document
.getElementById("speaker-video");
233 var desktopVideo
= document
.getElementById("desktop-video");
235 app
.changeVideoMount(speakerVideo
, s
.speakerMount
);
237 // Video element doesn't exist when the user
238 // hasn't elected to show it.
240 app
.changeVideoMount(desktopVideo
, s
.desktopMount
);
248 }, m("a.alt-a", { href
: "#" }, s
.name
));
252 function renderStats() {
255 if(stats
=== app
.nullStats
) {
256 info
= m("i", "not broadcasting");
258 info
= m("i", "live");
260 // else if(app.validStreamInfo(stats)) {
262 // m("strong", stats.server_name),
264 // m("i", stats.server_description)
271 // m(".col-sm-8", info),
272 // m(".col-sm-4.text-right", [
273 // m("strong", stats.listeners),
281 renderRoomSelector(),
282 m("h2", stream
.name
),
284 renderSpeakerStream(),
285 showDesktop
? renderDesktopStream() : null,
286 renderToggleDesktopStream(),
287 m("p", "Join the discussion online!"),
290 "Conference-wide Freenode IRC channel: ",
291 m("strong", "/join #libreplanet")
294 "Freenode IRC channel for ",
297 m("strong", ["/join ", stream
.ircChannel
])
300 "Conference-wide Mumble (voice chat) server: ",
301 m("strong", "mumble.fsf.org")
304 "Conference hashtag for ",
305 m("a", { href
: "https://fsf.org/twitter" }, "microblogging"),
307 m("strong", "#libreplanet")
314 m
.module(document
.getElementById("stream"), app
);