WebInterface: Add experimental browser based stream player. Test it on your local...
[enigma2-plugins.git] / webinterface / src / stream / js / main.js
1 var dreamboxWebSocket = (function() {
2         "use strict";
3         var _ws = null;
4         var _requestMap = {};
5         var _requestID = 0;
6         var _streamSession = userprefs.data["stream_session"];
7         var _sessionValid = userprefs.data["stream_session"] != undefined;
8         var _baseUrl = window.location.hostname + ":" + window.location.port;
9
10         var _authenticationRequest = function() {};
11         var _readyCallback = function() {};
12         var _closeCallback = function() {};
13
14         function connectInternal(authCb, readyCb, closeCb) {
15                 _authenticationRequest = authCb;
16                 _readyCallback = readyCb;
17                 _closeCallback = closeCb;
18                 reconnectInternal();
19         }
20
21         function reconnectInternal() {
22                 var protocol = window.location.protocol == "https" ? "wss" : "ws";
23                 var port = window.location.port;
24                 if (Number.parseInt(port) == Number.NaN) //no port value set for locations using the default port
25                         port = protocol ==  "ws" ? 80 : 443;
26                 var uri = protocol + "://" + window.location.hostname + ":" + port + "/ws";
27                 _ws = new WebSocket(uri);
28                 _ws.onmessage = onMessageInternal.bind(this);
29                 _ws.onclose = onCloseInternal.bind(this);
30                 _ws.onerror = onErrorInternal.bind(this);
31         }
32
33         function onCloseInternal(event) {
34                 if (!event.wasClean)
35                         reconnectInternal();
36                 _closeCallback(event);
37         }
38
39         function onErrorInternal(event) {
40                 console.log(event);
41         }
42
43         function disconnectInternal() {
44                 if (_ws == null)
45                         return;
46                 _ws.close();
47                 _ws = null;
48         }
49
50         function onMessageInternal(msg) {
51                 var data = JSON.parse(msg.data)
52                 console.log(data);
53                 if (data.type == "auth_ok") {
54                         _streamSession = data.session;
55                         userprefs.data["stream_session"] = _streamSession;
56                         userprefs.save();
57                         _sessionValid = true;
58                         _readyCallback();
59                         return;
60                 } else if (data.type == "auth_required") {
61                         if (_sessionValid && _streamSession != null) {
62                                 callFunctionInternal("auth", {"session" : _streamSession}, function(data){});
63                                 _sessionValid = false;
64                                 _streamSession = null;
65                         } else {
66                                 _authenticationRequest();
67                         }
68                 } else if (data.type == "result") {
69                         var callback = _requestMap[data.id]
70                         if (callback == undefined)
71                                 return;
72                         callback(data);
73                         delete _requestMap[data.id];
74                 }
75         }
76
77         function callFunctionInternal(type, data, callback) {
78                 _requestID++;
79                 data["type"] = type;
80                 data["id"] = _requestID;
81                 console.log(data);
82                 _requestMap[_requestID] = callback;
83                 _ws.send(JSON.stringify(data));
84                 return _requestID;
85         }
86
87         return {
88                 connect : connectInternal,
89                 disconnect : disconnectInternal,
90                 callFunction : callFunctionInternal,
91                 streamSession : _streamSession
92         }
93 })();
94
95 var dreamboxPlayer = (function() {
96         "use strict";
97         var _hls = null;
98         var _currentBouquet = null;
99         var _currentService = null;
100         var _pendingService = null;
101         var _streamHost = 'http://' + window.document.location.host + ':8080';
102         var _videoElement = document.getElementById('video');
103
104         var _dialogLogin = null;
105         var _dialogBitrates = null;
106
107         var _streamSettings = {};
108
109         function startInternal() {
110                 /* reload bouquet when drawer is opened via button */
111                 var drawerBtn = document.querySelector('.mdl-layout__drawer-button');
112                 drawerBtn.addEventListener('click', function(event) {
113                         if (_currentBouquet != null)
114                                 loadBouquetInternal(_currentBouquet);
115                 });
116                 setupDialogs();
117                 dreamboxWebSocket.onAuthenticationRequired = onAuthenticationRequired
118                 dreamboxWebSocket.connect(onAuthenticationRequired, onReady, onDisconnect);
119         }
120
121         function setupDialogs() {
122                 _dialogLogin = document.querySelector('#login_dialog');
123                 if (!_dialogLogin.showModal) {
124                         dialogPolyfill.registerDialog(_dialogLogin);
125                 }
126                 _dialogLogin.querySelector('.send').addEventListener('click', function(event) {
127                         dreamboxPlayer.login();
128                         return false;
129                 });
130                 var loginForm = _dialogLogin.querySelector('#login_form');
131
132                 _dialogBitrates = document.querySelector('#bitrate_dialog');
133                 var showDialogButton = document.querySelector('#button-bitrates');
134                 if (!_dialogBitrates.showModal) {
135                         dialogPolyfill.registerDialog(_dialogBitrates);
136                 }
137                 showDialogButton.addEventListener('click', showBitrateSettings);
138                 _dialogBitrates.querySelector('.close').addEventListener('click', function() {
139                         _dialogBitrates.close();
140                 });
141                 _dialogBitrates.querySelector('.ok').addEventListener('click', function() {
142                         var audioBitrateE = document.getElementById("audioBitrate");
143                         var videoBitrateE = document.getElementById("videoBitrate");
144                         applyBitrates(audioBitrateE.value,videoBitrateE.value);
145                         _dialogBitrates.close();
146                 });
147         }
148
149         function loadStreamSettings() {
150                 dreamboxWebSocket.callFunction("get_stream_settings", {}, onStreamSettingsReady.bind(this))
151         }
152
153         function onStreamSettingsReady(response) {
154                 if (!response.success) {
155                         notify("Error loading stream settings!");
156                         return;
157                 }
158                 _streamSettings = response.result;
159                 var videoBitrateE = document.getElementById("videoBitrate");
160                 videoBitrateE.value = _streamSettings.videoBitrate;
161                 videoBitrateE.parentElement.classList.add("is-dirty");
162
163                 var audioBitrateE = document.getElementById("audioBitrate");
164                 audioBitrateE.value = _streamSettings.audioBitrate;
165                 audioBitrateE.parentElement.classList.add("is-dirty");
166         }
167
168         function showBitrateSettings() {
169                 loadStreamSettings();
170                 _dialogBitrates.showModal();
171         }
172
173         function onAuthenticationRequired() {
174                 _dialogLogin.showModal();
175         }
176
177         function loginInternal() {
178                 var user = _dialogLogin.querySelector("#login_user").value;
179                 var pass = _dialogLogin.querySelector("#login_pass").value;
180                 var token = btoa(user + ":" + pass);
181                 dreamboxWebSocket.callFunction("auth", {"token" : token}, onLoginResult);
182         }
183
184         function onLoginResult(result) {
185                 var loginContainer = _dialogLogin.querySelector('#login_container');
186                 var classes = loginContainer.classList;
187                 if (result.success) {
188                         if (classes.contains('is-invalid'))
189                                 classes.remove('is-invalid');
190                 } else {
191                         if (!classes.contains('is-invalid'))
192                                 classes.add('is-invalid');
193                 }
194         }
195
196         function onReady() {
197                 notify("Welcome!");
198                 if (_dialogLogin.open)
199                         _dialogLogin.close();
200                 loadStreamSettings();
201                 loadServicesInternal(); // List of TV Bouquets
202         }
203
204         function onDisconnect(event) {
205                 if (!event.wasClean) {
206                         notify("Reconnecting...", 1000);
207                 } else {
208                         notify("Connection Lost - " + event.reason + " (" + event.code + ")", 1000);
209                 }
210                 console.log(event);
211         }
212
213         function setupHls() {
214                 var config = {
215                         xhrSetup : function(xhr, url) {
216                                 xhr.withCredentials = true; // do send cookies
217                                 //xhr.setRequestHeader("X-Stream-Session", dreamboxWebSocket.streamSession);
218                         },
219                         autoStartLoad : true,
220                         startPosition : -1,
221                         capLevelToPlayerSize : false,
222                         debug : true /*,
223                         liveSyncDurationCount : 3,
224                         liveMaxLatencyDurationCount : 6,
225                         initialLiveManifestSize : 1,
226                         maxBufferLength : 30,
227                         maxMaxBufferLength : 600,
228                         maxBufferSize : 60 * 1000 * 1000,
229                         maxBufferHole : 0.5,
230                         lowBufferWatchdogPeriod : 0.5,
231                         highBufferWatchdogPeriod : 3,
232                         nudgeOffset : 0.1,
233                         nudgeMaxRetry : 3,
234                         maxFragLookUpTolerance : 0.2,
235                         */
236                 };
237                 _hls = new Hls(config);
238
239                 var url = _streamHost + '/stream.m3u8';
240                 _hls.loadSource(url);
241                 _hls.attachMedia(_videoElement);
242                 _hls.on(Hls.Events.MANIFEST_PARSED, function() {
243                         _videoElement.play();
244                 });
245         }
246
247         function notify(message, timeout) {
248                 var snackbarContainer = document.querySelector('#snackbar');
249                 var data = {'message' : message};
250                 if (timeout != undefined)
251                         data['timeout'] = timeout;
252                 snackbarContainer.MaterialSnackbar.showSnackbar(data);
253         }
254
255         function changeServiceResponse(response) {
256                 if (this.readyState == 4 && this.status == 200) {
257                         var res = JSON.parse(this.responseText);
258                         console.log(res)
259                         if (!res.result) {
260                                 _pendingService = _currentService;
261                         }
262                 }
263         }
264
265         function changeService(reference) {
266                 _currentService = reference;
267                 console.log(reference);
268                 var url = 'http://' + window.document.location.host + ':8080/api.json?ref=' + encodeURIComponent(reference);
269                 var request = new XMLHttpRequest();
270                 request.addEventListener("readystatechange", changeServiceResponse);
271                 request.overrideMimeType('application/json');
272                 request.open("GET", url);
273                 request.send();
274         }
275
276         function changeBitrateResponse(response) {
277                 if (this.readyState == 4 && this.status == 200) {
278                         var res = JSON.parse(this.responseText);
279                         console.log(res)
280                         if (res.result) {
281                                 notify("New bitrates applied!");
282                         } else {
283                                 notify("Could not apply new bitrates!");
284                         }
285                 }
286         }
287
288         function applyBitrates(audioBitrate, videoBitrate) {
289                 var ab = Number.parseInt(audioBitrate);
290                 var vb = Number.parseInt(videoBitrate);
291                 if (ab == Number.NaN || vb == Number.NaN) {
292                         notify("Invalid Bitrate value!");
293                         return;
294                 }
295                 _streamSettings.audioBitrate = ab;
296                 _streamSettings.videoBitrate = vb;
297                 var url = 'http://' + window.document.location.host + ':8080/api.json?audio_bitrate=' + encodeURIComponent(audioBitrate) + '&video_bitrate=' + encodeURIComponent(videoBitrate);
298                 var request = new XMLHttpRequest();
299                 request.addEventListener("readystatechange", changeBitrateResponse);
300                 request.overrideMimeType('application/json');
301                 request.open("GET", url);
302                 request.send();
303         }
304
305         function onMetadataReady() {
306                 if (_pendingService != null) {
307                         changeService(_pendingService);
308                         _pendingService = null;
309                 }
310         }
311
312         function playInternal(reference) {
313                 notify("Stream is loading...", 6000);
314                 // use native where possible
315                 if (_videoElement.canPlayType('application/vnd.apple.mpegurl')) {
316                         _videoElement.src = _streamHost + '/stream.m3u8';
317                         _videoElement.addEventListener('canplay', function() {
318                                 _videoElement.play();
319                         });
320                 } else if (Hls.isSupported()) {
321                         if (_hls == null)
322                                 setupHls();
323                         _videoElement.play();
324                         // fallback to hls.js
325                 } else {
326                         notify("Sorry, your browser seems to be too old for this type of stream!");
327                         return;
328                 }
329                 if (reference != undefined)
330                         changeService(reference);
331         }
332
333         function onServiceClick(event) {
334                 var ref = event.target.getAttribute("data-reference");
335                 if (ref == null)
336                         ref = event.target.parentElement.getAttribute("data-reference");
337                 playInternal(ref);
338                 toggleDrawer();
339         }
340
341         function toggleDrawer() {
342                 var layout = document.querySelector('.mdl-layout');
343                 layout.MaterialLayout.toggleDrawer();
344         }
345
346         function onBouquetClick(event) {
347                 var ref = event.target.getAttribute("data-reference");
348                 if (ref == null)
349                         ref = event.target.parentElement.getAttribute("data-reference");
350                 loadBouquetInternal(ref);
351                 toggleDrawer();
352         }
353
354         function fillServiceList(data) {
355                 var parent = document.getElementById('services');
356                 parent.innerHTML = null;
357                 var result = data.result;
358                 var bouquetElement = document.getElementById('menu-bouquet')
359                 bouquetElement.innerHTML = result.name;
360
361                 result.data.forEach(function(evt) {
362                         var a = document.createElement('a');
363                         a.href = "#" + evt.reference;
364                         a.setAttribute('data-reference', evt.reference);
365                         a.className = 'mdl-navigation__link';
366                         a.style = "text-align: left;";
367                         var span = document.createElement('div');
368                         var textNode = document.createTextNode(evt.name);
369                         span.appendChild(textNode);
370                         span.className = "mdl-typography--title";
371                         componentHandler.upgradeElement(span);
372                         a.appendChild(span);
373
374                         if (evt.title != null) {
375                                 span = document.createElement('div');
376                                 textNode = document.createTextNode(evt.title);
377                                 span.appendChild(textNode);
378                                 componentHandler.upgradeElement(span);
379                                 a.appendChild(span);
380                         }
381                         componentHandler.upgradeElement(a);
382                         parent.appendChild(a)
383                         a.onclick = onServiceClick;
384                 });
385         }
386
387         function fillBouquetList(data) {
388                 var parent = document.getElementById('bouquets');
389                 parent.innerHTML = null;
390                 var result = data.result;
391                 var ref = null;
392                 result.data.forEach(function(service) {
393                         if (ref == null)
394                                 ref = service.reference;
395                         var a = document.createElement('a');
396                         a.href = '#';
397                         a.className = 'mdl-button mdl-js-button mdl-js-ripple-effect';
398                         a.style = 'color: white';
399                         a.setAttribute('data-reference', service.reference);
400                         a.onclick = onBouquetClick;
401                         var textNode = document.createTextNode(service.name);
402                         a.appendChild(textNode);
403                         componentHandler.upgradeElement(a);
404                         parent.appendChild(a)
405                 });
406                 loadBouquetInternal(ref); // Favorites TV
407         }
408
409         function loadServicesInternal(reference) {
410                 if (reference == undefined || reference == null) // TV Bouquets as default
411                         reference = "1:7:1:0:0:0:0:0:0:0:(type == 1) || (type == 17) || (type == 22) || (type == 25) || (type == 31) || (type == 134) || (type == 195) FROM BOUQUET \"bouquets.tv\" ORDER BY bouquet";
412                 dreamboxWebSocket.callFunction("get_services", {"reference" : reference}, fillBouquetList);
413         }
414
415         function loadBouquetInternal(reference) {
416                 if (reference == undefined || reference == null)
417                         reference = "1:7:1:0:0:0:0:0:0:0:FROM BOUQUET \"userbouquet.favourites.tv\" ORDER BY bouquet";
418                 _currentBouquet = reference;
419                 dreamboxWebSocket.callFunction("get_epg_nownext", {
420                         "reference" : reference,
421                         which : 0 // now
422                 }, fillServiceList);
423         }
424
425         return {
426                 start : startInternal,
427                 login : loginInternal,
428                 loadBouquet : loadBouquetInternal,
429                 loadServices : loadServicesInternal
430         }
431 })();
432 window.addEventListener('load', function() {
433         dreamboxPlayer.start();
434         const plyr = new Plyr('#video', {
435                 controls : [ 'play-large', 'play', 'mute', 'volume', 'settings', 'airplay', 'fullscreen', 'progress' ],
436                 settings : [ 'quality', 'speed' ],
437                 autoplay : true,
438                 disableContextMenu : false,
439                 displayDuration : false,
440                 fullscreen : {
441                         enabled : true,
442                         fallback : true,
443                         iosNative : false
444                 },
445                 iconUrl : window.location.protocol + '//' + window.location.hostname + '/stream/plyr.svg'
446         });
447 });