1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-08-03 07:47:01 +00:00

webOS: Reworked page navigation, service subscription, and resolution handling

This commit is contained in:
Michael Hollister 2025-07-15 17:22:12 -05:00
parent 0da73f1f5b
commit a549296aca
16 changed files with 1425 additions and 374 deletions

View file

@ -3,7 +3,7 @@
"version": "2.0.0",
"vendor": "FUTO",
"type": "web",
"main": "main_window/index.html",
"main": "index.html",
"title": "FCast Receiver",
"appDescription": "FCast Receiver",
"icon": "assets/icons/icon.png",

View file

@ -1,4 +1,9 @@
const logger = window.targetAPI.logger;
import { v4 as uuidv4 } from 'modules/uuid';
import { Logger, LoggerType } from 'common/Logger';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
const logger = new Logger('Common', LoggerType.FRONTEND);
const serviceId = 'com.futo.fcast.receiver.service';
export enum RemoteKeyCode {
@ -10,59 +15,104 @@ export enum RemoteKeyCode {
Back = 461,
}
export function requestService(method: string, successCb: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void): any {
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: {},
onSuccess: (message: any) => {
if (message.value?.subscribed === true) {
logger.info(`requestService: Registered ${method} handler with service`);
}
else {
successCb(message);
}
},
onFailure: (message: any) => {
logger.error(`requestService: ${method} ${JSON.stringify(message)}`);
export class ServiceManager {
private static serviceChannelSuccessCbHandler?: (message: any) => void;
private static serviceChannelFailureCbHandler?: (message: any) => void;
private static serviceChannelCompleteCbHandler?: (message: any) => void;
if (failureCb) {
failureCb(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
}
},
subscribe: true,
resubscribe: true
});
}
export function callService(method: string, parameters?: any, successCb?: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
return window.webOS.service.request(`luna://${serviceId}/`, {
method: method,
parameters: parameters,
constructor() {
// @ts-ignore
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'service_channel',
parameters: { subscriptionId: uuidv4() },
onSuccess: (message: any) => {
if (successCb) {
successCb(message);
if (message.value?.subscribed === true) {
logger.info(`requestService: Registered 'service_channel' handler with service`);
}
else if (ServiceManager.serviceChannelSuccessCbHandler) {
ServiceManager.serviceChannelSuccessCbHandler(message);
}
},
onFailure: (message: any) => {
logger.error(`callService: ${method} ${JSON.stringify(message)}`);
logger.error('Error subscribing to the service_channel:', message);
if (failureCb) {
failureCb(message);
if (ServiceManager.serviceChannelFailureCbHandler) {
ServiceManager.serviceChannelFailureCbHandler(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
if (ServiceManager.serviceChannelCompleteCbHandler) {
ServiceManager.serviceChannelCompleteCbHandler(message);
}
},
subscribe: false,
resubscribe: false
});
subscribe: true,
resubscribe: true
});
}
public subscribeToServiceChannel(successCb: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
ServiceManager.serviceChannelSuccessCbHandler = successCb;
ServiceManager.serviceChannelFailureCbHandler = failureCb;
ServiceManager.serviceChannelCompleteCbHandler = onCompleteCb;
}
public call(method: string, parameters?: any, successCb?: (message: any) => void, failureCb?: (message: any) => void, onCompleteCb?: (message: any) => void) {
// @ts-ignore
const service = window.webOS.service.request(`luna://${serviceId}/`, {
method: 'app_channel',
parameters: { event: method, value: parameters },
onSuccess: (message: any) => {
if (successCb) {
successCb(message);
}
},
onFailure: (message: any) => {
logger.error(`callService: ${method} ${JSON.stringify(message)}`);
if (failureCb) {
failureCb(message);
}
},
onComplete: (message: any) => {
if (onCompleteCb) {
onCompleteCb(message);
}
},
subscribe: false,
resubscribe: false
});
return service;
}
}
// CSS media queries do not work on older webOS versions...
export function initializeWindowSizeStylesheet() {
const resolution = sessionStorage.getItem('resolution');
if (resolution) {
window.onload = () => {
if (resolution == '1920x1080') {
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
}
else {
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
}
}
}
else {
window.onresize = () => {
if (window.innerWidth >= 1920 && window.innerHeight >= 1080) {
sessionStorage.setItem('resolution', '1920x1080');
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1920x1080.css" />');
}
else {
sessionStorage.setItem('resolution', '1280x720');
document.head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" href="./1280x720.css" />');
}
};
}
}
export function targetKeyUpEventListener(event: KeyboardEvent): { handledCase: boolean, key: string } {

View file

@ -0,0 +1,32 @@
import { Logger, LoggerType } from 'common/Logger';
import { ServiceManager } from 'lib/common';
declare global {
interface Window {
webOSApp: any;
}
}
const logger = new Logger('Main', LoggerType.FRONTEND);
const webPage: HTMLIFrameElement = document.getElementById('page') as HTMLIFrameElement;
let launchHandlerCallback = () => { logger.warn('No (re)launch handler set'); };
function loadPage(path: string) {
// @ts-ignore
webPage.src = path;
}
// We are embedding iframe element and using that for page navigation. This preserves a global JS context
// so bugs related to oversubscribing/canceling services are worked around by only subscribing once to
// required services
logger.info('Starting webOS application')
window.webOSApp = {
serviceManager: new ServiceManager(),
setLaunchHandler: (callback: () => void) => launchHandlerCallback = callback,
loadPage: loadPage
};
document.addEventListener('webOSLaunch', launchHandlerCallback);
document.addEventListener('webOSRelaunch', launchHandlerCallback);
loadPage('./main_window/index.html');

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<style>
iframe, body, html {
width: 100%;
height: 100%;
margin: 0;
border: none;
}
</style>
</head>
<body>
<iframe id="page", src="./main_window/index.html"></iframe>
<script src="./main.js"></script>
</body>
</html>

View file

@ -0,0 +1,101 @@
/* @media only screen and ((max-width: 1279px) or (max-height: 719px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
/* } */

View file

@ -0,0 +1,204 @@
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 20px;
margin: 5px;
margin-bottom: 10px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 32px;
height: 32px;
}
#overlay {
gap: 12.5vw;
font-size: 20px;
}
#title-text {
font-size: 100px;
}
#title-icon {
width: 84px;
height: 84px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 192px;
height: 192px;
margin: 15px auto;
padding: 12px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 4px;
margin-bottom: 4px;
}
#window-can-be-closed {
margin-bottom: 15px;
font-size: 18px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#connection-check {
width: 84px;
height: 84px;
margin: 24px;
}
#toast-notification {
padding: 8px;
top: -140px;
}
#toast-icon {
width: 60px;
height: 60px;
margin-right: 15px;
}
#toast-text {
font-size: 20px;
}
/* } */
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
}

