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 <gabek@real-ity.com>
This commit is contained in:
@@ -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?
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user