import { h, Component } from '/js/web_modules/preact.js'; import htm from '/js/web_modules/htm.js'; const html = htm.bind(h); import VideoPoster from './components/video-poster.js'; import { OwncastPlayer } from './components/player.js'; // We count viewers by concurrent websocket connections. // So for now let's make a connection just so it can // be counted. // https://github.com/owncast/owncast/discussions/374 import Websocket from './utils/websocket.js'; const websocket = new Websocket(); import { addNewlines, makeLastOnlineString, pluralize, } from './utils/helpers.js'; import { URL_CONFIG, URL_STATUS, URL_VIEWER_PING, TIMER_STATUS_UPDATE, TIMER_STREAM_DURATION_COUNTER, TEMP_IMAGE, MESSAGE_OFFLINE, MESSAGE_ONLINE, } from './utils/constants.js'; export default class VideoOnly extends Component { constructor(props, context) { super(props, context); this.state = { configData: {}, playerActive: false, // player object is active streamOnline: false, // stream is active/online isPlaying: false, //status streamStatusMessage: MESSAGE_OFFLINE, viewerCount: '', lastDisconnectTime: null, }; // timers this.playerRestartTimer = null; this.offlineTimer = null; this.statusTimer = null; this.streamDurationTimer = null; this.handleOfflineMode = this.handleOfflineMode.bind(this); this.handleOnlineMode = this.handleOnlineMode.bind(this); // player events this.handlePlayerReady = this.handlePlayerReady.bind(this); this.handlePlayerPlaying = this.handlePlayerPlaying.bind(this); this.handlePlayerEnded = this.handlePlayerEnded.bind(this); this.handlePlayerError = this.handlePlayerError.bind(this); // fetch events this.getConfig = this.getConfig.bind(this); this.getStreamStatus = this.getStreamStatus.bind(this); } componentDidMount() { this.getConfig(); this.player = new OwncastPlayer(); this.player.setupPlayerCallbacks({ onReady: this.handlePlayerReady, onPlaying: this.handlePlayerPlaying, onEnded: this.handlePlayerEnded, onError: this.handlePlayerError, }); this.player.init(); } componentWillUnmount() { // clear all the timers clearInterval(this.playerRestartTimer); clearInterval(this.offlineTimer); clearInterval(this.statusTimer); clearInterval(this.streamDurationTimer); } // 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(); }) .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}`); }); // Ping the API to let them know we're an active viewer fetch(URL_VIEWER_PING).catch((error) => { this.handleOfflineMode(); this.handleNetworkingError(`Viewer PING error: ${error}`); }); } setConfigData(data = {}) { const { title, summary } = data; window.document.title = title; this.setState({ configData: { ...data, summary: summary && addNewlines(summary), }, }); } // handle UI things from stream status result updateStreamStatus(status = {}) { const { streamOnline: curStreamOnline } = this.state; if (!status) { return; } const { viewerCount, online, lastDisconnectTime } = status; if (status.online && !curStreamOnline) { // stream has just come online. this.handleOnlineMode(); } else if (!status.online && curStreamOnline) { // stream has just flipped offline. this.handleOfflineMode(); } this.setState({ viewerCount, streamOnline: online, lastDisconnectTime, }); } // when videojs player is ready, start polling for stream handlePlayerReady() { this.getStreamStatus(); this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); } handlePlayerPlaying() { this.setState({ isPlaying: true, }); } // likely called some time after stream status has gone offline. // basically hide video and show underlying "poster" handlePlayerEnded() { this.setState({ playerActive: false, isPlaying: false, }); } handlePlayerError() { // do something? this.handleOfflineMode(); this.handlePlayerEnded(); } // stop status timer and disable chat after some time. handleOfflineMode() { clearInterval(this.streamDurationTimer); this.setState({ streamOnline: false, streamStatusMessage: MESSAGE_OFFLINE, }); } // play video! handleOnlineMode() { this.player.startPlayer(); this.streamDurationTimer = setInterval( this.setCurrentStreamDuration, TIMER_STREAM_DURATION_COUNTER ); this.setState({ playerActive: true, streamOnline: true, streamStatusMessage: MESSAGE_ONLINE, }); } handleNetworkingError(error) { console.log(`>>> App Error: ${error}`); } render(props, state) { const { configData, viewerCount, playerActive, streamOnline, streamStatusMessage, lastDisconnectTime, isPlaying, } = state; const { logo = TEMP_IMAGE, customStyles } = configData; let viewerCountMessage = ''; if (streamOnline && viewerCount > 0) { viewerCountMessage = html`${viewerCount} ${pluralize(' viewer', viewerCount)}`; } else if (lastDisconnectTime) { viewerCountMessage = makeLastOnlineString(lastDisconnectTime); } const mainClass = playerActive ? 'online' : ''; const poster = isPlaying ? null : html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `; return html`
${poster}
${streamStatusMessage} ${viewerCountMessage}
`; } }