mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-22 23:32:50 +00:00
Merge branch 'michael/webos' into 'master'
WebOS Receiver 2.0.0 See merge request videostreaming/fcast!18
This commit is contained in:
commit
64354001c5
44 changed files with 2731 additions and 832 deletions
|
@ -61,31 +61,7 @@ export class FCastSession {
|
|||
|
||||
const size = 1 + data.length;
|
||||
const header = Buffer.alloc(4 + 1);
|
||||
|
||||
// webOS 22 and earlier node versions do not support `writeUint32LE`,
|
||||
// so manually checking endianness and writing as LE
|
||||
// @ts-ignore
|
||||
if (TARGET === 'webOS') {
|
||||
let uInt32 = new Uint32Array([0x11223344]);
|
||||
let uInt8 = new Uint8Array(uInt32.buffer);
|
||||
|
||||
if(uInt8[0] === 0x44) {
|
||||
// LE
|
||||
header[0] = size & 0xFF;
|
||||
header[1] = size & 0xFF00;
|
||||
header[2] = size & 0xFF0000;
|
||||
header[3] = size & 0xFF000000;
|
||||
} else if (uInt8[0] === 0x11) {
|
||||
// BE
|
||||
header[0] = size & 0xFF000000;
|
||||
header[1] = size & 0xFF0000;
|
||||
header[2] = size & 0xFF00;
|
||||
header[3] = size & 0xFF;
|
||||
}
|
||||
} else {
|
||||
header.writeUint32LE(size, 0);
|
||||
}
|
||||
|
||||
header[4] = opcode;
|
||||
|
||||
let packet: Buffer;
|
||||
|
|
|
@ -22,6 +22,8 @@ export class Timer {
|
|||
private delay: number;
|
||||
private startTime: number;
|
||||
private remainingTime: number;
|
||||
private enabled: boolean;
|
||||
|
||||
public started: boolean;
|
||||
|
||||
constructor(callback: () => void, delay: number, autoStart: boolean = true) {
|
||||
|
@ -29,6 +31,7 @@ export class Timer {
|
|||
this.callback = callback;
|
||||
this.delay = delay;
|
||||
this.started = false;
|
||||
this.enabled = true;
|
||||
|
||||
if (autoStart) {
|
||||
this.start();
|
||||
|
@ -36,6 +39,7 @@ export class Timer {
|
|||
}
|
||||
|
||||
public start(delay?: number) {
|
||||
if (this.enabled) {
|
||||
this.delay = delay ? delay : this.delay;
|
||||
|
||||
if (this.handle) {
|
||||
|
@ -47,9 +51,10 @@ export class Timer {
|
|||
this.remainingTime = null;
|
||||
this.handle = window.setTimeout(this.callback, this.delay);
|
||||
}
|
||||
}
|
||||
|
||||
public pause() {
|
||||
if (this.handle) {
|
||||
if (this.enabled && this.handle) {
|
||||
window.clearTimeout(this.handle);
|
||||
this.handle = null;
|
||||
this.remainingTime = this.delay - (Date.now() - this.startTime);
|
||||
|
@ -57,7 +62,7 @@ export class Timer {
|
|||
}
|
||||
|
||||
public resume() {
|
||||
if (this.remainingTime) {
|
||||
if (this.enabled && this.remainingTime) {
|
||||
this.start(this.remainingTime);
|
||||
}
|
||||
}
|
||||
|
@ -70,4 +75,32 @@ export class Timer {
|
|||
this.started = false;
|
||||
}
|
||||
}
|
||||
|
||||
public end() {
|
||||
this.stop();
|
||||
this.callback();
|
||||
}
|
||||
|
||||
public enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public disable() {
|
||||
this.enabled = false;
|
||||
this.stop();
|
||||
}
|
||||
|
||||
public setDelay(delay: number) {
|
||||
this.stop();
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
public setCallback(callback: () => void) {
|
||||
this.stop();
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this.remainingTime !== null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,11 +84,20 @@ 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.initializeSubscribedKeysCb = () => { logger.error('Main: Callback not set while calling initializeSubscribedKeys'); };
|
||||
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: string[], keyUp: string[] }) => {
|
||||
preloadData.subscribedKeys.keyDown = new Set(value.keyDown);
|
||||
preloadData.subscribedKeys.keyUp = new Set(value.keyUp);
|
||||
};
|
||||
|
||||
preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => {
|
||||
toast(message, icon, duration);
|
||||
};
|
||||
|
||||
window.targetAPI = {
|
||||
|
@ -102,8 +111,21 @@ if (TARGET === 'electron') {
|
|||
return preloadData.getSessionsCb();
|
||||
}
|
||||
},
|
||||
initializeSubscribedKeys: (callback?: () => Promise<{ keyDown: string[], keyUp: string[] }>) => {
|
||||
if (callback) {
|
||||
preloadData.initializeSubscribedKeysCb = callback;
|
||||
}
|
||||
else {
|
||||
preloadData.initializeSubscribedKeysCb().then((value: { keyDown: Set<string>, keyUp: Set<string> }) => {
|
||||
preloadData.subscribedKeys.keyDown = new Set(value.keyDown);
|
||||
preloadData.subscribedKeys.keyUp = new Set(value.keyUp);
|
||||
});
|
||||
}
|
||||
},
|
||||
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 {
|
||||
|
|
|
@ -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');
|
||||
|
@ -202,13 +203,45 @@ function renderQRCode(url: string) {
|
|||
onQRCodeRendered();
|
||||
}
|
||||
|
||||
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)));
|
||||
export function keyDownEventHandler(event: KeyboardEvent) {
|
||||
// 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.toLowerCase()) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
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)));
|
||||
}
|
||||
});
|
||||
|
||||
if (window.targetAPI.getSubscribedKeys().keyDown.has(key)) {
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, key, event.repeat, handledCase)));
|
||||
}
|
||||
}
|
||||
|
||||
export function keyUpEventHandler(event: KeyboardEvent) {
|
||||
// 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.toLowerCase()) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.targetAPI.getSubscribedKeys().keyUp.has(key)) {
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, key, event.repeat, handledCase)));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDownEventHandler);
|
||||
document.addEventListener('keyup', keyUpEventHandler);
|
||||
|
|
|
@ -14,6 +14,8 @@ export class Player {
|
|||
private player: HTMLVideoElement;
|
||||
private playMessage: PlayMessage;
|
||||
private source: string;
|
||||
private playCb: any;
|
||||
private pauseCb: any;
|
||||
|
||||
// Todo: use a common event handler interface instead of exposing internal players
|
||||
public playerType: PlayerType;
|
||||
|
@ -23,6 +25,8 @@ export class Player {
|
|||
constructor(player: HTMLVideoElement, message: PlayMessage) {
|
||||
this.player = player;
|
||||
this.playMessage = message;
|
||||
this.playCb = null;
|
||||
this.pauseCb = null;
|
||||
|
||||
if (message.container === 'application/dash+xml') {
|
||||
this.playerType = PlayerType.Dash;
|
||||
|
@ -110,6 +114,8 @@ export class Player {
|
|||
this.hlsPlayer = null;
|
||||
this.playMessage = null;
|
||||
this.source = null;
|
||||
this.playCb = null;
|
||||
this.pauseCb = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,6 +149,10 @@ export class Player {
|
|||
} else { // HLS, HTML
|
||||
this.player.play();
|
||||
}
|
||||
|
||||
if (this.playCb) {
|
||||
this.playCb();
|
||||
}
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
|
@ -161,6 +171,15 @@ export class Player {
|
|||
} else { // HLS, HTML
|
||||
this.player.pause();
|
||||
}
|
||||
|
||||
if (this.pauseCb) {
|
||||
this.pauseCb();
|
||||
}
|
||||
}
|
||||
|
||||
public setPlayPauseCallback(playCallback: (() => void), pauseCallback: (() => void)) {
|
||||
this.playCb = playCallback;
|
||||
this.pauseCb = pauseCallback;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
|
|
@ -19,7 +19,6 @@ declare global {
|
|||
interface Window {
|
||||
electronAPI: any;
|
||||
tizenOSAPI: any;
|
||||
webOSAPI: any;
|
||||
webOS: any;
|
||||
targetAPI: any;
|
||||
}
|
||||
|
@ -75,32 +74,49 @@ 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.initializeSubscribedKeysCb = () => { logger.error('Player: Callback "initializeSubscribedKeys" not set'); };
|
||||
preloadData.onConnectCb = () => { logger.warn('Player: Callback "onConnect" not set'); };
|
||||
preloadData.onDisconnectCb = () => { logger.warn('Player: Callback "onDisconnect" not set'); };
|
||||
preloadData.onPlayPlaylistCb = () => { logger.error('Player: Callback "onPlayPlaylist" not set'); };
|
||||
|
||||
preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: string[], keyUp: string[] }) => {
|
||||
preloadData.subscribedKeys.keyDown = new Set(value.keyDown);
|
||||
preloadData.subscribedKeys.keyUp = new Set(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,21 @@ if (TARGET === 'electron') {
|
|||
return preloadData.getSessionsCb();
|
||||
}
|
||||
},
|
||||
initializeSubscribedKeys: (callback?: () => Promise<{ keyDown: string[], keyUp: string[] }>) => {
|
||||
if (callback) {
|
||||
preloadData.initializeSubscribedKeysCb = callback;
|
||||
}
|
||||
else {
|
||||
preloadData.initializeSubscribedKeysCb().then((value: { keyDown: Set<string>, keyUp: Set<string> }) => {
|
||||
preloadData.subscribedKeys.keyDown = new Set(value.keyDown);
|
||||
preloadData.subscribedKeys.keyUp = new Set(value.keyUp);
|
||||
});
|
||||
}
|
||||
},
|
||||
getSubscribedKeys: () => preloadData.subscribedKeys,
|
||||
onConnect: (callback: any) => { preloadData.onConnectCb = callback; },
|
||||
onDisconnect: (callback: any) => { preloadData.onDisconnectCb = callback; },
|
||||
onPlayPlaylist: (callback: any) => { preloadData.onPlayPlaylistCb = callback; },
|
||||
logger: loggerInterface,
|
||||
};
|
||||
} else {
|
||||
|
|
|
@ -12,7 +12,8 @@ import {
|
|||
targetKeyDownEventListener,
|
||||
captionsBaseHeightCollapsed,
|
||||
captionsBaseHeightExpanded,
|
||||
captionsLineHeight
|
||||
captionsLineHeight,
|
||||
targetKeyUpEventListener
|
||||
} from 'src/player/Renderer';
|
||||
|
||||
const logger = window.targetAPI.logger;
|
||||
|
@ -39,7 +40,7 @@ const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer")
|
|||
const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress");
|
||||
const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition");
|
||||
const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle");
|
||||
const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea");
|
||||
const playerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea");
|
||||
|
||||
const playerCtrlVolumeBar = document.getElementById("volumeBar");
|
||||
const playerCtrlVolumeBarProgress = document.getElementById("volumeBarProgress");
|
||||
|
@ -79,10 +80,7 @@ let playlistIndex = 0;
|
|||
let isMediaItem = false;
|
||||
let playItemCached = false;
|
||||
|
||||
let uiHideTimer = new Timer(() => {
|
||||
uiVisible = false;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}, 3000);
|
||||
let uiHideTimer = new Timer(() => { playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut); }, 3000);
|
||||
let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false);
|
||||
let showDurationTimer = new Timer(mediaEndHandler, 0, false);
|
||||
let mediaTitleShowTimer = new Timer(() => { mediaTitle.style.display = 'none'; }, 5000);
|
||||
|
@ -566,6 +564,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
|
|||
}
|
||||
|
||||
case PlayerControlEvent.UiFadeOut: {
|
||||
uiVisible = false;
|
||||
document.body.style.cursor = "none";
|
||||
playerControls.style.opacity = '0';
|
||||
captionsBaseHeight = captionsBaseHeightCollapsed;
|
||||
|
@ -581,6 +580,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
|
|||
}
|
||||
|
||||
case PlayerControlEvent.UiFadeIn: {
|
||||
uiVisible = true;
|
||||
document.body.style.cursor = "default";
|
||||
playerControls.style.opacity = '1';
|
||||
captionsBaseHeight = captionsBaseHeightExpanded;
|
||||
|
@ -643,7 +643,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
|
|||
|
||||
function scrubbingMouseUIHandler(e: MouseEvent) {
|
||||
const progressBarOffset = e.offsetX - playerCtrlProgressBar.offsetLeft;
|
||||
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBar.offsetLeft * 2);
|
||||
const progressBarWidth = playerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBar.offsetLeft * 2);
|
||||
let time = isLive ? Math.round((1 - (progressBarOffset / progressBarWidth)) * player?.getDuration()) : Math.round((progressBarOffset / progressBarWidth) * player?.getDuration());
|
||||
time = Math.min(player?.getDuration(), Math.max(0.0, time));
|
||||
|
||||
|
@ -656,7 +656,7 @@ function scrubbingMouseUIHandler(e: MouseEvent) {
|
|||
playerCtrlProgressBarPosition.textContent = isLive ? `${livePrefix}${formatDuration(time)}` : formatDuration(time);
|
||||
|
||||
let offset = e.offsetX - (playerCtrlProgressBarPosition.offsetWidth / 2);
|
||||
offset = Math.min(PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBarPosition.offsetWidth / 1), Math.max(8, offset));
|
||||
offset = Math.min(playerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBarPosition.offsetWidth / 1), Math.max(8, offset));
|
||||
playerCtrlProgressBarPosition.setAttribute("style", `display: block; left: ${offset}px`);
|
||||
}
|
||||
|
||||
|
@ -673,21 +673,21 @@ playerCtrlPlayPrevious.onclick = () => { setPlaylistItem(playlistIndex - 1); }
|
|||
playerCtrlPlayNext.onclick = () => { setPlaylistItem(playlistIndex + 1); }
|
||||
playerCtrlVolume.onclick = () => { player?.setMute(!player?.isMuted()); };
|
||||
|
||||
PlayerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) };
|
||||
PlayerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; };
|
||||
PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
|
||||
playerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) };
|
||||
playerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; };
|
||||
playerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
|
||||
if (e.buttons === 0) {
|
||||
volumeChanging = false;
|
||||
}
|
||||
|
||||
scrubbingMouseUIHandler(e);
|
||||
};
|
||||
PlayerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); };
|
||||
PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
|
||||
playerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); };
|
||||
playerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
|
||||
|
||||
function scrubbingMouseHandler(e: MouseEvent) {
|
||||
const progressBarOffset = e.offsetX - playerCtrlProgressBar.offsetLeft;
|
||||
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBar.offsetLeft * 2);
|
||||
const progressBarWidth = playerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBar.offsetLeft * 2);
|
||||
let time = Math.round((progressBarOffset / progressBarWidth) * player?.getDuration());
|
||||
time = Math.min(player?.getDuration(), Math.max(0.0, time));
|
||||
|
||||
|
@ -879,17 +879,11 @@ function stopUiHideTimer() {
|
|||
uiHideTimer.stop();
|
||||
|
||||
if (!uiVisible) {
|
||||
uiVisible = true;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
document.onmouseout = () => {
|
||||
uiHideTimer.stop();
|
||||
uiVisible = false;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
|
||||
document.onmouseout = () => { uiHideTimer.end(); }
|
||||
document.onmousemove = () => {
|
||||
stopUiHideTimer();
|
||||
|
||||
|
@ -909,31 +903,91 @@ document.addEventListener('click', (event: MouseEvent) => {
|
|||
});
|
||||
|
||||
// Add the keydown event listener to the document
|
||||
const skipInterval = 10;
|
||||
const minSkipInterval = 10;
|
||||
const volumeIncrement = 0.1;
|
||||
|
||||
function keyDownEventListener(event: KeyboardEvent) {
|
||||
// logger.info("KeyDown", event);
|
||||
let handledCase = targetKeyDownEventListener(event);
|
||||
let skipBackRepeat = false;
|
||||
let skipBackInterval = minSkipInterval;
|
||||
let skipBackIntervalIncrease = false;
|
||||
let skipBackTimer = new Timer(() => { skipBackIntervalIncrease = true; }, 2000, false);
|
||||
|
||||
let skipForwardRepeat = false;
|
||||
let skipForwardInterval = minSkipInterval;
|
||||
let skipForwardIntervalIncrease = false;
|
||||
let skipForwardTimer = new Timer(() => { skipForwardIntervalIncrease = true; }, 2000, false);
|
||||
|
||||
function skipBack(repeat: boolean = false) {
|
||||
if (!skipBackRepeat && repeat) {
|
||||
skipBackRepeat = true;
|
||||
skipBackTimer.start();
|
||||
}
|
||||
else if (skipBackRepeat && skipBackIntervalIncrease && repeat) {
|
||||
skipBackInterval = skipBackInterval === 10 ? 30 : Math.min(skipBackInterval + 30, 300);
|
||||
skipBackIntervalIncrease = false;
|
||||
skipBackTimer.start();
|
||||
}
|
||||
else if (!repeat) {
|
||||
skipBackTimer.stop();
|
||||
skipBackRepeat = false;
|
||||
skipBackIntervalIncrease = false;
|
||||
skipBackInterval = minSkipInterval;
|
||||
}
|
||||
|
||||
player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipBackInterval, 0));
|
||||
// Force time update since player triggered update only occurs in real-time if skipping within loaded buffer
|
||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||
}
|
||||
|
||||
function skipForward(repeat: boolean = false) {
|
||||
if (!skipForwardRepeat && repeat) {
|
||||
skipForwardRepeat = true;
|
||||
skipForwardTimer.start();
|
||||
}
|
||||
else if (skipForwardRepeat && skipForwardIntervalIncrease && repeat) {
|
||||
skipForwardInterval = skipForwardInterval === 10 ? 30 : Math.min(skipForwardInterval + 30, 300);
|
||||
skipForwardIntervalIncrease = false;
|
||||
skipForwardTimer.start();
|
||||
}
|
||||
else if (!repeat) {
|
||||
skipForwardTimer.stop();
|
||||
skipForwardRepeat = false;
|
||||
skipForwardIntervalIncrease = false;
|
||||
skipForwardInterval = minSkipInterval;
|
||||
}
|
||||
|
||||
if (!isLivePosition) {
|
||||
player?.setCurrentTime(Math.min(player?.getCurrentTime() + skipForwardInterval, player?.getDuration()));
|
||||
// Force time update since player triggered update only occurs in real-time if skipping within loaded buffer
|
||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
function keyDownEventHandler(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.code) {
|
||||
case 'ArrowLeft':
|
||||
skipBack();
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'arrowleft':
|
||||
skipBack(event.repeat);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
skipForward();
|
||||
case 'arrowright':
|
||||
skipForward(event.repeat);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case "Home":
|
||||
case "home":
|
||||
player?.setCurrentTime(0);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case "End":
|
||||
case "end":
|
||||
if (isLive) {
|
||||
setLivePosition();
|
||||
}
|
||||
|
@ -943,9 +997,9 @@ function keyDownEventListener(event: KeyboardEvent) {
|
|||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'KeyK':
|
||||
case 'Space':
|
||||
case 'Enter':
|
||||
case 'k':
|
||||
case ' ':
|
||||
case 'enter':
|
||||
// Play/pause toggle
|
||||
if (player?.isPaused()) {
|
||||
player?.play();
|
||||
|
@ -955,17 +1009,17 @@ function keyDownEventListener(event: KeyboardEvent) {
|
|||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'KeyM':
|
||||
case 'm':
|
||||
// Mute toggle
|
||||
player?.setMute(!player?.isMuted());
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'arrowup':
|
||||
// Volume up
|
||||
volumeChangeHandler(Math.min(player?.getVolume() + volumeIncrement, 1));
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'arrowdown':
|
||||
// Volume down
|
||||
volumeChangeHandler(Math.max(player?.getVolume() - volumeIncrement, 0));
|
||||
handledCase = true;
|
||||
|
@ -975,27 +1029,33 @@ 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)));
|
||||
}
|
||||
}
|
||||
|
||||
function skipBack() {
|
||||
player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipInterval, 0));
|
||||
}
|
||||
function keyUpEventHandler(event: KeyboardEvent) {
|
||||
// logger.info("KeyUp", event);
|
||||
let result = targetKeyUpEventListener(event);
|
||||
let handledCase = result.handledCase;
|
||||
|
||||
function skipForward() {
|
||||
if (!isLivePosition) {
|
||||
player?.setCurrentTime(Math.min(player?.getCurrentTime() + skipInterval, player?.getDuration()));
|
||||
// @ts-ignore
|
||||
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
|
||||
|
||||
if (!handledCase) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
if (window.targetAPI.getSubscribedKeys().keyUp.has(key)) {
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, key, event.repeat, handledCase)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDownEventHandler);
|
||||
document.addEventListener('keyup', keyUpEventHandler);
|
||||
|
||||
export {
|
||||
PlayerControlEvent,
|
||||
|
@ -1004,24 +1064,20 @@ export {
|
|||
idleIcon,
|
||||
videoElement,
|
||||
videoCaptions,
|
||||
playerCtrlProgressBar,
|
||||
playerCtrlProgressBarBuffer,
|
||||
playerCtrlProgressBarProgress,
|
||||
playerCtrlProgressBarHandle,
|
||||
playerCtrlVolumeBar,
|
||||
playerCtrlVolumeBarProgress,
|
||||
playerCtrlVolumeBarHandle,
|
||||
playerCtrlLiveBadge,
|
||||
playerCtrlPosition,
|
||||
playerCtrlDuration,
|
||||
playerCtrlCaptions,
|
||||
player,
|
||||
uiHideTimer,
|
||||
isLive,
|
||||
playlistIndex,
|
||||
captionsBaseHeight,
|
||||
captionsLineHeight,
|
||||
onPlay,
|
||||
onPlayPlaylist,
|
||||
setPlaylistItem,
|
||||
playerCtrlStateUpdate,
|
||||
formatDuration,
|
||||
skipBack,
|
||||
skipForward,
|
||||
keyDownEventHandler,
|
||||
keyUpEventHandler,
|
||||
};
|
||||
|
|
|
@ -417,7 +417,6 @@ body {
|
|||
|
||||
.captions_off {
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_cc_off.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
|
@ -429,7 +428,6 @@ body {
|
|||
|
||||
.captions_on {
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_cc_on.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
|
|
|
@ -5,7 +5,9 @@ import * as connectionMonitor from 'common/ConnectionMonitor';
|
|||
import { toast, ToastIcon } from 'common/components/Toast';
|
||||
import {
|
||||
targetPlayerCtrlStateUpdate,
|
||||
targetPlayerCtrlPostStateUpdate,
|
||||
targetKeyDownEventListener,
|
||||
targetKeyUpEventListener,
|
||||
} from 'src/viewer/Renderer';
|
||||
|
||||
const logger = window.targetAPI.logger;
|
||||
|
@ -33,10 +35,7 @@ let isMediaItem = false;
|
|||
let playItemCached = false;
|
||||
let imageViewerPlaybackState: PlaybackState = PlaybackState.Idle;
|
||||
|
||||
let uiHideTimer = new Timer(() => {
|
||||
uiVisible = false;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}, 3000);
|
||||
let uiHideTimer = new Timer(() => { playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut); }, 3000);
|
||||
let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false);
|
||||
|
||||
let showDurationTimer = new Timer(() => {
|
||||
|
@ -277,12 +276,14 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
|
|||
break;
|
||||
|
||||
case PlayerControlEvent.UiFadeOut: {
|
||||
uiVisible = false;
|
||||
document.body.style.cursor = "none";
|
||||
playerControls.style.opacity = '0';
|
||||
break;
|
||||
}
|
||||
|
||||
case PlayerControlEvent.UiFadeIn: {
|
||||
uiVisible = true;
|
||||
document.body.style.cursor = "default";
|
||||
playerControls.style.opacity = '1';
|
||||
break;
|
||||
|
@ -291,6 +292,8 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
targetPlayerCtrlPostStateUpdate(event);
|
||||
}
|
||||
|
||||
// Receiver generated event handlers
|
||||
|
@ -312,57 +315,58 @@ function stopUiHideTimer() {
|
|||
uiHideTimer.stop();
|
||||
|
||||
if (!uiVisible) {
|
||||
uiVisible = true;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
document.onmouseout = () => {
|
||||
uiHideTimer.stop();
|
||||
uiVisible = false;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
|
||||
document.onmouseout = () => { uiHideTimer.end(); }
|
||||
document.onmousemove = () => {
|
||||
stopUiHideTimer();
|
||||
uiHideTimer.start();
|
||||
};
|
||||
|
||||
function keyDownEventListener(event: KeyboardEvent) {
|
||||
function keyDownEventHandler(event: KeyboardEvent) {
|
||||
// logger.info("KeyDown", event);
|
||||
let handledCase = targetKeyDownEventListener(event);
|
||||
let result = targetKeyDownEventListener(event);
|
||||
let handledCase = result.handledCase;
|
||||
|
||||
if (!handledCase) {
|
||||
switch (event.code) {
|
||||
case 'ArrowLeft':
|
||||
// @ts-ignore
|
||||
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
|
||||
|
||||
if (!handledCase && isMediaItem) {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'arrowleft':
|
||||
setPlaylistItem(playlistIndex - 1);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'arrowright':
|
||||
setPlaylistItem(playlistIndex + 1);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case "Home":
|
||||
case "home":
|
||||
setPlaylistItem(0);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case "End":
|
||||
case "end":
|
||||
setPlaylistItem(cachedPlaylist.items.length - 1);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'KeyK':
|
||||
case 'Space':
|
||||
case 'Enter':
|
||||
case 'k':
|
||||
case ' ':
|
||||
case 'enter':
|
||||
// Play/pause toggle
|
||||
if (cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
|
||||
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Play);
|
||||
} else {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Pause);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
|
@ -371,17 +375,33 @@ 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)));
|
||||
function keyUpEventHandler(event: KeyboardEvent) {
|
||||
// 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.toLowerCase()) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (window.targetAPI.getSubscribedKeys().keyUp.has(key)) {
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, key, event.repeat, handledCase)));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDownEventHandler);
|
||||
document.addEventListener('keyup', keyUpEventHandler);
|
||||
|
||||
export {
|
||||
PlayerControlEvent,
|
||||
|
@ -389,6 +409,16 @@ export {
|
|||
idleIcon,
|
||||
imageViewer,
|
||||
genericViewer,
|
||||
uiHideTimer,
|
||||
showDurationTimer,
|
||||
isMediaItem,
|
||||
playlistIndex,
|
||||
cachedPlayMediaItem,
|
||||
imageViewerPlaybackState,
|
||||
onPlay,
|
||||
onPlayPlaylist,
|
||||
playerCtrlStateUpdate,
|
||||
setPlaylistItem,
|
||||
keyDownEventHandler,
|
||||
keyUpEventHandler,
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -52,17 +52,17 @@ export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
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 +71,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 {
|
||||
|
|
|
@ -40,17 +40,25 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
|
|||
return handledCase;
|
||||
}
|
||||
|
||||
export function targetKeyDownEventListener(event: KeyboardEvent): boolean {
|
||||
export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) {
|
||||
// Currently unused in electron player
|
||||
switch (event) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 +67,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: '', };
|
||||
};
|
||||
|
|
|
@ -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 \
|
||||
|
|
1
receivers/webos/fcast-receiver-service/.npmrc
Normal file
1
receivers/webos/fcast-receiver-service/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
@futo:registry=https://gitlab.futo.org/api/v4/projects/305/packages/npm/
|
247
receivers/webos/fcast-receiver-service/package-lock.json
generated
247
receivers/webos/fcast-receiver-service/package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,57 @@ 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 serviceChannelEvents = [
|
||||
'toast',
|
||||
'connect',
|
||||
'disconnect',
|
||||
'play',
|
||||
'pause',
|
||||
'resume',
|
||||
'stop',
|
||||
'seek',
|
||||
'setvolume',
|
||||
'setspeed',
|
||||
'setplaylistitem',
|
||||
'event_subscribed_keys_update'
|
||||
];
|
||||
private static serviceChannelEventTimestamps: Map<string, number> = new Map();
|
||||
|
||||
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)}`);
|
||||
});
|
||||
|
||||
Main.windowVisible = true;
|
||||
Main.windowType = 'player';
|
||||
}
|
||||
}
|
||||
|
||||
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,102 +94,146 @@ 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();
|
||||
|
||||
Main.tcpListenerService = new TcpListenerService();
|
||||
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 } });
|
||||
service.register('service_channel', (message: any) => {
|
||||
if (message.isSubscription) {
|
||||
Main.serviceChannelEvents.forEach((event) => {
|
||||
Main.emitter.on(event, (value) => {
|
||||
const timestamp = Date.now();
|
||||
const lastTimestamp = Main.serviceChannelEventTimestamps.get(event) ? Main.serviceChannelEventTimestamps.get(event) : -1;
|
||||
|
||||
if (lastTimestamp < timestamp) {
|
||||
Main.serviceChannelEventTimestamps.set(event, timestamp);
|
||||
message.respond({ returnValue: true, subscriptionId: message.payload.subscriptionId, timestamp: timestamp, event: event, value: value });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
message.respond({ returnValue: true, subscriptionId: message.payload.subscriptionId, timestamp: Date.now(), event: 'register', value: { subscribed: true }});
|
||||
},
|
||||
(message: any) => {
|
||||
logger.info(`Canceled 'service_channel' service subscriber`);
|
||||
Main.serviceChannelEvents.forEach((event) => {
|
||||
Main.emitter.removeAllListeners(event);
|
||||
});
|
||||
|
||||
message.respond({ returnValue: true, value: {} });
|
||||
});
|
||||
|
||||
service.register('app_channel', (message: any) => {
|
||||
switch (message.payload.event) {
|
||||
case 'send_playback_error': {
|
||||
const value: PlaybackErrorMessage = message.payload.value;
|
||||
Main.listeners.forEach(l => l.send(Opcode.PlaybackError, value));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'send_playback_update': {
|
||||
const value: PlaybackUpdateMessage = message.payload.value;
|
||||
Main.listeners.forEach(l => l.send(Opcode.PlaybackUpdate, value));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'send_volume_update': {
|
||||
const value: VolumeUpdateMessage = message.payload.value;
|
||||
Main.cache.playerVolume = value.volume;
|
||||
Main.listeners.forEach(l => l.send(Opcode.VolumeUpdate, value));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'send_event': {
|
||||
const value: EventMessage = message.payload.value;
|
||||
Main.listeners.forEach(l => l.send(Opcode.Event, value));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'play_request': {
|
||||
const value: PlayMessage = message.payload.value.message;
|
||||
const playlistIndex: number = message.payload.value.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);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get_sessions': {
|
||||
// Having to mix and match session ids and ip addresses until querying websocket remote addresses is fixed
|
||||
message.respond({
|
||||
returnValue: true,
|
||||
value: [].concat(Main.tcpListenerService.getSenders(), Main.webSocketListenerService.getSessions())
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
case 'get_subscribed_keys': {
|
||||
const tcpListenerSubscribedKeys = Main.tcpListenerService.getAllSubscribedKeys();
|
||||
const webSocketListenerSubscribedKeys = Main.webSocketListenerService.getAllSubscribedKeys();
|
||||
// webOS compatibility: Need to convert set objects to array objects since data needs to be JSON compatible
|
||||
const subscribeData = {
|
||||
keyDown: Array.from(new Set([...tcpListenerSubscribedKeys.keyDown, ...webSocketListenerSubscribedKeys.keyDown])),
|
||||
keyUp: Array.from(new Set([...tcpListenerSubscribedKeys.keyUp, ...webSocketListenerSubscribedKeys.keyUp])),
|
||||
};
|
||||
|
||||
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: subscribeData
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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 });
|
||||
case 'network_changed': {
|
||||
logger.info('Network interfaces have changed', message);
|
||||
Main.discoveryService.stop();
|
||||
Main.discoveryService.start();
|
||||
|
||||
if (message.payload.value.fallback) {
|
||||
message.respond({
|
||||
returnValue: true,
|
||||
value: getAllIPv4Addresses()
|
||||
});
|
||||
|
||||
registerService(service, 'pause', (message: any) => { return voidCb.bind(this, message) });
|
||||
registerService(service, 'resume', (message: any) => { return voidCb.bind(this, message) });
|
||||
|
||||
service.register('stop', (message: any) => {
|
||||
playData = null;
|
||||
|
||||
if (message.isSubscription) {
|
||||
stopClosureCb = voidCb.bind(this, message);
|
||||
Main.emitter.on('stop', stopClosureCb);
|
||||
}
|
||||
else {
|
||||
message.respond({ returnValue: true, value: {} });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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 });
|
||||
case 'visibility_changed': {
|
||||
logger.info('Window visibility has changed', message.payload.value);
|
||||
Main.windowVisible = !message.payload.value.hidden;
|
||||
Main.windowType = message.payload.value.window;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
|
||||
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: 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("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('setspeed', (message: SetSpeedMessage) => Main.emitter.emit('setspeed', message));
|
||||
|
||||
l.emitter.on('connect', (message) => {
|
||||
ConnectionMonitor.onConnect(l, message, l instanceof WebSocketListenerService, () => {
|
||||
|
@ -151,48 +251,65 @@ 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) => {
|
||||
l.subscribeEvent(message.sessionId, message.body.event);
|
||||
|
||||
if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) {
|
||||
const tcpListenerSubscribedKeys = Main.tcpListenerService.getAllSubscribedKeys();
|
||||
const webSocketListenerSubscribedKeys = Main.webSocketListenerService.getAllSubscribedKeys();
|
||||
// webOS compatibility: Need to convert set objects to array objects since data needs to be JSON compatible
|
||||
const subscribeData = {
|
||||
keyDown: Array.from(new Set([...tcpListenerSubscribedKeys.keyDown, ...webSocketListenerSubscribedKeys.keyDown])),
|
||||
keyUp: Array.from(new Set([...tcpListenerSubscribedKeys.keyUp, ...webSocketListenerSubscribedKeys.keyUp])),
|
||||
};
|
||||
|
||||
console.log('emitting set info ON SUBSCRIBE ONLY', subscribeData)
|
||||
Main.emitter.emit('event_subscribed_keys_update', subscribeData);
|
||||
}
|
||||
});
|
||||
l.emitter.on('unsubscribeevent', (message) => {
|
||||
l.unsubscribeEvent(message.sessionId, message.body.event);
|
||||
|
||||
if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) {
|
||||
const tcpListenerSubscribedKeys = Main.tcpListenerService.getAllSubscribedKeys();
|
||||
const webSocketListenerSubscribedKeys = Main.webSocketListenerService.getAllSubscribedKeys();
|
||||
// webOS compatibility: Need to convert set objects to array objects since data needs to be JSON compatible
|
||||
const subscribeData = {
|
||||
keyDown: Array.from(new Set([...tcpListenerSubscribedKeys.keyDown, ...webSocketListenerSubscribedKeys.keyDown])),
|
||||
keyUp: Array.from(new Set([...tcpListenerSubscribedKeys.keyUp, ...webSocketListenerSubscribedKeys.keyUp])),
|
||||
};
|
||||
|
||||
Main.emitter.emit('event_subscribed_keys_update', subscribeData);
|
||||
}
|
||||
});
|
||||
l.start();
|
||||
});
|
||||
|
||||
service.register("send_playback_error", (message: any) => {
|
||||
listeners.forEach(l => {
|
||||
const value: PlaybackErrorMessage = message.payload.error;
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
message.respond({ returnValue: true, value: { success: true } });
|
||||
});
|
||||
}
|
||||
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) {
|
||||
|
@ -203,19 +320,21 @@ export async function errorHandler(error: Error) {
|
|||
Main.emitter.emit('toast', { message: error, icon: ToastIcon.ERROR });
|
||||
}
|
||||
|
||||
function registerService(service: Service, method: string, callback: (message: any) => any) {
|
||||
let callbackRef = null;
|
||||
service.register(method, (message: any) => {
|
||||
if (message.isSubscription) {
|
||||
callbackRef = callback(message);
|
||||
Main.emitter.on(method, callbackRef);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.respond({ returnValue: true, value: { subscribed: true }});
|
||||
},
|
||||
(message: any) => {
|
||||
logger.info(`Canceled ${method} service subscriber`);
|
||||
Main.emitter.removeAllListeners(method);
|
||||
message.respond({ returnValue: true, value: message.payload });
|
||||
});
|
||||
return ipv4Addresses;
|
||||
}
|
||||
|
|
1
receivers/webos/fcast-receiver/.npmrc
Normal file
1
receivers/webos/fcast-receiver/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
@futo:registry=https://gitlab.futo.org/api/v4/projects/305/packages/npm/
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "com.futo.fcast.receiver",
|
||||
"version": "1.1.0",
|
||||
"version": "2.0.0",
|
||||
"vendor": "FUTO",
|
||||
"type": "web",
|
||||
"main": "main_window/index.html",
|
||||
"main": "index.html",
|
||||
"title": "FCast Receiver",
|
||||
"appDescription": "FCast Receiver",
|
||||
"icon": "assets/icons/icon.png",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 228 KiB |
173
receivers/webos/fcast-receiver/lib/common.ts
Normal file
173
receivers/webos/fcast-receiver/lib/common.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { v4 as uuidv4 } from 'modules/uuid';
|
||||
import { Logger, LoggerType } from 'common/Logger';
|
||||
require('lib/webOSTVjs-1.2.10/webOSTV.js');
|
||||
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
|
||||
|
||||
const logger = new Logger('Common', LoggerType.FRONTEND);
|
||||
const serviceId = 'com.futo.fcast.receiver.service';
|
||||
|
||||
export enum RemoteKeyCode {
|
||||
Stop = 413,
|
||||
Rewind = 412,
|
||||
Play = 415,
|
||||
Pause = 19,
|
||||
FastForward = 417,
|
||||
Back = 461,
|
||||
}
|
||||
|
||||
export enum KeyCode {
|
||||
ArrowUp = 38,
|
||||
ArrowDown = 40,
|
||||
ArrowLeft = 37,
|
||||
ArrowRight = 39,
|
||||
KeyK = 75,
|
||||
Space = 32,
|
||||
Enter = 13,
|
||||
}
|
||||
|
||||
export enum ControlBarMode {
|
||||
KeyboardMouse,
|
||||
Remote
|
||||
}
|
||||
|
||||
export class ServiceManager {
|
||||
private static serviceChannelSuccessCbHandler?: (message: any) => void;
|
||||
private static serviceChannelFailureCbHandler?: (message: any) => void;
|
||||
private static serviceChannelCompleteCbHandler?: (message: any) => void;
|
||||
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'service_channel',
|
||||
parameters: { subscriptionId: uuidv4() },
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value?.subscribed === true) {
|
||||
logger.info(`requestService: Registered 'service_channel' handler with service`);
|
||||
}
|
||||
else if (ServiceManager.serviceChannelSuccessCbHandler) {
|
||||
ServiceManager.serviceChannelSuccessCbHandler(message);
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
logger.error('Error subscribing to the service_channel:', message);
|
||||
|
||||
if (ServiceManager.serviceChannelFailureCbHandler) {
|
||||
ServiceManager.serviceChannelFailureCbHandler(message);
|
||||
}
|
||||
},
|
||||
onComplete: (message: any) => {
|
||||
if (ServiceManager.serviceChannelCompleteCbHandler) {
|
||||
ServiceManager.serviceChannelCompleteCbHandler(message);
|
||||
}
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public subscribeToServiceChannel(successCb: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
|
||||
ServiceManager.serviceChannelSuccessCbHandler = successCb;
|
||||
ServiceManager.serviceChannelFailureCbHandler = failureCb;
|
||||
ServiceManager.serviceChannelCompleteCbHandler = onCompleteCb;
|
||||
}
|
||||
|
||||
public call(method: string, parameters?: any, successCb?: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
|
||||
// @ts-ignore
|
||||
const service = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'app_channel',
|
||||
parameters: { event: method, value: 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
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
// CSS media queries do not work on older webOS versions...
|
||||
export function initializeWindowSizeStylesheet() {
|
||||
if (window.innerWidth !== 0 && window.innerHeight !== 0) {
|
||||
if (window.innerWidth >= 1920 && window.innerHeight >= 1080) {
|
||||
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
|
||||
}
|
||||
else {
|
||||
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
|
||||
}
|
||||
}
|
||||
else {
|
||||
const resolution = sessionStorage.getItem('resolution');
|
||||
|
||||
if (resolution) {
|
||||
window.onload = () => {
|
||||
if (resolution == '1920x1080') {
|
||||
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
|
||||
}
|
||||
else {
|
||||
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
window.onresize = () => {
|
||||
if (window.innerWidth >= 1920 && window.innerHeight >= 1080) {
|
||||
sessionStorage.setItem('resolution', '1920x1080');
|
||||
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
|
||||
}
|
||||
else {
|
||||
sessionStorage.setItem('resolution', '1280x720');
|
||||
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
};
|
211
receivers/webos/fcast-receiver/package-lock.json
generated
211
receivers/webos/fcast-receiver/package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
@ -29,15 +29,18 @@
|
|||
"webpack": "^5.99.6",
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"@comment dependencies.uuid": "Versions > 9.0.1 are broken on webOS",
|
||||
"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"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"utf-8-validate": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
|
59
receivers/webos/fcast-receiver/src/Main.ts
Normal file
59
receivers/webos/fcast-receiver/src/Main.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Logger, LoggerType } from 'common/Logger';
|
||||
import { ServiceManager } from 'lib/common';
|
||||
require('lib/webOSTVjs-1.2.10/webOSTV.js');
|
||||
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
webOSApp: any;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger('Main', LoggerType.FRONTEND);
|
||||
const webPage: HTMLIFrameElement = document.getElementById('page') as HTMLIFrameElement;
|
||||
let launchHandlerCallback = () => { logger.warn('No (re)launch handler set'); };
|
||||
let keyDownEventHandler = () => { logger.warn('No keyDown event handler set'); };
|
||||
let keyUpEventHandler = () => { logger.warn('No keyUp event handler set'); };
|
||||
|
||||
function loadPage(path: string) {
|
||||
// @ts-ignore
|
||||
webPage.src = path;
|
||||
}
|
||||
|
||||
// We are embedding iframe element and using that for page navigation. This preserves a global JS context
|
||||
// so bugs related to oversubscribing/canceling services are worked around by only subscribing once to
|
||||
// required services
|
||||
logger.info('Starting webOS application')
|
||||
window.webOS.deviceInfo((info) => { logger.info('Device info:', info); });
|
||||
|
||||
window.webOSApp = {
|
||||
serviceManager: new ServiceManager(),
|
||||
setLaunchHandler: (callback: () => void) => {
|
||||
document.removeEventListener('webOSLaunch', launchHandlerCallback);
|
||||
document.removeEventListener('webOSRelaunch', launchHandlerCallback);
|
||||
|
||||
launchHandlerCallback = callback;
|
||||
document.addEventListener('webOSLaunch', launchHandlerCallback);
|
||||
document.addEventListener('webOSRelaunch', launchHandlerCallback);
|
||||
},
|
||||
setKeyDownHandler: (callback: () => void) => {
|
||||
document.removeEventListener('keydown', keyDownEventHandler);
|
||||
|
||||
keyDownEventHandler = callback;
|
||||
document.addEventListener('keydown', keyDownEventHandler);
|
||||
},
|
||||
setKeyUpHandler: (callback: () => void) => {
|
||||
document.removeEventListener('keyup', keyUpEventHandler);
|
||||
|
||||
keyUpEventHandler = callback;
|
||||
document.addEventListener('keyup', keyUpEventHandler);
|
||||
},
|
||||
loadPage: loadPage,
|
||||
pendingPlay: null,
|
||||
};
|
||||
|
||||
document.addEventListener('webOSLaunch', launchHandlerCallback);
|
||||
document.addEventListener('webOSRelaunch', launchHandlerCallback);
|
||||
document.addEventListener('keydown', keyDownEventHandler);
|
||||
document.addEventListener('keyup', keyUpEventHandler);
|
||||
loadPage('./main_window/index.html');
|
17
receivers/webos/fcast-receiver/src/index.html
Normal file
17
receivers/webos/fcast-receiver/src/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
iframe, body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="page", src="./main_window/index.html"></iframe>
|
||||
<script src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
107
receivers/webos/fcast-receiver/src/main/1280x720.css
Normal file
107
receivers/webos/fcast-receiver/src/main/1280x720.css
Normal file
|
@ -0,0 +1,107 @@
|
|||
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
|
||||
.card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
line-height: 20px;
|
||||
margin: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-title-separator {
|
||||
margin: 3px 0px;
|
||||
}
|
||||
|
||||
.iconSize {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
gap: unset;
|
||||
/* gap: 12.5vw; */
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#main-view {
|
||||
margin-right: 12.5vw;
|
||||
}
|
||||
|
||||
#title-text {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
#title-icon {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#connection-status {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#connection-error-icon {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#connection-information-loading-text {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#scan-to-connect {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#qr-code {
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
margin: 15px auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#ips {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ip-entry-text {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#window-can-be-closed {
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
#connection-check {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 8px;
|
||||
top: -140px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
/* } */
|
112
receivers/webos/fcast-receiver/src/main/1920x1080.css
Normal file
112
receivers/webos/fcast-receiver/src/main/1920x1080.css
Normal file
|
@ -0,0 +1,112 @@
|
|||
/* @media only screen and ((max-width: 2559px) or (max-height: 1439px)) { */
|
||||
.card {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
line-height: 24px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.card-title-separator {
|
||||
margin: 3px 0px;
|
||||
}
|
||||
|
||||
.iconSize {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
gap: unset;
|
||||
/* gap: 15vw; */
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
#main-view {
|
||||
margin-right: 15vw;
|
||||
}
|
||||
|
||||
#title-text {
|
||||
font-size: 140px;
|
||||
}
|
||||
|
||||
#title-icon {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
#connection-status {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
#connection-error-icon {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#connection-information-loading-text {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#scan-to-connect {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#qr-code {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
margin: 20px auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#connection-details-separator {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#ips {
|
||||
margin-top: 20px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.ip-entry-text {
|
||||
margin-top: 7px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
#window-can-be-closed {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
#connection-check {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
margin: 28px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
top: -175px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 5px 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 28px;
|
||||
}
|
||||
/* } */
|
|
@ -1,153 +1,143 @@
|
|||
/* 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 { ServiceManager, initializeWindowSizeStylesheet } 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,
|
||||
declare global {
|
||||
interface Window {
|
||||
targetAPI: any;
|
||||
webOSApp: any;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let getSessions = null;
|
||||
const logger = window.targetAPI.logger;
|
||||
|
||||
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: {},
|
||||
try {
|
||||
initializeWindowSizeStylesheet();
|
||||
|
||||
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
|
||||
serviceManager.subscribeToServiceChannel((message: any) => {
|
||||
switch (message.event) {
|
||||
case 'toast':
|
||||
preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration);
|
||||
break;
|
||||
|
||||
case 'event_subscribed_keys_update':
|
||||
preloadData.onEventSubscribedKeysUpdate(message.value);
|
||||
break;
|
||||
|
||||
case 'connect':
|
||||
preloadData.onConnectCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
preloadData.onDisconnectCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'play':
|
||||
logger.info(`Main: Playing ${JSON.stringify(message)}`);
|
||||
play(message.value);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
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 = [];
|
||||
const connections: any[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
const ipsIfaceName = document.getElementById('ips-iface-name');
|
||||
ipsIfaceName.style.display = 'none';
|
||||
|
||||
serviceManager.call('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);
|
||||
});
|
||||
}
|
||||
else {
|
||||
serviceManager.call('network_changed', { fallback: fallback });
|
||||
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
|
||||
});
|
||||
|
||||
window.targetAPI.getSessions(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSessions = requestService('get_sessions', (message: any) => resolve(message.value), (message: any) => reject(message), false);
|
||||
serviceManager.call('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();
|
||||
|
||||
// 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.targetAPI.initializeSubscribedKeys(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
serviceManager.call('get_subscribed_keys', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
|
||||
});
|
||||
});
|
||||
|
||||
preloadData.sendEventCb = (event: EventMessage) => {
|
||||
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
|
||||
};
|
||||
|
||||
const launchHandler = () => {
|
||||
// Launch handler not supported in simulator due to JSON parsing errors of launch parameters
|
||||
const params = window.webOSDev.launchParams();
|
||||
logger.info(`Main: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
|
||||
|
||||
const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
|
||||
if (params.playData !== 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');
|
||||
// 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(sessionStorage.getItem('lastTimestamp'));
|
||||
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
|
||||
sessionStorage.setItem('lastTimestamp', params.timestamp);
|
||||
play(params.messageInfo);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('webOSLaunch', launchHandler);
|
||||
document.addEventListener('webOSRelaunch', launchHandler);
|
||||
window.parent.webOSApp.setLaunchHandler(launchHandler);
|
||||
document.addEventListener('visibilitychange', () => { serviceManager.call('visibility_changed', { hidden: document.hidden, window: 'main' }); });
|
||||
|
||||
// Cannot go back to a state where user was previously casting a video, so exit.
|
||||
// window.onpopstate = () => {
|
||||
// window.webOS.platformBack();
|
||||
// };
|
||||
const play = (messageInfo: any) => {
|
||||
sessionStorage.setItem('playInfo', JSON.stringify(messageInfo));
|
||||
getDeviceInfoService?.cancel();
|
||||
|
||||
document.addEventListener('keydown', (event: any) => {
|
||||
// logger.info("KeyDown", event);
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
window.parent.webOSApp.loadPage(`${messageInfo.contentViewer}/index.html`);
|
||||
};
|
||||
}
|
||||
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
|
||||
});
|
||||
logger.error(`Main: preload`, err);
|
||||
preloadData.onToastCb(`Error starting the application: ${JSON.stringify(err)}`, ToastIcon.ERROR);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'common/main/Renderer';
|
||||
import { keyDownEventHandler, keyUpEventHandler } from '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');
|
||||
|
@ -27,6 +29,51 @@ backgroundVideo.onplaying = () => {
|
|||
backgroundVideo.onplaying = null;
|
||||
};
|
||||
|
||||
window.parent.webOSApp.setKeyDownHandler(keyDownEventHandler);
|
||||
window.parent.webOSApp.setKeyUpHandler(keyUpEventHandler);
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
/* WebOS custom player styles */
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: InterBold;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
font-family: InterRegular;
|
||||
/* gap not supported in WebOS 6.0 */
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
#main-view {
|
||||
/* gap not supported in WebOS 6.0 */
|
||||
gap: unset;
|
||||
margin-right: 15vw;
|
||||
}
|
||||
|
||||
#title-text {
|
||||
|
@ -27,6 +25,15 @@
|
|||
font-family: InterBold;
|
||||
}
|
||||
|
||||
/* gap not supported in WebOS 6.0 */
|
||||
#ips {
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
#ips-iface-icon {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#window-can-be-closed {
|
||||
font-family: InterRegular;
|
||||
}
|
||||
|
|
30
receivers/webos/fcast-receiver/src/player/1280x720.css
Normal file
30
receivers/webos/fcast-receiver/src/player/1280x720.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
|
||||
#title-icon {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 5px 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
/* } */
|
30
receivers/webos/fcast-receiver/src/player/1920x1080.css
Normal file
30
receivers/webos/fcast-receiver/src/player/1920x1080.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* @media only screen and ((max-width: 2559px) or (max-height: 1439px)) { */
|
||||
#title-icon {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 5px 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
/* } */
|
|
@ -1,171 +1,153 @@
|
|||
/* 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 { ServiceManager, initializeWindowSizeStylesheet } 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');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
targetAPI: any;
|
||||
webOSApp: any;
|
||||
}
|
||||
}
|
||||
|
||||
const logger = window.targetAPI.logger;
|
||||
|
||||
try {
|
||||
const serviceId = 'com.futo.fcast.receiver.service';
|
||||
let getSessions = null;
|
||||
initializeWindowSizeStylesheet();
|
||||
|
||||
window.webOSAPI = {
|
||||
pendingPlay: JSON.parse(sessionStorage.getItem('playData'))
|
||||
};
|
||||
window.parent.webOSApp.pendingPlay = JSON.parse(sessionStorage.getItem('playInfo'));
|
||||
const contentViewer = window.parent.webOSApp.pendingPlay?.contentViewer;
|
||||
|
||||
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'send_playback_error',
|
||||
parameters: { error },
|
||||
onSuccess: () => {},
|
||||
onFailure: (message: any) => {
|
||||
logger.error(`Player: send_playback_error ${JSON.stringify(message)}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => {
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'send_playback_update',
|
||||
parameters: { update },
|
||||
// onSuccess: (message: any) => {
|
||||
// logger.info(`Player: send_playback_update ${JSON.stringify(message)}`);
|
||||
// },
|
||||
onSuccess: () => {},
|
||||
onFailure: (message: any) => {
|
||||
logger.error(`Player: send_playback_update ${JSON.stringify(message)}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => {
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'send_volume_update',
|
||||
parameters: { update },
|
||||
onSuccess: () => {},
|
||||
onFailure: (message: any) => {
|
||||
logger.error(`Player: send_volume_update ${JSON.stringify(message)}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
|
||||
serviceManager.subscribeToServiceChannel((message: any) => {
|
||||
switch (message.event) {
|
||||
case 'toast':
|
||||
preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration);
|
||||
break;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
if (message.value.playData !== null) {
|
||||
if (preloadData.onPlayCb === undefined) {
|
||||
window.webOSAPI.pendingPlay = message.value.playData;
|
||||
case 'play': {
|
||||
if (contentViewer !== message.value.contentViewer) {
|
||||
sessionStorage.setItem('playInfo', JSON.stringify(message.value));
|
||||
window.parent.webOSApp.loadPage(`${message.value.contentViewer}/index.html`);
|
||||
}
|
||||
else {
|
||||
preloadData.onPlayCb(null, message.value.playData);
|
||||
if (message.value.rendererEvent === 'play-playlist') {
|
||||
if (preloadData.onPlayCb === undefined) {
|
||||
window.parent.webOSApp.pendingPlay = message.value;
|
||||
}
|
||||
else {
|
||||
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.parent.webOSApp.pendingPlay = message.value;
|
||||
}
|
||||
else {
|
||||
preloadData.onPlayCb(null, message.value.rendererMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pause':
|
||||
preloadData.onPauseCb();
|
||||
break;
|
||||
|
||||
case 'resume':
|
||||
preloadData.onResumeCb();
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
window.parent.webOSApp.loadPage('main_window/index.html');
|
||||
break;
|
||||
|
||||
case 'seek':
|
||||
preloadData.onSeekCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'setvolume':
|
||||
preloadData.onSetVolumeCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'setspeed':
|
||||
preloadData.onSetSpeedCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'setplaylistitem':
|
||||
preloadData.onSetPlaylistItemCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'event_subscribed_keys_update':
|
||||
preloadData.onEventSubscribedKeysUpdate(message.value);
|
||||
break;
|
||||
|
||||
case 'connect':
|
||||
preloadData.onConnectCb(null, message.value);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
preloadData.onDisconnectCb(null, message.value);
|
||||
break;
|
||||
|
||||
// 'play-playlist' is handled in the 'play' message for webOS
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
getSessions?.cancel();
|
||||
onConnectService.cancel();
|
||||
onDisconnectService.cancel();
|
||||
|
||||
// WebOS 22 and earlier does not work well using the history API,
|
||||
// so manually handling page navigation...
|
||||
// history.back();
|
||||
window.open('../main_window/index.html', '_self');
|
||||
});
|
||||
|
||||
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); });
|
||||
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
|
||||
serviceManager.call('send_playback_error', error, null, (message: any) => { logger.error(`Player: send_playback_error ${JSON.stringify(message)}`); });
|
||||
};
|
||||
preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => {
|
||||
serviceManager.call('send_playback_update', update, null, (message: any) => { logger.error(`Player: send_playback_update ${JSON.stringify(message)}`); });
|
||||
};
|
||||
preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => {
|
||||
serviceManager.call('send_volume_update', update, null, (message: any) => { logger.error(`Player: send_volume_update ${JSON.stringify(message)}`); });
|
||||
};
|
||||
preloadData.sendEventCb = (event: EventMessage) => {
|
||||
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
|
||||
};
|
||||
|
||||
preloadData.sendPlayRequestCb = (message: PlayMessage, playlistIndex: number) => {
|
||||
serviceManager.call('play_request', { message: message, playlistIndex: playlistIndex }, null, (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);
|
||||
serviceManager.call('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
|
||||
});
|
||||
});
|
||||
window.targetAPI.initializeSubscribedKeys(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
serviceManager.call('get_subscribed_keys', {}, (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 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)}`);
|
||||
|
||||
const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
|
||||
if (params.playData !== undefined && params.timestamp != lastTimestamp) {
|
||||
localStorage.setItem('lastTimestamp', params.timestamp);
|
||||
sessionStorage.setItem('playData', JSON.stringify(params.playData));
|
||||
playService?.cancel();
|
||||
pauseService?.cancel();
|
||||
resumeService?.cancel();
|
||||
stopService?.cancel();
|
||||
seekService?.cancel();
|
||||
setVolumeService?.cancel();
|
||||
setSpeedService?.cancel();
|
||||
getSessions?.cancel();
|
||||
onConnectService?.cancel();
|
||||
onDisconnectService?.cancel();
|
||||
// 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(sessionStorage.getItem('lastTimestamp'));
|
||||
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
|
||||
sessionStorage.setItem('lastTimestamp', params.timestamp);
|
||||
sessionStorage.setItem('playInfo', JSON.stringify(params.messageInfo));
|
||||
|
||||
// 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.parent.webOSApp.loadPage(`${params.messageInfo.contentViewer}/index.html`);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('webOSLaunch', launchHandler);
|
||||
document.addEventListener('webOSRelaunch', launchHandler);
|
||||
|
||||
window.parent.webOSApp.setLaunchHandler(launchHandler);
|
||||
document.addEventListener('visibilitychange', () => serviceManager.call('visibility_changed', { hidden: document.hidden, window: contentViewer }));
|
||||
}
|
||||
catch (err) {
|
||||
logger.error(`Player: preload ${JSON.stringify(err)}`);
|
||||
logger.error(`Player: preload`, 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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,82 +1,125 @@
|
|||
import {
|
||||
isLive,
|
||||
onPlay,
|
||||
onPlayPlaylist,
|
||||
setPlaylistItem,
|
||||
playerCtrlStateUpdate,
|
||||
playlistIndex,
|
||||
player,
|
||||
uiHideTimer,
|
||||
PlayerControlEvent,
|
||||
playerCtrlCaptions,
|
||||
playerCtrlDuration,
|
||||
playerCtrlLiveBadge,
|
||||
playerCtrlPosition,
|
||||
playerCtrlProgressBar,
|
||||
playerCtrlProgressBarBuffer,
|
||||
playerCtrlProgressBarHandle,
|
||||
playerCtrlProgressBarProgress,
|
||||
playerCtrlStateUpdate,
|
||||
playerCtrlVolumeBar,
|
||||
playerCtrlVolumeBarHandle,
|
||||
playerCtrlVolumeBarProgress,
|
||||
videoCaptions,
|
||||
formatDuration,
|
||||
skipBack,
|
||||
skipForward,
|
||||
keyDownEventHandler,
|
||||
keyUpEventHandler,
|
||||
playerCtrlProgressBarHandle,
|
||||
} from 'common/player/Renderer';
|
||||
import { KeyCode, RemoteKeyCode, ControlBarMode } from 'lib/common';
|
||||
import * as common from 'lib/common';
|
||||
|
||||
const logger = window.targetAPI.logger;
|
||||
const captionsBaseHeightCollapsed = 150;
|
||||
const captionsBaseHeightExpanded = 320;
|
||||
const captionsLineHeight = 68;
|
||||
|
||||
enum RemoteKeyCode {
|
||||
Stop = 413,
|
||||
Rewind = 412,
|
||||
Play = 415,
|
||||
Pause = 19,
|
||||
FastForward = 417,
|
||||
Back = 461,
|
||||
const playPreviousContainer = document.getElementById('playPreviousContainer');
|
||||
const actionContainer = document.getElementById('actionContainer');
|
||||
const playNextContainer = document.getElementById('playNextContainer');
|
||||
|
||||
const playPrevious = document.getElementById('playPrevious');
|
||||
const playNext = document.getElementById('playNext');
|
||||
|
||||
enum ControlFocus {
|
||||
ProgressBar,
|
||||
Action,
|
||||
PlayPrevious,
|
||||
PlayNext,
|
||||
}
|
||||
|
||||
let controlMode = ControlBarMode.KeyboardMouse;
|
||||
let controlFocus = ControlFocus.ProgressBar;
|
||||
|
||||
// Hide
|
||||
// [<<][>][>>]
|
||||
// [|<][>][>|]
|
||||
// Hide
|
||||
let locationMap = {
|
||||
ProgressBar: playerCtrlProgressBarHandle,
|
||||
Action: actionContainer,
|
||||
PlayPrevious: playPreviousContainer,
|
||||
PlayNext: playNextContainer,
|
||||
};
|
||||
|
||||
window.parent.webOSApp.setKeyDownHandler(keyDownEventHandler);
|
||||
window.parent.webOSApp.setKeyUpHandler(keyUpEventHandler);
|
||||
|
||||
uiHideTimer.setDelay(5000);
|
||||
uiHideTimer.setCallback(() => {
|
||||
if (!player?.isPaused()) {
|
||||
controlMode = ControlBarMode.KeyboardMouse;
|
||||
removeFocus(controlFocus);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
});
|
||||
|
||||
// Leave control bar on screen if magic remote cursor leaves window
|
||||
document.onmouseout = () => {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
uiHideTimer.end();
|
||||
}
|
||||
}
|
||||
|
||||
function addFocus(location: ControlFocus) {
|
||||
if (location === ControlFocus.ProgressBar) {
|
||||
locationMap[ControlFocus[location]].classList.remove('progressBarHandleHide');
|
||||
}
|
||||
else {
|
||||
locationMap[ControlFocus[location]].classList.add('buttonFocus');
|
||||
}
|
||||
}
|
||||
|
||||
function removeFocus(location: ControlFocus) {
|
||||
if (location === ControlFocus.ProgressBar) {
|
||||
locationMap[ControlFocus[location]].classList.add('progressBarHandleHide');
|
||||
}
|
||||
else {
|
||||
locationMap[ControlFocus[location]].classList.remove('buttonFocus');
|
||||
}
|
||||
}
|
||||
|
||||
function remoteNavigateTo(location: ControlFocus) {
|
||||
// Issues with using standard focus, so manually managing styles
|
||||
removeFocus(controlFocus);
|
||||
controlFocus = location;
|
||||
addFocus(controlFocus);
|
||||
}
|
||||
|
||||
function setControlMode(mode: ControlBarMode, immediateHide: boolean = true) {
|
||||
if (mode === ControlBarMode.KeyboardMouse) {
|
||||
uiHideTimer.enable();
|
||||
|
||||
if (immediateHide) {
|
||||
removeFocus(controlFocus);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
else {
|
||||
uiHideTimer.start();
|
||||
}
|
||||
}
|
||||
else {
|
||||
remoteNavigateTo(ControlFocus.ProgressBar);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
|
||||
uiHideTimer.start();
|
||||
}
|
||||
|
||||
controlMode = mode;
|
||||
}
|
||||
|
||||
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
|
||||
let handledCase = false;
|
||||
|
||||
switch (event) {
|
||||
case PlayerControlEvent.Load: {
|
||||
playerCtrlProgressBarBuffer.setAttribute("style", "width: 0px");
|
||||
playerCtrlProgressBarProgress.setAttribute("style", "width: 0px");
|
||||
playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetLeft}px`);
|
||||
|
||||
const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth);
|
||||
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
|
||||
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`);
|
||||
|
||||
if (isLive) {
|
||||
playerCtrlLiveBadge.setAttribute("style", "display: block");
|
||||
playerCtrlPosition.setAttribute("style", "display: none");
|
||||
playerCtrlDuration.setAttribute("style", "display: none");
|
||||
}
|
||||
else {
|
||||
playerCtrlLiveBadge.setAttribute("style", "display: none");
|
||||
playerCtrlPosition.setAttribute("style", "display: block");
|
||||
playerCtrlDuration.setAttribute("style", "display: block");
|
||||
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
|
||||
playerCtrlDuration.innerHTML = formatDuration(player.getDuration());
|
||||
}
|
||||
|
||||
if (player.isCaptionsSupported()) {
|
||||
// Disabling receiver captions control on TV players
|
||||
playerCtrlCaptions.setAttribute("style", "display: none");
|
||||
// playerCtrlCaptions.setAttribute("style", "display: block");
|
||||
videoCaptions.setAttribute("style", "display: block");
|
||||
}
|
||||
else {
|
||||
playerCtrlCaptions.setAttribute("style", "display: none");
|
||||
videoCaptions.setAttribute("style", "display: none");
|
||||
player.enableCaptions(false);
|
||||
}
|
||||
playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
|
||||
|
||||
handledCase = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -84,22 +127,168 @@ 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 targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) {
|
||||
switch (event) {
|
||||
case PlayerControlEvent.Load: {
|
||||
player.setPlayPauseCallback(() => {
|
||||
uiHideTimer.enable();
|
||||
uiHideTimer.start();
|
||||
}, () => {
|
||||
uiHideTimer.disable();
|
||||
});
|
||||
|
||||
if (player.isCaptionsSupported()) {
|
||||
// Disabling receiver captions control on TV players
|
||||
// playerCtrlCaptions.style.display = 'block';
|
||||
playerCtrlCaptions.style.display = 'none';
|
||||
videoCaptions.style.display = 'block';
|
||||
}
|
||||
else {
|
||||
playerCtrlCaptions.style.display = 'none';
|
||||
videoCaptions.style.display = 'none';
|
||||
player.enableCaptions(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
|
||||
// logger.info("KeyDown", event.keyCode);
|
||||
let handledCase = false;
|
||||
let key = '';
|
||||
|
||||
switch (event.keyCode) {
|
||||
case RemoteKeyCode.Stop:
|
||||
// history.back();
|
||||
window.open('../main_window/index.html', '_self');
|
||||
case KeyCode.KeyK:
|
||||
case KeyCode.Space:
|
||||
// Play/pause toggle
|
||||
if (player?.isPaused()) {
|
||||
player?.play();
|
||||
} else {
|
||||
player?.pause();
|
||||
}
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
|
||||
case RemoteKeyCode.Rewind:
|
||||
skipBack();
|
||||
case KeyCode.Enter:
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.ProgressBar || controlFocus === ControlFocus.Action) {
|
||||
// Play/pause toggle
|
||||
if (player?.isPaused()) {
|
||||
player?.play();
|
||||
} else {
|
||||
player?.pause();
|
||||
}
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayPrevious) {
|
||||
setPlaylistItem(playlistIndex - 1);
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayNext) {
|
||||
setPlaylistItem(playlistIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case KeyCode.ArrowUp:
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.ProgressBar) {
|
||||
setControlMode(ControlBarMode.KeyboardMouse);
|
||||
}
|
||||
else {
|
||||
remoteNavigateTo(ControlFocus.ProgressBar);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case KeyCode.ArrowDown:
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.ProgressBar) {
|
||||
remoteNavigateTo(ControlFocus.Action);
|
||||
}
|
||||
else {
|
||||
setControlMode(ControlBarMode.KeyboardMouse);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case KeyCode.ArrowLeft:
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.ProgressBar || playPrevious?.style.display === 'none') {
|
||||
// Note that skip repeat does not trigger in simulator
|
||||
skipBack(event.repeat);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.Action) {
|
||||
remoteNavigateTo(ControlFocus.PlayPrevious);
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayNext) {
|
||||
remoteNavigateTo(ControlFocus.Action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case KeyCode.ArrowRight:
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.ProgressBar || playNext?.style.display === 'none') {
|
||||
// Note that skip repeat does not trigger in simulator
|
||||
skipForward(event.repeat);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.Action) {
|
||||
remoteNavigateTo(ControlFocus.PlayNext);
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayPrevious) {
|
||||
remoteNavigateTo(ControlFocus.Action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
|
||||
case RemoteKeyCode.Stop:
|
||||
window.parent.webOSApp.loadPage('main_window/index.html');
|
||||
handledCase = true;
|
||||
key = 'Stop';
|
||||
break;
|
||||
|
||||
// Note that in simulator rewind and fast forward key codes are sent twice...
|
||||
case RemoteKeyCode.Rewind:
|
||||
skipBack(event.repeat);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Rewind';
|
||||
break;
|
||||
|
||||
case RemoteKeyCode.Play:
|
||||
if (player.isPaused()) {
|
||||
|
@ -107,6 +296,7 @@ export function targetKeyDownEventListener(event: any): boolean {
|
|||
}
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Play';
|
||||
break;
|
||||
case RemoteKeyCode.Pause:
|
||||
if (!player.isPaused()) {
|
||||
|
@ -114,32 +304,42 @@ export function targetKeyDownEventListener(event: any): boolean {
|
|||
}
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Pause';
|
||||
break;
|
||||
|
||||
// Note that in simulator rewind and fast forward key codes are sent twice...
|
||||
case RemoteKeyCode.FastForward:
|
||||
skipForward();
|
||||
skipForward(event.repeat);
|
||||
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');
|
||||
window.parent.webOSApp.loadPage('main_window/index.html');
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Back';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return handledCase;
|
||||
return { handledCase: handledCase, key: key };
|
||||
};
|
||||
|
||||
if (window.webOSAPI.pendingPlay !== null) {
|
||||
onPlay(null, window.webOSAPI.pendingPlay);
|
||||
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
|
||||
return common.targetKeyUpEventListener(event);
|
||||
};
|
||||
|
||||
if (window.parent.webOSApp.pendingPlay !== null) {
|
||||
if (window.parent.webOSApp.pendingPlay.rendererEvent === 'play-playlist') {
|
||||
onPlayPlaylist(null, window.parent.webOSApp.pendingPlay.rendererMessage);
|
||||
}
|
||||
else {
|
||||
onPlay(null, window.parent.webOSApp.pendingPlay.rendererMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -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">
|
||||
|
@ -20,7 +25,7 @@
|
|||
<div id="progressBarProgress" class="progressBarProgress" ></div>
|
||||
<div id="progressBarPosition" class="progressBarPosition" ></div>
|
||||
<!-- <div class="progressBarChapterContainer"></div> -->
|
||||
<div id="progressBarHandle" class="progressBarHandle" ></div>
|
||||
<div id="progressBarHandle" class="progressBarHandle progressBarHandleHide" ></div>
|
||||
<div id="progressBarInteractiveArea" class="progressBarInteractiveArea" ></div>
|
||||
</div>
|
||||
|
||||
|
@ -32,7 +37,9 @@
|
|||
</div>
|
||||
|
||||
<div class="leftButtonContainer">
|
||||
<div id="action" class="play iconSize"></div>
|
||||
<div id="playPreviousContainer" class="buttonFocusContainer"><div id="playPrevious" class="playPrevious iconSize"></div></div>
|
||||
<div id="actionContainer" class="buttonFocusContainer"><div id="action" class="play iconSize"></div></div>
|
||||
<div id="playNextContainer" class="buttonFocusContainer"><div id="playNext" class="playNext iconSize"></div></div>
|
||||
|
||||
<div id="volume" class="volume_high iconSize"></div>
|
||||
<div class="volumeContainer">
|
||||
|
@ -52,7 +59,7 @@
|
|||
<div class="buttonContainer">
|
||||
<!-- <div id="fullscreen" class="fullscreen_on iconSize"></div> -->
|
||||
<div id="speed" class="speed iconSize"></div>
|
||||
<div id="captions" class="captions_off iconSize"></div>
|
||||
<div id="captions" class="captions_off iconSize" style="display: none"></div>
|
||||
<div id="duration" class="duration">00:00</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
/* WebOS custom player styles */
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 240px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 1.0) 0%, rgba(0, 0, 0, 0.0) 80%);
|
||||
}
|
||||
|
||||
.iconSize {
|
||||
|
@ -15,39 +20,9 @@
|
|||
}
|
||||
|
||||
.volumeContainer {
|
||||
height: 48px;
|
||||
width: 184px;
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
.volumeBar {
|
||||
left: 16px;
|
||||
top: 20px;
|
||||
height: 8px;
|
||||
width: 152px;
|
||||
}
|
||||
|
||||
.volumeBarInteractiveArea {
|
||||
height: 48px;
|
||||
width: 184px;
|
||||
}
|
||||
|
||||
.volumeBarHandle {
|
||||
left: 168px;
|
||||
top: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: 0px 64px 128px 0px rgba(0, 0, 0, 0.56), 0px 4px 42px 0px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.volumeBarProgress {
|
||||
left: 16px;
|
||||
top: 20px;
|
||||
height: 8px;
|
||||
width: 152px;
|
||||
}
|
||||
|
||||
.progressBarContainer {
|
||||
bottom: 120px;
|
||||
left: 32px;
|
||||
|
@ -134,21 +109,42 @@
|
|||
}
|
||||
|
||||
.leftButtonContainer {
|
||||
bottom: 48px;
|
||||
bottom: 24px;
|
||||
left: 48px;
|
||||
height: 48px;
|
||||
/* right: 320px; */
|
||||
right: 32px;
|
||||
gap: 48px;
|
||||
height: 96px;
|
||||
right: 48px;
|
||||
/* gap: 48px; */
|
||||
gap: unset;
|
||||
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.buttonFocusContainer {
|
||||
margin-right: 28px;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.buttonFocus {
|
||||
/* background-image: linear-gradient(to bottom, #008BD7 35%, #0069AA); */
|
||||
background-image: linear-gradient(to bottom, #808080 35%, #202020);
|
||||
border: 1px solid #4E4E4E;
|
||||
}
|
||||
|
||||
.progressBarHandle {
|
||||
border: 1px solid #4E4E4E;
|
||||
}
|
||||
|
||||
.progressBarHandleHide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
bottom: 48px;
|
||||
right: 48px;
|
||||
height: 48px;
|
||||
gap: 48px;
|
||||
/* gap: 48px; */
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
.captionsContainer {
|
||||
|
@ -180,3 +176,11 @@
|
|||
#captions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-family: InterRegular;
|
||||
}
|
||||
|
|
30
receivers/webos/fcast-receiver/src/viewer/1280x720.css
Normal file
30
receivers/webos/fcast-receiver/src/viewer/1280x720.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
|
||||
#titleIcon {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 5px 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
/* } */
|
30
receivers/webos/fcast-receiver/src/viewer/1920x1080.css
Normal file
30
receivers/webos/fcast-receiver/src/viewer/1920x1080.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* @media only screen and ((max-width: 2559px) or (max-height: 1439px)) { */
|
||||
#titleIcon {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 5px 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
/* } */
|
295
receivers/webos/fcast-receiver/src/viewer/Renderer.ts
Normal file
295
receivers/webos/fcast-receiver/src/viewer/Renderer.ts
Normal file
|
@ -0,0 +1,295 @@
|
|||
import {
|
||||
PlayerControlEvent,
|
||||
playerCtrlStateUpdate,
|
||||
onPlay,
|
||||
onPlayPlaylist,
|
||||
setPlaylistItem,
|
||||
playlistIndex,
|
||||
uiHideTimer,
|
||||
showDurationTimer,
|
||||
isMediaItem,
|
||||
cachedPlayMediaItem,
|
||||
imageViewerPlaybackState,
|
||||
keyDownEventHandler,
|
||||
keyUpEventHandler
|
||||
} from 'common/viewer/Renderer';
|
||||
import { KeyCode, RemoteKeyCode, ControlBarMode } from 'lib/common';
|
||||
import * as common from 'lib/common';
|
||||
import { PlaybackState } from 'common/Packets';
|
||||
|
||||
const logger = window.targetAPI.logger;
|
||||
|
||||
const playPreviousContainer = document.getElementById('playPreviousContainer');
|
||||
const actionContainer = document.getElementById('actionContainer');
|
||||
const playNextContainer = document.getElementById('playNextContainer');
|
||||
const action = document.getElementById('action');
|
||||
|
||||
enum ControlFocus {
|
||||
Action,
|
||||
PlayPrevious,
|
||||
PlayNext,
|
||||
}
|
||||
|
||||
let controlMode = ControlBarMode.KeyboardMouse;
|
||||
let controlFocus = ControlFocus.Action;
|
||||
|
||||
// Hide
|
||||
// [|<][>][>|]
|
||||
// Hide
|
||||
let locationMap = {
|
||||
Action: actionContainer,
|
||||
PlayPrevious: playPreviousContainer,
|
||||
PlayNext: playNextContainer,
|
||||
};
|
||||
|
||||
|
||||
window.parent.webOSApp.setKeyDownHandler(keyDownEventHandler);
|
||||
window.parent.webOSApp.setKeyUpHandler(keyUpEventHandler);
|
||||
|
||||
uiHideTimer.setDelay(5000);
|
||||
uiHideTimer.setCallback(() => {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse || !showDurationTimer.isPaused()) {
|
||||
controlMode = ControlBarMode.KeyboardMouse;
|
||||
locationMap[ControlFocus[controlFocus]].classList.remove('buttonFocus');
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
});
|
||||
|
||||
// Leave control bar on screen if magic remote cursor leaves window
|
||||
document.onmouseout = () => {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
uiHideTimer.end();
|
||||
}
|
||||
}
|
||||
|
||||
function remoteNavigateTo(location: ControlFocus) {
|
||||
// Issues with using standard focus, so manually managing styles
|
||||
locationMap[ControlFocus[controlFocus]].classList.remove('buttonFocus');
|
||||
controlFocus = location;
|
||||
locationMap[ControlFocus[controlFocus]].classList.add('buttonFocus');
|
||||
}
|
||||
|
||||
function setControlMode(mode: ControlBarMode, immediateHide: boolean = true) {
|
||||
if (mode === ControlBarMode.KeyboardMouse) {
|
||||
uiHideTimer.enable();
|
||||
|
||||
if (immediateHide) {
|
||||
locationMap[ControlFocus[controlFocus]].classList.remove('buttonFocus');
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
else {
|
||||
uiHideTimer.start();
|
||||
}
|
||||
}
|
||||
else {
|
||||
const focus = action?.style.display === 'none' ? ControlFocus.PlayNext : ControlFocus.Action;
|
||||
remoteNavigateTo(focus);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
|
||||
uiHideTimer.start();
|
||||
}
|
||||
|
||||
controlMode = mode;
|
||||
}
|
||||
|
||||
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
|
||||
let handledCase = false;
|
||||
|
||||
switch (event) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return handledCase;
|
||||
}
|
||||
|
||||
export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) {
|
||||
switch (event) {
|
||||
case PlayerControlEvent.Load: {
|
||||
if (!isMediaItem && controlMode === ControlBarMode.Remote) {
|
||||
setControlMode(ControlBarMode.KeyboardMouse);
|
||||
}
|
||||
if (action?.style.display === 'none') {
|
||||
actionContainer.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
actionContainer.style.display = 'block';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
|
||||
let handledCase = false;
|
||||
let key = '';
|
||||
|
||||
switch (event.keyCode) {
|
||||
case KeyCode.KeyK:
|
||||
case KeyCode.Space:
|
||||
if (isMediaItem) {
|
||||
// Play/pause toggle
|
||||
if (cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
|
||||
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Play);
|
||||
} else {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Pause);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.Enter:
|
||||
if (isMediaItem) {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.Action) {
|
||||
// Play/pause toggle
|
||||
if (cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
|
||||
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Play);
|
||||
} else {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Pause);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayPrevious) {
|
||||
setPlaylistItem(playlistIndex - 1);
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayNext) {
|
||||
setPlaylistItem(playlistIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.ArrowUp:
|
||||
case KeyCode.ArrowDown:
|
||||
if (isMediaItem) {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setControlMode(ControlBarMode.Remote);
|
||||
}
|
||||
else {
|
||||
setControlMode(ControlBarMode.KeyboardMouse);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.ArrowLeft:
|
||||
if (isMediaItem) {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setPlaylistItem(playlistIndex - 1);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.Action || action?.style.display === 'none') {
|
||||
remoteNavigateTo(ControlFocus.PlayPrevious);
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayNext) {
|
||||
remoteNavigateTo(ControlFocus.Action);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
}
|
||||
break;
|
||||
case KeyCode.ArrowRight:
|
||||
if (isMediaItem) {
|
||||
if (controlMode === ControlBarMode.KeyboardMouse) {
|
||||
setPlaylistItem(playlistIndex + 1);
|
||||
}
|
||||
else {
|
||||
if (controlFocus === ControlFocus.Action || action?.style.display === 'none') {
|
||||
remoteNavigateTo(ControlFocus.PlayNext);
|
||||
}
|
||||
else if (controlFocus === ControlFocus.PlayPrevious) {
|
||||
remoteNavigateTo(ControlFocus.Action);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case RemoteKeyCode.Stop:
|
||||
window.parent.webOSApp.loadPage('main_window/index.html');
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Stop';
|
||||
break;
|
||||
|
||||
// Note that in simulator rewind and fast forward key codes are sent twice...
|
||||
case RemoteKeyCode.Rewind:
|
||||
if (isMediaItem) {
|
||||
setPlaylistItem(playlistIndex - 1);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Rewind';
|
||||
}
|
||||
break;
|
||||
|
||||
case RemoteKeyCode.Play:
|
||||
if (isMediaItem) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Play);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Play';
|
||||
}
|
||||
break;
|
||||
case RemoteKeyCode.Pause:
|
||||
if (isMediaItem) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Pause);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'Pause';
|
||||
}
|
||||
break;
|
||||
|
||||
// Note that in simulator rewind and fast forward key codes are sent twice...
|
||||
case RemoteKeyCode.FastForward:
|
||||
if (isMediaItem) {
|
||||
setPlaylistItem(playlistIndex + 1);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
key = 'FastForward';
|
||||
}
|
||||
break;
|
||||
|
||||
case RemoteKeyCode.Back:
|
||||
window.parent.webOSApp.loadPage('main_window/index.html');
|
||||
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.parent.webOSApp.pendingPlay !== null) {
|
||||
if (window.parent.webOSApp.pendingPlay.rendererEvent === 'play-playlist') {
|
||||
onPlayPlaylist(null, window.parent.webOSApp.pendingPlay.rendererMessage);
|
||||
}
|
||||
else {
|
||||
onPlay(null, window.parent.webOSApp.pendingPlay.rendererMessage);
|
||||
}
|
||||
}
|
45
receivers/webos/fcast-receiver/src/viewer/index.html
Normal file
45
receivers/webos/fcast-receiver/src/viewer/index.html
Normal 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="playPreviousContainer" class="buttonFocusContainer"><div id="playPrevious" class="playPrevious iconSize" style="display: none"></div></div>
|
||||
<div id="actionContainer" class="buttonFocusContainer"><div id="action" class="play iconSize" style="display: none"></div></div>
|
||||
<div id="playlistLength" style="display: none"></div>
|
||||
<div id="playNextContainer" class="buttonFocusContainer"><div id="playNext" class="playNext iconSize" style="display: none"></div></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>
|
59
receivers/webos/fcast-receiver/src/viewer/style.css
Normal file
59
receivers/webos/fcast-receiver/src/viewer/style.css
Normal file
|
@ -0,0 +1,59 @@
|
|||
/* WebOS custom player styles */
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 140px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 1.0) 0%, rgba(0, 0, 0, 0.0) 100%);
|
||||
}
|
||||
|
||||
.iconSize {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#leftButtonContainer {
|
||||
left: 48px;
|
||||
|
||||
font-family: InterRegular;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
#centerButtonContainer {
|
||||
font-family: InterRegular;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
#playlistLength {
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
bottom: 24px;
|
||||
height: 96px;
|
||||
/* gap: 48px; */
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
.buttonFocusContainer {
|
||||
margin-right: 28px;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.buttonFocus {
|
||||
/* background-image: linear-gradient(to bottom, #008BD7 35%, #0069AA); */
|
||||
background-image: linear-gradient(to bottom, #808080 35%, #202020);
|
||||
border: 1px solid #4E4E4E;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
gap: unset;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-family: InterRegular;
|
||||
}
|
|
@ -15,16 +15,86 @@ module.exports = [
|
|||
{
|
||||
mode: buildMode,
|
||||
entry: {
|
||||
preload: './src/main/Preload.ts',
|
||||
renderer: './src/main/Renderer.ts',
|
||||
main: './src/Main.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' }]
|
||||
}
|
||||
],
|
||||
},
|
||||
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'),
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
// Common assets
|
||||
{
|
||||
from: '../common/assets/**',
|
||||
to: '[path][name][ext]',
|
||||
context: path.resolve(__dirname, '..', '..', 'common'),
|
||||
globOptions: { ignore: ['**/*.txt'] }
|
||||
},
|
||||
// Target assets
|
||||
{ from: 'appinfo.json', to: '[name][ext]' },
|
||||
{
|
||||
from: '**',
|
||||
to: 'assets/[path][name][ext]',
|
||||
context: path.resolve(__dirname, 'assets'),
|
||||
globOptions: { ignore: ['**/*.svg'] }
|
||||
},
|
||||
{
|
||||
from: '**',
|
||||
to: 'lib/[name][ext]',
|
||||
context: path.resolve(__dirname, 'lib'),
|
||||
globOptions: { ignore: ['**/*.txt'] }
|
||||
},
|
||||
{ from: './src/index.html', to: '[name][ext]' }
|
||||
],
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
TARGET: JSON.stringify(TARGET)
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
mode: buildMode,
|
||||
entry: {
|
||||
preload: './src/main/Preload.ts',
|
||||
renderer: './src/main/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' }]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
@ -45,31 +115,10 @@ module.exports = [
|
|||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
// Common assets
|
||||
{
|
||||
from: '../common/assets/**',
|
||||
to: '../[path][name][ext]',
|
||||
context: path.resolve(__dirname, '..', '..', 'common'),
|
||||
globOptions: { ignore: ['**/*.txt'] }
|
||||
},
|
||||
{
|
||||
from: '../../common/web/main/common.css',
|
||||
to: '[name][ext]',
|
||||
},
|
||||
// Target assets
|
||||
{ from: 'appinfo.json', to: '../[name][ext]' },
|
||||
{
|
||||
from: '**',
|
||||
to: '../assets/[path][name][ext]',
|
||||
context: path.resolve(__dirname, 'assets'),
|
||||
globOptions: { ignore: ['**/*.svg'] }
|
||||
},
|
||||
{
|
||||
from: '**',
|
||||
to: '../lib/[name][ext]',
|
||||
context: path.resolve(__dirname, 'lib'),
|
||||
globOptions: { ignore: ['**/*.txt'] }
|
||||
},
|
||||
{
|
||||
from: './src/main/*',
|
||||
to: '[name][ext]',
|
||||
|
@ -88,13 +137,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 +184,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)
|
||||
})
|
||||
]
|
||||
}
|
||||
];
|
||||
|
|
8
receivers/webos/scripts/reinstall_vm.sh
Executable file
8
receivers/webos/scripts/reinstall_vm.sh
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
version=$1
|
||||
|
||||
~/webOS_SDK/TV/Emulator/${version}/vm_remove.sh
|
||||
rm -rf ~/webOS_SDK/TV/Emulator/${version}
|
||||
unzip ~/webOS_SDK/TV/Emulator_tv_linux_${version}.zip -d ~/webOS_SDK/TV/
|
||||
chmod 755 ~/webOS_SDK/TV/Emulator/${version}/*
|
||||
~/webOS_SDK/TV/Emulator/${version}/vm_register.sh
|
Loading…
Add table
Add a link
Reference in a new issue