View file

@ -3,23 +3,56 @@
import { preloadData } from 'common/main/Preload';
import { ToastIcon } from 'common/components/Toast';
import { EventMessage } from 'common/Packets';
import { callService, requestService } from 'lib/common';
import { ServiceManager, initializeWindowSizeStylesheet } from 'lib/common';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
declare global {
interface Window {
targetAPI: any;
webOSApp: any;
}
}
const logger = window.targetAPI.logger;
try {
const serviceId = 'com.futo.fcast.receiver.service';
let getSessionsService = null;
let networkChangedService = null;
let visibilityChangedService = null;
initializeWindowSizeStylesheet();
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
serviceManager.subscribeToServiceChannel((message: any) => {
switch (message.event) {
case 'toast':
preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration);
break;
case 'event_subscribed_keys_update':
preloadData.onEventSubscribedKeysUpdate(message.value);
break;
case 'connect':
preloadData.onConnectCb(null, message.value);
break;
case 'disconnect':
preloadData.onDisconnectCb(null, message.value);
break;
case 'play':
logger.info(`Main: Playing ${JSON.stringify(message)}`);
play(message.value);
break;
default:
break;
}
});
const toastService = requestService('toast', (message: any) => { preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration); });
const getDeviceInfoService = window.webOSDev.connection.getStatus({
onSuccess: (message: any) => {
logger.info('Network info status message', message);
const deviceName = 'FCast-LGwebOSTV';
const connections = [];
const connections: any[] = [];
let fallback = true;
if (message.wired.state !== 'disconnected') {
@ -35,7 +68,10 @@ try {
}
if (fallback) {
networkChangedService = callService('network_changed', { fallback: fallback }, (message: any) => {
const ipsIfaceName = document.getElementById('ips-iface-name');
ipsIfaceName.style.display = 'none';
serviceManager.call('network_changed', { fallback: fallback }, (message: any) => {
logger.info('Fallback network interfaces', message);
for (const ipAddr of message.value) {
connections.push({ type: 'wired', name: 'Ethernet', address: ipAddr });
@ -46,14 +82,10 @@ try {
}, (message: any) => {
logger.error('Main: preload - error fetching network interfaces', message);
preloadData.onToastCb('Error detecting network interfaces', ToastIcon.ERROR);
}, () => {
networkChangedService = null;
});
}
else {
networkChangedService = callService('network_changed', { fallback: fallback }, null, null, () => {
networkChangedService = null;
});
serviceManager.call('network_changed', { fallback: fallback });
preloadData.deviceInfo = { name: deviceName, interfaces: connections };
preloadData.onDeviceInfoCb();
}
@ -66,74 +98,39 @@ try {
resubscribe: true
});
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
window.targetAPI.getSessions(() => {
return new Promise((resolve, reject) => {
getSessionsService = callService('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
serviceManager.call('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
});
});
const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); });
const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
preloadData.sendEventCb = (event: EventMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_event',
parameters: { event },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); },
});
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
};
const playService = requestService('play', (message: any) => {
logger.info(`Main: Playing ${JSON.stringify(message)}`);
play(message.value);
});
const launchHandler = () => {
const params = window.webOSDev.launchParams();
logger.info(`Main: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
// WebOS 6.0 and earlier: Timestamp tracking seems to be necessary as launch event is raised regardless if app is in foreground or not
const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
const lastTimestamp = Number(sessionStorage.getItem('lastTimestamp'));
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('lastTimestamp', params.timestamp);
play(params.messageInfo);
}
};
document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => {
visibilityChangedService = callService('visibility_changed', { hidden: document.hidden, window: 'main' }, null, null, () => {
visibilityChangedService = null;
})
});
// Cannot go back to a state where user was previously casting a video, so exit.
// window.onpopstate = () => {
// window.webOS.platformBack();
// };
window.parent.webOSApp.setLaunchHandler(launchHandler);
document.addEventListener('visibilitychange', () => { serviceManager.call('visibility_changed', { hidden: document.hidden, window: 'main' }); });
const play = (messageInfo: any) => {
sessionStorage.setItem('playInfo', JSON.stringify(messageInfo));
getDeviceInfoService?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
getSessionsService?.cancel();
toastService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
playService?.cancel();
networkChangedService?.cancel();
visibilityChangedService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${messageInfo.contentViewer}/index.html`, '_self');
window.parent.webOSApp.loadPage(`${messageInfo.contentViewer}/index.html`);
};
}
catch (err) {
logger.error(`Main: preload ${JSON.stringify(err)}`);
logger.error(`Main: preload`, err);
preloadData.onToastCb(`Error starting the application: ${JSON.stringify(err)}`, ToastIcon.ERROR);
}

View file

@ -1,3 +1,9 @@
/* WebOS custom player styles */
html {
overflow: hidden;
}
.card-title {
font-family: InterBold;
}
@ -27,6 +33,14 @@
font-family: InterBold;
}
#ips {
gap: unset;
}
#ips-iface-icon {
margin-right: 15px;
}
#window-can-be-closed {
font-family: InterRegular;
}

