cloud-game/web/js/stream/stream.js
2024-03-07 17:25:26 +03:00

202 lines
7.1 KiB
JavaScript

/**
* Game streaming module.
* Contains HTML5 AV media elements.
*
* @version 1
*/
const stream = (() => {
const screen = document.getElementById('stream');
let options = {
volume: 0.5,
poster: '/img/screen_loading.gif',
mirrorMode: null,
mirrorUpdateRate: 1 / 60,
},
state = {
screen: screen,
fullscreen: false,
timerId: null,
w: 0,
h: 0,
aspect: 4 / 3
};
const mute = (mute) => screen.muted = mute
const stream = () => {
screen.play()
.then(() => log.info('Media can autoplay'))
.catch(error => {
log.error('Media failed to play', error);
});
}
const toggle = (show) => {
state.screen.toggleAttribute('hidden', !show)
}
const toggleFullscreen = () => {
let h = parseFloat(getComputedStyle(state.screen, null)
.height
.replace('px', '')
)
env.display().toggleFullscreen(h !== window.innerHeight, state.screen);
}
const getVideoEl = () => screen
screen.onerror = (e) => {
// video playback failed - show a message saying why
switch (e.target.error.code) {
case e.target.error.MEDIA_ERR_ABORTED:
log.error('You aborted the video playback.');
break;
case e.target.error.MEDIA_ERR_NETWORK:
log.error('A network error caused the video download to fail part-way.');
break;
case e.target.error.MEDIA_ERR_DECODE:
log.error('The video playback was aborted due to a corruption problem or because the video used features your browser did not support.');
break;
case e.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
log.error('The video could not be loaded, either because the server or network failed or because the format is not supported.');
break;
default:
log.error('An unknown video error occurred.');
break;
}
};
screen.addEventListener('loadedmetadata', () => {
if (state.screen !== screen) {
state.screen.setAttribute('width', screen.videoWidth);
state.screen.setAttribute('height', screen.videoHeight);
}
}, false);
screen.addEventListener('loadstart', () => {
screen.volume = options.volume;
screen.poster = options.poster;
}, false);
screen.addEventListener('canplay', () => {
screen.poster = '';
useCustomScreen(options.mirrorMode === 'mirror');
}, false);
screen.addEventListener('fullscreenchange', () => {
state.fullscreen = !!document.fullscreenElement;
const w = window.screen.width ?? window.innerWidth;
const h = window.screen.height ?? window.innerHeight;
const ww = document.documentElement.innerWidth;
const hh = document.documentElement.innerHeight;
screen.style.padding = '0'
if (state.fullscreen) {
const dw = (w - ww * state.aspect) / 2
screen.style.padding = `0 ${dw}px`
// chrome bug
setTimeout(() => {
const dw = (h - hh * state.aspect) / 2
screen.style.padding = `0 ${dw}px`
}, 1)
makeFullscreen(true);
} else {
makeFullscreen(false);
}
// !to flipped
})
const makeFullscreen = (make = false) => {
screen.classList.toggle('no-media-controls', make)
}
const useCustomScreen = (use) => {
if (use) {
if (screen.paused || screen.ended) return;
let id = state.screen.getAttribute('id');
if (id === 'canvas-mirror') return;
const canvas = gui.create('canvas');
canvas.setAttribute('id', 'canvas-mirror');
canvas.setAttribute('hidden', '');
canvas.setAttribute('width', screen.videoWidth);
canvas.setAttribute('height', screen.videoHeight);
canvas.style['image-rendering'] = 'pixelated';
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.classList.add('game-screen');
// stretch depending on the video orientation
// portrait -- vertically, landscape -- horizontally
const isPortrait = screen.videoWidth < screen.videoHeight;
canvas.style.width = isPortrait ? 'auto' : canvas.style.width;
// canvas.style.height = isPortrait ? canvas.style.height : 'auto';
let surface = canvas.getContext('2d');
screen.parentNode.insertBefore(canvas, screen.nextSibling);
toggle(false)
state.screen = canvas
toggle(true)
state.timerId = setInterval(function () {
if (screen.paused || screen.ended || !surface) return;
surface.drawImage(screen, 0, 0);
}, options.mirrorUpdateRate);
} else {
clearInterval(state.timerId);
let mirror = state.screen;
state.screen = screen;
toggle(true);
if (mirror !== screen) {
mirror.parentNode.removeChild(mirror);
}
}
}
const init = () => {
options.mirrorMode = settings.loadOr(opts.MIRROR_SCREEN, 'none');
options.volume = settings.loadOr(opts.VOLUME, 50) / 100;
}
event.sub(SETTINGS_CHANGED, () => {
const newValue = settings.get()[opts.MIRROR_SCREEN];
if (newValue !== options.mirrorMode) {
useCustomScreen(newValue === 'mirror');
options.mirrorMode = newValue;
}
});
const fit = 'contain'
event.sub(APP_VIDEO_CHANGED, (payload) => {
const {w, h, a, s} = payload
const scale = !s ? 1 : s;
const ww = w * scale;
const hh = h * scale;
state.aspect = a
const a2 = (ww / hh).toFixed(6)
state.screen.style['object-fit'] = a > 1 && a.toFixed(6) !== a2 ? 'fill' : fit
state.h = hh
state.w = Math.floor(hh * a)
state.screen.setAttribute('width', '' + ww)
state.screen.setAttribute('height', '' + hh)
state.screen.style.aspectRatio = '' + state.aspect
})
return {
audio: {mute},
video: {toggleFullscreen, el: getVideoEl},
play: stream,
toggle,
useCustomScreen,
init
}
}
)(env, event, gui, log, opts, settings);