1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-07-27 04:17:00 +00:00

webOS: Initial port of Electron v2.2.0 changes

This commit is contained in:
Michael Hollister 2025-07-07 14:02:19 -05:00
parent b08c3dab95
commit 4c1cb6cf8e
28 changed files with 1273 additions and 532 deletions

View file

@ -84,11 +84,19 @@ if (TARGET === 'electron') {
// @ts-ignore // @ts-ignore
} else if (TARGET === 'webOS' || TARGET === 'tizenOS') { } else if (TARGET === 'webOS' || TARGET === 'tizenOS') {
preloadData = { preloadData.onDeviceInfoCb = () => { logger.warn('Main: Callback not set while fetching device info'); };
onDeviceInfoCb: () => { logger.error('Main: Callback not set while fetching device info'); }, preloadData.getSessionsCb = () => { logger.error('Main: Callback not set while calling getSessions'); };
getSessionsCb: () => { logger.error('Main: Callback not set while calling getSessions'); }, preloadData.onConnectCb = (_, value: any) => { logger.error('Main: Callback not set while calling onConnect'); };
onConnectCb: (_, value: any) => { logger.error('Main: Callback not set while calling onConnect'); }, preloadData.onDisconnectCb = (_, value: any) => { logger.error('Main: Callback not set while calling onDisconnect'); };
onDisconnectCb: (_, value: any) => { logger.error('Main: Callback not set while calling onDisconnect'); }, preloadData.sendEventCb = (message: EventMessage) => { logger.error('Main: Callback not set while calling onSendEventCb'); };
preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: Set<string>, keyUp: Set<string> }) => {
preloadData.subscribedKeys.keyDown = value.keyDown;
preloadData.subscribedKeys.keyUp = value.keyUp;
};
preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => {
toast(message, icon, duration);
}; };
window.targetAPI = { window.targetAPI = {
@ -102,8 +110,10 @@ if (TARGET === 'electron') {
return preloadData.getSessionsCb(); return preloadData.getSessionsCb();
} }
}, },
getSubscribedKeys: () => preloadData.subscribedKeys,
onConnect: (callback: (_, value: any) => void) => preloadData.onConnectCb = callback, onConnect: (callback: (_, value: any) => void) => preloadData.onConnectCb = callback,
onDisconnect: (callback: (_, value: any) => void) => preloadData.onDisconnectCb = callback, onDisconnect: (callback: (_, value: any) => void) => preloadData.onDisconnectCb = callback,
sendEvent: (message: EventMessage) => { preloadData.sendEventCb(message); },
logger: loggerInterface, logger: loggerInterface,
}; };
} else { } else {

View file

@ -1,9 +1,10 @@
import QRCode from 'modules/qrcode'; import QRCode from 'modules/qrcode';
import * as connectionMonitor from '../ConnectionMonitor'; import * as connectionMonitor from '../ConnectionMonitor';
import { onQRCodeRendered } from 'src/main/Renderer'; import { onQRCodeRendered, targetKeyUpEventListener } from 'src/main/Renderer';
import { toast, ToastIcon } from '../components/Toast'; import { toast, ToastIcon } from '../components/Toast';
import { EventMessage, EventType, KeyEvent } from 'common/Packets'; import { EventMessage, EventType, KeyEvent } from 'common/Packets';
import { targetKeyDownEventListener } from 'src/main/Renderer';
const connectionStatusText = document.getElementById('connection-status-text'); const connectionStatusText = document.getElementById('connection-status-text');
const connectionStatusSpinner = document.getElementById('connection-spinner'); const connectionStatusSpinner = document.getElementById('connection-spinner');
@ -203,12 +204,40 @@ function renderQRCode(url: string) {
} }
document.addEventListener('keydown', (event: KeyboardEvent) => { document.addEventListener('keydown', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) { // logger.info("KeyDown", event);
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, false))); let result = targetKeyDownEventListener(event);
let handledCase = result.handledCase;
// @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) {
switch (event.key) {
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyDown.has(key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, key, event.repeat, handledCase)));
} }
}); });
document.addEventListener('keyup', (event: KeyboardEvent) => { document.addEventListener('keyup', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { // logger.info("KeyUp", event);
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); let result = targetKeyUpEventListener(event);
let handledCase = result.handledCase;
// @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) {
switch (event.key) {
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyUp.has(key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, key, event.repeat, handledCase)));
} }
}); });

View file

@ -75,32 +75,48 @@ if (TARGET === 'electron') {
// @ts-ignore // @ts-ignore
} else if (TARGET === 'webOS' || TARGET === 'tizenOS') { } else if (TARGET === 'webOS' || TARGET === 'tizenOS') {
preloadData = { preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => { logger.error('Player: Callback "send_playback_update" not set'); };
sendPlaybackErrorCb: () => { logger.error('Player: Callback "send_playback_error" not set'); }, preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => { logger.error('Player: Callback "send_volume_update" not set'); };
sendPlaybackUpdateCb: () => { logger.error('Player: Callback "send_playback_update" not set'); }, preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => { logger.error('Player: Callback "send_playback_error" not set'); };
sendVolumeUpdateCb: () => { logger.error('Player: Callback "send_volume_update" not set'); }, preloadData.sendEventCb = (message: EventMessage) => { logger.error('Player: Callback "onSendEventCb" not set'); };
// onPlayCb: () => { logger.error('Player: Callback "play" not set'); }, // preloadData.onPlayCb = () => { logger.error('Player: Callback "play" not set'); };
onPlayCb: undefined, preloadData.onPlayCb = undefined;
onPauseCb: () => { logger.error('Player: Callback "pause" not set'); }, preloadData.onPauseCb = () => { logger.error('Player: Callback "pause" not set'); };
onResumeCb: () => { logger.error('Player: Callback "resume" not set'); }, preloadData.onResumeCb = () => { logger.error('Player: Callback "resume" not set'); };
onSeekCb: () => { logger.error('Player: Callback "onseek" not set'); }, preloadData.onSeekCb = () => { logger.error('Player: Callback "onseek" not set'); };
onSetVolumeCb: () => { logger.error('Player: Callback "setvolume" not set'); }, preloadData.onSetVolumeCb = () => { logger.error('Player: Callback "setvolume" not set'); };
onSetSpeedCb: () => { logger.error('Player: Callback "setspeed" not set'); }, preloadData.onSetSpeedCb = () => { logger.error('Player: Callback "setspeed" not set'); };
getSessionsCb: () => { logger.error('Player: Callback "getSessions" not set'); }, preloadData.onSetPlaylistItemCb = () => { logger.error('Player: Callback "onSetPlaylistItem" not set'); };
onConnectCb: () => { logger.error('Player: Callback "onConnect" not set'); },
onDisconnectCb: () => { logger.error('Player: Callback "onDisconnect" not set'); }, preloadData.sendPlayRequestCb = () => { logger.error('Player: Callback "sendPlayRequest" not set'); };
preloadData.getSessionsCb = () => { logger.error('Player: Callback "getSessions" not set'); };
preloadData.onConnectCb = () => { logger.error('Player: Callback "onConnect" not set'); };
preloadData.onDisconnectCb = () => { logger.error('Player: Callback "onDisconnect" not set'); };
preloadData.onPlayPlaylistCb = () => { logger.error('Player: Callback "onPlayPlaylist" not set'); };
preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: Set<string>, keyUp: Set<string> }) => {
preloadData.subscribedKeys.keyDown = value.keyDown;
preloadData.subscribedKeys.keyUp = value.keyUp;
};
preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => {
toast(message, icon, duration);
}; };
window.targetAPI = { window.targetAPI = {
sendPlaybackError: (error: PlaybackErrorMessage) => { preloadData.sendPlaybackErrorCb(error); },
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => { preloadData.sendPlaybackUpdateCb(update); }, sendPlaybackUpdate: (update: PlaybackUpdateMessage) => { preloadData.sendPlaybackUpdateCb(update); },
sendVolumeUpdate: (update: VolumeUpdateMessage) => { preloadData.sendVolumeUpdateCb(update); }, sendVolumeUpdate: (update: VolumeUpdateMessage) => { preloadData.sendVolumeUpdateCb(update); },
sendPlaybackError: (error: PlaybackErrorMessage) => { preloadData.sendPlaybackErrorCb(error); },
sendEvent: (message: EventMessage) => { preloadData.sendEventCb(message); },
onPlay: (callback: any) => { preloadData.onPlayCb = callback; }, onPlay: (callback: any) => { preloadData.onPlayCb = callback; },
onPause: (callback: any) => { preloadData.onPauseCb = callback; }, onPause: (callback: any) => { preloadData.onPauseCb = callback; },
onResume: (callback: any) => { preloadData.onResumeCb = callback; }, onResume: (callback: any) => { preloadData.onResumeCb = callback; },
onSeek: (callback: any) => { preloadData.onSeekCb = callback; }, onSeek: (callback: any) => { preloadData.onSeekCb = callback; },
onSetVolume: (callback: any) => { preloadData.onSetVolumeCb = callback; }, onSetVolume: (callback: any) => { preloadData.onSetVolumeCb = callback; },
onSetSpeed: (callback: any) => { preloadData.onSetSpeedCb = callback; }, onSetSpeed: (callback: any) => { preloadData.onSetSpeedCb = callback; },
onSetPlaylistItem: (callback: any) => { preloadData.onSetPlaylistItemCb = callback; },
sendPlayRequest: (message: PlayMessage, playlistIndex: number) => { preloadData.sendPlayRequestCb(message, playlistIndex); },
getSessions: (callback?: () => Promise<[any]>) => { getSessions: (callback?: () => Promise<[any]>) => {
if (callback) { if (callback) {
preloadData.getSessionsCb = callback; preloadData.getSessionsCb = callback;
@ -109,8 +125,10 @@ if (TARGET === 'electron') {
return preloadData.getSessionsCb(); return preloadData.getSessionsCb();
} }
}, },
getSubscribedKeys: () => preloadData.subscribedKeys,
onConnect: (callback: any) => { preloadData.onConnectCb = callback; }, onConnect: (callback: any) => { preloadData.onConnectCb = callback; },
onDisconnect: (callback: any) => { preloadData.onDisconnectCb = callback; }, onDisconnect: (callback: any) => { preloadData.onDisconnectCb = callback; },
onPlayPlaylist: (callback: any) => { preloadData.onPlayPlaylistCb = callback; },
logger: loggerInterface, logger: loggerInterface,
}; };
} else { } else {

View file

@ -12,7 +12,8 @@ import {
targetKeyDownEventListener, targetKeyDownEventListener,
captionsBaseHeightCollapsed, captionsBaseHeightCollapsed,
captionsBaseHeightExpanded, captionsBaseHeightExpanded,
captionsLineHeight captionsLineHeight,
targetKeyUpEventListener
} from 'src/player/Renderer'; } from 'src/player/Renderer';
const logger = window.targetAPI.logger; const logger = window.targetAPI.logger;
@ -912,74 +913,6 @@ document.addEventListener('click', (event: MouseEvent) => {
const skipInterval = 10; const skipInterval = 10;
const volumeIncrement = 0.1; const volumeIncrement = 0.1;
function keyDownEventListener(event: KeyboardEvent) {
// logger.info("KeyDown", event);
let handledCase = targetKeyDownEventListener(event);
if (!handledCase) {
switch (event.code) {
case 'ArrowLeft':
skipBack();
event.preventDefault();
handledCase = true;
break;
case 'ArrowRight':
skipForward();
event.preventDefault();
handledCase = true;
break;
case "Home":
player?.setCurrentTime(0);
event.preventDefault();
handledCase = true;
break;
case "End":
if (isLive) {
setLivePosition();
}
else {
player?.setCurrentTime(player?.getDuration());
}
event.preventDefault();
handledCase = true;
break;
case 'KeyK':
case 'Space':
case 'Enter':
// Play/pause toggle
if (player?.isPaused()) {
player?.play();
} else {
player?.pause();
}
event.preventDefault();
handledCase = true;
break;
case 'KeyM':
// Mute toggle
player?.setMute(!player?.isMuted());
handledCase = true;
break;
case 'ArrowUp':
// Volume up
volumeChangeHandler(Math.min(player?.getVolume() + volumeIncrement, 1));
handledCase = true;
break;
case 'ArrowDown':
// Volume down
volumeChangeHandler(Math.max(player?.getVolume() - volumeIncrement, 0));
handledCase = true;
break;
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
}
}
function skipBack() { function skipBack() {
player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipInterval, 0)); player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipInterval, 0));
} }
@ -990,10 +923,94 @@ function skipForward() {
} }
} }
document.addEventListener('keydown', keyDownEventListener); document.addEventListener('keydown', (event: KeyboardEvent) => {
// logger.info("KeyDown", event.key);
let result = targetKeyDownEventListener(event);
let handledCase = result.handledCase;
// @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) {
switch (event.key.toLowerCase()) {
case 'arrowleft':
skipBack();
event.preventDefault();
handledCase = true;
break;
case 'arrowright':
skipForward();
event.preventDefault();
handledCase = true;
break;
case "home":
player?.setCurrentTime(0);
event.preventDefault();
handledCase = true;
break;
case "end":
if (isLive) {
setLivePosition();
}
else {
player?.setCurrentTime(player?.getDuration());
}
event.preventDefault();
handledCase = true;
break;
case 'k':
case ' ':
case 'enter':
// Play/pause toggle
if (player?.isPaused()) {
player?.play();
} else {
player?.pause();
}
event.preventDefault();
handledCase = true;
break;
case 'm':
// Mute toggle
player?.setMute(!player?.isMuted());
handledCase = true;
break;
case 'arrowup':
// Volume up
volumeChangeHandler(Math.min(player?.getVolume() + volumeIncrement, 1));
handledCase = true;
break;
case 'arrowdown':
// Volume down
volumeChangeHandler(Math.max(player?.getVolume() - volumeIncrement, 0));
handledCase = true;
break;
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyDown.has(key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, key, event.repeat, handledCase)));
}
});
document.addEventListener('keyup', (event: KeyboardEvent) => { document.addEventListener('keyup', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { // logger.info("KeyUp", event);
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); let result = targetKeyUpEventListener(event);
let handledCase = result.handledCase;
// @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) {
switch (event.key) {
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyUp.has(key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, key, event.repeat, handledCase)));
} }
}); });
@ -1020,6 +1037,7 @@ export {
captionsBaseHeight, captionsBaseHeight,
captionsLineHeight, captionsLineHeight,
onPlay, onPlay,
onPlayPlaylist,
playerCtrlStateUpdate, playerCtrlStateUpdate,
formatDuration, formatDuration,
skipBack, skipBack,

View file

@ -6,6 +6,7 @@ import { toast, ToastIcon } from 'common/components/Toast';
import { import {
targetPlayerCtrlStateUpdate, targetPlayerCtrlStateUpdate,
targetKeyDownEventListener, targetKeyDownEventListener,
targetKeyUpEventListener,
} from 'src/viewer/Renderer'; } from 'src/viewer/Renderer';
const logger = window.targetAPI.logger; const logger = window.targetAPI.logger;
@ -328,9 +329,13 @@ document.onmousemove = () => {
uiHideTimer.start(); uiHideTimer.start();
}; };
function keyDownEventListener(event: KeyboardEvent) { document.addEventListener('keydown', (event: KeyboardEvent) => {
// logger.info("KeyDown", event); // logger.info("KeyDown", event);
let handledCase = targetKeyDownEventListener(event); let result = targetKeyDownEventListener(event);
let handledCase = result.handledCase;
// @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) { if (!handledCase) {
switch (event.code) { switch (event.code) {
@ -371,15 +376,27 @@ function keyDownEventListener(event: KeyboardEvent) {
} }
} }
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) { if (window.targetAPI.getSubscribedKeys().keyDown.has(key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase))); window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, key, event.repeat, handledCase)));
} }
} });
document.addEventListener('keydown', keyDownEventListener);
document.addEventListener('keyup', (event: KeyboardEvent) => { document.addEventListener('keyup', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { // logger.info("KeyUp", event);
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); let result = targetKeyUpEventListener(event);
let handledCase = result.handledCase;
// @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) {
switch (event.key) {
default:
break;
}
}
if (window.targetAPI.getSubscribedKeys().keyUp.has(key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, key, event.repeat, handledCase)));
} }
}); });
@ -389,6 +406,9 @@ export {
idleIcon, idleIcon,
imageViewer, imageViewer,
genericViewer, genericViewer,
playlistIndex,
onPlay, onPlay,
onPlayPlaylist,
playerCtrlStateUpdate, playerCtrlStateUpdate,
setPlaylistItem,
}; };

View file

@ -3,6 +3,16 @@ import 'common/main/Renderer';
const logger = window.targetAPI.logger; const logger = window.targetAPI.logger;
export function onQRCodeRendered() {} export function onQRCodeRendered() {}
export function targetKeyDownEventListener(_event: KeyboardEvent): { handledCase: boolean, key: string } {
// unused in Electron currently
return { handledCase: false, key: '', };
};
export function targetKeyUpEventListener(_event: KeyboardEvent): { handledCase: boolean, key: string } {
// unused in Electron currently
return { handledCase: false, key: '', };
};
const updateView = document.getElementById("update-view"); const updateView = document.getElementById("update-view");
const updateViewTitle = document.getElementById("update-view-title"); const updateViewTitle = document.getElementById("update-view-title");
const updateText = document.getElementById("update-text"); const updateText = document.getElementById("update-text");

View file

@ -44,25 +44,17 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase; return handledCase;
} }
export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) { export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
// Currently unused in electron player
switch (event) {
default:
break;
}
}
export function targetKeyDownEventListener(event: KeyboardEvent) {
let handledCase = false; let handledCase = false;
switch (event.code) { switch (event.key.toLowerCase()) {
case 'KeyF': case 'f':
case 'F11': case 'f11':
playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case 'Escape': case 'escape':
playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen); playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
@ -71,7 +63,12 @@ export function targetKeyDownEventListener(event: KeyboardEvent) {
break; break;
} }
return handledCase return { handledCase: handledCase, key: event.key, };
};
export function targetKeyUpEventListener(_event: KeyboardEvent): { handledCase: boolean, key: string } {
// unused in Electron currently
return { handledCase: false, key: '', };
}; };
export { export {

View file

@ -40,17 +40,17 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase; return handledCase;
} }
export function targetKeyDownEventListener(event: KeyboardEvent): boolean { export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false; let handledCase = false;
switch (event.code) { switch (event.key.toLowerCase()) {
case 'KeyF': case 'f':
case 'F11': case 'f11':
playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case 'Escape': case 'escape':
playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen); playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
@ -59,5 +59,10 @@ export function targetKeyDownEventListener(event: KeyboardEvent): boolean {
break; break;
} }
return handledCase return { handledCase: handledCase, key: event.key, };
};
export function targetKeyUpEventListener(_event: KeyboardEvent): { handledCase: boolean, key: string } {
// unused in Electron currently
return { handledCase: false, key: '', };
}; };

View file

@ -26,6 +26,8 @@ PASSPHRASE=YOUR_TV_PASSPHRASE
This information is found in the development app. This information is found in the development app.
Note that you may have to periodically rebuild the container to keep key information up-to-date with the TV device.
### Run ### Run
```bash ```bash
docker run --rm -it -w /app/receivers/webos --entrypoint='bash' --network host \ docker run --rm -it -w /app/receivers/webos --entrypoint='bash' --network host \

View file

@ -0,0 +1 @@
@futo:registry=https://gitlab.futo.org/api/v4/projects/305/packages/npm/

View file

