1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Receivers: Added support for viewing browser supported generic file content

This commit is contained in:
Michael Hollister 2025-05-12 23:49:10 -05:00
parent e3c437a280
commit 45b8e915e3
8 changed files with 346 additions and 9 deletions

View file

@ -0,0 +1,40 @@
export const streamingMediaTypes = [
"application/vnd.apple.mpegurl",
"application/x-mpegURL",
"application/dash+xml"
];
export const supportedPlayerTypes = streamingMediaTypes.concat([
'audio/aac',
'audio/midi',
'audio/x-midi',
'audio/mpeg',
'audio/ogg',
'audio/wav',
'audio/webm',
'audio/3gpp',
'audio/3gpp2',
'video/x-msvideo',
'video/mp4',
'video/mpeg',
'video/ogg',
'video/mp2t',
'video/webm',
'video/3gpp',
'video/3gpp2'
]);
export const supportedImageTypes = [
'image/apng',
'image/avif',
'image/bmp',
'image/gif',
'image/x-icon',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'image/vnd.microsoft.icon',
'image/webp'
];

View file

@ -1,4 +1,5 @@
import { PlayMessage, PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
import { PlayMessage } from 'common/Packets';
import { streamingMediaTypes } from 'common/MimeTypes';
import * as http from 'http';
import * as url from 'url';
import { AddressInfo } from 'modules/ws';
@ -80,14 +81,8 @@ export class NetworkService {
});
}
static streamingMediaTypes = [
"application/vnd.apple.mpegurl",
"application/x-mpegURL",
"application/dash+xml"
];
static async proxyPlayIfRequired(message: PlayMessage): Promise<PlayMessage> {
if (message.headers && message.url && !NetworkService.streamingMediaTypes.find(v => v === message.container.toLocaleLowerCase())) {
if (message.headers && message.url && !streamingMediaTypes.find(v => v === message.container.toLocaleLowerCase())) {
return { ...message, url: await NetworkService.proxyFile(message.url, message.headers) };
}
return message;

View file

@ -0,0 +1,61 @@
import { PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
import { supportedImageTypes } from 'common/MimeTypes';
import * as connectionMonitor from '../ConnectionMonitor';
import { toast, ToastIcon } from '../components/Toast';
const logger = window.targetAPI.logger;
const imageViewer = document.getElementById('viewer-image') as HTMLImageElement;
const genericViewer = document.getElementById('viewer-generic') as HTMLIFrameElement;
function onPlay(_event, value: PlayMessage) {
logger.info("Handle play message renderer", JSON.stringify(value));
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");
genericViewer.style.display = "none";
genericViewer.src = "";
imageViewer.src = src;
imageViewer.style.display = "block";
}
else if (src && genericViewer) {
logger.info("Loading generic viewer");
imageViewer.style.display = "none";
imageViewer.src = "";
genericViewer.src = src;
genericViewer.style.display = "block";
} else {
logger.error("Error loading content");
imageViewer.style.display = "none";
imageViewer.src = "";
genericViewer.style.display = "none";
genericViewer.src = "";
}
};
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'); });
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);

View file

@ -0,0 +1,164 @@
html {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
margin: 0;
padding: 0;
background-color: black;
color: white;
width: 100vw;
max-width: 100%;
height: 100vh;
max-height: 100%;
}
.viewer {
object-fit: contain;
width: 100%;
height: 100%;
}
*:focus {
outline: none;
box-shadow: none;
}
#toast-notification {
display: flex;
flex-direction: row;
align-items: center;
padding: 16px 20px;
position: relative;
top: calc(-100% + 20px);
margin: auto;
max-width: 25%;
width: fit-content;
background: rgba(0, 0, 0, 0.7);
border: 3px solid rgba(255, 255, 255, 0.08);
box-shadow: 0px 100px 80px rgba(0, 0, 0, 0.33), 0px 64.8148px 46.8519px rgba(0, 0, 0, 0.250556), 0px 38.5185px 25.4815px rgba(0, 0, 0, 0.200444), 0px 20px 13px rgba(0, 0, 0, 0.165), 0px 8.14815px 6.51852px rgba(0, 0, 0, 0.129556), 0px 1.85185px 3.14815px rgba(0, 0, 0, 0.0794444);
border-radius: 12px;
opacity: 0;
}
#toast-icon {
width: 88px;
height: 88px;
background-image: url(../assets/icons/app/info.svg);
background-size: cover;
filter: grayscale(0.5);
flex-shrink: 0;
}
#toast-text {
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
margin-right: 5px;
font-family: InterVariable;
font-size: 28px;
font-style: normal;
font-weight: 400;
}
.toast-fade-in {
animation: toast-fade-in 1.0s cubic-bezier(0.5, 0, 0.5, 1) 1;
}
.toast-fade-out {
animation: toast-fade-out 1.0s cubic-bezier(0.5, 0, 0.5, 1) 1;
}
@keyframes toast-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes toast-fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Display scaling (Minimum supported resolution is 960x540) */
@media only screen and ((min-width: 2560px) or (min-height: 1440px)) {
#toast-notification {
padding: 12px;
}
#toast-icon {
width: 70px;
height: 70px;
margin: 5px 10px;
margin-right: 15px;
}
#toast-text {
font-size: 28px;
}
}
@media only screen and ((max-width: 2559px) or (max-height: 1439px)) {
#toast-notification {
padding: 12px;
}
#toast-icon {
width: 60px;
height: 60px;
margin: 5px 5px;
margin-right: 10px;
}
#toast-text {
font-size: 22px;
}
}
@media only screen and ((max-width: 1919px) or (max-height: 1079px)) {
#toast-notification {
padding: 8px;
}
#toast-icon {
width: 40px;
height: 40px;
margin: 5px 5px;
margin-right: 10px;
}
#toast-text {
font-size: 16px;
}
}
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
#toast-notification {
padding: 4px;
}
#toast-icon {
width: 32px;
height: 32px;
margin: 5px 5px;
}
#toast-text {
font-size: 14px;
}
}

