2025-04-18 18:28:25 -05:00
import { BrowserWindow , ipcMain , IpcMainEvent , nativeImage , Tray , Menu , dialog , shell } from 'electron' ;
2025-06-12 14:13:05 -05:00
import { ToastIcon } from 'common/components/Toast' ;
2025-06-10 14:23:06 -05:00
import { Opcode , PlaybackErrorMessage , PlaybackUpdateMessage , VolumeUpdateMessage , PlayMessage , PlayUpdateMessage , EventMessage , EventType , ContentObject , ContentType , PlaylistContent , SeekMessage , SetVolumeMessage , SetSpeedMessage , SetPlaylistItemMessage } from 'common/Packets' ;
2025-05-12 23:49:10 -05:00
import { supportedPlayerTypes } from 'common/MimeTypes' ;
2024-12-09 00:56:55 -06:00
import { DiscoveryService } from 'common/DiscoveryService' ;
import { TcpListenerService } from 'common/TcpListenerService' ;
import { WebSocketListenerService } from 'common/WebSocketListenerService' ;
import { NetworkService } from 'common/NetworkService' ;
2025-05-01 13:28:56 -05:00
import { ConnectionMonitor } from 'common/ConnectionMonitor' ;
2025-05-01 10:37:21 -05:00
import { Logger , LoggerType } from 'common/Logger' ;
2025-06-10 14:23:06 -05:00
import { fetchJSON } from 'common/UtilityBackend' ;
import { MediaCache } from 'common/MediaCache' ;
2025-06-12 14:13:05 -05:00
import { Settings } from 'common/Settings' ;
2023-06-20 08:45:01 +02:00
import { Updater } from './Updater' ;
2024-01-04 12:38:39 +01:00
import * as os from 'os' ;
import * as path from 'path' ;
2024-09-06 15:44:20 -05:00
import yargs from 'yargs' ;
import { hideBin } from 'yargs/helpers' ;
2024-12-09 00:56:55 -06:00
const cp = require ( 'child_process' ) ;
2025-05-01 10:37:21 -05:00
let logger = null ;
2023-06-20 08:45:01 +02:00
2025-06-03 14:29:25 -05:00
class AppCache {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public interfaces : any = null ;
public appName : string = null ;
public appVersion : string = null ;
public playMessage : PlayMessage = null ;
2025-06-10 14:23:06 -05:00
public playerVolume : number = null ;
2025-06-03 14:29:25 -05:00
public subscribedKeys = new Set < string > ( ) ;
}
2024-12-09 00:56:55 -06:00
export class Main {
2024-09-06 15:44:20 -05:00
static shouldOpenMainWindow = true ;
2024-11-14 01:04:00 -06:00
static startFullscreen = false ;
2023-12-06 13:03:23 +01:00
static playerWindow : Electron.BrowserWindow ;
2023-06-20 08:45:01 +02:00
static mainWindow : Electron.BrowserWindow ;
static application : Electron.App ;
2023-12-06 11:50:26 +01:00
static tcpListenerService : TcpListenerService ;
static webSocketListenerService : WebSocketListenerService ;
2023-06-20 08:45:01 +02:00
static discoveryService : DiscoveryService ;
2025-05-01 13:28:56 -05:00
static connectionMonitor : ConnectionMonitor ;
2023-06-20 08:45:01 +02:00
static tray : Tray ;
2025-06-03 14:29:25 -05:00
static cache : AppCache = new AppCache ( ) ;
2023-06-20 08:45:01 +02:00
2025-05-13 00:22:24 -05:00
private static playerWindowContentViewer = null ;
2025-06-10 14:23:06 -05:00
private static listeners = [ ] ;
private static mediaCache : MediaCache = null ;
2025-05-01 12:18:59 -05:00
2024-11-14 01:04:00 -06:00
private static toggleMainWindow() {
if ( Main . mainWindow ) {
Main . mainWindow . close ( ) ;
}
else {
Main . openMainWindow ( ) ;
}
}
2024-11-17 23:36:16 -06:00
private static async checkForUpdates ( silent : boolean ) {
if ( Updater . updateDownloaded ) {
2025-04-29 12:17:29 -05:00
Main . mainWindow ? . webContents ? . send ( "download-complete" ) ;
2024-11-17 23:36:16 -06:00
return ;
}
try {
const updateAvailable = await Updater . checkForUpdates ( ) ;
if ( updateAvailable ) {
2025-04-29 12:17:29 -05:00
Main . mainWindow ? . webContents ? . send ( "update-available" ) ;
2024-11-17 23:36:16 -06:00
}
else {
if ( ! silent ) {
await dialog . showMessageBox ( {
type : 'info' ,
title : 'Already up-to-date' ,
message : 'The application is already on the latest version.' ,
buttons : [ 'OK' ] ,
defaultId : 0
} ) ;
}
}
} catch ( err ) {
if ( ! silent ) {
await dialog . showMessageBox ( {
type : 'error' ,
title : 'Failed to check for updates' ,
message : err ,
buttons : [ 'OK' ] ,
defaultId : 0
} ) ;
}
2025-05-01 10:37:21 -05:00
logger . error ( 'Failed to check for updates:' , err ) ;
2024-11-17 23:36:16 -06:00
}
}
2023-06-20 08:45:01 +02:00
private static createTray() {
2024-12-09 00:56:55 -06:00
const icon = ( process . platform === 'win32' ) ? path . join ( __dirname , 'assets/icons/app/icon.ico' ) : path . join ( __dirname , 'assets/icons/app/icon.png' ) ;
2023-06-20 08:45:01 +02:00
const trayicon = nativeImage . createFromPath ( icon )
const tray = new Tray ( trayicon . resize ( { width : 16 } ) ) ;
const contextMenu = Menu . buildFromTemplate ( [
2023-12-06 13:03:23 +01:00
{
2024-11-14 01:04:00 -06:00
label : 'Toggle window' ,
click : ( ) = > { Main . toggleMainWindow ( ) ; }
2023-12-06 13:03:23 +01:00
} ,
2023-06-20 08:45:01 +02:00
{
label : 'Check for updates' ,
2024-11-17 23:36:16 -06:00
click : async ( ) = > { await Main . checkForUpdates ( false ) ; } ,
2023-06-20 08:45:01 +02:00
} ,
2024-11-19 09:54:50 -06:00
{
label : 'About' ,
click : async ( ) = > {
2024-11-21 11:51:46 -06:00
let aboutMessage = ` Version: ${ Main . application . getVersion ( ) } \ n ` ;
if ( Updater . getCommit ( ) ) {
aboutMessage += ` Commit: ${ Updater . getCommit ( ) } \ n ` ;
}
aboutMessage += ` Release channel: ${ Updater . releaseChannel } \ n ` ;
2024-11-19 09:54:50 -06:00
if ( Updater . releaseChannel !== 'stable' ) {
aboutMessage += ` Release channel version: ${ Updater . getChannelVersion ( ) } \ n ` ;
}
aboutMessage += ` OS: ${ process . platform } ${ process . arch } \ n ` ;
await dialog . showMessageBox ( {
type : 'info' ,
title : 'Fcast Receiver' ,
message : aboutMessage ,
buttons : [ 'OK' ] ,
defaultId : 0
} ) ;
} ,
} ,
2023-06-20 08:45:01 +02:00
{
type : 'separator' ,
} ,
{
label : 'Restart' ,
click : ( ) = > {
this . application . relaunch ( ) ;
2024-11-11 12:24:17 -06:00
this . application . exit ( ) ;
2023-06-20 08:45:01 +02:00
}
} ,
{
label : 'Quit' ,
click : ( ) = > {
this . application . quit ( ) ;
}
}
] )
2024-09-06 15:44:20 -05:00
2023-06-20 08:45:01 +02:00
tray . setContextMenu ( contextMenu ) ;
2024-11-14 01:15:21 -06:00
// Left-click opens up tray menu, unlike in Windows/Linux
if ( process . platform !== 'darwin' ) {
tray . on ( 'click' , ( ) = > { Main . toggleMainWindow ( ) ; } ) ;
}
2023-06-20 08:45:01 +02:00
this . tray = tray ;
}
2025-06-10 14:23:06 -05:00
private static async play ( message : PlayMessage ) {
Main . listeners . forEach ( l = > l . send ( Opcode . PlayUpdate , new PlayUpdateMessage ( Date . now ( ) , message ) ) ) ;
Main . cache . playMessage = message ;
2024-09-06 15:44:20 -05:00
2025-06-10 14:23:06 -05:00
// Protocol v2 FCast PlayMessage does not contain volume field and could result in the receiver
// getting out-of-sync with the sender when player windows are closed and re-opened. Volume
// is cached in the play message when volume is not set in v3 PlayMessage.
2025-06-11 18:46:15 -05:00
message . volume = message . volume === undefined ? Main.cache.playerVolume : message.volume ;
2023-12-06 11:50:26 +01:00
2025-06-10 14:23:06 -05:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let rendererMessage : any = await NetworkService . proxyPlayIfRequired ( message ) ;
let rendererEvent = 'play' ;
let contentViewer = supportedPlayerTypes . find ( v = > v === message . container . toLocaleLowerCase ( ) ) ? 'player' : 'viewer' ;
if ( message . container === 'application/json' ) {
const json : ContentObject = message . url ? await fetchJSON ( message . url ) : JSON . parse ( message . content ) ;
if ( json && json . contentType !== undefined ) {
switch ( json . contentType ) {
case ContentType . Playlist : {
rendererMessage = json as PlaylistContent ;
rendererEvent = 'play-playlist' ;
if ( ( rendererMessage . forwardCache && rendererMessage . forwardCache > 0 ) || ( rendererMessage . backwardCache && rendererMessage . backwardCache > 0 ) ) {
Main . mediaCache ? . destroy ( ) ;
Main . mediaCache = new MediaCache ( rendererMessage ) ;
2023-12-06 11:50:26 +01:00
}
2024-09-06 15:44:20 -05:00
2025-06-10 14:23:06 -05:00
const offset = rendererMessage . offset ? rendererMessage.offset : 0 ;
contentViewer = supportedPlayerTypes . find ( v = > v === rendererMessage . items [ offset ] . container . toLocaleLowerCase ( ) ) ? 'player' : 'viewer' ;
break ;
}
2024-09-06 15:44:20 -05:00
2025-06-10 14:23:06 -05:00
default :
break ;
2025-05-13 00:22:24 -05:00
}
2025-06-10 14:23:06 -05:00
}
}
if ( ! Main . playerWindow ) {
Main . playerWindow = new BrowserWindow ( {
fullscreen : true ,
autoHideMenuBar : true ,
icon : path.join ( __dirname , 'icon512.png' ) ,
webPreferences : {
preload : path.join ( __dirname , 'player/preload.js' )
2024-09-06 15:44:20 -05:00
}
2025-06-10 14:23:06 -05:00
} ) ;
Main . playerWindow . setAlwaysOnTop ( false , 'pop-up-menu' ) ;
Main . playerWindow . show ( ) ;
2025-05-13 00:22:24 -05:00
2025-06-10 14:23:06 -05:00
Main . playerWindow . loadFile ( path . join ( __dirname , ` ${ contentViewer } /index.html ` ) ) ;
Main . playerWindow . on ( 'ready-to-show' , async ( ) = > {
Main . playerWindow ? . webContents ? . send ( rendererEvent , rendererMessage ) ;
2023-12-06 11:50:26 +01:00
} ) ;
2025-06-10 14:23:06 -05:00
Main . playerWindow . on ( 'closed' , ( ) = > {
Main . playerWindow = null ;
Main . playerWindowContentViewer = null ;
} ) ;
}
else if ( Main . playerWindow && contentViewer !== Main . playerWindowContentViewer ) {
Main . playerWindow . loadFile ( path . join ( __dirname , ` ${ contentViewer } /index.html ` ) ) ;
Main . playerWindow . on ( 'ready-to-show' , async ( ) = > {
Main . playerWindow ? . webContents ? . send ( rendererEvent , rendererMessage ) ;
} ) ;
} else {
Main . playerWindow ? . webContents ? . send ( rendererEvent , rendererMessage ) ;
}
Main . playerWindowContentViewer = contentViewer ;
}
private static onReady() {
Main . createTray ( ) ;
Main . connectionMonitor = new ConnectionMonitor ( ) ;
Main . discoveryService = new DiscoveryService ( ) ;
Main . discoveryService . start ( ) ;
Main . tcpListenerService = new TcpListenerService ( ) ;
Main . webSocketListenerService = new WebSocketListenerService ( ) ;
2024-09-06 15:44:20 -05:00
2025-06-10 14:23:06 -05:00
Main . listeners = [ Main . tcpListenerService , Main . webSocketListenerService ] ;
Main . listeners . forEach ( l = > {
l . emitter . on ( "play" , ( message : PlayMessage ) = > Main . play ( message ) ) ;
2023-12-06 13:03:23 +01:00
l . emitter . on ( "pause" , ( ) = > Main . playerWindow ? . webContents ? . send ( "pause" ) ) ;
l . emitter . on ( "resume" , ( ) = > Main . playerWindow ? . webContents ? . send ( "resume" ) ) ;
2024-09-06 15:44:20 -05:00
2023-12-06 11:50:26 +01:00
l . emitter . on ( "stop" , ( ) = > {
2025-05-13 00:22:24 -05:00
Main . playerWindow ? . close ( ) ;
Main . playerWindow = null ;
Main . playerWindowContentViewer = null ;
2023-12-06 11:50:26 +01:00
} ) ;
2024-09-06 15:44:20 -05:00
2025-06-10 14:23:06 -05:00
l . emitter . on ( "seek" , ( message : SeekMessage ) = > Main . playerWindow ? . webContents ? . send ( "seek" , message ) ) ;
l . emitter . on ( "setvolume" , ( message : SetVolumeMessage ) = > {
Main . cache . playerVolume = message . volume ;
Main . playerWindow ? . webContents ? . send ( "setvolume" , message ) ;
} ) ;
l . emitter . on ( "setspeed" , ( message : SetSpeedMessage ) = > Main . playerWindow ? . webContents ? . send ( "setspeed" , message ) ) ;
2025-01-06 20:35:57 -06:00
2025-04-25 14:04:59 -05:00
l . emitter . on ( 'connect' , ( message ) = > {
2025-05-02 15:42:44 -05:00
ConnectionMonitor . onConnect ( l , message , l instanceof WebSocketListenerService , ( ) = > {
Main . mainWindow ? . webContents ? . send ( 'connect' , message ) ;
Main . playerWindow ? . webContents ? . send ( 'connect' , message ) ;
} ) ;
2025-04-25 14:04:59 -05:00
} ) ;
l . emitter . on ( 'disconnect' , ( message ) = > {
2025-05-02 15:42:44 -05:00
ConnectionMonitor . onDisconnect ( l , message , l instanceof WebSocketListenerService , ( ) = > {
Main . mainWindow ? . webContents ? . send ( 'disconnect' , message ) ;
Main . playerWindow ? . webContents ? . send ( 'disconnect' , message ) ;
} ) ;
2025-04-25 14:04:59 -05:00
} ) ;
l . emitter . on ( 'ping' , ( message ) = > {
2025-05-02 15:42:44 -05:00
ConnectionMonitor . onPingPong ( message , l instanceof WebSocketListenerService ) ;
2025-04-25 14:04:59 -05:00
} ) ;
2025-04-29 11:57:25 -05:00
l . emitter . on ( 'pong' , ( message ) = > {
2025-05-02 15:42:44 -05:00
ConnectionMonitor . onPingPong ( message , l instanceof WebSocketListenerService ) ;
2025-04-29 11:57:25 -05:00
} ) ;
2025-06-03 14:29:25 -05:00
l . emitter . on ( 'initial' , ( message ) = > {
logger . info ( ` Received 'Initial' message from sender: ${ message } ` ) ;
} ) ;
2025-06-10 14:23:06 -05:00
l . emitter . on ( "setplaylistitem" , ( message : SetPlaylistItemMessage ) = > Main . playerWindow ? . webContents ? . send ( "setplaylistitem" , message ) ) ;
2025-06-03 14:29:25 -05:00
l . emitter . on ( 'subscribeevent' , ( message ) = > {
const subscribeData = l . subscribeEvent ( message . sessionId , message . body . event ) ;
if ( message . body . event . type === EventType . KeyDown . valueOf ( ) || message . body . event . type === EventType . KeyUp . valueOf ( ) ) {
Main . mainWindow ? . webContents ? . send ( "event-subscribed-keys-update" , subscribeData ) ;
Main . playerWindow ? . webContents ? . send ( "event-subscribed-keys-update" , subscribeData ) ;
}
} ) ;
l . emitter . on ( 'unsubscribeevent' , ( message ) = > {
const unsubscribeData = l . unsubscribeEvent ( message . sessionId , message . body . event ) ;
if ( message . body . event . type === EventType . KeyDown . valueOf ( ) || message . body . event . type === EventType . KeyUp . valueOf ( ) ) {
Main . mainWindow ? . webContents ? . send ( "event-subscribed-keys-update" , unsubscribeData ) ;
Main . playerWindow ? . webContents ? . send ( "event-subscribed-keys-update" , unsubscribeData ) ;
}
} ) ;
2023-12-06 11:50:26 +01:00
l . start ( ) ;
2023-06-20 08:45:01 +02:00
2023-12-07 16:10:18 +01:00
ipcMain . on ( 'send-playback-error' , ( event : IpcMainEvent , value : PlaybackErrorMessage ) = > {
2023-12-30 10:55:30 +01:00
l . send ( Opcode . PlaybackError , value ) ;
2023-12-07 16:10:18 +01:00
} ) ;
2023-12-06 11:50:26 +01:00
ipcMain . on ( 'send-playback-update' , ( event : IpcMainEvent , value : PlaybackUpdateMessage ) = > {
2023-12-30 10:55:30 +01:00
l . send ( Opcode . PlaybackUpdate , value ) ;
2023-12-06 11:50:26 +01:00
} ) ;
2024-09-06 15:44:20 -05:00
2023-12-06 11:50:26 +01:00
ipcMain . on ( 'send-volume-update' , ( event : IpcMainEvent , value : VolumeUpdateMessage ) = > {
2025-06-10 14:23:06 -05:00
Main . cache . playerVolume = value . volume ;
2023-12-30 10:55:30 +01:00
l . send ( Opcode . VolumeUpdate , value ) ;
2023-12-06 11:50:26 +01:00
} ) ;
2025-06-03 14:29:25 -05:00
2025-06-10 14:23:06 -05:00
ipcMain . on ( 'send-event' , ( event : IpcMainEvent , value : EventMessage ) = > {
2025-06-03 14:29:25 -05:00
l . send ( Opcode . Event , value ) ;
} ) ;
2025-04-22 17:28:02 -05:00
} ) ;
2024-11-17 23:12:24 -06:00
2025-06-10 14:23:06 -05:00
ipcMain . on ( 'play-request' , ( event : IpcMainEvent , value : PlayMessage , playlistIndex : number ) = > {
logger . debug ( ` Received play request for index ${ playlistIndex } : ` , value ) ;
2025-06-11 14:51:55 -05:00
value . url = Main . mediaCache . has ( playlistIndex ) ? Main . mediaCache . getUrl ( playlistIndex ) : value . url ;
Main . mediaCache . cacheItems ( playlistIndex ) ;
2025-06-10 14:23:06 -05:00
Main . play ( value ) ;
} ) ;
2025-04-22 17:28:02 -05:00
ipcMain . on ( 'send-download-request' , async ( ) = > {
if ( ! Updater . isDownloading ) {
try {
await Updater . downloadUpdate ( ) ;
2025-04-29 12:17:29 -05:00
Main . mainWindow ? . webContents ? . send ( "download-complete" ) ;
2025-04-22 17:28:02 -05:00
} catch ( err ) {
await dialog . showMessageBox ( {
type : 'error' ,
title : 'Failed to download update' ,
message : err ,
buttons : [ 'OK' ] ,
defaultId : 0
} ) ;
2025-05-01 10:37:21 -05:00
logger . error ( 'Failed to download update:' , err ) ;
2025-04-29 12:17:29 -05:00
Main . mainWindow ? . webContents ? . send ( "download-failed" ) ;
2024-11-17 23:12:24 -06:00
}
2025-04-22 17:28:02 -05:00
}
} ) ;
2024-11-17 23:12:24 -06:00
2025-04-22 17:28:02 -05:00
ipcMain . on ( 'send-restart-request' , async ( ) = > {
Updater . restart ( ) ;
2023-06-20 08:45:01 +02:00
} ) ;
2024-11-17 23:12:24 -06:00
ipcMain . handle ( 'updater-progress' , async ( ) = > { return Updater . updateProgress ; } ) ;
2024-11-04 09:17:20 -06:00
ipcMain . handle ( 'is-full-screen' , async ( ) = > {
const window = Main . playerWindow ;
if ( ! window ) {
return ;
}
return window . isFullScreen ( ) ;
} ) ;
2023-06-20 08:45:01 +02:00
ipcMain . on ( 'toggle-full-screen' , ( ) = > {
2023-12-06 13:03:23 +01:00
const window = Main . playerWindow ;
2023-06-20 08:45:01 +02:00
if ( ! window ) {
return ;
}
window . setFullScreen ( ! window . isFullScreen ( ) ) ;
} ) ;
ipcMain . on ( 'exit-full-screen' , ( ) = > {
2023-12-06 13:03:23 +01:00
const window = Main . playerWindow ;
2023-06-20 08:45:01 +02:00
if ( ! window ) {
return ;
}
window . setFullScreen ( false ) ;
} ) ;
2023-12-06 13:03:23 +01:00
2025-05-02 15:42:44 -05:00
// Having to mix and match session ids and ip addresses until querying websocket remote addresses is fixed
2025-04-29 13:03:10 -05:00
ipcMain . handle ( 'get-sessions' , ( ) = > {
2025-05-02 15:42:44 -05:00
return [ ] . concat ( Main . tcpListenerService . getSenders ( ) , Main . webSocketListenerService . getSessions ( ) ) ;
2025-04-29 13:03:10 -05:00
} ) ;
2025-04-30 10:17:20 -05:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain . on ( 'network-changed' , ( event : IpcMainEvent , value : any ) = > {
2025-06-03 14:29:25 -05:00
Main . cache . interfaces = value ;
2025-06-13 09:42:10 -05:00
Main . discoveryService . stop ( ) ;
Main . discoveryService . start ( ) ;
2025-04-30 10:17:20 -05:00
Main . mainWindow ? . webContents ? . send ( "device-info" , { name : os.hostname ( ) , interfaces : value } ) ;
} ) ;
2023-12-06 13:03:23 +01:00
if ( Main . shouldOpenMainWindow ) {
Main . openMainWindow ( ) ;
}
2024-11-17 23:12:24 -06:00
if ( Updater . updateError ) {
dialog . showMessageBox ( {
type : 'error' ,
title : 'Error applying update' ,
message : 'Please try again later or visit https://fcast.org to update.' ,
buttons : [ 'OK' ] ,
defaultId : 0
} ) ;
}
2024-11-17 23:36:16 -06:00
if ( Updater . checkForUpdatesOnStart ) {
Main . checkForUpdates ( true ) ;
}
2025-04-18 18:28:25 -05:00
2025-04-29 12:17:29 -05:00
Main . mainWindow ? . webContents ? . setWindowOpenHandler ( ( details ) = > {
2025-04-18 18:28:25 -05:00
shell . openExternal ( details . url ) ;
return { action : "deny" } ;
} ) ;
2023-06-20 08:45:01 +02:00
}
2023-12-06 13:03:23 +01:00
static openMainWindow() {
if ( Main . mainWindow ) {
Main . mainWindow . focus ( ) ;
return ;
}
Main . mainWindow = new BrowserWindow ( {
2024-11-14 01:04:00 -06:00
fullscreen : Main.startFullscreen ,
2023-12-06 13:03:23 +01:00
autoHideMenuBar : true ,
2024-10-21 20:51:07 +00:00
icon : path.join ( __dirname , 'icon512.png' ) ,
2023-12-06 13:03:23 +01:00
webPreferences : {
preload : path.join ( __dirname , 'main/preload.js' )
}
} ) ;
2025-04-30 10:17:20 -05:00
const networkWorker = new BrowserWindow ( {
2025-04-30 10:30:56 -05:00
show : false ,
2025-04-30 10:17:20 -05:00
webPreferences : {
nodeIntegration : true ,
contextIsolation : false ,
preload : path.join ( __dirname , 'main/networkWorker.js' )
}
} ) ;
2023-12-06 13:03:23 +01:00
Main . mainWindow . loadFile ( path . join ( __dirname , 'main/index.html' ) ) ;
Main . mainWindow . on ( 'closed' , ( ) = > {
Main . mainWindow = null ;
2025-05-01 09:44:59 -05:00
2025-05-01 10:37:21 -05:00
if ( ! networkWorker . isDestroyed ( ) ) {
2025-05-01 09:44:59 -05:00
networkWorker . close ( ) ;
}
2023-12-06 13:03:23 +01:00
} ) ;
2024-11-14 01:04:00 -06:00
Main . mainWindow . maximize ( ) ;
2023-12-06 13:03:23 +01:00
Main . mainWindow . show ( ) ;
Main . mainWindow . on ( 'ready-to-show' , ( ) = > {
2025-06-03 14:29:25 -05:00
if ( Main . cache . interfaces ) {
Main . mainWindow ? . webContents ? . send ( "device-info" , { name : os.hostname ( ) , interfaces : Main.cache.interfaces } ) ;
2025-05-01 12:18:59 -05:00
}
2025-04-23 19:21:17 -05:00
networkWorker . loadFile ( path . join ( __dirname , 'main/worker.html' ) ) ;
2023-12-06 13:03:23 +01:00
} ) ;
2024-09-06 15:44:20 -05:00
}
2023-12-06 13:03:23 +01:00
2024-11-11 12:24:17 -06:00
static async main ( app : Electron.App ) {
2024-11-15 00:43:01 -06:00
try {
Main . application = app ;
2025-06-03 14:29:25 -05:00
Main . cache . appName = app . name ;
Main . cache . appVersion = app . getVersion ( ) ;
2024-11-19 17:31:03 -06:00
2025-06-12 14:13:05 -05:00
// Using singleton classes for better compatibility running on webOS
const jsonPath = path . join ( app . getPath ( 'userData' ) , 'UserSettings.json' ) ;
new Settings ( jsonPath ) ;
if ( Settings . json . network . ignoreCertificateErrors ) {
app . commandLine . appendSwitch ( 'ignore-certificate-errors' ) ;
}
2024-11-19 17:31:03 -06:00
const argv = yargs ( hideBin ( process . argv ) )
. version ( app . getVersion ( ) )
. parserConfiguration ( {
'boolean-negation' : false
} )
. options ( {
'no-main-window' : { type : 'boolean' , default : false , desc : "Start minimized to tray" } ,
2025-05-01 10:37:21 -05:00
'fullscreen' : { type : 'boolean' , default : false , desc : "Start application in fullscreen" } ,
'log' : { chocies : [ 'ALL' , 'TRACE' , 'DEBUG' , 'INFO' , 'WARN' , 'ERROR' , 'FATAL' , 'MARK' , 'OFF' ] , alias : 'loglevel' , default : 'INFO' , desc : "Defines the verbosity level of the logger" } ,
2024-11-19 17:31:03 -06:00
} )
. parseSync ( ) ;
2025-06-12 11:56:44 -05:00
new Updater ( ) ;
2024-11-15 00:43:01 -06:00
const isUpdating = Updater . isUpdating ( ) ;
2024-11-17 11:54:02 -06:00
const fileLogType = isUpdating ? 'fileSync' : 'file' ;
2025-05-01 10:37:21 -05:00
Logger . initialize ( {
2024-11-15 00:43:01 -06:00
appenders : {
out : { type : 'stdout' } ,
2024-11-17 11:54:02 -06:00
log : { type : fileLogType , filename : path.join ( app . getPath ( 'logs' ) , 'fcast-receiver.log' ) , flags : 'a' , maxLogSize : '5M' } ,
2024-11-15 00:43:01 -06:00
} ,
categories : {
2025-05-01 10:37:21 -05:00
default : { appenders : [ 'out' , 'log' ] , level : argv.log } ,
2024-11-15 00:43:01 -06:00
} ,
} ) ;
2025-05-01 10:37:21 -05:00
logger = new Logger ( 'Main' , LoggerType . BACKEND ) ;
logger . info ( ` Starting application: ${ app . name } | ${ app . getAppPath ( ) } ` ) ;
logger . info ( ` Version: ${ app . getVersion ( ) } ` ) ;
logger . info ( ` Commit: ${ Updater . getCommit ( ) } ` ) ;
logger . info ( ` Release channel: ${ Updater . releaseChannel } - ${ Updater . getChannelVersion ( ) } ` ) ;
logger . info ( ` OS: ${ process . platform } ${ process . arch } ` ) ;
2024-11-11 12:24:17 -06:00
2025-05-01 12:15:33 -05:00
process . setUncaughtExceptionCaptureCallback ( async ( error ) = > await errorHandler ( error ) ) ;
2024-11-15 00:43:01 -06:00
if ( isUpdating ) {
await Updater . processUpdate ( ) ;
}
2024-11-11 12:24:17 -06:00
2024-11-15 00:43:01 -06:00
Main . startFullscreen = argv . fullscreen ;
Main . shouldOpenMainWindow = ! argv . noMainWindow ;
2025-04-18 11:32:01 -05:00
const lock = Main . application . requestSingleInstanceLock ( )
if ( ! lock ) {
Main . application . quit ( ) ;
return ;
}
Main . application . on ( 'second-instance' , ( ) = > {
if ( Main . mainWindow ) {
if ( Main . mainWindow . isMinimized ( ) ) {
Main . mainWindow . restore ( ) ;
}
Main . mainWindow . focus ( ) ;
}
else {
Main . openMainWindow ( ) ;
}
} )
2025-06-12 14:13:05 -05:00
Main . application . on ( 'certificate-error' , ( _event , _webContents , url , error , certificate ) = > {
toast ( 'Could not playback media (certificate error)' , ToastIcon . ERROR ) ;
logger . error ( 'Could not playback media (certificate error):' , { url : url , error : error , certificate : certificate } ) ;
} ) ;
2024-11-15 00:43:01 -06:00
Main . application . on ( 'ready' , Main . onReady ) ;
Main . application . on ( 'window-all-closed' , ( ) = > { } ) ;
}
catch ( err ) {
2025-05-01 10:37:21 -05:00
logger . error ( ` Error starting application: ${ err } ` ) ;
2024-11-15 00:43:01 -06:00
app . exit ( ) ;
2024-11-11 12:24:17 -06:00
}
2023-06-20 08:45:01 +02:00
}
2024-11-11 12:24:17 -06:00
}
2024-12-09 00:56:55 -06:00
2025-06-12 14:13:05 -05:00
export function toast ( message : string , icon : ToastIcon = ToastIcon . INFO , duration : number = 5000 ) {
Main . mainWindow ? . webContents ? . send ( 'toast' , message , icon , duration ) ;
Main . playerWindow ? . webContents ? . send ( 'toast' , message , icon , duration ) ;
}
2024-12-09 00:56:55 -06:00
export function getComputerName() {
switch ( process . platform ) {
case "win32" :
return process . env . COMPUTERNAME ;
case "darwin" :
return cp . execSync ( "scutil --get ComputerName" ) . toString ( ) . trim ( ) ;
case "linux" : {
let hostname : string ;
// Some distro's don't work with `os.hostname()`, but work with `hostnamectl` and vice versa...
try {
hostname = os . hostname ( ) ;
}
catch ( err ) {
2025-05-01 10:37:21 -05:00
logger . warn ( 'Error fetching hostname, trying different method...' ) ;
logger . warn ( err ) ;
2024-12-09 00:56:55 -06:00
try {
hostname = cp . execSync ( "hostnamectl hostname" ) . toString ( ) . trim ( ) ;
}
catch ( err2 ) {
2025-05-01 10:37:21 -05:00
logger . warn ( 'Error fetching hostname again, using generic name...' ) ;
logger . warn ( err2 ) ;
2024-12-09 00:56:55 -06:00
hostname = 'linux device' ;
}
}
return hostname ;
}
default :
return os . hostname ( ) ;
}
}
2025-06-03 14:29:25 -05:00
export function getAppName() {
return Main . cache . appName ;
}
export function getAppVersion() {
return Main . cache . appVersion ;
}
export function getPlayMessage() {
return Main . cache . playMessage ;
}
2025-05-01 12:15:33 -05:00
export async function errorHandler ( error : Error ) {
logger . error ( error ) ;
logger . shutdown ( ) ;
2024-12-09 00:56:55 -06:00
const restartPrompt = await dialog . showMessageBox ( {
type : 'error' ,
2025-05-02 15:42:44 -05:00
title : 'Application Error' ,
message : ` The application encountered an error: \ n \ n ${ error . stack } } ` ,
2024-12-09 00:56:55 -06:00
buttons : [ 'Restart' , 'Close' ] ,
defaultId : 0 ,
cancelId : 1
} ) ;
if ( restartPrompt . response === 0 ) {
Main . application . relaunch ( ) ;
Main . application . exit ( 0 ) ;
} else {
Main . application . exit ( 0 ) ;
}
}