diff --git a/receivers/common/web/MimeTypes.ts b/receivers/common/web/MimeTypes.ts new file mode 100644 index 0000000..bad3506 --- /dev/null +++ b/receivers/common/web/MimeTypes.ts @@ -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' +]; diff --git a/receivers/common/web/NetworkService.ts b/receivers/common/web/NetworkService.ts index 5386121..8b68b97 100644 --- a/receivers/common/web/NetworkService.ts +++ b/receivers/common/web/NetworkService.ts @@ -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 { - 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; diff --git a/receivers/common/web/viewer/Renderer.ts b/receivers/common/web/viewer/Renderer.ts new file mode 100644 index 0000000..cb0e55a --- /dev/null +++ b/receivers/common/web/viewer/Renderer.ts @@ -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); diff --git a/receivers/common/web/viewer/common.css b/receivers/common/web/viewer/common.css new file mode 100644 index 0000000..25c9d64 --- /dev/null +++ b/receivers/common/web/viewer/common.css @@ -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; + } +} diff --git a/receivers/electron/src/Main.ts b/receivers/electron/src/Main.ts index 68ec8d0..cb17c7c 100644 --- a/receivers/electron/src/Main.ts +++ b/receivers/electron/src/Main.ts @@ -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)); }); diff --git a/receivers/electron/src/viewer/Renderer.ts b/receivers/electron/src/viewer/Renderer.ts new file mode 100644 index 0000000..d31174d --- /dev/null +++ b/receivers/electron/src/viewer/Renderer.ts @@ -0,0 +1,3 @@ +import 'common/viewer/Renderer'; + +// const logger = window.targetAPI.logger; diff --git a/receivers/electron/src/viewer/index.html b/receivers/electron/src/viewer/index.html new file mode 100644 index 0000000..f0aa89a --- /dev/null +++ b/receivers/electron/src/viewer/index.html @@ -0,0 +1,24 @@ + + + + FCast Receiver + + + + + + +
+ + +
> + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/receivers/electron/webpack.config.js b/receivers/electron/webpack.config.js index 8489ea9..ca15c0c 100644 --- a/receivers/electron/webpack.config.js +++ b/receivers/electron/webpack.config.js @@ -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) + }) + ] } ]; \ No newline at end of file