View file

@ -1,5 +1,6 @@
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog, shell } from 'electron';
import { Opcode, PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
import { supportedPlayerTypes } from 'common/MimeTypes';
import { DiscoveryService } from 'common/DiscoveryService';
import { TcpListenerService } from 'common/TcpListenerService';
import { WebSocketListenerService } from 'common/WebSocketListenerService';
@ -168,7 +169,8 @@ export class Main {
Main.playerWindow.setAlwaysOnTop(false, 'pop-up-menu');
Main.playerWindow.show();
Main.playerWindow.loadFile(path.join(__dirname, 'player/index.html'));
const rendererPath = supportedPlayerTypes.find(v => v === message.container.toLocaleLowerCase()) ? 'player' : 'viewer';
Main.playerWindow.loadFile(path.join(__dirname, `${rendererPath}/index.html`));
Main.playerWindow.on('ready-to-show', async () => {
Main.playerWindow?.webContents?.send("play", await NetworkService.proxyPlayIfRequired(message));
});

View file

@ -0,0 +1,3 @@
import 'common/viewer/Renderer';
// const logger = window.targetAPI.logger;

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>FCast Receiver</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../assets/fonts/inter.css" />
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="viewer" class="viewer">
<img id="viewer-image" class="viewer" />
<iframe id="viewer-generic" class="viewer"></iframe>
</div>>
<div id="toast-notification">
<div id="toast-icon"></div>
<div id="toast-text"></div>
</div>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -152,5 +152,53 @@ module.exports = [
TARGET: JSON.stringify(TARGET)
})
]
},
{
mode: buildMode,
entry: {
// Player preload is intentionally reused
preload: './src/player/Preload.ts',
renderer: './src/viewer/Renderer.ts',
},
target: 'electron-renderer',
module: {
rules: [
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, '../common/web'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
}
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'modules': path.resolve(__dirname, 'node_modules'),
'common': path.resolve(__dirname, '../common/web'),
},
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/viewer'),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: '../common/web/viewer/common.css',
to: '[name][ext]',
},
{
from: './src/viewer/*',
to: '[name][ext]',
globOptions: { ignore: ['**/*.ts'] }
}
],
}),
new webpack.DefinePlugin({
TARGET: JSON.stringify(TARGET)
})
]
}
];