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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
11
webroot/js/chat/socketMessageTypes.js
Normal file
11
webroot/js/chat/socketMessageTypes.js
Normal 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'
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 };
|
||||
@@ -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
142
webroot/js/websocket.js
Normal 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;
|
||||
Reference in New Issue
Block a user