1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00
fcast/receivers/common/web/viewer/Renderer.ts
2025-06-18 16:58:35 -05:00

392 lines
14 KiB
TypeScript

import { EventMessage, EventType, GenericMediaMetadata, KeyEvent, MediaItem, MediaItemEvent, MetadataType, PlaybackState, PlaylistContent, PlayMessage, SeekMessage, SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
import { mediaItemFromPlayMessage, playMessageFromMediaItem, Timer } from 'common/UtilityFrontend';
import { supportedImageTypes } from 'common/MimeTypes';
import * as connectionMonitor from 'common/ConnectionMonitor';
import { toast, ToastIcon } from 'common/components/Toast';
import {
targetPlayerCtrlStateUpdate,
targetKeyDownEventListener,
} from 'src/viewer/Renderer';
const logger = window.targetAPI.logger;
// HTML elements
const idleBackground = document.getElementById('idleBackground');
const idleIcon = document.getElementById('titleIcon');
const loadingSpinner = document.getElementById('loadingSpinner');
const imageViewer = document.getElementById('viewerImage') as HTMLImageElement;
const genericViewer = document.getElementById('viewerGeneric') as HTMLIFrameElement;
const mediaTitle = document.getElementById("mediaTitle");
const playerControls = document.getElementById("controls");
const playerCtrlPlayPrevious = document.getElementById("playPrevious");
const playerCtrlAction = document.getElementById("action");
const playerCtrlPlaylistLength = document.getElementById("playlistLength");
const playerCtrlPlayNext = document.getElementById("playNext");
let cachedPlaylist: PlaylistContent = null;
let cachedPlayMediaItem: MediaItem = null;
let playlistIndex = 0;
let isMediaItem = false;
let playItemCached = false;
let imageViewerPlaybackState: PlaybackState = PlaybackState.Idle;
let uiHideTimer = new Timer(() => {
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}, 3000);
let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false);
let showDurationTimer = new Timer(() => {
if (playlistIndex < cachedPlaylist.items.length - 1) {
setPlaylistItem(playlistIndex + 1);
}
else {
logger.info('End of playlist');
imageViewer.style.display = 'none';
imageViewer.src = '';
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'block';
idleIcon.style.display = 'block';
playerCtrlAction.setAttribute("class", "play iconSize");
imageViewerPlaybackState = PlaybackState.Idle;
}
}, 0, false);
function onPlay(_event, value: PlayMessage) {
if (!playItemCached) {
cachedPlayMediaItem = mediaItemFromPlayMessage(value);
isMediaItem = false;
}
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
logger.info('Media playback changed:', cachedPlayMediaItem);
playItemCached = false;
showDurationTimer.stop();
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
const src = value.url ? value.url : value.content;
loadingTimer.start();
if (src && value.container && supportedImageTypes.find(v => v === value.container.toLocaleLowerCase()) && imageViewer) {
logger.info('Loading image viewer');
imageViewer.onload = (ev) => {
loadingTimer.stop();
loadingSpinner.style.display = 'none';
playerCtrlStateUpdate(PlayerControlEvent.Load);
};
genericViewer.onload = (ev) => {};
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'none';
idleIcon.style.display = 'none';
imageViewer.src = src;
imageViewer.style.display = 'block';
playerControls.style.display = 'block';
}
else if (src && genericViewer) {
logger.info('Loading generic viewer');
imageViewer.onload = (ev) => {};
genericViewer.onload = (ev) => {
loadingTimer.stop();
loadingSpinner.style.display = 'none';
playerCtrlStateUpdate(PlayerControlEvent.Load);
};
imageViewer.style.display = 'none';
imageViewer.src = '';
playerControls.style.display = 'none';
idleBackground.style.display = 'none';
idleIcon.style.display = 'none';
genericViewer.src = src;
genericViewer.style.display = 'block';
} else {
logger.error('Error loading content');
loadingTimer.stop();
loadingSpinner.style.display = 'none';
imageViewer.onload = (ev) => {};
genericViewer.onload = (ev) => {};
imageViewer.style.display = 'none';
imageViewer.src = '';
playerControls.style.display = 'none';
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'block';
idleIcon.style.display = 'block';
}
};
function onPlayPlaylist(_event, value: PlaylistContent) {
logger.info('Handle play playlist message', JSON.stringify(value));
cachedPlaylist = value;
const offset = value.offset ? value.offset : 0;
const volume = value.items[offset].volume ? value.items[offset].volume : value.volume;
const speed = value.items[offset].speed ? value.items[offset].speed : value.speed;
const playMessage = new PlayMessage(
value.items[offset].container, value.items[offset].url, value.items[offset].content,
value.items[offset].time, volume, speed, value.items[offset].headers, value.items[offset].metadata
);
isMediaItem = true;
cachedPlayMediaItem = value.items[offset];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessage, playlistIndex);
}
function setPlaylistItem(index: number) {
if (index === -1) {
logger.info('Looping playlist to end');
index = cachedPlaylist.items.length - 1;
}
else if (index === cachedPlaylist.items.length) {
logger.info('Looping playlist to start');
index = 0;
}
if (index >= 0 && index < cachedPlaylist.items.length) {
logger.info(`Setting playlist item to index ${index}`);
playlistIndex = index;
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
showDurationTimer.stop();
}
else {
logger.warn(`Playlist index out of bounds ${index}, ignoring...`);
}
playerCtrlPlaylistLength.textContent= `${playlistIndex+1} of ${cachedPlaylist.items.length}`;
}
window.targetAPI.onPause(() => { logger.warn('onPause handler invoked for generic content viewer'); });
window.targetAPI.onResume(() => { logger.warn('onResume handler invoked for generic content viewer'); });
window.targetAPI.onSeek((_event, value: SeekMessage) => { logger.warn('onSeek handler invoked for generic content viewer'); });
window.targetAPI.onSetVolume((_event, value: SetVolumeMessage) => { logger.warn('onSetVolume handler invoked for generic content viewer'); });
window.targetAPI.onSetSpeed((_event, value: SetSpeedMessage) => { logger.warn('onSetSpeed handler invoked for generic content viewer'); });
window.targetAPI.onSetPlaylistItem((_event, value: SetPlaylistItemMessage) => { setPlaylistItem(value.itemIndex); });
connectionMonitor.setUiUpdateCallbacks({
onConnect: (connections: string[], initialUpdate: boolean = false) => {
if (!initialUpdate) {
toast('Device connected', ToastIcon.INFO);
}
},
onDisconnect: (connections: string[]) => {
toast('Device disconnected. If you experience playback issues, please reconnect.', ToastIcon.INFO);
},
});
window.targetAPI.onPlay(onPlay);
window.targetAPI.onPlayPlaylist(onPlayPlaylist);
enum PlayerControlEvent {
Load,
Pause,
Play,
UiFadeOut,
UiFadeIn,
ToggleFullscreen,
ExitFullscreen,
}
// UI update handlers
function playerCtrlStateUpdate(event: PlayerControlEvent) {
const handledCase = targetPlayerCtrlStateUpdate(event);
if (handledCase) {
return;
}
switch (event) {
case PlayerControlEvent.Load: {
if (isMediaItem) {
playerCtrlPlayPrevious.style.display = 'block';
playerCtrlPlayNext.style.display = 'block';
if (cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
playerCtrlAction.style.display = 'block';
playerCtrlPlaylistLength.style.display = 'none';
if (imageViewerPlaybackState === PlaybackState.Idle || imageViewerPlaybackState === PlaybackState.Playing) {
showDurationTimer.start(cachedPlayMediaItem.showDuration * 1000);
playerCtrlAction.setAttribute("class", "pause iconSize");
imageViewerPlaybackState = PlaybackState.Playing;
}
}
else {
playerCtrlAction.style.display = 'none';
playerCtrlPlaylistLength.textContent= `${playlistIndex+1} of ${cachedPlaylist.items.length}`;
playerCtrlPlaylistLength.style.display = 'block';
}
}
else {
playerCtrlPlayPrevious.style.display = 'none';
playerCtrlPlayNext.style.display = 'none';
playerCtrlAction.style.display = 'none';
playerCtrlPlaylistLength.style.display = 'none';
}
if (cachedPlayMediaItem.metadata && cachedPlayMediaItem.metadata?.type === MetadataType.Generic) {
const metadata = cachedPlayMediaItem.metadata as GenericMediaMetadata;
if (metadata.title) {
mediaTitle.innerHTML = metadata.title;
}
}
break;
}
case PlayerControlEvent.Pause:
playerCtrlAction.setAttribute("class", "play iconSize");
imageViewerPlaybackState = PlaybackState.Paused;
showDurationTimer.pause();
break;
case PlayerControlEvent.Play:
playerCtrlAction.setAttribute("class", "pause iconSize");
if (imageViewerPlaybackState === PlaybackState.Idle) {
setPlaylistItem(0);
}
else {
if (showDurationTimer.started) {
showDurationTimer.resume();
}
else {
showDurationTimer.start(cachedPlayMediaItem.showDuration * 1000);
}
imageViewerPlaybackState = PlaybackState.Playing;
}
break;
case PlayerControlEvent.UiFadeOut: {
document.body.style.cursor = "none";
playerControls.style.opacity = '0';
break;
}
case PlayerControlEvent.UiFadeIn: {
document.body.style.cursor = "default";
playerControls.style.opacity = '1';
break;
}
default:
break;
}
}
// Receiver generated event handlers
playerCtrlAction.onclick = () => {
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
};
playerCtrlPlayPrevious.onclick = () => { setPlaylistItem(playlistIndex - 1); }
playerCtrlPlayNext.onclick = () => { setPlaylistItem(playlistIndex + 1); }
// Component hiding
let uiVisible = true;
function stopUiHideTimer() {
uiHideTimer.stop();
if (!uiVisible) {
uiVisible = true;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
}
}
document.onmouseout = () => {
uiHideTimer.stop();
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}
document.onmousemove = () => {
stopUiHideTimer();
uiHideTimer.start();
};
function keyDownEventListener(event: KeyboardEvent) {
// logger.info("KeyDown", event);
let handledCase = targetKeyDownEventListener(event);
if (!handledCase) {
switch (event.code) {
case 'ArrowLeft':
setPlaylistItem(playlistIndex - 1);
event.preventDefault();
handledCase = true;
break;
case 'ArrowRight':
setPlaylistItem(playlistIndex + 1);
event.preventDefault();
handledCase = true;
break;
case "Home":
setPlaylistItem(0);
event.preventDefault();
handledCase = true;
break;
case "End":
setPlaylistItem(cachedPlaylist.items.length - 1);
event.preventDefault();
handledCase = true;
break;
case 'KeyK':
case 'Space':
case 'Enter':
// Play/pause toggle
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
event.preventDefault();
handledCase = true;
break;
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
}
}
document.addEventListener('keydown', keyDownEventListener);
document.addEventListener('keyup', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
}
});
export {
PlayerControlEvent,
idleBackground,
idleIcon,
imageViewer,
genericViewer,
onPlay,
playerCtrlStateUpdate,
};