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:
parent
e3c437a280
commit
45b8e915e3
8 changed files with 346 additions and 9 deletions
40
receivers/common/web/MimeTypes.ts
Normal file
40
receivers/common/web/MimeTypes.ts
Normal 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'
|
||||
];
|
|
@ -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;
|
||||
|
|
61
receivers/common/web/viewer/Renderer.ts
Normal file
61
receivers/common/web/viewer/Renderer.ts
Normal 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);
|
164
receivers/common/web/viewer/common.css
Normal file
164
receivers/common/web/viewer/common.css
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
|
|
3
receivers/electron/src/viewer/Renderer.ts
Normal file
3
receivers/electron/src/viewer/Renderer.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'common/viewer/Renderer';
|
||||
|
||||
// const logger = window.targetAPI.logger;
|
24
receivers/electron/src/viewer/index.html
Normal file
24
receivers/electron/src/viewer/index.html
Normal 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>
|
|
@ -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)
|
||||
})
|
||||
]
|
||||
}
|
||||
];
|
Loading…
Add table
Add a link
Reference in a new issue