1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-07-26 03:47: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
} else if (TARGET === 'webOS' || TARGET === 'tizenOS') {
preloadData = {
onDeviceInfoCb: () => { logger.error('Main: Callback not set while fetching device info'); },
getSessionsCb: () => { logger.error('Main: Callback not set while calling getSessions'); },
onConnectCb: (_, value: any) => { logger.error('Main: Callback not set while calling onConnect'); },
onDisconnectCb: (_, value: any) => { logger.error('Main: Callback not set while calling onDisconnect'); },
preloadData.onDeviceInfoCb = () => { logger.warn('Main: Callback not set while fetching device info'); };
preloadData.getSessionsCb = () => { logger.error('Main: Callback not set while calling getSessions'); };
preloadData.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'); };
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 = {
@ -102,8 +110,10 @@ if (TARGET === 'electron') {
return preloadData.getSessionsCb();
}
},
getSubscribedKeys: () => preloadData.subscribedKeys,
onConnect: (callback: (_, value: any) => void) => preloadData.onConnectCb = callback,
onDisconnect: (callback: (_, value: any) => void) => preloadData.onDisconnectCb = callback,
sendEvent: (message: EventMessage) => { preloadData.sendEventCb(message); },
logger: loggerInterface,
};
} else {

View file

@ -1,9 +1,10 @@
import QRCode from 'modules/qrcode';
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 { EventMessage, EventType, KeyEvent } from 'common/Packets';
import { targetKeyDownEventListener } from 'src/main/Renderer';
const connectionStatusText = document.getElementById('connection-status-text');
const connectionStatusSpinner = document.getElementById('connection-spinner');
@ -203,12 +204,40 @@ function renderQRCode(url: string) {
}
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, false)));
// logger.info("KeyDown", event);
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) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
// logger.info("KeyUp", event);
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
} else if (TARGET === 'webOS' || TARGET === 'tizenOS') {
preloadData = {
sendPlaybackErrorCb: () => { logger.error('Player: Callback "send_playback_error" not set'); },
sendPlaybackUpdateCb: () => { logger.error('Player: Callback "send_playback_update" not set'); },
sendVolumeUpdateCb: () => { logger.error('Player: Callback "send_volume_update" not set'); },
// onPlayCb: () => { logger.error('Player: Callback "play" not set'); },
onPlayCb: undefined,
onPauseCb: () => { logger.error('Player: Callback "pause" not set'); },
onResumeCb: () => { logger.error('Player: Callback "resume" not set'); },
onSeekCb: () => { logger.error('Player: Callback "onseek" not set'); },
onSetVolumeCb: () => { logger.error('Player: Callback "setvolume" not set'); },
onSetSpeedCb: () => { logger.error('Player: Callback "setspeed" not set'); },
getSessionsCb: () => { logger.error('Player: Callback "getSessions" not set'); },
onConnectCb: () => { logger.error('Player: Callback "onConnect" not set'); },
onDisconnectCb: () => { logger.error('Player: Callback "onDisconnect" not set'); },
preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => { logger.error('Player: Callback "send_playback_update" not set'); };
preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => { logger.error('Player: Callback "send_volume_update" not set'); };
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => { logger.error('Player: Callback "send_playback_error" not set'); };
preloadData.sendEventCb = (message: EventMessage) => { logger.error('Player: Callback "onSendEventCb" not set'); };
// preloadData.onPlayCb = () => { logger.error('Player: Callback "play" not set'); };
preloadData.onPlayCb = undefined;
preloadData.onPauseCb = () => { logger.error('Player: Callback "pause" not set'); };
preloadData.onResumeCb = () => { logger.error('Player: Callback "resume" not set'); };
preloadData.onSeekCb = () => { logger.error('Player: Callback "onseek" not set'); };
preloadData.onSetVolumeCb = () => { logger.error('Player: Callback "setvolume" not set'); };
preloadData.onSetSpeedCb = () => { logger.error('Player: Callback "setspeed" not set'); };
preloadData.onSetPlaylistItemCb = () => { logger.error('Player: Callback "onSetPlaylistItem" 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 = {
sendPlaybackError: (error: PlaybackErrorMessage) => { preloadData.sendPlaybackErrorCb(error); },
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => { preloadData.sendPlaybackUpdateCb(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; },
onPause: (callback: any) => { preloadData.onPauseCb = callback; },
onResume: (callback: any) => { preloadData.onResumeCb = callback; },
onSeek: (callback: any) => { preloadData.onSeekCb = callback; },
onSetVolume: (callback: any) => { preloadData.onSetVolumeCb = 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]>) => {
if (callback) {
preloadData.getSessionsCb = callback;
@ -109,8 +125,10 @@ if (TARGET === 'electron') {
return preloadData.getSessionsCb();
}
},
getSubscribedKeys: () => preloadData.subscribedKeys,
onConnect: (callback: any) => { preloadData.onConnectCb = callback; },
onDisconnect: (callback: any) => { preloadData.onDisconnectCb = callback; },
onPlayPlaylist: (callback: any) => { preloadData.onPlayPlaylistCb = callback; },
logger: loggerInterface,
};
} else {

View file

@ -12,7 +12,8 @@ import {
targetKeyDownEventListener,
captionsBaseHeightCollapsed,
captionsBaseHeightExpanded,
captionsLineHeight
captionsLineHeight,
targetKeyUpEventListener
} from 'src/player/Renderer';
const logger = window.targetAPI.logger;
@ -912,74 +913,6 @@ document.addEventListener('click', (event: MouseEvent) => {
const skipInterval = 10;
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() {
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) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
// logger.info("KeyUp", event);
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,
captionsLineHeight,
onPlay,
onPlayPlaylist,
playerCtrlStateUpdate,
formatDuration,
skipBack,

View file

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

View file

@ -3,6 +3,16 @@ import 'common/main/Renderer';
const logger = window.targetAPI.logger;
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 updateViewTitle = document.getElementById("update-view-title");
const updateText = document.getElementById("update-text");

View file

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

View file

@ -40,17 +40,17 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase;
}
export function targetKeyDownEventListener(event: KeyboardEvent): boolean {
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false;
switch (event.code) {
case 'KeyF':
case 'F11':
switch (event.key.toLowerCase()) {
case 'f':
case 'f11':
playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen);
event.preventDefault();
handledCase = true;
break;
case 'Escape':
case 'escape':
playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen);
event.preventDefault();
handledCase = true;
@ -59,5 +59,10 @@ export function targetKeyDownEventListener(event: KeyboardEvent): boolean {
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.
Note that you may have to periodically rebuild the container to keep key information up-to-date with the TV device.
### Run
```bash
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",
"version": "1.1.0",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "com.futo.fcast.receiver.service",
"version": "1.1.0",
"version": "2.0.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"http": "^0.0.1-security",
"bufferutil": "^4.0.8",
"follow-redirects": "^1.15.9",
"log4js": "^6.9.1",
"memfs": "^4.17.2",
"url": "^0.11.4",
"uuid": "^11.0.3",
"uuid": "^9.0.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10",
@ -29,7 +32,6 @@
"eslint": "^9.25.0",
"globals": "^16.0.0",
"jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"patch-package": "^8.0.0",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
@ -713,6 +715,47 @@
"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": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1302,6 +1345,60 @@
"@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": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz",
@ -2521,8 +2618,6 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -3786,6 +3881,26 @@
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -4052,11 +4167,6 @@
"dev": true,
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -4084,6 +4194,15 @@
"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": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -5409,47 +5528,6 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -5460,6 +5538,25 @@
"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": {
"version": "2.0.0",
"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",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
@ -7047,6 +7143,18 @@
"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": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
@ -7135,6 +7243,22 @@
"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": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz",
@ -7254,6 +7378,12 @@
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -7470,16 +7600,15 @@
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {

View file

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

View file

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

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",
"version": "1.1.0",
"version": "2.0.0",
"vendor": "FUTO",
"type": "web",
"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",
"version": "1.1.0",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "com.futo.fcast.receiver",
"version": "1.1.0",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"bufferutil": "^4.0.8",
"dashjs": "^4.7.4",
"hls.js": "^1.5.15",
"http": "^0.0.1-security",
"https": "^1.0.0",
"log4js": "^6.9.1",
"qrcode": "^1.5.3",
"url": "^0.11.4",
"uuid": "^11.0.3",
"uuid": "^9.0.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10",
@ -32,13 +32,15 @@
"eslint": "^9.25.0",
"globals": "^16.0.0",
"jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
"typescript": "^5.5.4",
"typescript-eslint": "^8.4.0",
"webpack": "^5.99.6",
"webpack-cli": "^6.0.1"
},
"optionalDependencies": {
"utf-8-validate": "^6.0.5"
}
},
"node_modules/@ampproject/remapping": {
@ -837,6 +839,47 @@
"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": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -3045,11 +3088,19 @@
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
"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": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -3880,7 +3931,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true,
"license": "ISC"
},
"node_modules/forwarded": {
@ -3903,6 +3953,20 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -4071,7 +4135,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
@ -4134,11 +4197,6 @@
"dev": true,
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -4156,12 +4214,6 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -5261,6 +5313,15 @@
"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": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -5378,6 +5439,22 @@
"dev": true,
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -5430,47 +5507,6 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@ -5575,7 +5611,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
@ -6425,6 +6460,12 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@ -6840,6 +6881,20 @@
"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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -7358,6 +7413,15 @@
"dev": true,
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -7428,17 +7492,30 @@
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"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": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {

View file

@ -1,6 +1,6 @@
{
"name": "com.futo.fcast.receiver",
"version": "1.1.0",
"version": "2.0.0",
"description": "An application implementing a FCast receiver.",
"author": "FUTO",
"license": "MIT",
@ -10,6 +10,7 @@
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@futo/mdns-js": "1.0.3",
"@types/jest": "^29.5.11",
"@types/mdns": "^0.0.38",
"@types/node-forge": "^1.3.10",
@ -21,7 +22,6 @@
"eslint": "^9.25.0",
"globals": "^16.0.0",
"jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
"typescript": "^5.5.4",
@ -33,11 +33,13 @@
"bufferutil": "^4.0.8",
"dashjs": "^4.7.4",
"hls.js": "^1.5.15",
"http": "^0.0.1-security",
"https": "^1.0.0",
"log4js": "^6.9.1",
"qrcode": "^1.5.3",
"url": "^0.11.4",
"uuid": "^11.0.3",
"uuid": "^9.0.1",
"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-explicit-any */
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-dev.js');
const logger = window.targetAPI.logger;
enum RemoteKeyCode {
Stop = 413,
Rewind = 412,
Play = 415,
Pause = 19,
FastForward = 417,
Back = 461,
}
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 getDeviceInfoService = window.webOS.service.request('luna://com.palm.connectionmanager', {
method: 'getStatus',
parameters: {},
const toastService = requestService('toast', (message: any) => { preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration); });
const getDeviceInfoService = window.webOSDev.connection.getStatus({
onSuccess: (message: any) => {
// logger.info('Network info status message', message);
logger.info('Network info status message', message);
const deviceName = 'FCast-LGwebOSTV';
const connections = [];
let fallback = true;
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...)
// if (message.wifiDirect.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 };
preloadData.onDeviceInfoCb();
if (fallback) {
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) => {
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,
resubscribe: true
});
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
window.targetAPI.getSessions(() => {
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 onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
const playService = requestService('play', (message: any) => {
if (message.value !== undefined && message.value.playData !== undefined) {
logger.info(`Main: Playing ${JSON.stringify(message)}`);
sessionStorage.setItem('playData', JSON.stringify(message.value.playData));
getDeviceInfoService.cancel();
getSessions?.cancel();
toastService.cancel();
onConnectService.cancel();
onDisconnectService.cancel();
playService.cancel();
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)}`); },
});
};
// 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');
}
const playService = requestService('play', (message: any) => {
logger.info(`Main: Playing ${JSON.stringify(message)}`);
play(message.value);
});
const launchHandler = () => {
const params = window.webOSDev.launchParams();
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'));
if (params.playData !== undefined && params.timestamp != lastTimestamp) {
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('playData', JSON.stringify(params.playData));
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');
play(params.messageInfo);
}
};
document.addEventListener('webOSLaunch', 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.
// window.onpopstate = () => {
// window.webOS.platformBack();
// };
document.addEventListener('keydown', (event: any) => {
// logger.info("KeyDown", event);
const play = (messageInfo: any) => {
sessionStorage.setItem('playInfo', JSON.stringify(messageInfo));
switch (event.keyCode) {
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
case RemoteKeyCode.Back:
window.webOS.platformBack();
break;
default:
break;
}
});
getDeviceInfoService?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
getSessionsService?.cancel();
toastService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
playService?.cancel();
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) {
logger.error(`Main: preload ${JSON.stringify(err)}`);
toast(`Error starting the application (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(`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
});
preloadData.onToastCb(`Error starting the application: ${JSON.stringify(err)}`, ToastIcon.ERROR);
}

View file

@ -1,4 +1,6 @@
import 'common/main/Renderer';
import { RemoteKeyCode } from 'lib/common';
import * as common from 'lib/common';
const backgroundVideo = document.getElementById('video-player');
const loadingScreen = document.getElementById('loading-screen');
@ -30,3 +32,45 @@ backgroundVideo.onplaying = () => {
export function onQRCodeRendered() {
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>
<div id="main-container">
<img id="image-background"/>
<video id="video-player" class="video" autoplay loop>
<source src="../assets/video/background.mp4" type="video/mp4">
</video>

View file

@ -1,19 +1,21 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-explicit-any */
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';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
const logger = window.targetAPI.logger;
const serviceId = 'com.futo.fcast.receiver.service';
try {
const serviceId = 'com.futo.fcast.receiver.service';
let getSessions = null;
window.webOSAPI = {
pendingPlay: JSON.parse(sessionStorage.getItem('playData'))
pendingPlay: JSON.parse(sessionStorage.getItem('playInfo'))
};
const contentViewer = window.webOSAPI.pendingPlay?.contentViewer;
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
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}/`, {
method:"play",
parameters: {},
onSuccess: (message: any) => {
// logger.info(JSON.stringify(message));
if (message.value.subscribed === true) {
logger.info('Player: Registered play handler with service');
}
const playService = requestService('play', (message: any) => {
if (contentViewer !== message.value.contentViewer) {
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
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) {
window.webOSAPI.pendingPlay = message.value.playData;
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayCb(null, message.value.playData);
preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
}
}
},
onFailure: (message: any) => {
logger.error(`Player: play ${JSON.stringify(message)}`);
},
subscribe: true,
resubscribe: true
else {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayCb(null, message.value.rendererMessage);
}
}
}
}, (message: any) => {
logger.error(`Player: play ${JSON.stringify(message)}`);
});
const pauseService = requestService('pause', () => { preloadData.onPauseCb(); });
const resumeService = requestService('resume', () => { preloadData.onResumeCb(); });
const stopService = requestService('stop', () => {
playService.cancel();
pauseService.cancel();
resumeService.cancel();
stopService.cancel();
seekService.cancel();
setVolumeService.cancel();
setSpeedService.cancel();
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onConnectService.cancel();
onDisconnectService.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
@ -97,25 +127,38 @@ try {
const seekService = requestService('seek', (message: any) => { preloadData.onSeekCb(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 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(() => {
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 onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
const onPlayPlaylistService = requestService('play-playlist', (message: any) => { preloadData.onPlayPlaylistCb(null, message.value); });
const launchHandler = () => {
// args don't seem to be passed in via event despite what documentation says...
const params = window.webOSDev.launchParams();
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'));
if (params.playData !== undefined && params.timestamp != lastTimestamp) {
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('playData', JSON.stringify(params.playData));
sessionStorage.setItem('playInfo', JSON.stringify(params.messageInfo));
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
@ -123,49 +166,26 @@ try {
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.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');
window.open(`../${params.messageInfo.contentViewer}/index.html`, '_self');
}
};
document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => callService('visibility_changed', { hidden: document.hidden, window: contentViewer }));
}
catch (err) {
logger.error(`Player: preload ${JSON.stringify(err)}`);
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 {
isLive,
onPlay,
onPlayPlaylist,
player,
PlayerControlEvent,
playerCtrlCaptions,
@ -20,20 +21,13 @@ import {
skipBack,
skipForward,
} from 'common/player/Renderer';
import { RemoteKeyCode } from 'lib/common';
import * as common from 'lib/common';
const captionsBaseHeightCollapsed = 150;
const captionsBaseHeightExpanded = 320;
const captionsLineHeight = 68;
enum RemoteKeyCode {
Stop = 413,
Rewind = 412,
Play = 415,
Pause = 19,
FastForward = 417,
Back = 461,
}
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
let handledCase = false;
@ -84,21 +78,23 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function targetKeyDownEventListener(event: any): boolean {
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');
handledCase = true;
key = 'Stop';
break;
case RemoteKeyCode.Rewind:
skipBack();
event.preventDefault();
handledCase = true;
key = 'Rewind';
break;
case RemoteKeyCode.Play:
@ -107,6 +103,7 @@ export function targetKeyDownEventListener(event: any): boolean {
}
event.preventDefault();
handledCase = true;
key = 'Play';
break;
case RemoteKeyCode.Pause:
if (!player.isPaused()) {
@ -114,12 +111,14 @@ export function targetKeyDownEventListener(event: any): boolean {
}
event.preventDefault();
handledCase = true;
key = 'Pause';
break;
case RemoteKeyCode.FastForward:
skipForward();
event.preventDefault();
handledCase = true;
key = 'FastForward';
break;
// 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');
event.preventDefault();
handledCase = true;
key = 'Back';
break;
default:
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) {
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 {

View file

@ -10,7 +10,12 @@
<script src="./preload.js"></script>
</head>
<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>
<div id="mediaTitle" class="captionsContainer"></div>
<div id="videoCaptions" class="captionsContainer"></div>
<div id="controls" class="container">
@ -32,7 +37,9 @@
</div>
<div class="leftButtonContainer">
<div id="playPrevious" class="playPrevious 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 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',
renderer: './src/main/Renderer.ts',
},
target: 'web',
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' }]
}
],
},
@ -88,13 +93,18 @@ module.exports = [
preload: './src/player/Preload.ts',
renderer: './src/player/Renderer.ts',
},
target: 'web',
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' }]
}
],
},
@ -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)
})
]
}
];