mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-07 01:32:49 +00:00
webOS: Initial port of Electron v2.2.0 changes
This commit is contained in:
parent
b08c3dab95
commit
4c1cb6cf8e
28 changed files with 1273 additions and 532 deletions
|
@ -5,17 +5,30 @@
|
|||
const Service = __non_webpack_require__('webos-service');
|
||||
// const Service = require('webos-service');
|
||||
|
||||
import { Opcode, PlayMessage, PlaybackErrorMessage, PlaybackUpdateMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from 'common/Packets';
|
||||
import { EventMessage, EventType, Opcode, PlayMessage, PlayUpdateMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlaylistContent, SeekMessage,
|
||||
SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from 'common/Packets';
|
||||
import { DiscoveryService } from 'common/DiscoveryService';
|
||||
import { TcpListenerService } from 'common/TcpListenerService';
|
||||
import { WebSocketListenerService } from 'common/WebSocketListenerService';
|
||||
import { NetworkService } from 'common/NetworkService';
|
||||
import { ConnectionMonitor } from 'common/ConnectionMonitor';
|
||||
import { Logger, LoggerType } from 'common/Logger';
|
||||
import { MediaCache } from 'common/MediaCache';
|
||||
import { preparePlayMessage } from 'common/UtilityBackend';
|
||||
import * as os from 'os';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ToastIcon } from 'common/components/Toast';
|
||||
const logger = new Logger('Main', LoggerType.BACKEND);
|
||||
const serviceId = 'com.futo.fcast.receiver.service';
|
||||
const service = new Service(serviceId);
|
||||
|
||||
class AppCache {
|
||||
public interfaces: any = null;
|
||||
public appName: string = null;
|
||||
public appVersion: string = null;
|
||||
public playMessage: PlayMessage = null;
|
||||
public playerVolume: number = null;
|
||||
public subscribedKeys = new Set<string>();
|
||||
}
|
||||
|
||||
export class Main {
|
||||
static tcpListenerService: TcpListenerService;
|
||||
|
@ -23,14 +36,39 @@ export class Main {
|
|||
static discoveryService: DiscoveryService;
|
||||
static connectionMonitor: ConnectionMonitor;
|
||||
static emitter: EventEmitter;
|
||||
static cache: AppCache = new AppCache();
|
||||
|
||||
private static listeners = [];
|
||||
private static mediaCache: MediaCache = null;
|
||||
|
||||
private static windowVisible: boolean = false;
|
||||
private static windowType: string = 'main';
|
||||
|
||||
private static async play(message: PlayMessage) {
|
||||
Main.listeners.forEach(l => l.send(Opcode.PlayUpdate, new PlayUpdateMessage(Date.now(), message)));
|
||||
Main.cache.playMessage = message;
|
||||
const messageInfo = await preparePlayMessage(message, Main.cache.playerVolume, (playMessage: PlaylistContent) => {
|
||||
Main.mediaCache?.destroy();
|
||||
Main.mediaCache = new MediaCache(playMessage);
|
||||
});
|
||||
|
||||
Main.emitter.emit('play', messageInfo);
|
||||
if (!Main.windowVisible) {
|
||||
const appId = 'com.futo.fcast.receiver';
|
||||
service.call("luna://com.webos.applicationManager/launch", {
|
||||
'id': appId,
|
||||
'params': { timestamp: Date.now(), messageInfo: messageInfo }
|
||||
}, (response: any) => {
|
||||
logger.info(`Launch response: ${JSON.stringify(response)}`);
|
||||
logger.info(`Relaunching FCast Receiver with args: ${messageInfo.rendererEvent} ${JSON.stringify(messageInfo.rendererMessage)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
try {
|
||||
logger.info(`OS: ${process.platform} ${process.arch}`);
|
||||
|
||||
const serviceId = 'com.futo.fcast.receiver.service';
|
||||
const service = new Service(serviceId);
|
||||
|
||||
// Service will timeout and casting will disconnect if not forced to be kept alive
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let keepAlive;
|
||||
|
@ -38,22 +76,6 @@ export class Main {
|
|||
keepAlive = activity;
|
||||
});
|
||||
|
||||
const voidCb = (message: any) => { message.respond({ returnValue: true, value: {} }); };
|
||||
const objectCb = (message: any, value: any) => { message.respond({ returnValue: true, value: value }); };
|
||||
|
||||
registerService(service, 'toast', (message: any) => { return objectCb.bind(this, message) });
|
||||
|
||||
// getDeviceInfo and network-changed handled in frontend
|
||||
service.register("get_sessions", (message: any) => {
|
||||
message.respond({
|
||||
returnValue: true,
|
||||
value: [].concat(Main.tcpListenerService.getSenders(), Main.webSocketListenerService.getSessions())
|
||||
});
|
||||
});
|
||||
|
||||
registerService(service, 'connect', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'disconnect', (message: any) => { return objectCb.bind(this, message) });
|
||||
|
||||
Main.connectionMonitor = new ConnectionMonitor();
|
||||
Main.discoveryService = new DiscoveryService();
|
||||
Main.discoveryService.start();
|
||||
|
@ -62,78 +84,35 @@ export class Main {
|
|||
Main.webSocketListenerService = new WebSocketListenerService();
|
||||
|
||||
Main.emitter = new EventEmitter();
|
||||
let playData: PlayMessage = null;
|
||||
|
||||
let playClosureCb = null;
|
||||
const playCb = (message: any, playMessage: PlayMessage) => {
|
||||
playData = playMessage;
|
||||
message.respond({ returnValue: true, value: { playData: playData } });
|
||||
};
|
||||
|
||||
let stopClosureCb: any = null;
|
||||
const seekCb = (message: any, seekMessage: SeekMessage) => { message.respond({ returnValue: true, value: seekMessage }); };
|
||||
const setVolumeCb = (message: any, volumeMessage: SetVolumeMessage) => { message.respond({ returnValue: true, value: volumeMessage }); };
|
||||
const setSpeedCb = (message: any, speedMessage: SetSpeedMessage) => { message.respond({ returnValue: true, value: speedMessage }); };
|
||||
|
||||
// Note: When logging the `message` object, do NOT use JSON.stringify, you can log messages directly. Seems to be a circular reference causing errors...
|
||||
service.register('play', (message: any) => {
|
||||
if (message.isSubscription) {
|
||||
playClosureCb = playCb.bind(this, message);
|
||||
Main.emitter.on('play', playClosureCb);
|
||||
}
|
||||
|
||||
message.respond({ returnValue: true, value: { subscribed: true, playData: playData }});
|
||||
},
|
||||
(message: any) => {
|
||||
logger.info('Canceled play service subscriber');
|
||||
Main.emitter.removeAllListeners('play');
|
||||
message.respond({ returnValue: true, value: message.payload });
|
||||
});
|
||||
const voidCb = (message: any) => { message.respond({ returnValue: true, value: {} }); };
|
||||
const objectCb = (message: any, value: any) => { message.respond({ returnValue: true, value: value }); };
|
||||
|
||||
registerService(service, 'toast', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'connect', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'disconnect', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'play', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'pause', (message: any) => { return voidCb.bind(this, message) });
|
||||
registerService(service, 'resume', (message: any) => { return voidCb.bind(this, message) });
|
||||
registerService(service, 'stop', (message: any) => { return voidCb.bind(this, message) });
|
||||
registerService(service, 'seek', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'setvolume', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'setspeed', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'setplaylistitem', (message: any) => { return objectCb.bind(this, message) });
|
||||
registerService(service, 'event_subscribed_keys_update', (message: any) => { return objectCb.bind(this, message) });
|
||||
|
||||
service.register('stop', (message: any) => {
|
||||
playData = null;
|
||||
|
||||
if (message.isSubscription) {
|
||||
stopClosureCb = voidCb.bind(this, message);
|
||||
Main.emitter.on('stop', stopClosureCb);
|
||||
}
|
||||
|
||||
message.respond({ returnValue: true, value: { subscribed: true }});
|
||||
},
|
||||
(message: any) => {
|
||||
logger.info('Canceled stop service subscriber');
|
||||
Main.emitter.removeAllListeners('stop');
|
||||
message.respond({ returnValue: true, value: message.payload });
|
||||
});
|
||||
|
||||
registerService(service, 'seek', (message: any) => { return seekCb.bind(this, message) });
|
||||
registerService(service, 'setvolume', (message: any) => { return setVolumeCb.bind(this, message) });
|
||||
registerService(service, 'setspeed', (message: any) => { return setSpeedCb.bind(this, message) });
|
||||
|
||||
const listeners = [Main.tcpListenerService, Main.webSocketListenerService];
|
||||
listeners.forEach(l => {
|
||||
l.emitter.on("play", async (message) => {
|
||||
await NetworkService.proxyPlayIfRequired(message);
|
||||
Main.emitter.emit('play', message);
|
||||
|
||||
const appId = 'com.futo.fcast.receiver';
|
||||
service.call("luna://com.webos.applicationManager/launch", {
|
||||
'id': appId,
|
||||
'params': { timestamp: Date.now(), playData: message }
|
||||
}, (response: any) => {
|
||||
logger.info(`Launch response: ${JSON.stringify(response)}`);
|
||||
logger.info(`Relaunching FCast Receiver with args: ${JSON.stringify(message)}`);
|
||||
});
|
||||
});
|
||||
Main.listeners = [Main.tcpListenerService, Main.webSocketListenerService];
|
||||
Main.listeners.forEach(l => {
|
||||
l.emitter.on("play", (message: PlayMessage) => Main.play(message));
|
||||
l.emitter.on("pause", () => Main.emitter.emit('pause'));
|
||||
l.emitter.on("resume", () => Main.emitter.emit('resume'));
|
||||
l.emitter.on("stop", () => Main.emitter.emit('stop'));
|
||||
l.emitter.on("seek", (message) => Main.emitter.emit('seek', message));
|
||||
l.emitter.on("setvolume", (message) => Main.emitter.emit('setvolume', message));
|
||||
l.emitter.on("setspeed", (message) => Main.emitter.emit('setspeed', message));
|
||||
l.emitter.on("seek", (message: SeekMessage) => Main.emitter.emit('seek', message));
|
||||
l.emitter.on("setvolume", (message: SetVolumeMessage) => {
|
||||
Main.cache.playerVolume = message.volume;
|
||||
Main.emitter.emit('setvolume', message);
|
||||
});
|
||||
l.emitter.on("setspeed", (message: SetSpeedMessage) => Main.emitter.emit('setspeed', message));
|
||||
|
||||
l.emitter.on('connect', (message) => {
|
||||
ConnectionMonitor.onConnect(l, message, l instanceof WebSocketListenerService, () => {
|
||||
|
@ -151,48 +130,117 @@ export class Main {
|
|||
l.emitter.on('pong', (message) => {
|
||||
ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService);
|
||||
});
|
||||
l.emitter.on('initial', (message) => {
|
||||
logger.info(`Received 'Initial' message from sender: ${message}`);
|
||||
});
|
||||
l.emitter.on("setplaylistitem", (message: SetPlaylistItemMessage) => Main.emitter.emit('setplaylistitem', message));
|
||||
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.emitter.emit('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.emitter.emit('event_subscribed_keys_update', unsubscribeData);
|
||||
}
|
||||
});
|
||||
l.start();
|
||||
});
|
||||
|
||||
service.register("send_playback_error", (message: any) => {
|
||||
listeners.forEach(l => {
|
||||
const value: PlaybackErrorMessage = message.payload.error;
|
||||
l.send(Opcode.PlaybackError, value);
|
||||
});
|
||||
|
||||
const value: PlaybackErrorMessage = message.payload.error;
|
||||
Main.listeners.forEach(l => l.send(Opcode.PlaybackError, value));
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
|
||||
service.register("send_playback_update", (message: any) => {
|
||||
// logger.info("In send_playback_update callback");
|
||||
|
||||
listeners.forEach(l => {
|
||||
const value: PlaybackUpdateMessage = message.payload.update;
|
||||
l.send(Opcode.PlaybackUpdate, value);
|
||||
});
|
||||
|
||||
const value: PlaybackUpdateMessage = message.payload.update;
|
||||
Main.listeners.forEach(l => l.send(Opcode.PlaybackUpdate, value));
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
|
||||
service.register("send_volume_update", (message: any) => {
|
||||
listeners.forEach(l => {
|
||||
const value: VolumeUpdateMessage = message.payload.update;
|
||||
l.send(Opcode.VolumeUpdate, value);
|
||||
});
|
||||
const value: VolumeUpdateMessage = message.payload.update;
|
||||
Main.cache.playerVolume = value.volume;
|
||||
Main.listeners.forEach(l => l.send(Opcode.VolumeUpdate, value));
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
|
||||
service.register("send_event", (message: any) => {
|
||||
const value: EventMessage = message.payload.event;
|
||||
Main.listeners.forEach(l => l.send(Opcode.Event, value));
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
|
||||
service.register("play_request", (message: any) => {
|
||||
const value: PlayMessage = message.payload.message;
|
||||
const playlistIndex: number = message.payload.playlistIndex;
|
||||
|
||||
logger.debug(`Received play request for index ${playlistIndex}:`, value);
|
||||
value.url = Main.mediaCache?.has(playlistIndex) ? Main.mediaCache?.getUrl(playlistIndex) : value.url;
|
||||
Main.mediaCache?.cacheItems(playlistIndex);
|
||||
Main.play(value);
|
||||
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
|
||||
// Having to mix and match session ids and ip addresses until querying websocket remote addresses is fixed
|
||||
service.register("get_sessions", (message: any) => {
|
||||
message.respond({
|
||||
returnValue: true,
|
||||
value: [].concat(Main.tcpListenerService.getSenders(), Main.webSocketListenerService.getSessions())
|
||||
});
|
||||
});
|
||||
|
||||
service.register("network_changed", (message: any) => {
|
||||
logger.info('Network interfaces have changed', message);
|
||||
Main.discoveryService.stop();
|
||||
Main.discoveryService.start();
|
||||
|
||||
if (message.payload.fallback) {
|
||||
message.respond({
|
||||
returnValue: true,
|
||||
value: getAllIPv4Addresses()
|
||||
});
|
||||
}
|
||||
else {
|
||||
message.respond({ returnValue: true, value: {} });
|
||||
}
|
||||
});
|
||||
|
||||
service.register("visibility_changed", (message: any) => {
|
||||
logger.info('Window visibility has changed', message.payload);
|
||||
Main.windowVisible = !message.payload.hidden;
|
||||
Main.windowType = message.payload.window;
|
||||
message.respond({ returnValue: true, value: {} });
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger.error("Error initializing service:", err);
|
||||
Main.emitter.emit('toast', { message: `Error initializing service: ${err}`, icon: ToastIcon.ERROR });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function getComputerName() {
|
||||
return os.hostname();
|
||||
return `FCast-${os.hostname()}`;
|
||||
}
|
||||
|
||||
export function getAppName() {
|
||||
return Main.cache.appName;
|
||||
}
|
||||
|
||||
export function getAppVersion() {
|
||||
return Main.cache.appVersion;
|
||||
}
|
||||
|
||||
export function getPlayMessage() {
|
||||
return Main.cache.playMessage;
|
||||
}
|
||||
|
||||
export async function errorHandler(error: Error) {
|
||||
|
@ -219,3 +267,22 @@ function registerService(service: Service, method: string, callback: (message: a
|
|||
message.respond({ returnValue: true, value: message.payload });
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for simulator or TV devices that don't work with the luna://com.palm.connectionmanager/getStatus method
|
||||
function getAllIPv4Addresses() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const ipv4Addresses: string[] = [];
|
||||
|
||||
for (const interfaceName in interfaces) {
|
||||
const addresses = interfaces[interfaceName];
|
||||
if (!addresses) continue;
|
||||
|
||||
for (const addressInfo of addresses) {
|
||||
if (addressInfo.family === 'IPv4' && !addressInfo.internal) {
|
||||
ipv4Addresses.push(addressInfo.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ipv4Addresses;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue