diff --git a/receivers/common/web/FCastSession.ts b/receivers/common/web/FCastSession.ts index ede0a58..a489545 100644 --- a/receivers/common/web/FCastSession.ts +++ b/receivers/common/web/FCastSession.ts @@ -61,7 +61,31 @@ export class FCastSession { const size = 1 + data.length; const header = Buffer.alloc(4 + 1); - header.writeUint32LE(size, 0); + + // Web OS 22 and earlier node versions do not support `writeUint32LE`, + // so manually checking endianness and writing as LE + // @ts-ignore + if (TARGET === 'webOS') { + let uInt32 = new Uint32Array([0x11223344]); + let uInt8 = new Uint8Array(uInt32.buffer); + + if(uInt8[0] === 0x44) { + // LE + header[0] = size & 0xFF; + header[1] = size & 0xFF00; + header[2] = size & 0xFF0000; + header[3] = size & 0xFF000000; + } else if (uInt8[0] === 0x11) { + // BE + header[0] = size & 0xFF000000; + header[1] = size & 0xFF0000; + header[2] = size & 0xFF00; + header[3] = size & 0xFF; + } + } else { + header.writeUint32LE(size, 0); + } + header[4] = opcode; let packet: Buffer; diff --git a/receivers/common/web/main/Preload.ts b/receivers/common/web/main/Preload.ts index 851df3c..4f10f6d 100644 --- a/receivers/common/web/main/Preload.ts +++ b/receivers/common/web/main/Preload.ts @@ -33,7 +33,19 @@ if (TARGET === 'electron') { require('lib/webOSTVjs-1.2.10/webOSTV.js'); require('lib/webOSTVjs-1.2.10/webOSTV-dev.js'); const serviceId = 'com.futo.fcast.receiver.service'; - let onDeviceInfoCb: any; + let onDeviceInfoCb = () => { console.log('Main: Callback not set while fetching device info'); }; + + const keepAliveService = window.webOS.service.request(`luna://${serviceId}/`, { + method:"keepAlive", + parameters: {}, + onSuccess: (message: any) => { + console.log(`Main: keepAlive ${JSON.stringify(message)}`); + }, + onFailure: (message: any) => { + console.error(`Main: keepAlive ${JSON.stringify(message)}`); + }, + // onComplete: (message) => {}, + }); const getDeviceInfoService = window.webOS.service.request(`luna://${serviceId}/`, { method:"getDeviceInfo", @@ -58,9 +70,16 @@ if (TARGET === 'electron') { console.log('Main: Registered play handler with service'); } else { - console.log(`Main: Playing ${JSON.stringify(message)}`); - getDeviceInfoService.cancel(); - playService.cancel(); + if (message.value !== undefined && message.value.playData !== undefined) { + console.log(`Main: Playing ${JSON.stringify(message)}`); + getDeviceInfoService.cancel(); + playService.cancel(); + + // WebOS 22 and earlier does not work well using the history API, + // so manually handling page navigation... + // history.pushState({}, '', '../main_window/index.html'); + window.open('../player/index.html'); + } } }, onFailure: (message: any) => { @@ -71,14 +90,14 @@ if (TARGET === 'electron') { }); window.targetAPI = { - onDeviceInfo: (callback: any) => onDeviceInfoCb = callback, + onDeviceInfo: (callback: () => void) => onDeviceInfoCb = callback, getDeviceInfo: () => deviceInfo, }; document.addEventListener('webOSRelaunch', (args: any) => { console.log(`Relaunching FCast Receiver with args: ${JSON.stringify(args)}`); - if (args.playData !== null) { + if (args.playData !== undefined) { if (getDeviceInfoService !== undefined) { getDeviceInfoService.cancel(); } @@ -86,7 +105,9 @@ if (TARGET === 'electron') { playService.cancel(); } - history.pushState({}, '', '../main_window/index.html'); + // WebOS 22 and earlier does not work well using the history API, + // so manually handling page navigation... + // history.pushState({}, '', '../main_window/index.html'); window.open('../player/index.html'); } }); diff --git a/receivers/common/web/main/Renderer.ts b/receivers/common/web/main/Renderer.ts index 77bb5cc..fb341a1 100644 --- a/receivers/common/web/main/Renderer.ts +++ b/receivers/common/web/main/Renderer.ts @@ -43,8 +43,13 @@ function renderIPsAndQRCode() { }, errorCorrectionLevel : "M", }, - (e) => { - console.log(`Error rendering QR Code: ${e}`) + (err) => { + if (err) { + console.error(`Error rendering QR Code: ${err}`); + } + else { + console.log(`Rendered QR Code`); + } }); onQRCodeRendered(); diff --git a/receivers/common/web/player/Preload.ts b/receivers/common/web/player/Preload.ts index 73974d4..2c16830 100644 --- a/receivers/common/web/player/Preload.ts +++ b/receivers/common/web/player/Preload.ts @@ -40,11 +40,23 @@ if (TARGET === 'electron') { let onSeekCb, onSetVolumeCb, onSetSpeedCb; let playerWindowOpen = false; + const keepAliveService = window.webOS.service.request(`luna://${serviceId}/`, { + method:"keepAlive", + parameters: {}, + onSuccess: (message: any) => { + console.log(`Player: keepAlive ${JSON.stringify(message)}`); + }, + onFailure: (message: any) => { + console.error(`Player: keepAlive ${JSON.stringify(message)}`); + }, + // onComplete: (message) => {}, + }); + const playService = window.webOS.service.request(`luna://${serviceId}/`, { method:"play", parameters: {}, onSuccess: (message: any) => { - console.log(JSON.stringify(message)); + // console.log(JSON.stringify(message)); if (message.value.subscribed === true) { console.log('Player: Registered play handler with service'); } @@ -122,8 +134,10 @@ if (TARGET === 'electron') { setVolumeService.cancel(); setSpeedService.cancel(); - // window.open('../main_window/index.html'); - history.back(); + // WebOS 22 and earlier does not work well using the history API, + // so manually handling page navigation... + // history.back(); + window.open('../main_window/index.html'); } }, onFailure: (message: any) => { diff --git a/receivers/common/web/player/Renderer.ts b/receivers/common/web/player/Renderer.ts index e410b8a..2960a77 100644 --- a/receivers/common/web/player/Renderer.ts +++ b/receivers/common/web/player/Renderer.ts @@ -258,6 +258,14 @@ function onPlay(_event, value: PlayMessage) { hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => { isLive = level.details.live; isLivePosition = isLive ? true : false; + + // Event can fire after video load and play initialization + if (isLive && playerCtrlLiveBadge.style.display === "none") { + playerCtrlLiveBadge.style.display = "block"; + playerCtrlPosition.style.display = "none"; + playerCtrlDurationSeparator.style.display = "none"; + playerCtrlDuration.style.display = "none"; + } }); player = new Player(PlayerType.Hls, videoElement, hlsPlayer); @@ -717,6 +725,10 @@ const volumeIncrement = 0.1; function keyDownEventListener(event: any) { // console.log("KeyDown", event); + const handledCase = targetKeyDownEventListener(event); + if (handledCase) { + return; + } switch (event.code) { case 'KeyF': @@ -729,15 +741,11 @@ function keyDownEventListener(event: any) { event.preventDefault(); break; case 'ArrowLeft': - // Skip back - player.setCurrentTime(Math.max(player.getCurrentTime() - skipInterval, 0)); + skipBack(); event.preventDefault(); break; case 'ArrowRight': - // Skip forward - if (!isLivePosition) { - player.setCurrentTime(Math.min(player.getCurrentTime() + skipInterval, player.getDuration())); - } + skipForward(); event.preventDefault(); break; case "Home": @@ -756,7 +764,7 @@ function keyDownEventListener(event: any) { case 'KeyK': case 'Space': case 'Enter': - // Pause/Continue + // Play/pause toggle if (player.isPaused()) { player.play(); } else { @@ -777,11 +785,20 @@ function keyDownEventListener(event: any) { volumeChangeHandler(Math.max(player.getVolume() - volumeIncrement, 0)); break; default: - targetKeyDownEventListener(event); break; } } +function skipBack() { + player.setCurrentTime(Math.max(player.getCurrentTime() - skipInterval, 0)); +} + +function skipForward() { + if (!isLivePosition) { + player.setCurrentTime(Math.min(player.getCurrentTime() + skipInterval, player.getDuration())); + } +} + document.addEventListener('keydown', keyDownEventListener); export { @@ -806,4 +823,6 @@ export { onPlay, playerCtrlStateUpdate, formatDuration, + skipBack, + skipForward, }; diff --git a/receivers/electron/src/player/Renderer.ts b/receivers/electron/src/player/Renderer.ts index a7ed7ab..761c052 100644 --- a/receivers/electron/src/player/Renderer.ts +++ b/receivers/electron/src/player/Renderer.ts @@ -42,20 +42,26 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function targetKeyDownEventListener(event: any) { +export function targetKeyDownEventListener(event: any): boolean { + let handledCase = false; + switch (event.code) { case 'KeyF': case 'F11': playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); event.preventDefault(); + handledCase = true; break; case 'Escape': playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen); event.preventDefault(); + handledCase = true; break; default: break; } + + return handledCase }; export { diff --git a/receivers/webos/fcast-receiver-service/src/Main.ts b/receivers/webos/fcast-receiver-service/src/Main.ts index e277b28..e9d743e 100644 --- a/receivers/webos/fcast-receiver-service/src/Main.ts +++ b/receivers/webos/fcast-receiver-service/src/Main.ts @@ -37,10 +37,16 @@ export class Main { const serviceId = 'com.futo.fcast.receiver.service'; const service = new Service(serviceId); + // Not compatible with WebOS 22 and earlier simulator? // Service will timeout and casting will disconnect if not forced to be kept alive - let keepAlive; - service.activityManager.create("keepAlive", function(activity) { - keepAlive = activity; + // let keepAlive; + // service.activityManager.create("keepAlive", function(activity) { + // keepAlive = activity; + // }); + + service.register("keepAlive", (_message: any) => { + Main.logger.info("In keepAlive callback"); + // Do not respond to keep service alive }); service.register("getDeviceInfo", (message: any) => { diff --git a/receivers/webos/fcast-receiver/appinfo.json b/receivers/webos/fcast-receiver/appinfo.json index d75d78c..91ef5cb 100644 --- a/receivers/webos/fcast-receiver/appinfo.json +++ b/receivers/webos/fcast-receiver/appinfo.json @@ -9,5 +9,6 @@ "icon": "assets/icons/icon.png", "largeIcon": "assets/icons/largeIcon.png", "iconColor": "#0a62f5", - "splashBackground": "assets/images/splash.png" + "splashBackground": "assets/images/splash.png", + "disableBackHistoryAPI": "true" } \ No newline at end of file diff --git a/receivers/webos/fcast-receiver/src/main/Preload.ts b/receivers/webos/fcast-receiver/src/main/Preload.ts index 88de86d..321b8a0 100644 --- a/receivers/webos/fcast-receiver/src/main/Preload.ts +++ b/receivers/webos/fcast-receiver/src/main/Preload.ts @@ -1,6 +1,29 @@ import 'common/main/Preload'; +enum RemoteKeyCode { + Stop = 413, + Rewind = 412, + Play = 415, + Pause = 19, + FastForward = 417, + Back = 461, +} + // Cannot go back to a state where user was previously casting a video, so exit. -window.onpopstate = () => { - window.webOS.platformBack(); -}; +// window.onpopstate = () => { +// window.webOS.platformBack(); +// }; + +document.addEventListener('keydown', (event: any) => { + // console.log("KeyDown", event); + + switch (event.keyCode) { + // WebOS 22 and earlier does not work well using the history API, + // so manually handling page navigation... + case RemoteKeyCode.Back: + window.webOS.platformBack(); + break; + default: + break; + } +}); diff --git a/receivers/webos/fcast-receiver/src/main/Renderer.ts b/receivers/webos/fcast-receiver/src/main/Renderer.ts index c5054fd..d40a73f 100644 --- a/receivers/webos/fcast-receiver/src/main/Renderer.ts +++ b/receivers/webos/fcast-receiver/src/main/Renderer.ts @@ -2,14 +2,19 @@ import 'common/main/Renderer'; const backgroundVideo = document.getElementById('video-player'); const loadingScreen = document.getElementById('loading-screen'); -let backgroundVideoLoaded = false; -let qrCodeRendered = false; + +// WebOS 6.0 requires global scope for access during callback invocation +// eslint-disable-next-line no-var +var backgroundVideoLoaded: boolean; +// eslint-disable-next-line no-var +var qrCodeRendered: boolean; backgroundVideo.onplaying = () => { backgroundVideoLoaded = true; if (backgroundVideoLoaded && qrCodeRendered) { loadingScreen.style.display = 'none'; + backgroundVideo.onplaying = null; } }; diff --git a/receivers/webos/fcast-receiver/src/main/style.css b/receivers/webos/fcast-receiver/src/main/style.css index 439def3..f884474 100644 --- a/receivers/webos/fcast-receiver/src/main/style.css +++ b/receivers/webos/fcast-receiver/src/main/style.css @@ -5,6 +5,17 @@ #overlay { font-family: InterRegular; font-size: 28px; + + /* gap not supported in WebOS 6.0 */ + gap: unset; +} + +#main-view { + padding: 25px; + + /* gap not supported in WebOS 6.0 */ + gap: unset; + margin-right: 15vw; } #title-text { diff --git a/receivers/webos/fcast-receiver/src/player/Renderer.ts b/receivers/webos/fcast-receiver/src/player/Renderer.ts index 37aa807..cb39d23 100644 --- a/receivers/webos/fcast-receiver/src/player/Renderer.ts +++ b/receivers/webos/fcast-receiver/src/player/Renderer.ts @@ -17,12 +17,23 @@ import { playerCtrlVolumeBarProgress, videoCaptions, formatDuration, + skipBack, + skipForward, } from 'common/player/Renderer'; const captionsBaseHeightCollapsed = 150; const captionsBaseHeightExpanded = 320; const captionsLineHeight = 68; +enum RemoteKeyCode { + Stop = 413, + Rewind = 412, + Play = 415, + Pause = 19, + FastForward = 417, + Back = 461, +} + export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean { let handledCase = false; @@ -74,11 +85,57 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function targetKeyDownEventListener(event: any) { - switch (event.code) { +export function targetKeyDownEventListener(event: any): boolean { + let handledCase = false; + + switch (event.keyCode) { + case RemoteKeyCode.Stop: + // history.back(); + window.open('../main_window/index.html'); + handledCase = true; + break; + + case RemoteKeyCode.Rewind: + skipBack(); + event.preventDefault(); + handledCase = true; + break; + + case RemoteKeyCode.Play: + if (player.isPaused()) { + player.play(); + } + event.preventDefault(); + handledCase = true; + break; + case RemoteKeyCode.Pause: + if (!player.isPaused()) { + player.pause(); + } + event.preventDefault(); + handledCase = true; + break; + + case RemoteKeyCode.FastForward: + skipForward(); + event.preventDefault(); + handledCase = true; + 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'); + event.preventDefault(); + handledCase = true; + break; + default: break; } + + return handledCase; }; if (window.webOSAPI.pendingPlay !== null) { diff --git a/receivers/webos/fcast-receiver/tsconfig.json b/receivers/webos/fcast-receiver/tsconfig.json index 335fb40..d05958f 100644 --- a/receivers/webos/fcast-receiver/tsconfig.json +++ b/receivers/webos/fcast-receiver/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "node", + "target": "ES2015", + "module": "ES2015", + "moduleResolution": "node10", "sourceMap": false, "emitDecoratorMetadata": true, "esModuleInterop": true,