UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

377 lines (311 loc) • 11 kB
import { BehaviorSubject } from "rxjs"; import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage"; import { isLgPlatform, isSamsungPlatform, } from "@applicaster/zapp-react-dom-app/App/Loader/utils/platform"; import { PLATFORM_KEYS, PLATFORMS, ZappPlatform } from "./const"; import { createLogger, utilsLogger } from "../../logger"; import { getPlatform } from "../../reactUtils"; import { appStore } from "@applicaster/zapp-react-native-redux/AppStore"; import { calculateReadingTime } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/utils"; const { log_debug } = createLogger({ category: "General", subsystem: "PlatformUtils", parent: utilsLogger, }); export const getZappPlatform = (): ZappPlatform => { const platform = appStore.get("appData")?.platform; switch (platform) { case "amazon_fire_tv": return ZappPlatform.AmazonFireTV; case ZappPlatform.Vizio: return ZappPlatform.Vizio; default: // TODO: Use only ZappPlatform enum return getPlatform() as ZappPlatform; } }; export const getUserAgent = () => window?.navigator?.userAgent; /** VIZIO UTILS */ /** * Checks if we are on the Vizio platform by using the userAgent string * * Use this method when you need to identify the platform on runtime * before the VIZIO Companion Library has loaded */ export const isVizioPlatform = () => { const userAgent = getUserAgent(); if (!userAgent) return false; const isVizioAgent = userAgent.includes(PLATFORMS.vizio) || userAgent.includes(PLATFORMS.smartcast) || userAgent.includes(PLATFORMS.conjure); return isVizioAgent; }; /** * Checks if we are on the Vizio platform by checking for the global VIZIO object * and the VIZIO Companion Library. This method does not use the userAgent string, * because it mainly checks whether we have access to apis, which may not be available yet */ export const hasVizioAPIs = () => { const hasAPIs = typeof window.VIZIO !== "undefined" && window?.applicaster?.vizioLibraryDidLoad; return hasAPIs; }; /** * Manages device based closed captioning state * Returns an observable that emits the current state and updates whenever it changes */ export class ClosedCaptioningManager { private ccState$ = new BehaviorSubject<boolean>(false); private static ccManagerInstance: ClosedCaptioningManager; private constructor() { this.initialize(); window["vizioDebug"] = { setCCEnabled: (isCCEnabled: boolean) => { this.ccState$.next(isCCEnabled); }, }; } static getInstance(): ClosedCaptioningManager { if (!ClosedCaptioningManager.ccManagerInstance) { log_debug("ClosedCaptioningManager: Creating new instance"); ClosedCaptioningManager.ccManagerInstance = new ClosedCaptioningManager(); } log_debug("ClosedCaptioningManager: Returning existing instance"); return ClosedCaptioningManager.ccManagerInstance; } async initialize() { const existingState = await sessionStorage.getItem( PLATFORM_KEYS.deviceClosedCaptioningEnabled ); if (existingState !== null) { log_debug( `ClosedCaptioningManager: Setting initial state to ${existingState}` ); this.ccState$.next(existingState === "true" || existingState === true); } else { log_debug("ClosedCaptioningManager: No existing state found"); } if (hasVizioAPIs()) { log_debug("ClosedCaptioningManager: Setting closed captioning handler"); window.VIZIO!.setClosedCaptionHandler((isCCEnabled) => { log_debug( "ClosedCaptioningManager: There was a change in the CC state", { isCCEnabled } ); sessionStorage.setItem( PLATFORM_KEYS.deviceClosedCaptioningEnabled, isCCEnabled ); log_debug( "ClosedCaptioningManager: Saved CC state to session storage", { isCCEnabled } ); this.ccState$.next(isCCEnabled); }); } else { log_debug( "ClosedCaptioningManager: Vizio APIs are not available, skipping setup" ); } } getCurrentState(): boolean { return this.ccState$.getValue(); } getStateAsObservable() { return this.ccState$.asObservable(); } } /** * Subscribe to closed captioning state changes * @returns Observable<boolean> * @example * const subscription = getClosedCaptionState().subscribe( * isEnabled => setClosedCaptionState(isEnabled) * ); * subscription.unsubscribe(), []); */ export const getClosedCaptionState = () => { const ccManager = ClosedCaptioningManager.getInstance(); return ccManager.getStateAsObservable(); }; /** * Manages device based closed captioning state * Returns an observable that emits the current state and updates whenever it changes */ export class TTSManager { private ttsState$ = new BehaviorSubject<boolean>(false); private screenReaderEnabled$ = new BehaviorSubject<boolean>(false); private static ttsManagerInstance: TTSManager; private samsungListenerId: number | null = null; private constructor() { this.initialize(); } static getInstance(): TTSManager { if (!TTSManager.ttsManagerInstance) { TTSManager.ttsManagerInstance = new TTSManager(); } return TTSManager.ttsManagerInstance; } async initialize() { if (isVizioPlatform()) { document.addEventListener( "VIZIO_TTS_ENABLED", () => { log_debug("Vizio screen reader enabled"); this.screenReaderEnabled$.next(true); }, false ); document.addEventListener( "VIZIO_TTS_DISABLED", () => { log_debug("Vizio screen reader disabled"); this.screenReaderEnabled$.next(false); }, false ); } if (isLgPlatform() && window.webOS?.service) { try { // https://webostv.developer.lge.com/develop/references/settings-service window.webOS.service.request("luna://com.webos.settingsservice", { method: "getSystemSettings", parameters: { category: "option", keys: ["audioGuidance"], subscribe: true, // Request a subscription to changes }, onSuccess: (response: any) => { const isEnabled = response?.settings?.audioGuidance === "on"; log_debug("LG Audio Guidance status changed", { isEnabled, response, }); this.screenReaderEnabled$.next(isEnabled); }, onFailure: (error: any) => { log_debug("webOS settings subscription failed", { error }); this.screenReaderEnabled$.next(false); }, }); } catch (error) { log_debug("webOS settings service request error", { error }); // Fallback to false if the service is not available this.screenReaderEnabled$.next(false); } } if (isSamsungPlatform() && typeof window.webapis !== "undefined") { try { if ( window.webapis?.tvinfo && typeof window.webapis.tvinfo.getMenuValue === "function" && typeof window.webapis.tvinfo.addCaptionChangeListener === "function" ) { // Get initial Voice Guide status const initialStatus = window.webapis.tvinfo.getMenuValue( window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY ); const isEnabled = initialStatus === window.webapis.tvinfo.TvInfoMenuValue.ON; log_debug("Samsung Voice Guide initial status", { isEnabled, initialStatus, }); this.screenReaderEnabled$.next(isEnabled); // Listen for Voice Guide status changes const onChange = () => { const currentStatus = window.webapis.tvinfo.getMenuValue( window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY ); const enabled = currentStatus === window.webapis.tvinfo.TvInfoMenuValue.ON; log_debug("Samsung Voice Guide status changed", { enabled, currentStatus, }); this.screenReaderEnabled$.next(enabled); }; this.samsungListenerId = window.webapis.tvinfo.addCaptionChangeListener( window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY, onChange ); log_debug("Samsung Voice Guide listener registered", { listenerId: this.samsungListenerId, }); } else { log_debug("Samsung TvInfo API not available"); this.screenReaderEnabled$.next(false); } } catch (error) { log_debug("Samsung Voice Guide listener error", { error }); // Fallback to false if the service is not available this.screenReaderEnabled$.next(false); } } } getCurrentState(): boolean { return this.ttsState$.getValue(); } getStateAsObservable() { return this.ttsState$.asObservable(); } getScreenReaderEnabledAsObservable() { return this.screenReaderEnabled$.asObservable(); } readText(text: string) { this.ttsState$.next(true); if (isSamsungPlatform() && window.speechSynthesis) { const utterance = new SpeechSynthesisUtterance(text); window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text window.speechSynthesis.speak(utterance); // Estimate reading time and set inactive when done this.scheduleTTSComplete(text); } if (isLgPlatform() && window.webOS?.service) { try { window.webOS.service.request("luna://com.webos.service.tts", { method: "speak", onFailure: (error: any) => { log_debug("There was a failure setting up webOS TTS service", { error, }); this.ttsState$.next(false); }, onSuccess: (response: any) => { log_debug("webOS TTS service is configured successfully", { response, }); // Estimate reading time and set inactive when done this.scheduleTTSComplete(text); }, parameters: { text, clear: true, // Clear any previous speech before speaking new text }, }); } catch (error) { log_debug("webOS TTS service error", { error }); this.ttsState$.next(false); } } if (!window.VIZIO?.Chromevox) { // For platforms without TTS, estimate reading time this.scheduleTTSComplete(text); return; } window.VIZIO.Chromevox.play(text); // Estimate reading time and set inactive when done this.scheduleTTSComplete(text); } private scheduleTTSComplete(text: string) { const readingTime = calculateReadingTime(text); setTimeout(() => { this.ttsState$.next(false); }, readingTime); } }