From 42b0b05d7838ac4b53d4decda2a44fc6c009b482 Mon Sep 17 00:00:00 2001 From: gingervitis Date: Thu, 16 Jul 2020 12:17:05 -0700 Subject: [PATCH] App Javascript refactor (#56) * objectify app away from window. wip * fix messaging obj binding; put logo behind video; fix /null issue with temp logo image * first pass at js refactor * remove unused files that had been consolidated during refactor * set up vue before getting config * add a few comments * dont use big arrow function, just bind, for safari * add airplay after instantiating video; check if input exists before disabling it;: * only set poster on pause during playback, and onEnded; take out sample videoJS tech options * disable chat after 5mins after going offline * move 'online' class to video container as it conflicts with dynamically change classnames from non-vue sources * disable chat based on lastdisconnecttime * fix typo; do offline mode onEnded instead of status offline * move offline ui display things to offline mode function; move poster setting on pause to main app to keep player obj cleaner; use opacity to hide video element on offline as sometimes control bars may still linger with vis:hidden * fixes' * don't autoplay. just show play button when stream is online so that it's easier to start playign without looking for the unmute button * clean up console logs Co-authored-by: Gabe Kangas --- webroot/index.html | 39 ++-- webroot/js/app.js | 337 +++++++++++++++++++++++++++-------- webroot/js/config.js | 16 -- webroot/js/message.js | 97 +++++++--- webroot/js/player.js | 115 ++++++++++++ webroot/js/player/airplay.js | 23 --- webroot/js/player/player.js | 75 -------- webroot/js/status.js | 44 ----- webroot/js/utils.js | 59 +++--- webroot/styles/layout.css | 25 ++- 10 files changed, 504 insertions(+), 326 deletions(-) delete mode 100644 webroot/js/config.js create mode 100644 webroot/js/player.js delete mode 100644 webroot/js/player/airplay.js delete mode 100644 webroot/js/player/player.js delete mode 100644 webroot/js/status.js diff --git a/webroot/index.html b/webroot/index.html index 235a13548..fb9726a81 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -13,20 +13,10 @@ - -
+

@@ -69,17 +59,18 @@ GW TODO:

-
-
+
+
@@ -109,7 +100,6 @@ GW TODO:
-
@@ -172,17 +162,22 @@ GW TODO:
+
- + - + - - + \ No newline at end of file diff --git a/webroot/js/app.js b/webroot/js/app.js index 3a095e9e6..04b97512b 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -1,98 +1,279 @@ -async function setupApp() { - Vue.filter('plural', pluralize); +class Owncast { + constructor() { + this.player; + this.streamStatus = null; - window.app = new Vue({ - el: "#app-container", - data: { - streamStatus: MESSAGE_OFFLINE, // Default state. - viewerCount: 0, - sessionMaxViewerCount: 0, - overallMaxViewerCount: 0, - messages: [], - extraUserContent: "", - isOnline: false, - layout: "desktop", + this.websocket = null; + this.configData; + this.vueApp; + this.messagingInterface = null; - // from config - logo: null, - socialHandles: [], - streamerName: "", - summary: "", - tags: [], - title: "", - appVersion: "", - }, - watch: { - messages: { - deep: true, - handler: function (newMessages, oldMessages) { - if (newMessages.length !== oldMessages.length) { - // jump to bottom - jumpToBottom(appMessaging.scrollableMessagesContainer); - } + // timers + this.websocketReconnectTimer = null; + this.playerRestartTimer = null; + this.offlineTimer = null; + this.statusTimer = null; + this.disableChatTimer = null; + + // misc + this.streamIsOnline = false; + this.lastDisconnectTime = null; + + Vue.filter('plural', pluralize); + + // bindings + this.vueAppMounted = this.vueAppMounted.bind(this); + this.setConfigData = this.setConfigData.bind(this); + this.setupWebsocket = this.setupWebsocket.bind(this); + this.getStreamStatus = this.getStreamStatus.bind(this); + this.getExtraUserContent = this.getExtraUserContent.bind(this); + this.updateStreamStatus = this.updateStreamStatus.bind(this); + this.handleNetworkingError = this.handleNetworkingError.bind(this); + this.handleOfflineMode = this.handleOfflineMode.bind(this); + this.handleOnlineMode = this.handleOnlineMode.bind(this); + this.handleNetworkingError = this.handleNetworkingError.bind(this); + this.handlePlayerReady = this.handlePlayerReady.bind(this); + this.handlePlayerPlaying = this.handlePlayerPlaying.bind(this); + this.handlePlayerEnded = this.handlePlayerEnded.bind(this); + this.handlePlayerError = this.handlePlayerError.bind(this); + } + + init() { + this.messagingInterface = new MessagingInterface(); + this.websocket = this.setupWebsocket(); + + this.vueApp = new Vue({ + el: '#app-container', + data: { + isOnline: false, + layout: hasTouchScreen() ? 'touch' : 'desktop', + messages: [], + overallMaxViewerCount: 0, + sessionMaxViewerCount: 0, + streamStatus: MESSAGE_OFFLINE, // Default state. + viewerCount: 0, + + // from config + appVersion: '', + extraUserContent: '', + logo: TEMP_IMAGE, + logoLarge: TEMP_IMAGE, + socialHandles: [], + streamerName: '', + summary: '', + tags: [], + title: '', + }, + watch: { + messages: { + deep: true, + handler: this.messagingInterface.onReceivedMessages, }, }, - }, - }); + mounted: this.vueAppMounted, + }); + } + // do all these things after Vue.js has mounted, else we'll get weird DOM issues. + vueAppMounted() { + this.getConfig(); + this.messagingInterface.init(); + this.player = new OwncastPlayer(); + this.player.setupPlayerCallbacks({ + onReady: this.handlePlayerReady, + onPlaying: this.handlePlayerPlaying, + onEnded: this.handlePlayerEnded, + onError: this.handlePlayerError, + }); + this.player.init(); + }; - // init messaging interactions - var appMessaging = new Messaging(); - appMessaging.init(); + setConfigData(data) { + this.vueApp.appVersion = data.version; + this.vueApp.logo = data.logo.small; + this.vueApp.logoLarge = data.logo.large; + this.vueApp.socialHandles = data.socialHandles; + this.vueApp.streamerName = data.name; + this.vueApp.summary = data.summary && addNewlines(data.summary); + this.vueApp.tags = data.tags; + this.vueApp.title = data.title; - const config = await new Config().init(); - app.logo = config.logo.small; - app.socialHandles = config.socialHandles; - app.streamerName = config.name; - app.summary = config.summary && addNewlines(config.summary); - app.tags = config.tags; - app.appVersion = config.version; - app.title = config.title; - window.document.title = config.title; + window.document.title = data.title; - getExtraUserContent(`${URL_PREFIX}${config.extraUserInfoFileName}`); -} + this.getExtraUserContent(`${URL_PREFIX}${data.extraUserInfoFileName}`); -var websocketReconnectTimer; -function setupWebsocket() { - clearTimeout(websocketReconnectTimer); + this.configData = data; + } - var ws = new WebSocket(URL_WEBSOCKET); + // websocket for messaging + setupWebsocket() { + var ws = new WebSocket(URL_WEBSOCKET); + ws.onopen = (e) => { + if (this.websocketReconnectTimer) { + clearTimeout(this.websocketReconnectTimer); + } + }; + ws.onclose = (e) => { + // connection closed, discard old websocket and create a new one in 5s + this.websocket = null; + this.messagingInterface.disableChat(); + this.handleNetworkingError('Websocket closed.'); + this.websocketReconnectTimer = setTimeout(this.setupWebsocket, TIMER_WEBSOCKET_RECONNECT); + }; + // On ws error just close the socket and let it re-connect again for now. + ws.onerror = e => { + this.handleNetworkingError(`Stream status: ${e}`); + ws.close(); + }; + ws.onmessage = (e) => { + const model = JSON.parse(e.data); + // Ignore non-chat messages (such as keepalive PINGs) + if (model.type !== SOCKET_MESSAGE_TYPES.CHAT) { + return; + } + const message = new Message(model); + const existing = this.vueApp.messages.filter(function (item) { + return item.id === message.id; + }) + if (existing.length === 0 || !existing) { + this.vueApp.messages = [...this.vueApp.messages, message]; + } + }; + this.websocket = ws; + this.messagingInterface.setWebsocket(this.websocket); + }; - ws.onmessage = (e) => { - const model = JSON.parse(e.data); - - // Ignore non-chat messages (such as keepalive PINGs) - if (model.type !== SOCKET_MESSAGE_TYPES.CHAT) { return; } - - const message = new Message(model); - - const existing = this.app.messages.filter(function (item) { - return item.id === message.id; + // fetch /config data + getConfig() { + fetch(URL_CONFIG) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.json(); }) - - if (existing.length === 0 || !existing) { - this.app.messages = [...this.app.messages, message]; + .then(json => { + this.setConfigData(json); + }) + .catch(error => { + this.handleNetworkingError(`Fetch config: ${error}`); + }); + } + + // fetch stream status + getStreamStatus() { + fetch(URL_STATUS) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.json(); + }) + .then(json => { + this.updateStreamStatus(json); + }) + .catch(error => { + this.handleOfflineMode(); + this.handleNetworkingError(`Stream status: ${error}`); + }); + }; + + // fetch content.md + getExtraUserContent(path) { + fetch(path) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok ${response.ok}`); + } + return response.text(); + }) + .then(text => { + const descriptionHTML = new showdown.Converter().makeHtml(text); + this.vueApp.extraUserContent = descriptionHTML; + }) + .catch(error => { + this.handleNetworkingError(`Fetch extra content: ${error}`); + }); + }; + + // handle UI things from stream status result + updateStreamStatus(status) { + // update UI + this.vueApp.streamStatus = status.online ? MESSAGE_ONLINE : MESSAGE_OFFLINE; + this.vueApp.viewerCount = status.viewerCount; + this.vueApp.sessionMaxViewerCount = status.sessionMaxViewerCount; + this.vueApp.overallMaxViewerCount = status.overallMaxViewerCount; + + this.lastDisconnectTime = status.lastDisconnectTime; + + if (status.online && !this.streamIsOnline) { + // stream has just come online. + this.handleOnlineMode(); + } else if (!status.online && !this.streamStatus) { + // stream has just gone offline. + // display offline mode the first time we get status, and it's offline. + this.handleOfflineMode(); } + + if (status.online) { + // only do this if video is paused, so no unnecessary img fetches + if (this.player.vjsPlayer && this.player.vjsPlayer.paused()) { + this.player.setPoster(); + } + } + + this.streamStatus = status; + }; + + handleNetworkingError(error) { + console.log(`>>> App Error: ${error}`) + }; + + // basically hide video and show underlying "poster" + handleOfflineMode() { + this.streamIsOnline = false; + this.vueApp.isOnline = false; + this.vueApp.streamStatus = MESSAGE_OFFLINE; + + if (this.lastDisconnectTime) { + const remainingChatTime = TIMER_DISABLE_CHAT_AFTER_OFFLINE - (Date.now() - new Date(this.lastDisconnectTime)); + const countdown = (remainingChatTime < 0) ? 0 : remainingChatTime; + this.disableChatTimer = setTimeout(this.messagingInterface.disableChat, countdown); + } + }; + + // play video! + handleOnlineMode() { + this.streamIsOnline = true; + this.vueApp.isOnline = true; + this.vueApp.streamStatus = MESSAGE_ONLINE; + + this.player.startPlayer(); + clearTimeout(this.disableChatTimer); + this.disableChatTimer = null; + this.messagingInterface.enableChat(); } - ws.onclose = (e) => { - // connection closed, discard old websocket and create a new one in 5s - ws = null; - console.log("Websocket closed.") - websocketReconnectTimer = setTimeout(setupWebsocket, 5000); - } + // when videojs player is ready, start polling for stream + handlePlayerReady() { + this.getStreamStatus(); + this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); + }; - // On ws error just close the socket and let it re-connect again for now. - ws.onerror = (e) => { - console.log("Websocket error: ", e); - ws.close(); - } - window.ws = ws; -} + handlePlayerPlaying() { + // do something? + }; -setupApp(); -setupWebsocket(); + handlePlayerEnded() { + // do something? + this.handleOfflineMode(); + }; + handlePlayerError() { + // do something? + this.handleOfflineMode(); + // stop timers? + }; +}; diff --git a/webroot/js/config.js b/webroot/js/config.js deleted file mode 100644 index 1602ea62e..000000000 --- a/webroot/js/config.js +++ /dev/null @@ -1,16 +0,0 @@ -// add more to the promises later. -class Config { - async init() { - const configFileLocation = "/config"; - - try { - const response = await fetch(configFileLocation); - const configData = await response.json(); - Object.assign(this, configData); - return this; - } catch(error) { - console.log(error); - // No config file present. That's ok. It's not required. - } - } -} \ No newline at end of file diff --git a/webroot/js/message.js b/webroot/js/message.js index dd8760ff3..5fc0687ae 100644 --- a/webroot/js/message.js +++ b/webroot/js/message.js @@ -25,36 +25,42 @@ class Message { -class Messaging { +class MessagingInterface { constructor() { + this.websocket = null; this.chatDisplayed = false; this.username = ''; - this.messageCharCount = 0; this.maxMessageLength = 500; this.maxMessageBuffer = 20; - this.tagAppContainer = document.querySelector('#app-container'); - this.tagChatToggle = document.querySelector('#chat-toggle'); - this.tagUserInfoChanger = document.querySelector('#user-info-change'); - this.tagUsernameDisplay = document.querySelector('#username-display'); - this.tagMessageFormWarning = document.querySelector('#message-form-warning'); - - this.inputMessageAuthor = document.querySelector('#self-message-author'); - this.inputChangeUserName = document.querySelector('#username-change-input'); - - this.btnUpdateUserName = document.querySelector('#button-update-username'); - this.btnCancelUpdateUsername = document.querySelector('#button-cancel-change'); - this.btnSubmitMessage = document.querySelector('#button-submit-message'); - - this.formMessageInput = document.querySelector('#message-body-form'); - - this.imgUsernameAvatar = document.querySelector('#username-avatar'); - this.textUserInfoDisplay = document.querySelector('#user-info-display'); - - this.scrollableMessagesContainer = document.querySelector('#messages-container'); + this.onReceivedMessages = this.onReceivedMessages.bind(this); + this.disableChat = this.disableChat.bind(this); + this.enableChat = this.enableChat.bind(this); } + init() { + this.tagAppContainer = document.getElementById('app-container'); + this.tagChatToggle = document.getElementById('chat-toggle'); + this.tagUserInfoChanger = document.getElementById('user-info-change'); + this.tagUsernameDisplay = document.getElementById('username-display'); + this.tagMessageFormWarning = document.getElementById('message-form-warning'); + + this.inputMessageAuthor = document.getElementById('self-message-author'); + this.inputChangeUserName = document.getElementById('username-change-input'); + + this.btnUpdateUserName = document.getElementById('button-update-username'); + this.btnCancelUpdateUsername = document.getElementById('button-cancel-change'); + this.btnSubmitMessage = document.getElementById('button-submit-message'); + + this.formMessageInput = document.getElementById('message-body-form'); + + this.imgUsernameAvatar = document.getElementById('username-avatar'); + this.textUserInfoDisplay = document.getElementById('user-info-display'); + + this.scrollableMessagesContainer = document.getElementById('messages-container'); + + // add events this.tagChatToggle.addEventListener('click', this.handleChatToggle.bind(this)); this.textUserInfoDisplay.addEventListener('click', this.handleShowChangeNameForm.bind(this)); @@ -64,21 +70,23 @@ class Messaging { this.inputChangeUserName.addEventListener('keydown', this.handleUsernameKeydown.bind(this)); this.formMessageInput.addEventListener('keydown', this.handleMessageInputKeydown.bind(this)); this.btnSubmitMessage.addEventListener('click', this.handleSubmitChatButton.bind(this)); + this.initLocalStates(); if (hasTouchScreen()) { this.scrollableMessagesContainer = document.body; this.tagAppContainer.classList.add('touch-screen'); - window.app.layout = 'touch'; window.onorientationchange = this.handleOrientationChange.bind(this); this.handleOrientationChange(); } else { this.tagAppContainer.classList.add('desktop'); - window.app.layout = 'desktop'; - } } + setWebsocket(socket) { + this.websocket = socket; + } + initLocalStates() { this.username = getLocalStorage(KEY_USERNAME) || generateUsername(); this.imgUsernameAvatar.src = @@ -88,11 +96,13 @@ class Messaging { this.chatDisplayed = getLocalStorage(KEY_CHAT_DISPLAYED) || false; this.displayChat(); } + updateUsernameFields(username) { this.tagUsernameDisplay.innerText = username; this.inputChangeUserName.value = username; this.inputMessageAuthor.value = username; } + displayChat() { if (this.chatDisplayed) { this.tagAppContainer.classList.add('chat'); @@ -106,14 +116,15 @@ class Messaging { handleOrientationChange() { var isPortrait = Math.abs(window.orientation % 180) === 0; - if(!isPortrait) { if (document.body.clientWidth < 1024) { this.tagAppContainer.classList.add('no-chat'); this.tagAppContainer.classList.add('landscape'); } } else { - if (this.chatDisplayed) this.tagAppContainer.classList.remove('no-chat'); + if (this.chatDisplayed) { + this.tagAppContainer.classList.remove('no-chat'); + } this.tagAppContainer.classList.remove('landscape'); } } @@ -127,6 +138,7 @@ class Messaging { } this.displayChat(); } + handleShowChangeNameForm() { this.textUserInfoDisplay.style.display = 'none'; this.tagUserInfoChanger.style.display = 'flex'; @@ -134,14 +146,15 @@ class Messaging { this.tagChatToggle.style.display = 'none'; } } + handleHideChangeNameForm() { this.textUserInfoDisplay.style.display = 'flex'; this.tagUserInfoChanger.style.display = 'none'; if (document.body.clientWidth < 640) { this.tagChatToggle.style.display = 'inline-block'; - } } + handleUpdateUsername() { var newValue = this.inputChangeUserName.value; newValue = newValue.trim(); @@ -194,6 +207,7 @@ class Messaging { this.tagMessageFormWarning.innerText = ''; } } + handleSubmitChatButton(event) { var value = this.formMessageInput.value.trim(); if (value) { @@ -204,6 +218,7 @@ class Messaging { event.preventDefault(); return false; } + submitChat(content) { if (!content) { return; @@ -214,12 +229,36 @@ class Messaging { image: this.imgUsernameAvatar.src, }); const messageJSON = JSON.stringify(message); - if (window && window.ws) { - window.ws.send(messageJSON); + if (this.websocket) { + try { + this.websocket.send(messageJSON); + } catch(e) { + console.log('Message send error:', e); + return; + } } // clear out things. this.formMessageInput.value = ''; this.tagMessageFormWarning.innerText = ''; } + + disableChat() { + if (this.formMessageInput) { + this.formMessageInput.disabled = true; + } + // also show "disabled" text/message somewhere. + } + enableChat() { + if (this.formMessageInput) { + this.formMessageInput.disabled = false; + } + } + // handle Vue.js message display + onReceivedMessages(newMessages, oldMessages) { + if (newMessages.length !== oldMessages.length) { + // jump to bottom + jumpToBottom(this.scrollableMessagesContainer); + } + } } \ No newline at end of file diff --git a/webroot/js/player.js b/webroot/js/player.js new file mode 100644 index 000000000..275001659 --- /dev/null +++ b/webroot/js/player.js @@ -0,0 +1,115 @@ +// https://docs.videojs.com/player + +class OwncastPlayer { + constructor() { + window.VIDEOJS_NO_DYNAMIC_STYLE = true; // style override + + this.vjsPlayer = null; + + this.appPlayerReadyCallback = null; + this.appPlayerPlayingCallback = null; + this.appPlayerEndedCallback = null; + + // bind all the things because safari + this.startPlayer = this.startPlayer.bind(this); + this.handleReady = this.handleReady.bind(this); + this.handlePlaying = this.handlePlaying.bind(this); + this.handleEnded = this.handleEnded.bind(this); + this.handleError = this.handleError.bind(this); + } + init() { + this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS); + this.addAirplay(); + this.vjsPlayer.ready(this.handleReady); + } + + setupPlayerCallbacks(callbacks) { + const { onReady, onPlaying, onEnded, onError } = callbacks; + + this.appPlayerReadyCallback = onReady; + this.appPlayerPlayingCallback = onPlaying; + this.appPlayerEndedCallback = onEnded; + this.appPlayerErrorCallback = onError; + } + + // play + startPlayer() { + this.log('Start playing'); + this.vjsPlayer.src(VIDEO_SRC); + // this.vjsPlayer.play(); + }; + + handleReady() { + this.log('on Ready'); + this.vjsPlayer.on('error', this.handleError); + this.vjsPlayer.on('playing', this.handlePlaying); + this.vjsPlayer.on('ended', this.handleEnded); + + if (this.appPlayerReadyCallback) { + // start polling + this.appPlayerReadyCallback(); + } + } + + handlePlaying() { + this.log('on Playing'); + if (this.appPlayerPlayingCallback) { + // start polling + this.appPlayerPlayingCallback(); + } + } + + handleEnded() { + this.log('on Ended'); + if (this.appPlayerEndedCallback) { + this.appPlayerEndedCallback(); + } + this.setPoster(); + } + + handleError(e) { + this.log(`on Error: ${JSON.stringify(e)}`); + if (this.appPlayerEndedCallback) { + this.appPlayerEndedCallback(); + } + } + + setPoster() { + const cachebuster = Math.round(new Date().getTime() / 1000); + const poster = POSTER_THUMB + "?okhi=" + cachebuster; + + this.vjsPlayer.poster(poster); + } + + log(message) { + console.log(`>>> Player: ${message}`); + } + + addAirplay() { + videojs.hookOnce('setup', function (player) { + if (window.WebKitPlaybackTargetAvailabilityEvent) { + var videoJsButtonClass = videojs.getComponent('Button'); + var concreteButtonClass = videojs.extend(videoJsButtonClass, { + + // The `init()` method will also work for constructor logic here, but it is + // deprecated. If you provide an `init()` method, it will override the + // `constructor()` method! + constructor: function () { + videoJsButtonClass.call(this, player); + }, // notice the comma + + handleClick: function () { + const videoElement = document.getElementsByTagName('video')[0]; + videoElement.webkitShowPlaybackTargetPicker(); + } + }); + + var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(new concreteButtonClass()); + concreteButtonInstance.addClass("vjs-airplay"); + } + }); + } + + +} + diff --git a/webroot/js/player/airplay.js b/webroot/js/player/airplay.js deleted file mode 100644 index ddec78981..000000000 --- a/webroot/js/player/airplay.js +++ /dev/null @@ -1,23 +0,0 @@ -videojs.hookOnce('setup', function (player) { - if (window.WebKitPlaybackTargetAvailabilityEvent) { - var videoJsButtonClass = videojs.getComponent('Button'); - var concreteButtonClass = videojs.extend(videoJsButtonClass, { - - // The `init()` method will also work for constructor logic here, but it is - // deprecated. If you provide an `init()` method, it will override the - // `constructor()` method! - constructor: function () { - videoJsButtonClass.call(this, player); - }, // notice the comma - - handleClick: function () { - const videoElement = document.getElementsByTagName('video')[0]; - videoElement.webkitShowPlaybackTargetPicker(); - } - }); - - var concreteButtonInstance = player.controlBar.addChild(new concreteButtonClass()); - concreteButtonInstance.addClass("vjs-airplay"); - } - -}); \ No newline at end of file diff --git a/webroot/js/player/player.js b/webroot/js/player/player.js deleted file mode 100644 index def48bc00..000000000 --- a/webroot/js/player/player.js +++ /dev/null @@ -1,75 +0,0 @@ -// const streamURL = '/hls/stream.m3u8'; -const streamURL = '/hls/stream.m3u8'; // Uncomment me to point to remote video - -// style hackings -window.VIDEOJS_NO_DYNAMIC_STYLE = true; - -// Create the player for the first time -const player = videojs(VIDEO_ID, null, function () { - getStatus(); - setInterval(getStatus, 5000); - setupPlayerEventHandlers(); -}) - -player.ready(function () { - console.log('Player ready.') - resetPlayer(player); -}); - -function resetPlayer(player) { - player.reset(); - player.src({ type: 'application/x-mpegURL', src: URL_STREAM }); - setVideoPoster(app.isOnline); -} - -function setupPlayerEventHandlers() { - const player = videojs(VIDEO_ID); - - player.on('error', function (e) { - console.log("Player error: ", e); - }) - - // player.on('loadeddata', function (e) { - // console.log("loadeddata"); - // }) - - player.on('ended', function (e) { - console.log("ended"); - resetPlayer(player); - }) - // - // player.on('abort', function (e) { - // console.log("abort"); - // }) - // - // player.on('durationchange', function (e) { - // console.log("durationchange"); - // }) - // - // player.on('stalled', function (e) { - // console.log("stalled"); - // }) - // - player.on('playing', function (e) { - if (playerRestartTimer) { - clearTimeout(playerRestartTimer); - } - }) - // - // player.on('waiting', function (e) { - // // console.log("waiting"); - // }) -} - -function restartPlayer() { - try { - const player = videojs(VIDEO_ID); - player.pause(); - player.src(player.src()); // Reload the same video - player.load(); - player.play(); - } catch (e) { - console.log(e) - } - -} diff --git a/webroot/js/status.js b/webroot/js/status.js deleted file mode 100644 index 120ebac1a..000000000 --- a/webroot/js/status.js +++ /dev/null @@ -1,44 +0,0 @@ -var playerRestartTimer; - - -function handleStatus(status) { - clearTimeout(playerRestartTimer); - if (!app.isOnline && status.online) { - // The stream was offline, but now it's online. Force start of playback after an arbitrary delay to make sure the stream has actual data ready to go. - playerRestartTimer = setTimeout(restartPlayer, 3000); - } - - app.streamStatus = status.online ? MESSAGE_ONLINE : MESSAGE_OFFLINE; - - app.viewerCount = status.viewerCount; - app.sessionMaxViewerCount = status.sessionMaxViewerCount; - app.overallMaxViewerCount = status.overallMaxViewerCount; - app.isOnline = status.online; - // setVideoPoster(app.isOnline); -} - -function handleOffline() { - const player = videojs(VIDEO_ID); - player.poster(POSTER_DEFAULT); - app.streamStatus = MESSAGE_OFFLINE; - app.viewerCount = 0; -} - -function getStatus() { - const options = { - // mode: 'no-cors', - } - fetch(URL_STATUS, options) - .then(response => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then(json => { - handleStatus(json); - }) - .catch(error => { - handleOffline(); - }); -} diff --git a/webroot/js/utils.js b/webroot/js/utils.js index 55015e692..103700cee 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -1,14 +1,10 @@ -const LOCAL_TEST = false; +const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; -const MESSAGE_OFFLINE = 'Stream is offline.'; -const MESSAGE_ONLINE = 'Stream is online.'; - -const URL_PREFIX = LOCAL_TEST ? 'https://goth.land' : ''; +const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : ''; const URL_STATUS = `${URL_PREFIX}/status`; const URL_STREAM = `${URL_PREFIX}/hls/stream.m3u8`; - const URL_WEBSOCKET = LOCAL_TEST ? 'wss://goth.land/entry' : `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; @@ -16,18 +12,42 @@ const URL_WEBSOCKET = LOCAL_TEST const POSTER_DEFAULT = `${URL_PREFIX}/img/logo.png`; const POSTER_THUMB = `${URL_PREFIX}/thumbnail.jpg`; +const URL_CONFIG = `${URL_PREFIX}/config`; + +const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer + + +// Webscoket setup const SOCKET_MESSAGE_TYPES = { CHAT: 'CHAT', PING: 'PING' } +// Video setup +const VIDEO_ID = 'video'; +const VIDEO_SRC = { + src: URL_STREAM, + type: 'application/x-mpegURL', +}; +const VIDEO_OPTIONS = { + autoplay: false, + liveui: true, // try this + sources: [VIDEO_SRC], +}; + +// local storage keys for chat const KEY_USERNAME = 'owncast_username'; const KEY_AVATAR = 'owncast_avatar'; const KEY_CHAT_DISPLAYED = 'owncast_chat'; -const VIDEO_ID = 'video'; +const TIMER_STATUS_UPDATE = 5000; // ms +const TIMER_WEBSOCKET_RECONNECT = 5000; // ms +const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins -const URL_OWNCAST = 'https://github.com/gabek/owncast'; +const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + +const MESSAGE_OFFLINE = 'Stream is offline.'; +const MESSAGE_ONLINE = 'Stream is online.'; function getLocalStorage(key) { @@ -121,26 +141,3 @@ function generateUsername() { return `User ${(Math.floor(Math.random() * 42) + 1)}`; } -function setVideoPoster(online) { - const player = videojs(VIDEO_ID); - var cachebuster = Math.round(new Date().getTime() / 1000); - const poster = online ? POSTER_THUMB + "?okhi=" + cachebuster : POSTER_DEFAULT; - player.poster(poster); -} - -function getExtraUserContent(path) { - fetch(path) - .then(response => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.text(); - }) - .then(text => { - const descriptionHTML = new showdown.Converter().makeHtml(text); - app.extraUserContent = descriptionHTML; - }) - .catch(error => { - console.log("Error", error); - }); -} \ No newline at end of file diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index 14eeca425..91c6bd63a 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -243,8 +243,8 @@ h2 { } #username-avatar { - height: 1.75em; - width: 1.75em; + height: 2.1em; + width: 2.1em; margin-right: .5em; } #username-display { @@ -290,6 +290,10 @@ h2 { height: calc(var(--video-container-height)); width: 100%; margin-top: var(--header-height); + background-position: center center; + background-repeat: no-repeat; + + background-size: 30%; } .owncast-video-container { @@ -310,18 +314,19 @@ h2 { min-height: 100% } -.video-js .vjs-big-play-button { - left: 50%; - top: 50%; - margin-left: -1.5em; - margin-top: -0.75em; -} .vjs-airplay .vjs-icon-placeholder::before { /* content: 'AP'; */ content: url("../img/airplay.png"); } +#video { + transition: opacity .5s; + opacity: 0; +} +.online #video { + opacity: 1; +} /* ************************************************8 */ @@ -360,6 +365,7 @@ h2 { justify-content: flex-end; } + #messages-container { overflow: auto; padding: 1em 0; @@ -378,6 +384,9 @@ h2 { #message-body-form { font-size: 1em; } +#message-body-form:disabled{ + opacity: .5; +} #message-form-actions { flex-direction: row; justify-content: space-between;