summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabe Kangas <gabek@real-ity.com>2020-08-06 10:55:33 -0700
committerGitHub <noreply@github.com>2020-08-06 10:55:33 -0700
commitdf04af0f38583120d5ba5c620a848e12c933331d (patch)
tree0e0ab86a64299a257eaf073155ca745b767cfefe
parentb0b5801c5f5491ea38f4fabe52c403e19ccc8511 (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.html16
-rw-r--r--webroot/js/app.js82
-rw-r--r--webroot/js/chat/socketMessageTypes.js11
-rw-r--r--webroot/js/message.js38
-rw-r--r--webroot/js/player.js29
-rw-r--r--webroot/js/utils.js67
-rw-r--r--webroot/js/websocket.js142
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