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
This commit is contained in:
Gabe Kangas
2020-08-06 10:55:33 -07:00
committed by GitHub
parent b0b5801c5f
commit df04af0f38
7 changed files with 246 additions and 139 deletions

View File

@@ -181,12 +181,12 @@
<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>
<script type="module">
import Owncast from './js/app.js';
(function () {
const app = new Owncast();
app.init();

View File

@@ -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,54 +127,18 @@ class Owncast {
// websocket for messaging
setupWebsocket() {
var ws = new WebSocket(URL_WEBSOCKET);
ws.onopen = (e) => {
if (this.websocketReconnectTimer) {
clearTimeout(this.websocketReconnectTimer);
}
this.websocket = new Websocket();
this.websocket.addListener('rawWebsocketMessageReceived', this.receivedWebsocketMessage.bind(this));
this.messagingInterface.send = this.websocket.send;
};
// 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) {
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);
}
};
this.websocket = ws;
this.messagingInterface.setWebsocket(this.websocket);
};
sendPong(ws) {
try {
const pong = { type: SOCKET_MESSAGE_TYPES.PONG };
ws.send(JSON.stringify(pong));
} catch (e) {
console.log('PONG error:', e);
}
}
addMessage(message) {
@@ -349,3 +331,5 @@ class Owncast {
this.handlePlayerEnded();
};
};
export default Owncast;

View File

@@ -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'
}

View File

@@ -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);
}
}
send(messageJSON) {
console.error('MessagingInterface send() is not linked to the websocket component.');
}
}
export { Message, MessagingInterface }

View File

@@ -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 };

View File

@@ -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";
}

142
webroot/js/websocket.js Normal file
View File

@@ -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;