@ -1,23 +1,26 @@
{ {
"name": "com.futo.fcast.receiver.service", "name": "com.futo.fcast.receiver.service",
"version": "1.1.0", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "com.futo.fcast.receiver.service", "name": "com.futo.fcast.receiver.service",
"version": "1.1.0", "version": "2.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"http": "^0.0.1-security", "bufferutil": "^4.0.8",
"follow-redirects": "^1.15.9",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"memfs": "^4.17.2",
"url": "^0.11.4", "url": "^0.11.4",
"uuid": "^11.0.3", "uuid": "^9.0.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38", "@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10", "@types/node-forge": "^1.3.10",
@ -29,7 +32,6 @@
"eslint": "^9.25.0", "eslint": "^9.25.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
@ -713,6 +715,47 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@futo/mdns-js": {
"version": "1.0.3",
"resolved": "https://gitlab.futo.org/api/v4/projects/305/packages/npm/@futo/mdns-js/-/@futo/mdns-js-1.0.3.tgz",
"integrity": "sha1-y25rzWUSYkRu0bhkR472LJLMdcc=",
"dev": true,
"dependencies": {
"debug": "~3.1.0",
"dns-js": "~0.2.1",
"semver": "~5.4.1"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@futo/mdns-js/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/@futo/mdns-js/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/@futo/mdns-js/node_modules/semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1302,6 +1345,60 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@jsonjoy.com/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/json-pack": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz",
"integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==",
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/base64": "^1.1.1",
"@jsonjoy.com/util": "^1.1.2",
"hyperdyperid": "^1.2.0",
"thingies": "^1.20.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/util": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz",
"integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.11.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz",
@ -2521,8 +2618,6 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"node-gyp-build": "^4.3.0" "node-gyp-build": "^4.3.0"
}, },
@ -3786,6 +3881,26 @@
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -4052,11 +4167,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/http": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz",
"integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g=="
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -4084,6 +4194,15 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
"license": "MIT",
"engines": {
"node": ">=10.18"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -5409,47 +5528,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/mdns-js": {
"version": "1.0.3",
"resolved": "git+ssh://git@github.com/mdns-js/node-mdns-js.git#4fb9220ec8852bae9e2781917f649821b9df539d",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "~3.1.0",
"dns-js": "~0.2.1",
"semver": "~5.4.1"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/mdns-js/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/mdns-js/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/mdns-js/node_modules/semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -5460,6 +5538,25 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/memfs": {
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz",
"integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==",
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/json-pack": "^1.0.3",
"@jsonjoy.com/util": "^1.3.0",
"tree-dump": "^1.0.1",
"tslib": "^2.0.0"
},
"engines": {
"node": ">= 4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
}
},
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@ -5595,7 +5692,6 @@
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"node-gyp-build": "bin.js", "node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js", "node-gyp-build-optional": "optional.js",
@ -7047,6 +7143,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/thingies": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
"integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
"license": "Unlicense",
"engines": {
"node": ">=10.18"
},
"peerDependencies": {
"tslib": "^2"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
@ -7135,6 +7243,22 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tree-dump": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz",
"integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz",
@ -7254,6 +7378,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -7470,16 +7600,15 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "11.1.0", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT",
"bin": { "bin": {
"uuid": "dist/esm/bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {

View file

@ -1,6 +1,6 @@
{ {
"name": "com.futo.fcast.receiver.service", "name": "com.futo.fcast.receiver.service",
"version": "1.1.0", "version": "2.0.0",
"description": "FCast network service", "description": "FCast network service",
"author": "FUTO", "author": "FUTO",
"license": "MIT", "license": "MIT",
@ -12,6 +12,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38", "@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10", "@types/node-forge": "^1.3.10",
@ -23,7 +24,6 @@
"eslint": "^9.25.0", "eslint": "^9.25.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
@ -33,10 +33,12 @@
"webpack-cli": "^6.0.1" "webpack-cli": "^6.0.1"
}, },
"dependencies": { "dependencies": {
"http": "^0.0.1-security", "bufferutil": "^4.0.8",
"follow-redirects": "^1.15.9",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"memfs": "^4.17.2",
"url": "^0.11.4", "url": "^0.11.4",
"uuid": "^11.0.3", "uuid": "^9.0.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -5,17 +5,30 @@
const Service = __non_webpack_require__('webos-service'); const Service = __non_webpack_require__('webos-service');
// const Service = 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 { DiscoveryService } from 'common/DiscoveryService';
import { TcpListenerService } from 'common/TcpListenerService'; import { TcpListenerService } from 'common/TcpListenerService';
import { WebSocketListenerService } from 'common/WebSocketListenerService'; import { WebSocketListenerService } from 'common/WebSocketListenerService';
import { NetworkService } from 'common/NetworkService';
import { ConnectionMonitor } from 'common/ConnectionMonitor'; import { ConnectionMonitor } from 'common/ConnectionMonitor';
import { Logger, LoggerType } from 'common/Logger'; import { Logger, LoggerType } from 'common/Logger';
import { MediaCache } from 'common/MediaCache';
import { preparePlayMessage } from 'common/UtilityBackend';
import * as os from 'os'; import * as os from 'os';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { ToastIcon } from 'common/components/Toast'; import { ToastIcon } from 'common/components/Toast';
const logger = new Logger('Main', LoggerType.BACKEND); 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 { export class Main {
static tcpListenerService: TcpListenerService; static tcpListenerService: TcpListenerService;
@ -23,14 +36,39 @@ export class Main {
static discoveryService: DiscoveryService; static discoveryService: DiscoveryService;
static connectionMonitor: ConnectionMonitor; static connectionMonitor: ConnectionMonitor;
static emitter: EventEmitter; 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 { static {
try { try {
logger.info(`OS: ${process.platform} ${process.arch}`); 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 // Service will timeout and casting will disconnect if not forced to be kept alive
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
let keepAlive; let keepAlive;
@ -38,22 +76,6 @@ export class Main {
keepAlive = activity; 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.connectionMonitor = new ConnectionMonitor();
Main.discoveryService = new DiscoveryService(); Main.discoveryService = new DiscoveryService();
Main.discoveryService.start(); Main.discoveryService.start();
@ -62,78 +84,35 @@ export class Main {
Main.webSocketListenerService = new WebSocketListenerService(); Main.webSocketListenerService = new WebSocketListenerService();
Main.emitter = new EventEmitter(); Main.emitter = new EventEmitter();
let playData: PlayMessage = null;
let playClosureCb = null; const voidCb = (message: any) => { message.respond({ returnValue: true, value: {} }); };
const playCb = (message: any, playMessage: PlayMessage) => { const objectCb = (message: any, value: any) => { message.respond({ returnValue: true, value: value }); };
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 });
});
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, 'pause', (message: any) => { return voidCb.bind(this, message) });
registerService(service, 'resume', (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) => { Main.listeners = [Main.tcpListenerService, Main.webSocketListenerService];
playData = null; Main.listeners.forEach(l => {
l.emitter.on("play", (message: PlayMessage) => Main.play(message));
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)}`);
});
});
l.emitter.on("pause", () => Main.emitter.emit('pause')); l.emitter.on("pause", () => Main.emitter.emit('pause'));
l.emitter.on("resume", () => Main.emitter.emit('resume')); l.emitter.on("resume", () => Main.emitter.emit('resume'));
l.emitter.on("stop", () => Main.emitter.emit('stop')); l.emitter.on("stop", () => Main.emitter.emit('stop'));
l.emitter.on("seek", (message) => Main.emitter.emit('seek', message)); l.emitter.on("seek", (message: SeekMessage) => Main.emitter.emit('seek', message));
l.emitter.on("setvolume", (message) => Main.emitter.emit('setvolume', message)); l.emitter.on("setvolume", (message: SetVolumeMessage) => {
l.emitter.on("setspeed", (message) => Main.emitter.emit('setspeed', message)); 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) => { l.emitter.on('connect', (message) => {
ConnectionMonitor.onConnect(l, message, l instanceof WebSocketListenerService, () => { ConnectionMonitor.onConnect(l, message, l instanceof WebSocketListenerService, () => {
@ -151,48 +130,117 @@ export class Main {
l.emitter.on('pong', (message) => { l.emitter.on('pong', (message) => {
ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService); 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(); l.start();
}); });
service.register("send_playback_error", (message: any) => { service.register("send_playback_error", (message: any) => {
listeners.forEach(l => { const value: PlaybackErrorMessage = message.payload.error;
const value: PlaybackErrorMessage = message.payload.error; Main.listeners.forEach(l => l.send(Opcode.PlaybackError, value));
l.send(Opcode.PlaybackError, value);
});
message.respond({ returnValue: true, value: { success: true } }); message.respond({ returnValue: true, value: { success: true } });
}); });
service.register("send_playback_update", (message: any) => { service.register("send_playback_update", (message: any) => {
// logger.info("In send_playback_update callback"); // logger.info("In send_playback_update callback");
const value: PlaybackUpdateMessage = message.payload.update;
listeners.forEach(l => { Main.listeners.forEach(l => l.send(Opcode.PlaybackUpdate, value));
const value: PlaybackUpdateMessage = message.payload.update;
l.send(Opcode.PlaybackUpdate, value);
});
message.respond({ returnValue: true, value: { success: true } }); message.respond({ returnValue: true, value: { success: true } });
}); });
service.register("send_volume_update", (message: any) => { service.register("send_volume_update", (message: any) => {
listeners.forEach(l => { const value: VolumeUpdateMessage = message.payload.update;
const value: VolumeUpdateMessage = message.payload.update; Main.cache.playerVolume = value.volume;
l.send(Opcode.VolumeUpdate, value); 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 } }); 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) { catch (err) {
logger.error("Error initializing service:", err); logger.error("Error initializing service:", err);
Main.emitter.emit('toast', { message: `Error initializing service: ${err}`, icon: ToastIcon.ERROR }); Main.emitter.emit('toast', { message: `Error initializing service: ${err}`, icon: ToastIcon.ERROR });
} }
} }
} }
export function getComputerName() { 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) { 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 }); 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;
}

View file

@ -0,0 +1 @@
@futo:registry=https://gitlab.futo.org/api/v4/projects/305/packages/npm/

View file

@ -1,6 +1,6 @@
{ {
"id": "com.futo.fcast.receiver", "id": "com.futo.fcast.receiver",
"version": "1.1.0", "version": "2.0.0",
"vendor": "FUTO", "vendor": "FUTO",
"type": "web", "type": "web",
"main": "main_window/index.html", "main": "main_window/index.html",

View file

@ -0,0 +1,98 @@
const logger = window.targetAPI.logger;
const serviceId = 'com.futo.fcast.receiver.service';
export enum RemoteKeyCode {
Stop = 413,
Rewind = 412,
Play = 415,
Pause = 19,
FastForward = 417,
Back = 461,
}
export function requestService(method: string, successCb: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void): any {
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: {},
onSuccess: (message: any) => {
if (message.value?.subscribed === true) {
logger.info(`requestService: Registered ${method} handler with service`);
}
else {
successCb(message);
}
},
onFailure: (message: any) => {
logger.error(`requestService: ${method} ${JSON.stringify(message)}`);
if (failureCb) {
failureCb(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
}
},
subscribe: true,
resubscribe: true
});
}
export function callService(method: string, parameters?: any, successCb?: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: parameters,
onSuccess: (message: any) => {
if (successCb) {
successCb(message);
}
},
onFailure: (message: any) => {
logger.error(`callService: ${method} ${JSON.stringify(message)}`);
if (failureCb) {
failureCb(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
}
},
subscribe: false,
resubscribe: false
});
}
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false;
let key = '';
// .keyCode instead of alternatives is required to work properly on webOS
switch (event.keyCode) {
// Unhandled cases (used for replacing undefined key codes)
case RemoteKeyCode.Stop:
key = 'Stop';
break;
case RemoteKeyCode.Rewind:
key = 'Rewind';
break;
case RemoteKeyCode.Play:
key = 'Play';
break;
case RemoteKeyCode.Pause:
key = 'Pause';
break;
case RemoteKeyCode.FastForward:
key = 'FastForward';
break;
case RemoteKeyCode.Back:
key = 'Back';
break;
default:
break;
}
return { handledCase: handledCase, key: key };
};

View file

@ -1,26 +1,26 @@
{ {
"name": "com.futo.fcast.receiver", "name": "com.futo.fcast.receiver",
"version": "1.1.0", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "com.futo.fcast.receiver", "name": "com.futo.fcast.receiver",
"version": "1.1.0", "version": "2.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bufferutil": "^4.0.8", "bufferutil": "^4.0.8",
"dashjs": "^4.7.4", "dashjs": "^4.7.4",
"hls.js": "^1.5.15", "hls.js": "^1.5.15",
"http": "^0.0.1-security", "log4js": "^6.9.1",
"https": "^1.0.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"url": "^0.11.4", "url": "^0.11.4",
"uuid": "^11.0.3", "uuid": "^9.0.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38", "@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10", "@types/node-forge": "^1.3.10",
@ -32,13 +32,15 @@
"eslint": "^9.25.0", "eslint": "^9.25.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.4.0", "typescript-eslint": "^8.4.0",
"webpack": "^5.99.6", "webpack": "^5.99.6",
"webpack-cli": "^6.0.1" "webpack-cli": "^6.0.1"
},
"optionalDependencies": {
"utf-8-validate": "^6.0.5"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -837,6 +839,47 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@futo/mdns-js": {
"version": "1.0.3",
"resolved": "https://gitlab.futo.org/api/v4/projects/305/packages/npm/@futo/mdns-js/-/@futo/mdns-js-1.0.3.tgz",
"integrity": "sha1-y25rzWUSYkRu0bhkR472LJLMdcc=",
"dev": true,
"dependencies": {
"debug": "~3.1.0",
"dns-js": "~0.2.1",
"semver": "~5.4.1"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@futo/mdns-js/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/@futo/mdns-js/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/@futo/mdns-js/node_modules/semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -3045,11 +3088,19 @@
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-format": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
"integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -3880,7 +3931,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/forwarded": { "node_modules/forwarded": {
@ -3903,6 +3953,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -4071,7 +4135,6 @@
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/graphemer": { "node_modules/graphemer": {
@ -4134,11 +4197,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/http": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz",
"integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g=="
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -4156,12 +4214,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/human-signals": { "node_modules/human-signals": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -5261,6 +5313,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -5378,6 +5439,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/log4js": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz",
"integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==",
"license": "Apache-2.0",
"dependencies": {
"date-format": "^4.0.14",
"debug": "^4.3.4",
"flatted": "^3.2.7",
"rfdc": "^1.3.0",
"streamroller": "^3.1.5"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -5430,47 +5507,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/mdns-js": {
"version": "1.0.3",
"resolved": "git+ssh://git@github.com/mdns-js/node-mdns-js.git#4fb9220ec8852bae9e2781917f649821b9df539d",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "~3.1.0",
"dns-js": "~0.2.1",
"semver": "~5.4.1"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/mdns-js/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/mdns-js/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/mdns-js/node_modules/semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -5575,7 +5611,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/natural-compare": { "node_modules/natural-compare": {
@ -6425,6 +6460,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/router": { "node_modules/router": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@ -6840,6 +6881,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamroller": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz",
"integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==",
"license": "MIT",
"dependencies": {
"date-format": "^4.0.14",
"debug": "^4.3.4",
"fs-extra": "^8.1.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/string-length": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -7358,6 +7413,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -7428,17 +7492,30 @@
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/utf-8-validate": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz",
"integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "11.1.0", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [ "funding": [
"https://github.com/sponsors/broofa", "https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT",
"bin": { "bin": {
"uuid": "dist/esm/bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {

View file

@ -1,6 +1,6 @@
{ {
"name": "com.futo.fcast.receiver", "name": "com.futo.fcast.receiver",
"version": "1.1.0", "version": "2.0.0",
"description": "An application implementing a FCast receiver.", "description": "An application implementing a FCast receiver.",
"author": "FUTO", "author": "FUTO",
"license": "MIT", "license": "MIT",
@ -10,6 +10,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38", "@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10", "@types/node-forge": "^1.3.10",
@ -21,7 +22,6 @@
"eslint": "^9.25.0", "eslint": "^9.25.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"typescript": "^5.5.4", "typescript": "^5.5.4",
@ -33,11 +33,13 @@
"bufferutil": "^4.0.8", "bufferutil": "^4.0.8",
"dashjs": "^4.7.4", "dashjs": "^4.7.4",
"hls.js": "^1.5.15", "hls.js": "^1.5.15",
"http": "^0.0.1-security", "log4js": "^6.9.1",
"https": "^1.0.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"url": "^0.11.4", "url": "^0.11.4",
"uuid": "^11.0.3", "uuid": "^9.0.1",
"ws": "^8.18.0" "ws": "^8.18.0"
},
"optionalDependencies": {
"utf-8-validate": "^6.0.5"
} }
} }

View file

@ -1,153 +1,139 @@
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { preloadData } from 'common/main/Preload'; import { preloadData } from 'common/main/Preload';
import { toast, ToastIcon } from 'common/components/Toast'; import { ToastIcon } from 'common/components/Toast';
import { EventMessage } from 'common/Packets';
import { callService, requestService } from 'lib/common';
require('lib/webOSTVjs-1.2.10/webOSTV.js'); require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js'); require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
const logger = window.targetAPI.logger; const logger = window.targetAPI.logger;
enum RemoteKeyCode {
Stop = 413,
Rewind = 412,
Play = 415,
Pause = 19,
FastForward = 417,
Back = 461,
}
try { try {
let getSessions = null; const serviceId = 'com.futo.fcast.receiver.service';
let getSessionsService = null;
let networkChangedService = null;
let visibilityChangedService = null;
const toastService = requestService('toast', (message: any) => { toast(message.value.message, message.value.icon, message.value.duration); }); const toastService = requestService('toast', (message: any) => { preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration); });
const getDeviceInfoService = window.webOS.service.request('luna://com.palm.connectionmanager', { const getDeviceInfoService = window.webOSDev.connection.getStatus({
method: 'getStatus',
parameters: {},
onSuccess: (message: any) => { onSuccess: (message: any) => {
// logger.info('Network info status message', message); logger.info('Network info status message', message);
const deviceName = 'FCast-LGwebOSTV'; const deviceName = 'FCast-LGwebOSTV';
const connections = []; const connections = [];
let fallback = true;
if (message.wired.state !== 'disconnected') { if (message.wired.state !== 'disconnected') {
connections.push({ type: 'wired', name: 'Ethernet', address: message.wired.ipAddress }) connections.push({ type: 'wired', name: 'Ethernet', address: message.wired.ipAddress });
fallback = false;
} }
// wifiDirect never seems to be connected, despite being connected (which is needed for signalLevel...) // wifiDirect never seems to be connected, despite being connected (which is needed for signalLevel...)
// if (message.wifiDirect.state !== 'disconnected') { // if (message.wifiDirect.state !== 'disconnected') {
if (message.wifi.state !== 'disconnected') { if (message.wifi.state !== 'disconnected') {
connections.push({ type: 'wireless', name: message.wifi.ssid, address: message.wifi.ipAddress, signalLevel: 100 }) connections.push({ type: 'wireless', name: message.wifi.ssid, address: message.wifi.ipAddress, signalLevel: 100 });
fallback = false;
} }
preloadData.deviceInfo = { name: deviceName, interfaces: connections }; if (fallback) {
preloadData.onDeviceInfoCb(); networkChangedService = callService('network_changed', { fallback: fallback }, (message: any) => {
logger.info('Fallback network interfaces', message);
for (const ipAddr of message.value) {
connections.push({ type: 'wired', name: 'Ethernet', address: ipAddr });
}
preloadData.deviceInfo = { name: deviceName, interfaces: connections };
preloadData.onDeviceInfoCb();
}, (message: any) => {
logger.error('Main: preload - error fetching network interfaces', message);
preloadData.onToastCb('Error detecting network interfaces', ToastIcon.ERROR);
}, () => {
networkChangedService = null;
});
}
else {
networkChangedService = callService('network_changed', { fallback: fallback }, null, null, () => {
networkChangedService = null;
});
preloadData.deviceInfo = { name: deviceName, interfaces: connections };
preloadData.onDeviceInfoCb();
}
}, },
onFailure: (message: any) => { onFailure: (message: any) => {
logger.error(`Main: com.palm.connectionmanager/getStatus ${JSON.stringify(message)}`); logger.error(`Main: com.palm.connectionmanager/getStatus ${JSON.stringify(message)}`);
toast(`Main: com.palm.connectionmanager/getStatus ${JSON.stringify(message)}`, ToastIcon.ERROR); preloadData.onToastCb(`Main: com.palm.connectionmanager/getStatus ${JSON.stringify(message)}`, ToastIcon.ERROR);
}, },
// onComplete: (message) => {},
subscribe: true, subscribe: true,
resubscribe: true resubscribe: true
}); });
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
window.targetAPI.getSessions(() => { window.targetAPI.getSessions(() => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getSessions = requestService('get_sessions', (message: any) => resolve(message.value), (message: any) => reject(message), false); getSessionsService = callService('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
}); });
}); });
const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); }); const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); });
const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); }); const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
const playService = requestService('play', (message: any) => { preloadData.sendEventCb = (event: EventMessage) => {
if (message.value !== undefined && message.value.playData !== undefined) { window.webOS.service.request(`luna://${serviceId}/`, {
logger.info(`Main: Playing ${JSON.stringify(message)}`); method: 'send_event',
sessionStorage.setItem('playData', JSON.stringify(message.value.playData)); parameters: { event },
getDeviceInfoService.cancel(); onSuccess: () => {},
getSessions?.cancel(); onFailure: (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); },
toastService.cancel(); });
onConnectService.cancel(); };
onDisconnectService.cancel();
playService.cancel();
// WebOS 22 and earlier does not work well using the history API, const playService = requestService('play', (message: any) => {
// so manually handling page navigation... logger.info(`Main: Playing ${JSON.stringify(message)}`);
// history.pushState({}, '', '../main_window/index.html'); play(message.value);
window.open('../player/index.html', '_self');
}
}); });
const launchHandler = () => { const launchHandler = () => {
const params = window.webOSDev.launchParams(); const params = window.webOSDev.launchParams();
logger.info(`Main: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`); logger.info(`Main: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
// WebOS 6.0 and earlier: Timestamp tracking seems to be necessary as launch event is raised regardless if app is in foreground or not
const lastTimestamp = Number(localStorage.getItem('lastTimestamp')); const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
if (params.playData !== undefined && params.timestamp != lastTimestamp) { if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp); localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('playData', JSON.stringify(params.playData)); play(params.messageInfo);
toastService?.cancel();
getDeviceInfoService?.cancel();
getSessions?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
playService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open('../player/index.html', '_self');
} }
}; };
document.addEventListener('webOSLaunch', launchHandler); document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler); document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => {
visibilityChangedService = callService('visibility_changed', { hidden: document.hidden, window: 'main' }, null, null, () => {
visibilityChangedService = null;
})
});
// Cannot go back to a state where user was previously casting a video, so exit. // Cannot go back to a state where user was previously casting a video, so exit.
// window.onpopstate = () => { // window.onpopstate = () => {
// window.webOS.platformBack(); // window.webOS.platformBack();
// }; // };
document.addEventListener('keydown', (event: any) => { const play = (messageInfo: any) => {
// logger.info("KeyDown", event); sessionStorage.setItem('playInfo', JSON.stringify(messageInfo));
switch (event.keyCode) { getDeviceInfoService?.cancel();
// WebOS 22 and earlier does not work well using the history API, onEventSubscribedKeysUpdateService?.cancel();
// so manually handling page navigation... getSessionsService?.cancel();
case RemoteKeyCode.Back: toastService?.cancel();
window.webOS.platformBack(); onConnectService?.cancel();
break; onDisconnectService?.cancel();
default: playService?.cancel();
break; networkChangedService?.cancel();
} visibilityChangedService?.cancel();
});
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${messageInfo.contentViewer}/index.html`, '_self');
};
} }
catch (err) { catch (err) {
logger.error(`Main: preload ${JSON.stringify(err)}`); logger.error(`Main: preload ${JSON.stringify(err)}`);
toast(`Error starting the application (preload): ${JSON.stringify(err)}`, ToastIcon.ERROR); preloadData.onToastCb(`Error starting the application: ${JSON.stringify(err)}`, ToastIcon.ERROR);
}
function requestService(method: string, successCallback: (message: any) => void, failureCallback?: (message: any) => void, subscribe: boolean = true): any {
const serviceId = 'com.futo.fcast.receiver.service';
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: {},
onSuccess: (message: any) => {
if (message.value?.subscribed === true) {
logger.info(`Main: Registered ${method} handler with service`);
}
else {
successCallback(message);
}
},
onFailure: (message: any) => {
logger.error(`Main: ${method} ${JSON.stringify(message)}`);
if (failureCallback) {
failureCallback(message);
}
},
// onComplete: (message) => {},
subscribe: subscribe,
resubscribe: subscribe
});
} }

View file

@ -1,4 +1,6 @@
import 'common/main/Renderer'; import 'common/main/Renderer';
import { RemoteKeyCode } from 'lib/common';
import * as common from 'lib/common';
const backgroundVideo = document.getElementById('video-player'); const backgroundVideo = document.getElementById('video-player');
const loadingScreen = document.getElementById('loading-screen'); const loadingScreen = document.getElementById('loading-screen');
@ -30,3 +32,45 @@ backgroundVideo.onplaying = () => {
export function onQRCodeRendered() { export function onQRCodeRendered() {
qrCodeRendered = true; qrCodeRendered = true;
} }
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false;
let key = '';
switch (event.keyCode) {
// Unhandled cases (used for replacing undefined key codes)
case RemoteKeyCode.Stop:
key = 'Stop';
break;
case RemoteKeyCode.Rewind:
key = 'Rewind';
break;
case RemoteKeyCode.Play:
key = 'Play';
break;
case RemoteKeyCode.Pause:
key = 'Pause';
break;
case RemoteKeyCode.FastForward:
key = 'FastForward';
break;
// Handled cases
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
case RemoteKeyCode.Back:
window.webOS.platformBack();
handledCase = true;
key = 'Back';
break;
default:
break;
}
return { handledCase: handledCase, key: key };
};
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
return common.targetKeyUpEventListener(event);
};

View file

@ -15,6 +15,7 @@
<div id="spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div> <div id="spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
</div> </div>
<div id="main-container"> <div id="main-container">
<img id="image-background"/>
<video id="video-player" class="video" autoplay loop> <video id="video-player" class="video" autoplay loop>
<source src="../assets/video/background.mp4" type="video/mp4"> <source src="../assets/video/background.mp4" type="video/mp4">
</video> </video>

View file

@ -1,19 +1,21 @@
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { preloadData } from 'common/player/Preload'; import { preloadData } from 'common/player/Preload';
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets'; import { EventMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, VolumeUpdateMessage } from 'common/Packets';
import { callService, requestService } from 'lib/common';
import { toast, ToastIcon } from 'common/components/Toast'; import { toast, ToastIcon } from 'common/components/Toast';
require('lib/webOSTVjs-1.2.10/webOSTV.js'); require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js'); require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
const logger = window.targetAPI.logger; const logger = window.targetAPI.logger;
const serviceId = 'com.futo.fcast.receiver.service';
try { try {
const serviceId = 'com.futo.fcast.receiver.service';
let getSessions = null; let getSessions = null;
window.webOSAPI = { window.webOSAPI = {
pendingPlay: JSON.parse(sessionStorage.getItem('playData')) pendingPlay: JSON.parse(sessionStorage.getItem('playInfo'))
}; };
const contentViewer = window.webOSAPI.pendingPlay?.contentViewer;
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => { preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, { window.webOS.service.request(`luna://${serviceId}/`, {
@ -48,45 +50,73 @@ try {
}, },
}); });
}; };
preloadData.sendEventCb = (event: EventMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_event',
parameters: { event },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); },
});
};
const playService = window.webOS.service.request(`luna://${serviceId}/`, { const playService = requestService('play', (message: any) => {
method:"play", if (contentViewer !== message.value.contentViewer) {
parameters: {}, playService?.cancel();
onSuccess: (message: any) => { pauseService?.cancel();
// logger.info(JSON.stringify(message)); resumeService?.cancel();
if (message.value.subscribed === true) { stopService?.cancel();
logger.info('Player: Registered play handler with service'); seekService?.cancel();
} setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
if (message.value.playData !== null) { // WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${message.value.contentViewer}/index.html`, '_self');
}
else {
if (message.value.rendererEvent === 'play-playlist') {
if (preloadData.onPlayCb === undefined) { if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value.playData; window.webOSAPI.pendingPlay = message.value;
} }
else { else {
preloadData.onPlayCb(null, message.value.playData); preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
} }
} }
}, else {
onFailure: (message: any) => { if (preloadData.onPlayCb === undefined) {
logger.error(`Player: play ${JSON.stringify(message)}`); window.webOSAPI.pendingPlay = message.value;
}, }
subscribe: true, else {
resubscribe: true preloadData.onPlayCb(null, message.value.rendererMessage);
}
}
}
}, (message: any) => {
logger.error(`Player: play ${JSON.stringify(message)}`);
}); });
const pauseService = requestService('pause', () => { preloadData.onPauseCb(); }); const pauseService = requestService('pause', () => { preloadData.onPauseCb(); });
const resumeService = requestService('resume', () => { preloadData.onResumeCb(); }); const resumeService = requestService('resume', () => { preloadData.onResumeCb(); });
const stopService = requestService('stop', () => { const stopService = requestService('stop', () => {
playService.cancel(); playService?.cancel();
pauseService.cancel(); pauseService?.cancel();
resumeService.cancel(); resumeService?.cancel();
stopService.cancel(); stopService?.cancel();
seekService.cancel(); seekService?.cancel();
setVolumeService.cancel(); setVolumeService?.cancel();
setSpeedService.cancel(); setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel(); getSessions?.cancel();
onConnectService.cancel(); onEventSubscribedKeysUpdateService?.cancel();
onDisconnectService.cancel(); onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API, // WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation... // so manually handling page navigation...
@ -97,25 +127,38 @@ try {
const seekService = requestService('seek', (message: any) => { preloadData.onSeekCb(null, message.value); }); const seekService = requestService('seek', (message: any) => { preloadData.onSeekCb(null, message.value); });
const setVolumeService = requestService('setvolume', (message: any) => { preloadData.onSetVolumeCb(null, message.value); }); const setVolumeService = requestService('setvolume', (message: any) => { preloadData.onSetVolumeCb(null, message.value); });
const setSpeedService = requestService('setspeed', (message: any) => { preloadData.onSetSpeedCb(null, message.value); }); const setSpeedService = requestService('setspeed', (message: any) => { preloadData.onSetSpeedCb(null, message.value); });
const onSetPlaylistItemService = requestService('setplaylistitem', (message: any) => { preloadData.onSetPlaylistItemCb(null, message.value); });
preloadData.sendPlayRequestCb = (message: PlayMessage, playlistIndex: number) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'play_request',
parameters: { message: message, playlistIndex: playlistIndex },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: play_request ${playlistIndex} ${JSON.stringify(message)}`); },
});
};
window.targetAPI.getSessions(() => { window.targetAPI.getSessions(() => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getSessions = requestService('get_sessions', (message: any) => resolve(message.value), (message: any) => reject(message), false); getSessions = callService('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
}); });
}); });
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); }); const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); });
const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); }); const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
const onPlayPlaylistService = requestService('play-playlist', (message: any) => { preloadData.onPlayPlaylistCb(null, message.value); });
const launchHandler = () => { const launchHandler = () => {
// args don't seem to be passed in via event despite what documentation says... // args don't seem to be passed in via event despite what documentation says...
const params = window.webOSDev.launchParams(); const params = window.webOSDev.launchParams();
logger.info(`Player: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`); logger.info(`Player: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
// WebOS 6.0 and earlier: Timestamp tracking seems to be necessary as launch event is raised regardless if app is in foreground or not
const lastTimestamp = Number(localStorage.getItem('lastTimestamp')); const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
if (params.playData !== undefined && params.timestamp != lastTimestamp) { if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp); localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('playData', JSON.stringify(params.playData)); sessionStorage.setItem('playInfo', JSON.stringify(params.messageInfo));
playService?.cancel(); playService?.cancel();
pauseService?.cancel(); pauseService?.cancel();
resumeService?.cancel(); resumeService?.cancel();
@ -123,49 +166,26 @@ try {
seekService?.cancel(); seekService?.cancel();
setVolumeService?.cancel(); setVolumeService?.cancel();
setSpeedService?.cancel(); setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel(); getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel(); onConnectService?.cancel();
onDisconnectService?.cancel(); onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API, // WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation... // so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html'); // history.pushState({}, '', '../main_window/index.html');
window.open('../player/index.html', '_self'); window.open(`../${params.messageInfo.contentViewer}/index.html`, '_self');
} }
}; };
document.addEventListener('webOSLaunch', launchHandler); document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler); document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => callService('visibility_changed', { hidden: document.hidden, window: contentViewer }));
} }
catch (err) { catch (err) {
logger.error(`Player: preload ${JSON.stringify(err)}`); logger.error(`Player: preload ${JSON.stringify(err)}`);
toast(`Error starting the video player (preload): ${JSON.stringify(err)}`, ToastIcon.ERROR); toast(`Error starting the video player (preload): ${JSON.stringify(err)}`, ToastIcon.ERROR);
} }
function requestService(method: string, successCallback: (message: any) => void, failureCallback?: (message: any) => void, subscribe: boolean = true): any {
const serviceId = 'com.futo.fcast.receiver.service';
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: {},
onSuccess: (message: any) => {
if (message.value?.subscribed === true) {
logger.info(`Player: Registered ${method} handler with service`);
}
else {
successCallback(message);
}
},
onFailure: (message: any) => {
logger.error(`Main: ${method} ${JSON.stringify(message)}`);
if (failureCallback) {
failureCallback(message);
}
},
// onComplete: (message) => {},
subscribe: subscribe,
resubscribe: subscribe
});
}

View file

@ -1,6 +1,7 @@
import { import {
isLive, isLive,
onPlay, onPlay,
onPlayPlaylist,
player, player,
PlayerControlEvent, PlayerControlEvent,
playerCtrlCaptions, playerCtrlCaptions,
@ -20,20 +21,13 @@ import {
skipBack, skipBack,
skipForward, skipForward,
} from 'common/player/Renderer'; } from 'common/player/Renderer';
import { RemoteKeyCode } from 'lib/common';
import * as common from 'lib/common';
const captionsBaseHeightCollapsed = 150; const captionsBaseHeightCollapsed = 150;
const captionsBaseHeightExpanded = 320; const captionsBaseHeightExpanded = 320;
const captionsLineHeight = 68; const captionsLineHeight = 68;
enum RemoteKeyCode {
Stop = 413,
Rewind = 412,
Play = 415,
Pause = 19,
FastForward = 417,
Back = 461,
}
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean { export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
let handledCase = false; let handledCase = false;
@ -84,21 +78,23 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase; return handledCase;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
export function targetKeyDownEventListener(event: any): boolean {
let handledCase = false; let handledCase = false;
let key = '';
switch (event.keyCode) { switch (event.keyCode) {
case RemoteKeyCode.Stop: case RemoteKeyCode.Stop:
// history.back(); // history.back();
window.open('../main_window/index.html', '_self'); window.open('../main_window/index.html', '_self');
handledCase = true; handledCase = true;
key = 'Stop';
break; break;
case RemoteKeyCode.Rewind: case RemoteKeyCode.Rewind:
skipBack(); skipBack();
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Rewind';
break; break;
case RemoteKeyCode.Play: case RemoteKeyCode.Play:
@ -107,6 +103,7 @@ export function targetKeyDownEventListener(event: any): boolean {
} }
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Play';
break; break;
case RemoteKeyCode.Pause: case RemoteKeyCode.Pause:
if (!player.isPaused()) { if (!player.isPaused()) {
@ -114,12 +111,14 @@ export function targetKeyDownEventListener(event: any): boolean {
} }
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Pause';
break; break;
case RemoteKeyCode.FastForward: case RemoteKeyCode.FastForward:
skipForward(); skipForward();
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'FastForward';
break; break;
// WebOS 22 and earlier does not work well using the history API, // WebOS 22 and earlier does not work well using the history API,
@ -129,17 +128,27 @@ export function targetKeyDownEventListener(event: any): boolean {
window.open('../main_window/index.html', '_self'); window.open('../main_window/index.html', '_self');
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Back';
break; break;
default: default:
break; break;
} }
return handledCase; return { handledCase: handledCase, key: key };
};
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
return common.targetKeyUpEventListener(event);
}; };
if (window.webOSAPI.pendingPlay !== null) { if (window.webOSAPI.pendingPlay !== null) {
onPlay(null, window.webOSAPI.pendingPlay); if (window.webOSAPI.pendingPlay.rendererEvent === 'play-playlist') {
onPlayPlaylist(null, window.webOSAPI.pendingPlay.rendererMessage);
}
else {
onPlay(null, window.webOSAPI.pendingPlay.rendererMessage);
}
} }
export { export {

View file

@ -10,7 +10,12 @@
<script src="./preload.js"></script> <script src="./preload.js"></script>
</head> </head>
<body> <body>
<div id="title-icon"></div>
<div id="loading-spinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div id="idle-background"></div>
<img id="thumbnailImage" />
<video id="videoPlayer" autoplay preload="auto"></video> <video id="videoPlayer" autoplay preload="auto"></video>
<div id="mediaTitle" class="captionsContainer"></div>
<div id="videoCaptions" class="captionsContainer"></div> <div id="videoCaptions" class="captionsContainer"></div>
<div id="controls" class="container"> <div id="controls" class="container">
@ -32,7 +37,9 @@
</div> </div>
<div class="leftButtonContainer"> <div class="leftButtonContainer">
<div id="playPrevious" class="playPrevious iconSize"></div>
<div id="action" class="play iconSize"></div> <div id="action" class="play iconSize"></div>
<div id="playNext" class="playNext iconSize"></div>
<div id="volume" class="volume_high iconSize"></div> <div id="volume" class="volume_high iconSize"></div>
<div class="volumeContainer"> <div class="volumeContainer">

View file

@ -0,0 +1,78 @@
import { PlayerControlEvent, playerCtrlStateUpdate, onPlay, onPlayPlaylist, setPlaylistItem, playlistIndex } from 'common/viewer/Renderer';
import { RemoteKeyCode } from 'lib/common';
import * as common from 'lib/common';
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
let handledCase = false;
return handledCase;
}
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false;
let key = '';
switch (event.keyCode) {
case RemoteKeyCode.Stop:
// history.back();
window.open('../main_window/index.html', '_self');
event.preventDefault();
handledCase = true;
key = 'Stop';
break;
case RemoteKeyCode.Rewind:
setPlaylistItem(playlistIndex - 1);
event.preventDefault();
handledCase = true;
key = 'Rewind';
break;
case RemoteKeyCode.Play:
playerCtrlStateUpdate(PlayerControlEvent.Play);
event.preventDefault();
handledCase = true;
key = 'Play';
break;
case RemoteKeyCode.Pause:
playerCtrlStateUpdate(PlayerControlEvent.Pause);
event.preventDefault();
handledCase = true;
key = 'Pause';
break;
case RemoteKeyCode.FastForward:
setPlaylistItem(playlistIndex + 1);
event.preventDefault();
handledCase = true;
key = 'FastForward';
break;
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
case RemoteKeyCode.Back:
// history.back();
window.open('../main_window/index.html', '_self');
event.preventDefault();
handledCase = true;
key = 'Back';
break;
default:
break;
}
return { handledCase: handledCase, key: key };
};
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
return common.targetKeyUpEventListener(event);
};
if (window.webOSAPI.pendingPlay !== null) {
if (window.webOSAPI.pendingPlay.rendererEvent === 'play-playlist') {
onPlayPlaylist(null, window.webOSAPI.pendingPlay.rendererMessage);
}
else {
onPlay(null, window.webOSAPI.pendingPlay.rendererMessage);
}
}