View file

@ -0,0 +1,101 @@
/* @media only screen and ((max-width: 1279px) or (max-height: 719px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
/* } */

View file

@ -0,0 +1,204 @@
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 20px;
margin: 5px;
margin-bottom: 10px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 32px;
height: 32px;
}
#overlay {
gap: 12.5vw;
font-size: 20px;
}
#title-text {
font-size: 100px;
}
#title-icon {
width: 84px;
height: 84px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 192px;
height: 192px;
margin: 15px auto;
padding: 12px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 4px;
margin-bottom: 4px;
}
#window-can-be-closed {
margin-bottom: 15px;
font-size: 18px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#connection-check {
width: 84px;
height: 84px;
margin: 24px;
}
#toast-notification {
padding: 8px;
top: -140px;
}
#toast-icon {
width: 60px;
height: 60px;
margin-right: 15px;
}
#toast-text {
font-size: 20px;
}
/* } */
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
}

View file

@ -2,190 +2,149 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { preloadData } from 'common/player/Preload';
import { EventMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, VolumeUpdateMessage } from 'common/Packets';
import { callService, requestService } from 'lib/common';
import { ServiceManager, initializeWindowSizeStylesheet } from 'lib/common';
import { toast, ToastIcon } from 'common/components/Toast';
require('lib/webOSTVjs-1.2.10/webOSTV.js');
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
declare global {
interface Window {
targetAPI: any;
webOSAPI: any;
webOSApp: any;
}
}
const logger = window.targetAPI.logger;
const serviceId = 'com.futo.fcast.receiver.service';
try {
let getSessions = null;
initializeWindowSizeStylesheet();
window.webOSAPI = {
pendingPlay: JSON.parse(sessionStorage.getItem('playInfo'))
};
const contentViewer = window.webOSAPI.pendingPlay?.contentViewer;
const serviceManager: ServiceManager = window.parent.webOSApp.serviceManager;
serviceManager.subscribeToServiceChannel((message: any) => {
switch (message.event) {
case 'toast':
preloadData.onToastCb(message.value.message, message.value.icon, message.value.duration);
break;
case 'play': {
if (contentViewer !== message.value.contentViewer) {
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;
}
else {
preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
}
}
else {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayCb(null, message.value.rendererMessage);
}
}
}
break;
}
case 'pause':
preloadData.onPauseCb();
break;
case 'resume':
preloadData.onResumeCb();
break;
case 'stop':
window.parent.webOSApp.loadPage('main_window/index.html');
break;
case 'seek':
preloadData.onSeekCb(null, message.value);
break;
case 'setvolume':
preloadData.onSetVolumeCb(null, message.value);
break;
case 'setspeed':
preloadData.onSetSpeedCb(null, message.value);
break;
case 'setplaylistitem':
preloadData.onSetPlaylistItemCb(null, message.value);
break;
case 'event_subscribed_keys_update':
preloadData.onEventSubscribedKeysUpdate(message.value);
break;
case 'connect':
preloadData.onConnectCb(null, message.value);
break;
case 'disconnect':
preloadData.onDisconnectCb(null, message.value);
break;
// 'play-playlist' is handled in the 'play' message for webOS
default:
break;
}
});
preloadData.sendPlaybackErrorCb = (error: PlaybackErrorMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_playback_error',
parameters: { error },
onSuccess: () => {},
onFailure: (message: any) => {
logger.error(`Player: send_playback_error ${JSON.stringify(message)}`);
},
});
serviceManager.call('send_playback_error', error, null, (message: any) => { logger.error(`Player: send_playback_error ${JSON.stringify(message)}`); });
};
preloadData.sendPlaybackUpdateCb = (update: PlaybackUpdateMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_playback_update',
parameters: { update },
// onSuccess: (message: any) => {
// logger.info(`Player: send_playback_update ${JSON.stringify(message)}`);
// },
onSuccess: () => {},
onFailure: (message: any) => {
logger.error(`Player: send_playback_update ${JSON.stringify(message)}`);
},
});
serviceManager.call('send_playback_update', update, null, (message: any) => { logger.error(`Player: send_playback_update ${JSON.stringify(message)}`); });
};
preloadData.sendVolumeUpdateCb = (update: VolumeUpdateMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_volume_update',
parameters: { update },
onSuccess: () => {},
onFailure: (message: any) => {
logger.error(`Player: send_volume_update ${JSON.stringify(message)}`);
},
});
serviceManager.call('send_volume_update', update, null, (message: any) => { logger.error(`Player: send_volume_update ${JSON.stringify(message)}`); });
};
preloadData.sendEventCb = (event: EventMessage) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'send_event',
parameters: { event },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); },
});
serviceManager.call('send_event', event, null, (message: any) => { logger.error(`Player: send_event ${JSON.stringify(message)}`); });
};
const playService = requestService('play', (message: any) => {
if (contentViewer !== message.value.contentViewer) {
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.pushState({}, '', '../main_window/index.html');
window.open(`../${message.value.contentViewer}/index.html`, '_self');
}
else {
if (message.value.rendererEvent === 'play-playlist') {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayPlaylistCb(null, message.value.rendererMessage);
}
}
else {
if (preloadData.onPlayCb === undefined) {
window.webOSAPI.pendingPlay = message.value;
}
else {
preloadData.onPlayCb(null, message.value.rendererMessage);
}
}
}
}, (message: any) => {
logger.error(`Player: play ${JSON.stringify(message)}`);
});
const pauseService = requestService('pause', () => { preloadData.onPauseCb(); });
const resumeService = requestService('resume', () => { preloadData.onResumeCb(); });
const stopService = requestService('stop', () => {
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// WebOS 22 and earlier does not work well using the history API,
// so manually handling page navigation...
// history.back();
window.open('../main_window/index.html', '_self');
});
const seekService = requestService('seek', (message: any) => { preloadData.onSeekCb(null, message.value); });
const setVolumeService = requestService('setvolume', (message: any) => { preloadData.onSetVolumeCb(null, message.value); });
const setSpeedService = requestService('setspeed', (message: any) => { preloadData.onSetSpeedCb(null, message.value); });
const onSetPlaylistItemService = requestService('setplaylistitem', (message: any) => { preloadData.onSetPlaylistItemCb(null, message.value); });
preloadData.sendPlayRequestCb = (message: PlayMessage, playlistIndex: number) => {
window.webOS.service.request(`luna://${serviceId}/`, {
method: 'play_request',
parameters: { message: message, playlistIndex: playlistIndex },
onSuccess: () => {},
onFailure: (message: any) => { logger.error(`Player: play_request ${playlistIndex} ${JSON.stringify(message)}`); },
});
serviceManager.call('play_request', { message: message, playlistIndex: playlistIndex }, null, (message: any) => { logger.error(`Player: play_request ${playlistIndex} ${JSON.stringify(message)}`); });
};
window.targetAPI.getSessions(() => {
return new Promise((resolve, reject) => {
getSessions = callService('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
serviceManager.call('get_sessions', {}, (message: any) => resolve(message.value), (message: any) => reject(message));
});
});
const onEventSubscribedKeysUpdateService = requestService('event_subscribed_keys_update', (message: any) => { preloadData.onEventSubscribedKeysUpdate(message.value); });
const onConnectService = requestService('connect', (message: any) => { preloadData.onConnectCb(null, message.value); });
const onDisconnectService = requestService('disconnect', (message: any) => { preloadData.onDisconnectCb(null, message.value); });
const onPlayPlaylistService = requestService('play-playlist', (message: any) => { preloadData.onPlayPlaylistCb(null, message.value); });
const launchHandler = () => {
// args don't seem to be passed in via event despite what documentation says...
const params = window.webOSDev.launchParams();
logger.info(`Player: (Re)launching FCast Receiver with args: ${JSON.stringify(params)}`);
// WebOS 6.0 and earlier: Timestamp tracking seems to be necessary as launch event is raised regardless if app is in foreground or not
const lastTimestamp = Number(localStorage.getItem('lastTimestamp'));
const lastTimestamp = Number(sessionStorage.getItem('lastTimestamp'));
if (params.messageInfo !== undefined && params.timestamp != lastTimestamp) {
localStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('lastTimestamp', params.timestamp);
sessionStorage.setItem('playInfo', JSON.stringify(params.messageInfo));
playService?.cancel();
pauseService?.cancel();
resumeService?.cancel();
stopService?.cancel();
seekService?.cancel();
setVolumeService?.cancel();
setSpeedService?.cancel();
onSetPlaylistItemService?.cancel();
getSessions?.cancel();
onEventSubscribedKeysUpdateService?.cancel();
onConnectService?.cancel();
onDisconnectService?.cancel();
onPlayPlaylistService?.cancel();
// 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(`../${params.messageInfo.contentViewer}/index.html`, '_self');
window.parent.webOSApp.loadPage(`${params.messageInfo.contentViewer}/index.html`);
}
};
document.addEventListener('webOSLaunch', launchHandler);
document.addEventListener('webOSRelaunch', launchHandler);
document.addEventListener('visibilitychange', () => callService('visibility_changed', { hidden: document.hidden, window: contentViewer }));
window.parent.webOSApp.setLaunchHandler(launchHandler);
document.addEventListener('visibilitychange', () => serviceManager.call('visibility_changed', { hidden: document.hidden, window: contentViewer }));
}
catch (err) {
logger.error(`Player: preload ${JSON.stringify(err)}`);
logger.error(`Player: preload`, err);
toast(`Error starting the video player (preload): ${JSON.stringify(err)}`, ToastIcon.ERROR);
}

View file

@ -0,0 +1,101 @@
/* @media only screen and ((max-width: 1279px) or (max-height: 719px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
/* } */

View file

@ -0,0 +1,204 @@
/* @media only screen and ((max-width: 1919px) or (max-height: 1079px)) { */
.card {
padding: 15px;
}
.card-title {
line-height: 20px;
margin: 5px;
margin-bottom: 10px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 32px;
height: 32px;
}
#overlay {
gap: 12.5vw;
font-size: 20px;
}
#title-text {
font-size: 100px;
}
#title-icon {
width: 84px;
height: 84px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 192px;
height: 192px;
margin: 15px auto;
padding: 12px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 4px;
margin-bottom: 4px;
}
#window-can-be-closed {
margin-bottom: 15px;
font-size: 18px;
}
.lds-ring {
width: 100px;
height: 100px;
}
.lds-ring div {
width: 84px;
height: 84px;
}
#connection-check {
width: 84px;
height: 84px;
margin: 24px;
}
#toast-notification {
padding: 8px;
top: -140px;
}
#toast-icon {
width: 60px;
height: 60px;
margin-right: 15px;
}
#toast-text {
font-size: 20px;
}
/* } */
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
.card {
padding: 15px;
}
.card-title {
line-height: 18px;
margin: 5px;
}
.card-title-separator {
margin: 3px 0px;
}
.iconSize {
width: 24px;
height: 24px;
}
#overlay {
gap: 10vw;
font-size: 18px;
}
#title-text {
font-size: 80px;
}
#title-icon {
width: 64px;
height: 64px;
margin-right: 15px;
}
#connection-status {
padding: 15px;
}
#connection-error-icon {
margin-top: 10px;
}
#connection-information-loading-text {
margin: 10px;
}
#scan-to-connect {
margin-top: 10px;
}
#qr-code {
width: 128px;
height: 128px;
margin: 15px auto;
padding: 8px;
}
#ips {
margin-top: 10px;
}
.ip-entry-text {
margin-top: 1.5px;
margin-bottom: 1.5px;
}
#window-can-be-closed {
margin-bottom: 10px;
font-size: 16px;
}
.lds-ring {
width: 80px;
height: 80px;
}
.lds-ring div {
width: 64px;
height: 64px;
}
#connection-check {
width: 64px;
height: 64px;
margin: 20px;
}
#toast-notification {
padding: 4px;
top: -100px;
}
#toast-icon {
width: 40px;
height: 40px;
}
#toast-text {
font-size: 18px;
}
}

