From 8dde1ec5b3f1e4fd107f3d9b0d4e0b8b0e8675e1 Mon Sep 17 00:00:00 2001 From: Michael Hollister Date: Mon, 21 Jul 2025 14:30:47 -0500 Subject: [PATCH] webOS: Add support for remote control bar --- receivers/common/web/UtilityFrontend.ts | 53 ++- receivers/common/web/main/Preload.ts | 18 +- receivers/common/web/main/Renderer.ts | 4 +- receivers/common/web/player/Player.ts | 19 ++ receivers/common/web/player/Preload.ts | 23 +- receivers/common/web/player/Renderer.ts | 108 +++--- receivers/common/web/viewer/Renderer.ts | 54 +-- receivers/electron/src/player/Renderer.ts | 8 + receivers/electron/src/viewer/Renderer.ts | 8 + .../webos/fcast-receiver-service/src/Main.ts | 39 ++- receivers/webos/fcast-receiver/lib/common.ts | 15 + receivers/webos/fcast-receiver/src/Main.ts | 3 +- .../webos/fcast-receiver/src/main/Preload.ts | 6 + .../fcast-receiver/src/player/Preload.ts | 17 +- .../fcast-receiver/src/player/Renderer.ts | 316 ++++++++++++++---- .../fcast-receiver/src/player/index.html | 10 +- .../webos/fcast-receiver/src/player/style.css | 64 ++-- .../fcast-receiver/src/viewer/Renderer.ts | 270 +++++++++++++-- .../fcast-receiver/src/viewer/index.html | 6 +- .../webos/fcast-receiver/src/viewer/style.css | 46 +++ 20 files changed, 855 insertions(+), 232 deletions(-) diff --git a/receivers/common/web/UtilityFrontend.ts b/receivers/common/web/UtilityFrontend.ts index df29f9d..8daa802 100644 --- a/receivers/common/web/UtilityFrontend.ts +++ b/receivers/common/web/UtilityFrontend.ts @@ -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,20 +39,22 @@ export class Timer { } public start(delay?: number) { - this.delay = delay ? delay : this.delay; + if (this.enabled) { + this.delay = delay ? delay : this.delay; - if (this.handle) { - window.clearTimeout(this.handle); + if (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() { - 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; + } } diff --git a/receivers/common/web/main/Preload.ts b/receivers/common/web/main/Preload.ts index 0fce9c3..843a879 100644 --- a/receivers/common/web/main/Preload.ts +++ b/receivers/common/web/main/Preload.ts @@ -86,13 +86,14 @@ if (TARGET === 'electron') { } else if (TARGET === 'webOS' || TARGET === 'tizenOS') { 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: Set, keyUp: Set }) => { - preloadData.subscribedKeys.keyDown = value.keyDown; - preloadData.subscribedKeys.keyUp = value.keyUp; + 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) => { @@ -110,6 +111,17 @@ 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, keyUp: Set }) => { + 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, diff --git a/receivers/common/web/main/Renderer.ts b/receivers/common/web/main/Renderer.ts index 61b4954..f8b737e 100644 --- a/receivers/common/web/main/Renderer.ts +++ b/receivers/common/web/main/Renderer.ts @@ -212,7 +212,7 @@ export function keyDownEventHandler(event: KeyboardEvent) { let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; if (!handledCase) { - switch (event.key) { + switch (event.key.toLowerCase()) { default: break; } @@ -232,7 +232,7 @@ export function keyUpEventHandler(event: KeyboardEvent) { let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; if (!handledCase) { - switch (event.key) { + switch (event.key.toLowerCase()) { default: break; } diff --git a/receivers/common/web/player/Player.ts b/receivers/common/web/player/Player.ts index c7f62fd..c0a13af 100644 --- a/receivers/common/web/player/Player.ts +++ b/receivers/common/web/player/Player.ts @@ -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() { diff --git a/receivers/common/web/player/Preload.ts b/receivers/common/web/player/Preload.ts index 6e0877c..3d44b5c 100644 --- a/receivers/common/web/player/Preload.ts +++ b/receivers/common/web/player/Preload.ts @@ -19,7 +19,6 @@ declare global { interface Window { electronAPI: any; tizenOSAPI: any; - webOSAPI: any; webOS: any; targetAPI: any; } @@ -90,13 +89,14 @@ if (TARGET === 'electron') { preloadData.sendPlayRequestCb = () => { logger.error('Player: Callback "sendPlayRequest" not set'); }; preloadData.getSessionsCb = () => { logger.error('Player: Callback "getSessions" not set'); }; - preloadData.onConnectCb = () => { logger.error('Player: Callback "onConnect" not set'); }; - preloadData.onDisconnectCb = () => { logger.error('Player: Callback "onDisconnect" not set'); }; + preloadData.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: Set, keyUp: Set }) => { - preloadData.subscribedKeys.keyDown = value.keyDown; - preloadData.subscribedKeys.keyUp = value.keyUp; + 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) => { @@ -125,6 +125,17 @@ 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, keyUp: Set }) => { + 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; }, diff --git a/receivers/common/web/player/Renderer.ts b/receivers/common/web/player/Renderer.ts index edf6a0f..ec083b3 100644 --- a/receivers/common/web/player/Renderer.ts +++ b/receivers/common/web/player/Renderer.ts @@ -40,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"); @@ -80,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); @@ -567,6 +564,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) { } case PlayerControlEvent.UiFadeOut: { + uiVisible = false; document.body.style.cursor = "none"; playerControls.style.opacity = '0'; captionsBaseHeight = captionsBaseHeightCollapsed; @@ -582,6 +580,7 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) { } case PlayerControlEvent.UiFadeIn: { + uiVisible = true; document.body.style.cursor = "default"; playerControls.style.opacity = '1'; captionsBaseHeight = captionsBaseHeightExpanded; @@ -644,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)); @@ -657,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`); } @@ -674,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)); @@ -880,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(); @@ -910,16 +903,62 @@ document.addEventListener('click', (event: MouseEvent) => { }); // Add the keydown event listener to the document -const skipInterval = 10; +const minSkipInterval = 10; const volumeIncrement = 0.1; -function skipBack() { - player?.setCurrentTime(Math.max(player?.getCurrentTime() - skipInterval, 0)); +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() { +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() + 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) { switch (event.key.toLowerCase()) { case 'arrowleft': - skipBack(); + skipBack(event.repeat); event.preventDefault(); handledCase = true; break; case 'arrowright': - skipForward(); + skipForward(event.repeat); event.preventDefault(); handledCase = true; break; @@ -1004,7 +1043,7 @@ function keyUpEventHandler(event: KeyboardEvent) { let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; if (!handledCase) { - switch (event.key) { + switch (event.key.toLowerCase()) { default: break; } @@ -1025,25 +1064,18 @@ 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, diff --git a/receivers/common/web/viewer/Renderer.ts b/receivers/common/web/viewer/Renderer.ts index 0477ac7..1ec790a 100644 --- a/receivers/common/web/viewer/Renderer.ts +++ b/receivers/common/web/viewer/Renderer.ts @@ -5,6 +5,7 @@ import * as connectionMonitor from 'common/ConnectionMonitor'; import { toast, ToastIcon } from 'common/components/Toast'; import { targetPlayerCtrlStateUpdate, + targetPlayerCtrlPostStateUpdate, targetKeyDownEventListener, targetKeyUpEventListener, } from 'src/viewer/Renderer'; @@ -34,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(() => { @@ -278,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; @@ -292,6 +292,8 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) { default: break; } + + targetPlayerCtrlPostStateUpdate(event); } // Receiver generated event handlers @@ -313,17 +315,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(); uiHideTimer.start(); @@ -337,37 +333,40 @@ function keyDownEventHandler(event: KeyboardEvent) { // @ts-ignore let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; - if (!handledCase) { - switch (event.code) { - case 'ArrowLeft': + 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 (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) { - playerCtrlStateUpdate(PlayerControlEvent.Play); - } else { - playerCtrlStateUpdate(PlayerControlEvent.Pause); + 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; @@ -390,7 +389,7 @@ function keyUpEventHandler(event: KeyboardEvent) { let key = (TARGET === 'webOS' && result.key !== '') ? result.key : event.key; if (!handledCase) { - switch (event.key) { + switch (event.key.toLowerCase()) { default: break; } @@ -410,7 +409,12 @@ export { idleIcon, imageViewer, genericViewer, + uiHideTimer, + showDurationTimer, + isMediaItem, playlistIndex, + cachedPlayMediaItem, + imageViewerPlaybackState, onPlay, onPlayPlaylist, playerCtrlStateUpdate, diff --git a/receivers/electron/src/player/Renderer.ts b/receivers/electron/src/player/Renderer.ts index 4057a5e..2ddb3c9 100644 --- a/receivers/electron/src/player/Renderer.ts +++ b/receivers/electron/src/player/Renderer.ts @@ -44,6 +44,14 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean return handledCase; } +export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) { + // Currently unused in electron player + switch (event) { + default: + break; + } +} + export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } { let handledCase = false; diff --git a/receivers/electron/src/viewer/Renderer.ts b/receivers/electron/src/viewer/Renderer.ts index 365111b..a7d62ed 100644 --- a/receivers/electron/src/viewer/Renderer.ts +++ b/receivers/electron/src/viewer/Renderer.ts @@ -40,6 +40,14 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean return handledCase; } +export function targetPlayerCtrlPostStateUpdate(event: PlayerControlEvent) { + // Currently unused in electron player + switch (event) { + default: + break; + } +} + export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } { let handledCase = false; diff --git a/receivers/webos/fcast-receiver-service/src/Main.ts b/receivers/webos/fcast-receiver-service/src/Main.ts index 7081700..bae5f8c 100644 --- a/receivers/webos/fcast-receiver-service/src/Main.ts +++ b/receivers/webos/fcast-receiver-service/src/Main.ts @@ -175,6 +175,22 @@ export class Main { 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': { logger.info('Network interfaces have changed', message); Main.discoveryService.stop(); @@ -240,17 +256,34 @@ export class Main { }); l.emitter.on('setplaylistitem', (message: SetPlaylistItemMessage) => Main.emitter.emit('setplaylistitem', message)); l.emitter.on('subscribeevent', (message) => { - const subscribeData = l.subscribeEvent(message.sessionId, message.body.event); + 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) => { - 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()) { - 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(); diff --git a/receivers/webos/fcast-receiver/lib/common.ts b/receivers/webos/fcast-receiver/lib/common.ts index 64a7a8c..ae945db 100644 --- a/receivers/webos/fcast-receiver/lib/common.ts +++ b/receivers/webos/fcast-receiver/lib/common.ts @@ -15,6 +15,21 @@ export enum RemoteKeyCode { 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; diff --git a/receivers/webos/fcast-receiver/src/Main.ts b/receivers/webos/fcast-receiver/src/Main.ts index e1bfea0..897c651 100644 --- a/receivers/webos/fcast-receiver/src/Main.ts +++ b/receivers/webos/fcast-receiver/src/Main.ts @@ -48,7 +48,8 @@ window.webOSApp = { keyUpEventHandler = callback; document.addEventListener('keyup', keyUpEventHandler); }, - loadPage: loadPage + loadPage: loadPage, + pendingPlay: null, }; document.addEventListener('webOSLaunch', launchHandlerCallback); diff --git a/receivers/webos/fcast-receiver/src/main/Preload.ts b/receivers/webos/fcast-receiver/src/main/Preload.ts index b68b495..173a227 100644 --- a/receivers/webos/fcast-receiver/src/main/Preload.ts +++ b/receivers/webos/fcast-receiver/src/main/Preload.ts @@ -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) => { serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); }); }; diff --git a/receivers/webos/fcast-receiver/src/player/Preload.ts b/receivers/webos/fcast-receiver/src/player/Preload.ts index a0a726b..4845cde 100644 --- a/receivers/webos/fcast-receiver/src/player/Preload.ts +++ b/receivers/webos/fcast-receiver/src/player/Preload.ts @@ -10,7 +10,6 @@ require('lib/webOSTVjs-1.2.10/webOSTV-dev.js'); declare global { interface Window { targetAPI: any; - webOSAPI: any; webOSApp: any; } } @@ -20,10 +19,8 @@ const logger = window.targetAPI.logger; try { initializeWindowSizeStylesheet(); - window.webOSAPI = { - pendingPlay: JSON.parse(sessionStorage.getItem('playInfo')) - }; - const contentViewer = window.webOSAPI.pendingPlay?.contentViewer; + window.parent.webOSApp.pendingPlay = JSON.parse(sessionStorage.getItem('playInfo')); + const contentViewer = window.parent.webOSApp.pendingPlay?.contentViewer; const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager; serviceManager.subscribeToServiceChannel((message: any) => { @@ -34,12 +31,13 @@ try { case 'play': { if (contentViewer !== message.value.contentViewer) { + sessionStorage.setItem('playInfo', JSON.stringify(message.value)); window.parent.webOSApp.loadPage(`${message.value.contentViewer}/index.html`); } else { if (message.value.rendererEvent === 'play-playlist') { if (preloadData.onPlayCb === undefined) { - window.webOSAPI.pendingPlay = message.value; + window.parent.webOSApp.pendingPlay = message.value; } else { preloadData.onPlayPlaylistCb(null, message.value.rendererMessage); @@ -47,7 +45,7 @@ try { } else { if (preloadData.onPlayCb === undefined) { - window.webOSAPI.pendingPlay = message.value; + window.parent.webOSApp.pendingPlay = message.value; } else { preloadData.onPlayCb(null, message.value.rendererMessage); @@ -125,6 +123,11 @@ try { 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 = () => { // args don't seem to be passed in via event despite what documentation says... diff --git a/receivers/webos/fcast-receiver/src/player/Renderer.ts b/receivers/webos/fcast-receiver/src/player/Renderer.ts index 15b2f0d..47d69f9 100644 --- a/receivers/webos/fcast-receiver/src/player/Renderer.ts +++ b/receivers/webos/fcast-receiver/src/player/Renderer.ts @@ -1,81 +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 { RemoteKeyCode } from 'lib/common'; +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; +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; } @@ -83,20 +127,164 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean 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 } { + // logger.info("KeyDown", event.keyCode); let handledCase = false; let key = ''; 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: - // history.back(); - window.open('../main_window/index.html', '_self'); + 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(); + skipBack(event.repeat); event.preventDefault(); handledCase = true; key = 'Rewind'; @@ -119,18 +307,16 @@ export function targetKeyDownEventListener(event: KeyboardEvent): { handledCase: 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'; @@ -147,12 +333,12 @@ export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: b return common.targetKeyUpEventListener(event); }; -if (window.webOSAPI.pendingPlay !== null) { - if (window.webOSAPI.pendingPlay.rendererEvent === 'play-playlist') { - onPlayPlaylist(null, window.webOSAPI.pendingPlay.rendererMessage); +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.webOSAPI.pendingPlay.rendererMessage); + onPlay(null, window.parent.webOSApp.pendingPlay.rendererMessage); } } diff --git a/receivers/webos/fcast-receiver/src/player/index.html b/receivers/webos/fcast-receiver/src/player/index.html index ac23ed7..2fe5613 100644 --- a/receivers/webos/fcast-receiver/src/player/index.html +++ b/receivers/webos/fcast-receiver/src/player/index.html @@ -25,7 +25,7 @@
-
+
@@ -37,9 +37,9 @@
-
-
-
+
+
+
@@ -59,7 +59,7 @@
-
+
00:00
diff --git a/receivers/webos/fcast-receiver/src/player/style.css b/receivers/webos/fcast-receiver/src/player/style.css index 23f2186..ed70369 100644 --- a/receivers/webos/fcast-receiver/src/player/style.css +++ b/receivers/webos/fcast-receiver/src/player/style.css @@ -6,6 +6,7 @@ html { .container { height: 240px; + background: linear-gradient(to top, rgba(0, 0, 0, 1.0) 0%, rgba(0, 0, 0, 0.0) 80%); } .iconSize { @@ -19,39 +20,9 @@ html { } .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; @@ -138,21 +109,42 @@ html { } .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 { diff --git a/receivers/webos/fcast-receiver/src/viewer/Renderer.ts b/receivers/webos/fcast-receiver/src/viewer/Renderer.ts index 9bb751d..0730703 100644 --- a/receivers/webos/fcast-receiver/src/viewer/Renderer.ts +++ b/receivers/webos/fcast-receiver/src/viewer/Renderer.ts @@ -1,60 +1,274 @@ -import { PlayerControlEvent, playerCtrlStateUpdate, onPlay, onPlayPlaylist, setPlaylistItem, playlistIndex, keyDownEventHandler, keyUpEventHandler } from 'common/viewer/Renderer'; -import { RemoteKeyCode } from 'lib/common'; +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: - // history.back(); - window.open('../main_window/index.html', '_self'); + 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: - setPlaylistItem(playlistIndex - 1); - event.preventDefault(); - handledCase = true; - key = 'Rewind'; + if (isMediaItem) { + setPlaylistItem(playlistIndex - 1); + event.preventDefault(); + handledCase = true; + key = 'Rewind'; + } break; case RemoteKeyCode.Play: - playerCtrlStateUpdate(PlayerControlEvent.Play); - event.preventDefault(); - handledCase = true; - key = 'Play'; + if (isMediaItem) { + playerCtrlStateUpdate(PlayerControlEvent.Play); + event.preventDefault(); + handledCase = true; + key = 'Play'; + } break; case RemoteKeyCode.Pause: - playerCtrlStateUpdate(PlayerControlEvent.Pause); - event.preventDefault(); - handledCase = true; - key = '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: - setPlaylistItem(playlistIndex + 1); - event.preventDefault(); - handledCase = true; - key = 'FastForward'; + if (isMediaItem) { + setPlaylistItem(playlistIndex + 1); + event.preventDefault(); + handledCase = true; + key = 'FastForward'; + } break; - // WebOS 22 and earlier does not work well using the history API, - // so manually handling page navigation... case RemoteKeyCode.Back: - // history.back(); - window.open('../main_window/index.html', '_self'); + window.parent.webOSApp.loadPage('main_window/index.html'); event.preventDefault(); handledCase = true; key = 'Back'; @@ -71,11 +285,11 @@ export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: b return common.targetKeyUpEventListener(event); }; -if (window.webOSAPI.pendingPlay !== null) { - if (window.webOSAPI.pendingPlay.rendererEvent === 'play-playlist') { - onPlayPlaylist(null, window.webOSAPI.pendingPlay.rendererMessage); +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.webOSAPI.pendingPlay.rendererMessage); + onPlay(null, window.parent.webOSApp.pendingPlay.rendererMessage); } } diff --git a/receivers/webos/fcast-receiver/src/viewer/index.html b/receivers/webos/fcast-receiver/src/viewer/index.html index 6e36154..1aef77c 100644 --- a/receivers/webos/fcast-receiver/src/viewer/index.html +++ b/receivers/webos/fcast-receiver/src/viewer/index.html @@ -24,10 +24,10 @@
- - +
+
- +