diff options
author | Gabe Kangas <gabek@real-ity.com> | 2020-08-06 10:55:33 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-06 10:55:33 -0700 |
commit | df04af0f38583120d5ba5c620a848e12c933331d (patch) | |
tree | 0e0ab86a64299a257eaf073155ca745b767cfefe | |
parent | b0b5801c5f5491ea38f4fabe52c403e19ccc8511 (diff) |
Websocket refactor: Pull it out of the UI and support callbacks (#104)
* Websocket refactor: Pull it out of the UI and support listeners
* Changes required for Safari to be happy with modules
* Move to explicit ad-hoc callback registration
-rw-r--r-- | webroot/index.html | 16 | ||||
-rw-r--r-- | webroot/js/app.js | 82 | ||||
-rw-r--r-- | webroot/js/chat/socketMessageTypes.js | 11 | ||||
-rw-r--r-- | webroot/js/message.js | 38 | ||||
-rw-r--r-- | webroot/js/player.js | 29 | ||||
-rw-r--r-- | webroot/js/utils.js | 67 | ||||
-rw-r--r-- | webroot/js/websocket.js | 142 |
7 files changed, 246 insertions, 139 deletions
diff --git a/webroot/index.html b/webroot/index.html index a7e356dd4..b9e112ee8 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -181,16 +181,16 @@ <script src="js/usercolors.js"></script> <script src="js/utils.js?v=2"></script> - <script src="js/message.js?v=2"></script> + <script type="module" src="js/message.js?v=2"></script> <script src="js/social.js"></script> <script src="js/components.js"></script> - <script src="js/player.js"></script> - <script src="js/app.js?v=2"></script> - <script> - (function () { - const app = new Owncast(); - app.init(); - })(); + <script type="module"> + import Owncast from './js/app.js'; + + (function () { + const app = new Owncast(); + app.init(); + })(); </script> <noscript> diff --git a/webroot/js/app.js b/webroot/js/app.js index f2e9d2101..e4582adf9 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -1,14 +1,33 @@ +import Websocket from './websocket.js'; +import { MessagingInterface, Message } from './message.js'; +import SOCKET_MESSAGE_TYPES from './chat/socketMessageTypes.js'; +import { OwncastPlayer } from './player.js'; + +const MESSAGE_OFFLINE = 'Stream is offline.'; +const MESSAGE_ONLINE = 'Stream is online'; + +const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + +const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; + +const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : ''; +const URL_CONFIG = `${URL_PREFIX}/config`; +const URL_STATUS = `${URL_PREFIX}/status`; +const URL_CHAT_HISTORY = `${URL_PREFIX}/chat`; + +const TIMER_STATUS_UPDATE = 5000; // ms +const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins +const TIMER_STREAM_DURATION_COUNTER = 1000; + class Owncast { constructor() { this.player; - this.websocket = null; this.configData; this.vueApp; this.messagingInterface = null; // timers - this.websocketReconnectTimer = null; this.playerRestartTimer = null; this.offlineTimer = null; this.statusTimer = null; @@ -23,7 +42,6 @@ class Owncast { // 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); @@ -40,7 +58,7 @@ class Owncast { init() { this.messagingInterface = new MessagingInterface(); - this.websocket = this.setupWebsocket(); + this.setupWebsocket(); this.vueApp = new Vue({ el: '#app-container', @@ -109,53 +127,17 @@ class Owncast { // websocket for messaging setupWebsocket() { - var ws = new WebSocket(URL_WEBSOCKET); - ws.onopen = (e) => { - if (this.websocketReconnectTimer) { - clearTimeout(this.websocketReconnectTimer); - } - - // If we're "online" then enable the chat. - if (this.streamStatus && this.streamStatus.online) { - this.messagingInterface.enableChat(); - } - }; - 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(`Socket error: ${JSON.parse(e)}`); - ws.close(); - }; - ws.onmessage = (e) => { - const model = JSON.parse(e.data); - - // Send PONGs - if (model.type === SOCKET_MESSAGE_TYPES.PING) { - this.sendPong(ws); - return; - } else if (model.type === SOCKET_MESSAGE_TYPES.CHAT) { - const message = new Message(model); - this.addMessage(message); - } else if (model.type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { - this.addMessage(model); - } - }; - this.websocket = ws; - this.messagingInterface.setWebsocket(this.websocket); + this.websocket = new Websocket(); + this.websocket.addListener('rawWebsocketMessageReceived', this.receivedWebsocketMessage.bind(this)); + this.messagingInterface.send = this.websocket.send; }; - sendPong(ws) { - try { - const pong = { type: SOCKET_MESSAGE_TYPES.PONG }; - ws.send(JSON.stringify(pong)); - } catch (e) { - console.log('PONG error:', e); + receivedWebsocketMessage(model) { + if (model.type === SOCKET_MESSAGE_TYPES.CHAT) { + const message = new Message(model); + this.addMessage(message); + } else if (model.type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { + this.addMessage(model); } } @@ -349,3 +331,5 @@ class Owncast { this.handlePlayerEnded(); }; }; + +export default Owncast;
\ No newline at end of file diff --git a/webroot/js/chat/socketMessageTypes.js b/webroot/js/chat/socketMessageTypes.js new file mode 100644 index 000000000..54116e1b0 --- /dev/null +++ b/webroot/js/chat/socketMessageTypes.js @@ -0,0 +1,11 @@ +/** + * These are the types of messages that we can handle with the websocket. + * Mostly used by `websocket.js` but if other components need to handle + * different types then it can import this file. + */ +export default { + CHAT: 'CHAT', + PING: 'PING', + NAME_CHANGE: 'NAME_CHANGE', + PONG: 'PONG' +}
\ No newline at end of file diff --git a/webroot/js/message.js b/webroot/js/message.js index 4f2645f1b..cd1c81460 100644 --- a/webroot/js/message.js +++ b/webroot/js/message.js @@ -1,3 +1,13 @@ +import SOCKET_MESSAGE_TYPES from './chat/socketMessageTypes.js'; + +const KEY_USERNAME = 'owncast_username'; +const KEY_AVATAR = 'owncast_avatar'; +const KEY_CHAT_DISPLAYED = 'owncast_chat'; +const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; +const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; +const CHAT_PLACEHOLDER_TEXT = 'Message'; +const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; + class Message { constructor(model) { this.author = model.author; @@ -36,7 +46,6 @@ class Message { class MessagingInterface { constructor() { - this.websocket = null; this.chatDisplayed = false; this.username = ''; this.messageCharCount = 0; @@ -89,11 +98,6 @@ class MessagingInterface { window.addEventListener("orientationchange", setVHvar); this.tagAppContainer.classList.add('touch-screen'); } - - } - - setWebsocket(socket) { - this.websocket = socket; } initLocalStates() { @@ -188,9 +192,7 @@ class MessagingInterface { image: image, }; - const jsonMessage = JSON.stringify(nameChange); - - this.websocket.send(jsonMessage) + this.send(nameChange); } handleMessageInputKeydown(event) { @@ -252,15 +254,7 @@ class MessagingInterface { image: this.imgUsernameAvatar.src, type: SOCKET_MESSAGE_TYPES.CHAT, }); - const messageJSON = JSON.stringify(message); - if (this.websocket) { - try { - this.websocket.send(messageJSON); - } catch(e) { - console.log('Message send error:', e); - return; - } - } + this.send(message); // clear out things. this.formMessageInput.value = ''; @@ -298,4 +292,10 @@ class MessagingInterface { jumpToBottom(this.scrollableMessagesContainer); } } -}
\ No newline at end of file + + send(messageJSON) { + console.error('MessagingInterface send() is not linked to the websocket component.'); + } +} + +export { Message, MessagingInterface }
\ No newline at end of file diff --git a/webroot/js/player.js b/webroot/js/player.js index 35442aa83..890bc2833 100644 --- a/webroot/js/player.js +++ b/webroot/js/player.js @@ -1,5 +1,33 @@ // https://docs.videojs.com/player +const VIDEO_ID = 'video'; +const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; +const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : ''; +const URL_STREAM = `${URL_PREFIX}/hls/stream.m3u8`; + +// Video setup +const VIDEO_SRC = { + src: URL_STREAM, + type: 'application/x-mpegURL', +}; +const VIDEO_OPTIONS = { + autoplay: false, + liveui: true, // try this + preload: 'auto', + html5: { + vhs: { + // used to select the lowest bitrate playlist initially. This helps to decrease playback start time. This setting is false by default. + enableLowInitialPlaylist: true, + + } + }, + liveTracker: { + trackingThreshold: 0, + }, + sources: [VIDEO_SRC], +}; + + class OwncastPlayer { constructor() { window.VIDEOJS_NO_DYNAMIC_STYLE = true; // style override @@ -124,3 +152,4 @@ class OwncastPlayer { } +export { OwncastPlayer };
\ No newline at end of file diff --git a/webroot/js/utils.js b/webroot/js/utils.js index f6c43801c..ba09ca479 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -1,75 +1,12 @@ const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; - - const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : ''; -const URL_STATUS = `${URL_PREFIX}/status`; -const URL_CHAT_HISTORY = `${URL_PREFIX}/chat`; -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`; 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', - NAME_CHANGE: 'NAME_CHANGE', - PONG: 'PONG' -} - -// 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 - preload: 'auto', - html5: { - vhs: { - // used to select the lowest bitrate playlist initially. This helps to decrease playback start time. This setting is false by default. - enableLowInitialPlaylist: true, - - } - }, - liveTracker: { - trackingThreshold: 0, - }, - 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 KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; - -const TIMER_STATUS_UPDATE = 5000; // ms -const TIMER_WEBSOCKET_RECONNECT = 5000; // ms -const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins -const TIMER_STREAM_DURATION_COUNTER = 1000; - -const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - -const MESSAGE_OFFLINE = 'Stream is offline.'; -const MESSAGE_ONLINE = 'Stream is online'; - -const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; -const CHAT_PLACEHOLDER_TEXT = 'Message'; -const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; - - function getLocalStorage(key) { try { return localStorage.getItem(key); @@ -182,3 +119,7 @@ function setVHvar() { document.documentElement.style.setProperty('--vh', `${vh}px`); console.log("== new vh", vh) } + +function doesObjectSupportFunction(object, functionName) { + return typeof object[functionName] === "function"; +}
\ No newline at end of file diff --git a/webroot/js/websocket.js b/webroot/js/websocket.js new file mode 100644 index 000000000..bc4efd1d9 --- /dev/null +++ b/webroot/js/websocket.js @@ -0,0 +1,142 @@ +import SOCKET_MESSAGE_TYPES from './chat/socketMessageTypes.js'; + +const LOCAL_TEST = window.location.href.indexOf('localhost:') >= 0; +const URL_WEBSOCKET = LOCAL_TEST + ? 'wss://goth.land/entry' + : `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; + +const TIMER_WEBSOCKET_RECONNECT = 5000; // ms + +const CALLBACKS = { + RAW_WEBSOCKET_MESSAGE_RECEIVED: 'rawWebsocketMessageReceived', + WEBSOCKET_CONNECTED: 'websocketConnected', + WEBSOCKET_DISCONNECTED: 'websocketDisconnected', +} + +class Websocket { + + constructor() { + this.websocket = null; + this.websocketReconnectTimer = null; + + this.websocketConnectedListeners = []; + this.websocketDisconnectListeners = []; + this.rawMessageListeners = []; + + this.send = this.send.bind(this); + + const ws = new WebSocket(URL_WEBSOCKET); + ws.onopen = this.onOpen.bind(this); + ws.onclose = this.onClose.bind(this); + ws.onerror = this.onError.bind(this); + ws.onmessage = this.onMessage.bind(this); + + this.websocket = ws; + } + + // Other components should register for websocket callbacks. + addListener(type, callback) { + if (type == CALLBACKS.WEBSOCKET_CONNECTED) { + this.websocketConnectedListeners.push(callback); + } else if (type == CALLBACKS.WEBSOCKET_DISCONNECTED) { + this.websocketDisconnectListeners.push(callback); + } else if (type == CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED) { + this.rawMessageListeners.push(callback); + } + } + + + // Interface with other components + + // Outbound: Other components can pass an object to `send`. + send(message) { + // Sanity check that what we're sending is a valid type. + if (!message.type || !SOCKET_MESSAGE_TYPES[message.type]) { + console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`); + } + + const messageJSON = JSON.stringify(message); + this.websocket.send(messageJSON); + } + + // Private methods + + // Fire the callbacks of the listeners. + + notifyWebsocketConnectedListeners(message) { + this.websocketConnectedListeners.forEach(function (callback) { + callback(message); + }); + } + + notifyWebsocketDisconnectedListeners(message) { + this.websocketDisconnectListeners.forEach(function (callback) { + callback(message); + }); + } + + + notifyRawMessageListeners(message) { + this.rawMessageListeners.forEach(function (callback) { + callback(message); + }); + } + + // Internal websocket callbacks + + onOpen(e) { + if (this.websocketReconnectTimer) { + clearTimeout(this.websocketReconnectTimer); + } + + this.notifyWebsocketConnectedListeners(); + } + + onClose(e) { + // connection closed, discard old websocket and create a new one in 5s + this.websocket = null; + this.notifyWebsocketDisconnectedListeners(); + 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. + onError(e) { + this.handleNetworkingError(`Socket error: ${JSON.parse(e)}`); + this.websocket.close(); + } + + /* + onMessage is fired when an inbound object comes across the websocket. + If the message is of type `PING` we send a `PONG` back and do not + pass it along to listeners. + */ + onMessage(e) { + try { + var model = JSON.parse(e.data); + } catch (e) { + console.log(e) + } + + // Send PONGs + if (model.type === SOCKET_MESSAGE_TYPES.PING) { + this.sendPong(); + return; + } + + // Notify any of the listeners via the raw socket message callback. + this.notifyRawMessageListeners(model); + } + + // Reply to a PING as a keep alive. + sendPong() { + const pong = { type: SOCKET_MESSAGE_TYPES.PONG }; + this.send(pong); + } + + handleNetworkingError(error) { + console.error(`Websocket Error: ${error}`) + }; +} + +export default Websocket;
\ No newline at end of file |