View file

@ -1 +1,5 @@
/* Stub for future use */
/* WebOS custom player styles */
html {
overflow: hidden;
}

View file

@ -12,6 +12,71 @@ const TARGET = 'webOS';
// const TARGET = 'tizenOS';
module.exports = [
{
mode: buildMode,
entry: {
main: './src/Main.ts',
},
target: ['web', 'es5'],
module: {
rules: [
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, '../../common/web'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
},
{
test: /\.tsx?$/,
include: [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'src')],
use: [{ loader: 'ts-loader' }]
}
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'lib': path.resolve(__dirname, 'lib'),
'modules': path.resolve(__dirname, 'node_modules'),
'common': path.resolve(__dirname, '../../common/web'),
},
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
// Common assets
{
from: '../common/assets/**',
to: '[path][name][ext]',
context: path.resolve(__dirname, '..', '..', 'common'),
globOptions: { ignore: ['**/*.txt'] }
},
// Target assets
{ from: 'appinfo.json', to: '[name][ext]' },
{
from: '**',
to: 'assets/[path][name][ext]',
context: path.resolve(__dirname, 'assets'),
globOptions: { ignore: ['**/*.svg'] }
},
{
from: '**',
to: 'lib/[name][ext]',
context: path.resolve(__dirname, 'lib'),
globOptions: { ignore: ['**/*.txt'] }
},
{ from: './src/index.html', to: '[name][ext]' }
],
}),
new webpack.DefinePlugin({
TARGET: JSON.stringify(TARGET)
})
]
},
{
mode: buildMode,
entry: {
@ -50,31 +115,10 @@ module.exports = [
plugins: [
new CopyWebpackPlugin({
patterns: [
// Common assets
{
from: '../common/assets/**',
to: '../[path][name][ext]',
context: path.resolve(__dirname, '..', '..', 'common'),
globOptions: { ignore: ['**/*.txt'] }
},
{
from: '../../common/web/main/common.css',
to: '[name][ext]',
},
// Target assets
{ from: 'appinfo.json', to: '../[name][ext]' },
{
from: '**',
to: '../assets/[path][name][ext]',
context: path.resolve(__dirname, 'assets'),
globOptions: { ignore: ['**/*.svg'] }
},
{
from: '**',
to: '../lib/[name][ext]',
context: path.resolve(__dirname, 'lib'),
globOptions: { ignore: ['**/*.txt'] }
},
{
from: './src/main/*',
to: '[name][ext]',