View file

@ -0,0 +1,45 @@
<!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" />
<link rel="stylesheet" href="./style.css" />
<script src="../player/preload.js"></script>
</head>
<body>
<!-- Empty video element as a workaround to fix issue with white border outline without it... -->
<video id="idleBackground" class="video"></video>
<div id="viewer" class="viewer">
<div id="titleIcon"></div>
<div id="loadingSpinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
<img id="viewerImage" class="viewer" />
<iframe id="viewerGeneric" class="viewer"></iframe>
</div>>
<div id="controls" class="container">
<div id="leftButtonContainer" class="buttonContainer">
<div id="mediaTitle"></div>
</div>
<div id="centerButtonContainer" class="buttonContainer">
<div id="playPrevious" class="playPrevious iconSize" style="display: none"></div>
<div id="action" class="play iconSize" style="display: none"></div>
<div id="playlistLength" style="display: none"></div>
<div id="playNext" class="playNext iconSize" style="display: none"></div>
</div>
<!-- <div id="rightButtonContainer" class="buttonContainer">
<div id="fullscreen" class="fullscreen_on iconSize"></div>
</div> -->
</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

@ -0,0 +1 @@
/* Stub for future use */

View file

@ -18,13 +18,18 @@ module.exports = [
preload: './src/main/Preload.ts', preload: './src/main/Preload.ts',
renderer: './src/main/Renderer.ts', renderer: './src/main/Renderer.ts',
}, },
target: 'web', target: ['web', 'es5'],
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')], include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }] use: [{ loader: 'ts-loader' }]
},
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
} }
], ],
}, },
@ -88,13 +93,18 @@ module.exports = [
preload: './src/player/Preload.ts', preload: './src/player/Preload.ts',
renderer: './src/player/Renderer.ts', renderer: './src/player/Renderer.ts',
}, },
target: 'web', target: ['web', 'es5'],
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')], include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }] use: [{ loader: 'ts-loader' }]
},
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
} }
], ],
}, },
@ -130,4 +140,58 @@ module.exports = [
}) })
] ]
}, },
{
mode: buildMode,
entry: {
// Player preload is intentionally reused
preload: './src/player/Preload.ts',
renderer: './src/viewer/Renderer.ts',
},
target: ['web', 'es5'],
module: {
rules: [
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
},
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
}
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'lib': path.resolve(__dirname, 'lib'),
'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)
})
]
}
]; ];