1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-09-03 04:53:06 +00:00

webOS: Add support for remote control bar

This commit is contained in:
Michael Hollister 2025-07-21 14:30:47 -05:00
parent 144c3e17f5
commit 8dde1ec5b3
20 changed files with 855 additions and 232 deletions

View file

@ -22,6 +22,8 @@ export class Timer {
private delay: number; private delay: number;
private startTime: number; private startTime: number;
private remainingTime: number; private remainingTime: number;
private enabled: boolean;
public started: boolean; public started: boolean;
constructor(callback: () => void, delay: number, autoStart: boolean = true) { constructor(callback: () => void, delay: number, autoStart: boolean = true) {
@ -29,6 +31,7 @@ export class Timer {
this.callback = callback; this.callback = callback;
this.delay = delay; this.delay = delay;
this.started = false; this.started = false;
this.enabled = true;
if (autoStart) { if (autoStart) {
this.start(); this.start();
@ -36,20 +39,22 @@ export class Timer {
} }
public start(delay?: number) { public start(delay?: number) {
this.delay = delay ? delay : this.delay; if (this.enabled) {
this.delay = delay ? delay : this.delay;
if (this.handle) { if (this.handle) {
window.clearTimeout(this.handle); window.clearTimeout(this.handle);
}
this.started = true;
this.startTime = Date.now();
this.remainingTime = null;
this.handle = window.setTimeout(this.callback, this.delay);
} }
this.started = true;
this.startTime = Date.now();
this.remainingTime = null;
this.handle = window.setTimeout(this.callback, this.delay);
} }
public pause() { public pause() {
if (this.handle) { if (this.enabled && this.handle) {
window.clearTimeout(this.handle); window.clearTimeout(this.handle);
this.handle = null; this.handle = null;
this.remainingTime = this.delay - (Date.now() - this.startTime); this.remainingTime = this.delay - (Date.now() - this.startTime);
@ -57,7 +62,7 @@ export class Timer {
} }
public resume() { public resume() {
if (this.remainingTime) { if (this.enabled && this.remainingTime) {
this.start(this.remainingTime); this.start(this.remainingTime);
} }
} }
@ -70,4 +75,32 @@ export class Timer {
this.started = false; 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;
}
} }

View file

@ -86,13 +86,14 @@ if (TARGET === 'electron') {
} else if (TARGET === 'webOS' || TARGET === 'tizenOS') { } else if (TARGET === 'webOS' || TARGET === 'tizenOS') {
preloadData.onDeviceInfoCb = () => { logger.warn('Main: Callback not set while fetching device info'); }; preloadData.onDeviceInfoCb = () => { logger.warn('Main: Callback not set while fetching device info'); };
preloadData.getSessionsCb = () => { logger.error('Main: Callback not set while calling getSessions'); }; 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.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.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.sendEventCb = (message: EventMessage) => { logger.error('Main: Callback not set while calling onSendEventCb'); };
preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: Set<string>, keyUp: Set<string> }) => { preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: string[], keyUp: string[] }) => {
preloadData.subscribedKeys.keyDown = value.keyDown; preloadData.subscribedKeys.keyDown = new Set(value.keyDown);
preloadData.subscribedKeys.keyUp = value.keyUp; preloadData.subscribedKeys.keyUp = new Set(value.keyUp);
}; };
preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => { preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => {
@ -110,6 +111,17 @@ if (TARGET === 'electron') {
return preloadData.getSessionsCb(); 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, getSubscribedKeys: () => preloadData.subscribedKeys,
onConnect: (callback: (_, value: any) => void) => preloadData.onConnectCb = callback, onConnect: (callback: (_, value: any) => void) => preloadData.onConnectCb = callback,
onDisconnect: (callback: (_, value: any) => void) => preloadData.onDisconnectCb = callback, onDisconnect: (callback: (_, value: any) => void) => preloadData.onDisconnectCb = callback,

View file

@ -212,7 +212,7 @@ export function keyDownEventHandler(event: KeyboardEvent) {
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) { if (!handledCase) {
switch (event.key) { switch (event.key.toLowerCase()) {
default: default:
break; break;
} }
@ -232,7 +232,7 @@ export function keyUpEventHandler(event: KeyboardEvent) {
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) { if (!handledCase) {
switch (event.key) { switch (event.key.toLowerCase()) {
default: default:
break; break;
} }

View file

@ -14,6 +14,8 @@ export class Player {
private player: HTMLVideoElement; private player: HTMLVideoElement;
private playMessage: PlayMessage; private playMessage: PlayMessage;
private source: string; private source: string;
private playCb: any;
private pauseCb: any;
// Todo: use a common event handler interface instead of exposing internal players // Todo: use a common event handler interface instead of exposing internal players
public playerType: PlayerType; public playerType: PlayerType;
@ -23,6 +25,8 @@ export class Player {
constructor(player: HTMLVideoElement, message: PlayMessage) { constructor(player: HTMLVideoElement, message: PlayMessage) {
this.player = player; this.player = player;
this.playMessage = message; this.playMessage = message;
this.playCb = null;
this.pauseCb = null;
if (message.container === 'application/dash+xml') { if (message.container === 'application/dash+xml') {
this.playerType = PlayerType.Dash; this.playerType = PlayerType.Dash;
@ -110,6 +114,8 @@ export class Player {
this.hlsPlayer = null; this.hlsPlayer = null;
this.playMessage = null; this.playMessage = null;
this.source = null; this.source = null;
this.playCb = null;
this.pauseCb = null;
} }
/** /**
@ -143,6 +149,10 @@ export class Player {
} else { // HLS, HTML } else { // HLS, HTML
this.player.play(); this.player.play();
} }
if (this.playCb) {
this.playCb();
}
} }
public isPaused(): boolean { public isPaused(): boolean {
@ -161,6 +171,15 @@ export class Player {
} else { // HLS, HTML } else { // HLS, HTML
this.player.pause(); this.player.pause();
} }
if (this.pauseCb) {
this.pauseCb();
}
}
public setPlayPauseCallback(playCallback: (() => void), pauseCallback: (() => void)) {
this.playCb = playCallback;
this.pauseCb = pauseCallback;
} }
public stop() { public stop() {

View file

@ -19,7 +19,6 @@ declare global {
interface Window { interface Window {
electronAPI: any; electronAPI: any;
tizenOSAPI: any; tizenOSAPI: any;
webOSAPI: any;
webOS: any; webOS: any;
targetAPI: any; targetAPI: any;
} }
@ -90,13 +89,14 @@ if (TARGET === 'electron') {
preloadData.sendPlayRequestCb = () => { logger.error('Player: Callback "sendPlayRequest" not set'); }; preloadData.sendPlayRequestCb = () => { logger.error('Player: Callback "sendPlayRequest" not set'); };
preloadData.getSessionsCb = () => { logger.error('Player: Callback "getSessions" not set'); }; preloadData.getSessionsCb = () => { logger.error('Player: Callback "getSessions" not set'); };
preloadData.onConnectCb = () => { logger.error('Player: Callback "onConnect" not set'); }; preloadData.initializeSubscribedKeysCb = () => { logger.error('Player: Callback "initializeSubscribedKeys" not set'); };
preloadData.onDisconnectCb = () => { logger.error('Player: Callback "onDisconnect" 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.onPlayPlaylistCb = () => { logger.error('Player: Callback "onPlayPlaylist" not set'); };
preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: Set<string>, keyUp: Set<string> }) => { preloadData.onEventSubscribedKeysUpdate = (value: { keyDown: string[], keyUp: string[] }) => {
preloadData.subscribedKeys.keyDown = value.keyDown; preloadData.subscribedKeys.keyDown = new Set(value.keyDown);
preloadData.subscribedKeys.keyUp = value.keyUp; preloadData.subscribedKeys.keyUp = new Set(value.keyUp);
}; };
preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => { preloadData.onToast = (message: string, icon: ToastIcon = ToastIcon.INFO, duration: number = 5000) => {
@ -125,6 +125,17 @@ if (TARGET === 'electron') {
return preloadData.getSessionsCb(); 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, getSubscribedKeys: () => preloadData.subscribedKeys,
onConnect: (callback: any) => { preloadData.onConnectCb = callback; }, onConnect: (callback: any) => { preloadData.onConnectCb = callback; },
onDisconnect: (callback: any) => { preloadData.onDisconnectCb = callback; }, onDisconnect: (callback: any) => { preloadData.onDisconnectCb = callback; },

View file

@ -40,7 +40,7 @@ const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer")
const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress"); const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress");
const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition"); const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition");
const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle"); const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle");
const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea"); const playerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea");
const playerCtrlVolumeBar = document.getElementById("volumeBar"); const playerCtrlVolumeBar = document.getElementById("volumeBar");
const playerCtrlVolumeBarProgress = document.getElementById("volumeBarProgress"); const playerCtrlVolumeBarProgress = document.getElementById("volumeBarProgress");
@ -80,10 +80,7 @@ let playlistIndex = 0;
let isMediaItem = false; let isMediaItem = false;
let playItemCached = false; let playItemCached = false;
let uiHideTimer = new Timer(() => { let uiHideTimer = new Timer(() => { playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut); }, 3000);
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}, 3000);
let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false); let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false);
let showDurationTimer = new Timer(mediaEndHandler, 0, false); let showDurationTimer = new Timer(mediaEndHandler, 0, false);
let mediaTitleShowTimer = new Timer(() => { mediaTitle.style.display = 'none'; }, 5000); let mediaTitleShowTimer = new Timer(() => { mediaTitle.style.display = 'none'; }, 5000);
@ -567,6 +564,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
} }
case PlayerControlEvent.UiFadeOut: { case PlayerControlEvent.UiFadeOut: {
uiVisible = false;
document.body.style.cursor = "none"; document.body.style.cursor = "none";
playerControls.style.opacity = '0'; playerControls.style.opacity = '0';
captionsBaseHeight = captionsBaseHeightCollapsed; captionsBaseHeight = captionsBaseHeightCollapsed;
@ -582,6 +580,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
} }
case PlayerControlEvent.UiFadeIn: { case PlayerControlEvent.UiFadeIn: {
uiVisible = true;
document.body.style.cursor = "default"; document.body.style.cursor = "default";
playerControls.style.opacity = '1'; playerControls.style.opacity = '1';
captionsBaseHeight = captionsBaseHeightExpanded; captionsBaseHeight = captionsBaseHeightExpanded;
@ -644,7 +643,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
function scrubbingMouseUIHandler(e: MouseEvent) { function scrubbingMouseUIHandler(e: MouseEvent) {
const progressBarOffset = e.offsetX - playerCtrlProgressBar.offsetLeft; 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()); 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)); time = Math.min(player?.getDuration(), Math.max(0.0, time));
@ -657,7 +656,7 @@ function scrubbingMouseUIHandler(e: MouseEvent) {
playerCtrlProgressBarPosition.textContent = isLive ? `${livePrefix}${formatDuration(time)}` : formatDuration(time); playerCtrlProgressBarPosition.textContent = isLive ? `${livePrefix}${formatDuration(time)}` : formatDuration(time);
let offset = e.offsetX - (playerCtrlProgressBarPosition.offsetWidth / 2); 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`); playerCtrlProgressBarPosition.setAttribute("style", `display: block; left: ${offset}px`);
} }
@ -674,21 +673,21 @@ playerCtrlPlayPrevious.onclick = () => { setPlaylistItem(playlistIndex - 1); }
playerCtrlPlayNext.onclick = () => { setPlaylistItem(playlistIndex + 1); } playerCtrlPlayNext.onclick = () => { setPlaylistItem(playlistIndex + 1); }
playerCtrlVolume.onclick = () => { player?.setMute(!player?.isMuted()); }; playerCtrlVolume.onclick = () => { player?.setMute(!player?.isMuted()); };
PlayerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) }; playerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) };
PlayerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; }; playerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; };
PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => { playerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
if (e.buttons === 0) { if (e.buttons === 0) {
volumeChanging = false; volumeChanging = false;
} }
scrubbingMouseUIHandler(e); scrubbingMouseUIHandler(e);
}; };
PlayerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); }; playerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); };
PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) }; playerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
function scrubbingMouseHandler(e: MouseEvent) { function scrubbingMouseHandler(e: MouseEvent) {
const progressBarOffset = e.offsetX - playerCtrlProgressBar.offsetLeft; 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()); let time = Math.round((progressBarOffset / progressBarWidth) * player?.getDuration());
time = Math.min(player?.getDuration(), Math.max(0.0, time)); time = Math.min(player?.getDuration(), Math.max(0.0, time));
@ -880,17 +879,11 @@ function stopUiHideTimer() {
uiHideTimer.stop(); uiHideTimer.stop();
if (!uiVisible) { if (!uiVisible) {
uiVisible = true;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn); playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
} }
} }
document.onmouseout = () => { document.onmouseout = () => { uiHideTimer.end(); }
uiHideTimer.stop();
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}
document.onmousemove = () => { document.onmousemove = () => {
stopUiHideTimer(); stopUiHideTimer();
@ -910,16 +903,62 @@ document.addEventListener('click', (event: MouseEvent) => {
}); });
// Add the keydown event listener to the document // Add the keydown event listener to the document
const skipInterval = 10; const minSkipInterval = 10;
const volumeIncrement = 0.1; const volumeIncrement = 0.1;
function skipBack() { let skipBackRepeat = false;
player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipInterval, 0)); 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() { 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) { if (!isLivePosition) {
player?.setCurrentTime(Math.min(player?.getCurrentTime() + skipInterval, player?.getDuration())); 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);
} }
} }
@ -934,12 +973,12 @@ function keyDownEventHandler(event: KeyboardEvent) {
if (!handledCase) { if (!handledCase) {
switch (event.key.toLowerCase()) { switch (event.key.toLowerCase()) {
case 'arrowleft': case 'arrowleft':
skipBack(); skipBack(event.repeat);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case 'arrowright': case 'arrowright':
skipForward(); skipForward(event.repeat);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
@ -1004,7 +1043,7 @@ function keyUpEventHandler(event: KeyboardEvent) {
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) { if (!handledCase) {
switch (event.key) { switch (event.key.toLowerCase()) {
default: default:
break; break;
} }
@ -1025,25 +1064,18 @@ export {
idleIcon, idleIcon,
videoElement, videoElement,
videoCaptions, videoCaptions,
playerCtrlProgressBar,
playerCtrlProgressBarBuffer,
playerCtrlProgressBarProgress,
playerCtrlProgressBarHandle, playerCtrlProgressBarHandle,
playerCtrlVolumeBar,
playerCtrlVolumeBarProgress,
playerCtrlVolumeBarHandle,
playerCtrlLiveBadge,
playerCtrlPosition,
playerCtrlDuration,
playerCtrlCaptions, playerCtrlCaptions,
player, player,
uiHideTimer,
isLive, isLive,
playlistIndex,
captionsBaseHeight, captionsBaseHeight,
captionsLineHeight, captionsLineHeight,
onPlay, onPlay,
onPlayPlaylist, onPlayPlaylist,
setPlaylistItem,
playerCtrlStateUpdate, playerCtrlStateUpdate,
formatDuration,
skipBack, skipBack,
skipForward, skipForward,
keyDownEventHandler, keyDownEventHandler,

View file

@ -5,6 +5,7 @@ import * as connectionMonitor from 'common/ConnectionMonitor';
import { toast, ToastIcon } from 'common/components/Toast'; import { toast, ToastIcon } from 'common/components/Toast';
import { import {
targetPlayerCtrlStateUpdate, targetPlayerCtrlStateUpdate,
targetPlayerCtrlPostStateUpdate,
targetKeyDownEventListener, targetKeyDownEventListener,
targetKeyUpEventListener, targetKeyUpEventListener,
} from 'src/viewer/Renderer'; } from 'src/viewer/Renderer';
@ -34,10 +35,7 @@ let isMediaItem = false;
let playItemCached = false; let playItemCached = false;
let imageViewerPlaybackState: PlaybackState = PlaybackState.Idle; let imageViewerPlaybackState: PlaybackState = PlaybackState.Idle;
let uiHideTimer = new Timer(() => { let uiHideTimer = new Timer(() => { playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut); }, 3000);
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}, 3000);
let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false); let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false);
let showDurationTimer = new Timer(() => { let showDurationTimer = new Timer(() => {
@ -278,12 +276,14 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
break; break;
case PlayerControlEvent.UiFadeOut: { case PlayerControlEvent.UiFadeOut: {
uiVisible = false;
document.body.style.cursor = "none"; document.body.style.cursor = "none";
playerControls.style.opacity = '0'; playerControls.style.opacity = '0';
break; break;
} }
case PlayerControlEvent.UiFadeIn: { case PlayerControlEvent.UiFadeIn: {
uiVisible = true;
document.body.style.cursor = "default"; document.body.style.cursor = "default";
playerControls.style.opacity = '1'; playerControls.style.opacity = '1';
break; break;
@ -292,6 +292,8 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
default: default:
break; break;
} }
targetPlayerCtrlPostStateUpdate(event);
} }
// Receiver generated event handlers // Receiver generated event handlers
@ -313,17 +315,11 @@ function stopUiHideTimer() {
uiHideTimer.stop(); uiHideTimer.stop();
if (!uiVisible) { if (!uiVisible) {
uiVisible = true;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn); playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
} }
} }
document.onmouseout = () => { document.onmouseout = () => { uiHideTimer.end(); }
uiHideTimer.stop();
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}
document.onmousemove = () => { document.onmousemove = () => {
stopUiHideTimer(); stopUiHideTimer();
uiHideTimer.start(); uiHideTimer.start();
@ -337,37 +333,40 @@ function keyDownEventHandler(event: KeyboardEvent) {
// @ts-ignore // @ts-ignore
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) { if (!handledCase && isMediaItem) {
switch (event.code) { switch (event.key.toLowerCase()) {
case 'ArrowLeft': case 'arrowleft':
setPlaylistItem(playlistIndex - 1); setPlaylistItem(playlistIndex - 1);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case 'ArrowRight': case 'arrowright':
setPlaylistItem(playlistIndex + 1); setPlaylistItem(playlistIndex + 1);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case "Home": case "home":
setPlaylistItem(0); setPlaylistItem(0);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case "End": case "end":
setPlaylistItem(cachedPlaylist.items.length - 1); setPlaylistItem(cachedPlaylist.items.length - 1);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
case 'KeyK': case 'k':
case 'Space': case ' ':
case 'Enter': case 'enter':
// Play/pause toggle // Play/pause toggle
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) { if (cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
playerCtrlStateUpdate(PlayerControlEvent.Play); if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
} else { playerCtrlStateUpdate(PlayerControlEvent.Play);
playerCtrlStateUpdate(PlayerControlEvent.Pause); } else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
} }
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
break; break;
@ -390,7 +389,7 @@ function keyUpEventHandler(event: KeyboardEvent) {
let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key;
if (!handledCase) { if (!handledCase) {
switch (event.key) { switch (event.key.toLowerCase()) {
default: default:
break; break;
} }
@ -410,7 +409,12 @@ export {
idleIcon, idleIcon,
imageViewer, imageViewer,
genericViewer, genericViewer,
uiHideTimer,
showDurationTimer,
isMediaItem,
playlistIndex, playlistIndex,
cachedPlayMediaItem,
imageViewerPlaybackState,
onPlay, onPlay,
onPlayPlaylist, onPlayPlaylist,
playerCtrlStateUpdate, playerCtrlStateUpdate,

View file

@ -44,6 +44,14 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase; return handledCase;
} }
export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) {
// Currently unused in electron player
switch (event) {
default:
break;
}
}
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } { export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false; let handledCase = false;

View file

@ -40,6 +40,14 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase; return handledCase;
} }
export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) {
// Currently unused in electron player
switch (event) {
default:
break;
}
}
export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } { export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false; let handledCase = false;

View file

@ -175,6 +175,22 @@ export class Main {
return; 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])),
};
message.respond({
returnValue: true,
value: subscribeData
});
return;
}
case 'network_changed': { case 'network_changed': {
logger.info('Network interfaces have changed', message); logger.info('Network interfaces have changed', message);
Main.discoveryService.stop(); Main.discoveryService.stop();
@ -240,17 +256,34 @@ export class Main {
}); });
l.emitter.on('setplaylistitem', (message: SetPlaylistItemMessage) => Main.emitter.emit('setplaylistitem', message)); l.emitter.on('setplaylistitem', (message: SetPlaylistItemMessage) => Main.emitter.emit('setplaylistitem', message));
l.emitter.on('subscribeevent', (message) => { l.emitter.on('subscribeevent', (message) => {
const subscribeData = l.subscribeEvent(message.sessionId, message.body.event); l.subscribeEvent(message.sessionId, message.body.event);
if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) { 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); Main.emitter.emit('event_subscribed_keys_update', subscribeData);
} }
}); });
l.emitter.on('unsubscribeevent', (message) => { l.emitter.on('unsubscribeevent', (message) => {
const unsubscribeData = l.unsubscribeEvent(message.sessionId, message.body.event); l.unsubscribeEvent(message.sessionId, message.body.event);
if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) { if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) {
Main.emitter.emit('event_subscribed_keys_update', unsubscribeData); 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(); l.start();

View file

@ -15,6 +15,21 @@ export enum RemoteKeyCode {
Back = 461, 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 { export class ServiceManager {
private static serviceChannelSuccessCbHandler?: (message: any) => void; private static serviceChannelSuccessCbHandler?: (message: any) => void;
private static serviceChannelFailureCbHandler?: (message: any) => void; private static serviceChannelFailureCbHandler?: (message: any) => void;

View file

@ -48,7 +48,8 @@ window.webOSApp = {
keyUpEventHandler = callback; keyUpEventHandler = callback;
document.addEventListener('keyup', keyUpEventHandler); document.addEventListener('keyup', keyUpEventHandler);
}, },
loadPage: loadPage loadPage: loadPage,
pendingPlay: null,
}; };
document.addEventListener('webOSLaunch', launchHandlerCallback); document.addEventListener('webOSLaunch', launchHandlerCallback);

View file

@ -104,6 +104,12 @@ try {
}); });
}); });
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) => { preloadData.sendEventCb = (event: EventMessage) => {
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); }); serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
}; };

View file

@ -10,7 +10,6 @@ require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
declare global { declare global {
interface Window { interface Window {
targetAPI: any; targetAPI: any;
webOSAPI: any;
webOSApp: any; webOSApp: any;
} }
} }
@ -20,10 +19,8 @@ const logger = window.targetAPI.logger;
try { try {
initializeWindowSizeStylesheet(); initializeWindowSizeStylesheet();
window.webOSAPI = { window.parent.webOSApp.pendingPlay = JSON.parse(sessionStorage.getItem('playInfo'));
pendingPlay: JSON.parse(sessionStorage.getItem('playInfo')) const contentViewer = window.parent.webOSApp.pendingPlay?.contentViewer;
};
const contentViewer = window.webOSAPI.pendingPlay?.contentViewer;
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager; const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
serviceManager.subscribeToServiceChannel((message: any) => { serviceManager.subscribeToServiceChannel((message: any) => {
@ -34,12 +31,13 @@ try {
case 'play': { case 'play': {
if (contentViewer !== message.value.contentViewer) { if (contentViewer !== message.value.contentViewer) {
sessionStorage.setItem('playInfo', JSON.stringify(message.value));
window.parent.webOSApp.loadPage(`${message.value.contentViewer}/index.html`); window.parent.webOSApp.loadPage(`${message.value.contentViewer}/index.html`);
} }
else { else {
if (message.value.rendererEvent === 'play-playlist') { if (message.value.rendererEvent === 'play-playlist') {
if (preloadData.onPlayCb === undefined) { if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value; window.parent.webOSApp.pendingPlay = message.value;
} }
else { else {
preloadData.onPlayPlaylistCb(null, message.value.rendererMessage); preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
@ -47,7 +45,7 @@ try {
} }
else { else {
if (preloadData.onPlayCb === undefined) { if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value; window.parent.webOSApp.pendingPlay = message.value;
} }
else { else {
preloadData.onPlayCb(null, message.value.rendererMessage); preloadData.onPlayCb(null, message.value.rendererMessage);
@ -125,6 +123,11 @@ try {
serviceManager.call('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message)); 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 launchHandler = () => { const launchHandler = () => {
// args don't seem to be passed in via event despite what documentation says... // args don't seem to be passed in via event despite what documentation says...

View file

@ -1,81 +1,125 @@
import { import {
isLive,
onPlay, onPlay,
onPlayPlaylist, onPlayPlaylist,
setPlaylistItem,
playerCtrlStateUpdate,
playlistIndex,
player, player,
uiHideTimer,
PlayerControlEvent, PlayerControlEvent,
playerCtrlCaptions, playerCtrlCaptions,
playerCtrlDuration,
playerCtrlLiveBadge,
playerCtrlPosition,
playerCtrlProgressBar,
playerCtrlProgressBarBuffer,
playerCtrlProgressBarHandle,
playerCtrlProgressBarProgress,
playerCtrlStateUpdate,
playerCtrlVolumeBar,
playerCtrlVolumeBarHandle,
playerCtrlVolumeBarProgress,
videoCaptions, videoCaptions,
formatDuration,
skipBack, skipBack,
skipForward, skipForward,
keyDownEventHandler, keyDownEventHandler,
keyUpEventHandler, keyUpEventHandler,
playerCtrlProgressBarHandle,
} from 'common/player/Renderer'; } from 'common/player/Renderer';
import { RemoteKeyCode } from 'lib/common'; import { KeyCode, RemoteKeyCode, ControlBarMode } from 'lib/common';
import * as common from 'lib/common'; import * as common from 'lib/common';
const logger = window.targetAPI.logger;
const captionsBaseHeightCollapsed = 150; const captionsBaseHeightCollapsed = 150;
const captionsBaseHeightExpanded = 320; const captionsBaseHeightExpanded = 320;
const captionsLineHeight = 68; const captionsLineHeight = 68;
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.setKeyDownHandler(keyDownEventHandler);
window.parent.webOSApp.setKeyUpHandler(keyUpEventHandler); 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 { export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
let handledCase = false; let handledCase = false;
switch (event) { 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: default:
break; break;
} }
@ -83,20 +127,164 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
return handledCase; return handledCase;
} }
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 } { export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
// logger.info("KeyDown", event.keyCode);
let handledCase = false; let handledCase = false;
let key = ''; let key = '';
switch (event.keyCode) { switch (event.keyCode) {
case KeyCode.KeyK:
case KeyCode.Space:
// Play/pause toggle
if (player?.isPaused()) {
player?.play();
} else {
player?.pause();
}
event.preventDefault();
handledCase = true;
break;
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: case RemoteKeyCode.Stop:
// history.back(); window.parent.webOSApp.loadPage('main_window/index.html');
window.open('../main_window/index.html', '_self');
handledCase = true; handledCase = true;
key = 'Stop'; key = 'Stop';
break; break;
// Note that in simulator rewind and fast forward key codes are sent twice...
case RemoteKeyCode.Rewind: case RemoteKeyCode.Rewind:
skipBack(); skipBack(event.repeat);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Rewind'; key = 'Rewind';
@ -119,18 +307,16 @@ export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase:
key = 'Pause'; key = 'Pause';
break; break;
// Note that in simulator rewind and fast forward key codes are sent twice...
case RemoteKeyCode.FastForward: case RemoteKeyCode.FastForward:
skipForward(); skipForward(event.repeat);
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'FastForward'; key = 'FastForward';
break; break;
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
case RemoteKeyCode.Back: case RemoteKeyCode.Back:
// history.back(); window.parent.webOSApp.loadPage('main_window/index.html');
window.open('../main_window/index.html', '_self');
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Back'; key = 'Back';
@ -147,12 +333,12 @@ export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: b
return common.targetKeyUpEventListener(event); return common.targetKeyUpEventListener(event);
}; };
if (window.webOSAPI.pendingPlay !== null) { if (window.parent.webOSApp.pendingPlay !== null) {
if (window.webOSAPI.pendingPlay.rendererEvent === 'play-playlist') { if (window.parent.webOSApp.pendingPlay.rendererEvent === 'play-playlist') {
onPlayPlaylist(null, window.webOSAPI.pendingPlay.rendererMessage); onPlayPlaylist(null, window.parent.webOSApp.pendingPlay.rendererMessage);
} }
else { else {
onPlay(null, window.webOSAPI.pendingPlay.rendererMessage); onPlay(null, window.parent.webOSApp.pendingPlay.rendererMessage);
} }
} }

View file

@ -25,7 +25,7 @@
<div id="progressBarProgress" class="progressBarProgress" ></div> <div id="progressBarProgress" class="progressBarProgress" ></div>
<div id="progressBarPosition" class="progressBarPosition" ></div> <div id="progressBarPosition" class="progressBarPosition" ></div>
<!-- <div class="progressBarChapterContainer"></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 id="progressBarInteractiveArea" class="progressBarInteractiveArea" ></div>
</div> </div>
@ -37,9 +37,9 @@
</div> </div>
<div class="leftButtonContainer"> <div class="leftButtonContainer">
<div id="playPrevious" class="playPrevious iconSize"></div> <div id="playPreviousContainer" class="buttonFocusContainer"><div id="playPrevious" class="playPrevious iconSize"></div></div>
<div id="action" class="play iconSize"></div> <div id="actionContainer" class="buttonFocusContainer"><div id="action" class="play iconSize"></div></div>
<div id="playNext" class="playNext iconSize"></div> <div id="playNextContainer" class="buttonFocusContainer"><div id="playNext" class="playNext iconSize"></div></div>
<div id="volume" class="volume_high iconSize"></div> <div id="volume" class="volume_high iconSize"></div>
<div class="volumeContainer"> <div class="volumeContainer">
@ -59,7 +59,7 @@
<div class="buttonContainer"> <div class="buttonContainer">
<!-- <div id="fullscreen" class="fullscreen_on iconSize"></div> --> <!-- <div id="fullscreen" class="fullscreen_on iconSize"></div> -->
<div id="speed" class="speed 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 id="duration" class="duration">00:00</div>
</div> </div>

View file

@ -6,6 +6,7 @@ html {
.container { .container {
height: 240px; height: 240px;
background: linear-gradient(to top, rgba(0, 0, 0, 1.0) 0%, rgba(0, 0, 0, 0.0) 80%);
} }
.iconSize { .iconSize {
@ -19,39 +20,9 @@ html {
} }
.volumeContainer { .volumeContainer {
height: 48px;
width: 184px;
display: none; 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 { .progressBarContainer {
bottom: 120px; bottom: 120px;
left: 32px; left: 32px;
@ -138,21 +109,42 @@ html {
} }
.leftButtonContainer { .leftButtonContainer {
bottom: 48px; bottom: 24px;
left: 48px; left: 48px;
height: 48px; height: 96px;
/* right: 320px; */ right: 48px;
right: 32px; /* gap: 48px; */
gap: 48px; gap: unset;
justify-content: center; 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 { .buttonContainer {
bottom: 48px; bottom: 48px;
right: 48px; right: 48px;
height: 48px; height: 48px;
gap: 48px; /* gap: 48px; */
gap: unset;
} }
.captionsContainer { .captionsContainer {

View file

@ -1,60 +1,274 @@
import { PlayerControlEvent, playerCtrlStateUpdate, onPlay, onPlayPlaylist, setPlaylistItem, playlistIndex, keyDownEventHandler, keyUpEventHandler } from 'common/viewer/Renderer'; import {
import { RemoteKeyCode } from 'lib/common'; 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 * 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.setKeyDownHandler(keyDownEventHandler);
window.parent.webOSApp.setKeyUpHandler(keyUpEventHandler); 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 { export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
let handledCase = false; let handledCase = false;
switch (event) {
default:
break;
}
return handledCase; 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 } { export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {
let handledCase = false; let handledCase = false;
let key = ''; let key = '';
switch (event.keyCode) { 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: case RemoteKeyCode.Stop:
// history.back(); window.parent.webOSApp.loadPage('main_window/index.html');
window.open('../main_window/index.html', '_self');
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Stop'; key = 'Stop';
break; break;
// Note that in simulator rewind and fast forward key codes are sent twice...
case RemoteKeyCode.Rewind: case RemoteKeyCode.Rewind:
setPlaylistItem(playlistIndex - 1); if (isMediaItem) {
event.preventDefault(); setPlaylistItem(playlistIndex - 1);
handledCase = true; event.preventDefault();
key = 'Rewind'; handledCase = true;
key = 'Rewind';
}
break; break;
case RemoteKeyCode.Play: case RemoteKeyCode.Play:
playerCtrlStateUpdate(PlayerControlEvent.Play); if (isMediaItem) {
event.preventDefault(); playerCtrlStateUpdate(PlayerControlEvent.Play);
handledCase = true; event.preventDefault();
key = 'Play'; handledCase = true;
key = 'Play';
}
break; break;
case RemoteKeyCode.Pause: case RemoteKeyCode.Pause:
playerCtrlStateUpdate(PlayerControlEvent.Pause); if (isMediaItem) {
event.preventDefault(); playerCtrlStateUpdate(PlayerControlEvent.Pause);
handledCase = true; event.preventDefault();
key = 'Pause'; handledCase = true;
key = 'Pause';
}
break; break;
// Note that in simulator rewind and fast forward key codes are sent twice...
case RemoteKeyCode.FastForward: case RemoteKeyCode.FastForward:
setPlaylistItem(playlistIndex + 1); if (isMediaItem) {
event.preventDefault(); setPlaylistItem(playlistIndex + 1);
handledCase = true; event.preventDefault();
key = 'FastForward'; handledCase = true;
key = 'FastForward';
}
break; break;
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
case RemoteKeyCode.Back: case RemoteKeyCode.Back:
// history.back(); window.parent.webOSApp.loadPage('main_window/index.html');
window.open('../main_window/index.html', '_self');
event.preventDefault(); event.preventDefault();
handledCase = true; handledCase = true;
key = 'Back'; key = 'Back';
@ -71,11 +285,11 @@ export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: b
return common.targetKeyUpEventListener(event); return common.targetKeyUpEventListener(event);
}; };
if (window.webOSAPI.pendingPlay !== null) { if (window.parent.webOSApp.pendingPlay !== null) {
if (window.webOSAPI.pendingPlay.rendererEvent === 'play-playlist') { if (window.parent.webOSApp.pendingPlay.rendererEvent === 'play-playlist') {
onPlayPlaylist(null, window.webOSAPI.pendingPlay.rendererMessage); onPlayPlaylist(null, window.parent.webOSApp.pendingPlay.rendererMessage);
} }
else { else {
onPlay(null, window.webOSAPI.pendingPlay.rendererMessage); onPlay(null, window.parent.webOSApp.pendingPlay.rendererMessage);
} }
} }

View file

@ -24,10 +24,10 @@
<div id="mediaTitle"></div> <div id="mediaTitle"></div>
</div> </div>
<div id="centerButtonContainer" class="buttonContainer"> <div id="centerButtonContainer" class="buttonContainer">
<div id="playPrevious" class="playPrevious iconSize" style="display: none"></div> <div id="playPreviousContainer" class="buttonFocusContainer"><div id="playPrevious" class="playPrevious iconSize" style="display: none"></div></div>
<div id="action" class="play iconSize" style="display: none"></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="playlistLength" style="display: none"></div>
<div id="playNext" class="playNext iconSize" style="display: none"></div> <div id="playNextContainer" class="buttonFocusContainer"><div id="playNext" class="playNext iconSize" style="display: none"></div></div>
</div> </div>
<!-- <div id="rightButtonContainer" class="buttonContainer"> <!-- <div id="rightButtonContainer" class="buttonContainer">

View file

@ -4,6 +4,52 @@ html {
overflow: hidden; 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 { #toast-notification {
gap: unset; gap: unset;
} }