1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-08-08 18:22:50 +00:00

Receivers: Added playlist support

This commit is contained in:
Michael Hollister 2025-06-10 14:23:06 -05:00
parent 72d5c10918
commit 1afd421f7d
22 changed files with 1613 additions and 453 deletions

View file

@ -1,57 +1,138 @@
import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlaylistContent, PlayMessage, SeekMessage, SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
import { mediaItemFromPlayMessage, playMessageFromMediaItem } from 'common/UtilityFrontend';
import { supportedImageTypes } from 'common/MimeTypes';
import * as connectionMonitor from '../ConnectionMonitor';
import { toast, ToastIcon } from '../components/Toast';
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;
const idleBackground = document.getElementById('video-player');
const idleIcon = document.getElementById('title-icon');
// todo: add callbacks for on-load events for image and generic content viewer
const loadingSpinner = document.getElementById('loading-spinner');
const imageViewer = document.getElementById('viewer-image') as HTMLImageElement;
const genericViewer = document.getElementById('viewer-generic') as HTMLIFrameElement;
let cachedPlaylist: PlaylistContent = null;
let cachedPlayMediaItem: MediaItem = null;
let showDurationTimeout: number = null;
let playlistIndex = 0;
let isMediaItem = false;
let playItemCached = false;
function onPlay(_event, value: PlayMessage) {
logger.info("Handle play message renderer", JSON.stringify(value));
const playMediaItem = new MediaItem(
value.container, value.url, value.content,
value.time, value.volume, value.speed,
null, null, value.headers, value.metadata
);
window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, playMediaItem)));
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;
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
const src = value.url ? value.url : value.content;
if (src && value.container && supportedImageTypes.find(v => v === value.container.toLocaleLowerCase()) && imageViewer) {
logger.info("Loading image viewer");
logger.info('Loading image viewer');
genericViewer.style.display = "none";
genericViewer.src = "";
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'none';
idleIcon.style.display = 'none';
imageViewer.src = src;
imageViewer.style.display = "block";
imageViewer.style.display = 'block';
}
else if (src && genericViewer) {
logger.info("Loading generic viewer");
logger.info('Loading generic viewer');
imageViewer.style.display = "none";
imageViewer.src = "";
imageViewer.style.display = 'none';
imageViewer.src = '';
idleBackground.style.display = 'none';
idleIcon.style.display = 'none';
genericViewer.src = src;
genericViewer.style.display = "block";
genericViewer.style.display = 'block';
} else {
logger.error("Error loading content");
logger.error('Error loading content');
imageViewer.style.display = "none";
imageViewer.src = "";
imageViewer.style.display = 'none';
imageViewer.src = '';
genericViewer.style.display = "none";
genericViewer.src = "";
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'block';
idleIcon.style.display = 'block';
}
if (isMediaItem && cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
showDurationTimeout = window.setTimeout(() => {
playlistIndex++;
if (playlistIndex < cachedPlaylist.items.length) {
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
}
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';
}
}, cachedPlayMediaItem.showDuration * 1000);
}
};
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);
}
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) => {
if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
logger.info(`Setting playlist item to index ${value.itemIndex}`);
playlistIndex = value.itemIndex;
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
if (showDurationTimeout) {
window.clearTimeout(showDurationTimeout);
showDurationTimeout = null;
}
}
else {
logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
}
});
connectionMonitor.setUiUpdateCallbacks({
onConnect: (connections: string[], initialUpdate: boolean = false) => {
@ -65,14 +146,120 @@ connectionMonitor.setUiUpdateCallbacks({
});
window.targetAPI.onPlay(onPlay);
window.targetAPI.onPlayPlaylist(onPlayPlaylist);
enum PlayerControlEvent {
Load,
Pause,
Play,
VolumeChange,
TimeUpdate,
UiFadeOut,
UiFadeIn,
SetCaptions,
ToggleSpeedMenu,
SetPlaybackRate,
ToggleFullscreen,
ExitFullscreen,
}
// UI update handlers
function playerCtrlStateUpdate(event: PlayerControlEvent) {
const handledCase = targetPlayerCtrlStateUpdate(event);
if (handledCase) {
return;
}
switch (event) {
case PlayerControlEvent.Load: {
break;
}
case PlayerControlEvent.UiFadeOut: {
break;
}
case PlayerControlEvent.UiFadeIn: {
break;
}
default:
break;
}
}
document.addEventListener('keydown', (event: KeyboardEvent) => {
// logger.info("KeyDown", event);
let handledCase = targetKeyDownEventListener(event);
if (!handledCase) {
switch (event.code) {
case 'ArrowLeft': {
// skipBack();
// event.preventDefault();
// handledCase = true;
// const value = { itemIndex: playlistIndex - 1 };
// if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
// logger.info(`Setting playlist item to index ${value.itemIndex}`);
// playlistIndex = value.itemIndex;
// cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
// playItemCached = true;
// window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
// if (showDurationTimeout) {
// window.clearTimeout(showDurationTimeout);
// showDurationTimeout = null;
// }
// }
// else {
// logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
// }
break;
}
case 'ArrowRight': {
// skipForward();
// event.preventDefault();
// handledCase = true;
// const value = { itemIndex: playlistIndex + 1 };
// if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
// logger.info(`Setting playlist item to index ${value.itemIndex}`);
// playlistIndex = value.itemIndex;
// cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
// playItemCached = true;
// window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
// if (showDurationTimeout) {
// window.clearTimeout(showDurationTimeout);
// showDurationTimeout = null;
// }
// }
// else {
// logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
// }
break;
}
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, false)));
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
}
});
document.addEventListener('keyup', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) {
window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
}
});
export {
PlayerControlEvent,
onPlay,
playerCtrlStateUpdate,
};

View file

@ -15,15 +15,66 @@ body {
max-height: 100%;
}
*:focus {
outline: none;
box-shadow: none;
}
.video {
height: 100%;
width: 100%;
object-fit: cover;
}
.viewer {
object-fit: contain;
width: 100%;
height: 100%;
}
*:focus {
outline: none;
box-shadow: none;
#title-icon {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-image: url(../assets/icons/app/icon.svg);
background-size: cover;
}
.lds-ring {
display: block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
margin: 8px;
border: 8px solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#toast-notification {
@ -97,6 +148,20 @@ body {
/* Display scaling (Minimum supported resolution is 960x540) */
@media only screen and ((min-width: 2560px) or (min-height: 1440px)) {
#title-icon {
width: 164px;
height: 164px;
}
.lds-ring {
width: 140px;
height: 140px;
}
.lds-ring div {
width: 124px;
height: 124px;
}
#toast-notification {
padding: 12px;
}
@ -114,6 +179,20 @@ body {
}
@media only screen and ((max-width: 2559px) or (max-height: 1439px)) {
#title-icon {
width: 124px;
height: 124px;
}
.lds-ring {
width: 120px;
height: 120px;
}
.lds-ring div {
width: 104px;
height: 104px;
}
#toast-notification {
padding: 12px;
}
@ -131,6 +210,20 @@ body {
}
@media only screen and ((max-width: 1919px) or (max-height: 1079px)) {
#title-icon {
width: 84px;
height: 84px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#toast-notification {
padding: 8px;
}
@ -148,6 +241,20 @@ body {
}
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
#title-icon {
width: 64px;
height: 64px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#toast-notification {
padding: 4px;
}