@applicaster/zapp-react-native-utils 15.0.0-rc.4 → 15.0.0-rc.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
  2. package/appUtils/accessibilityManager/const.ts +4 -0
  3. package/appUtils/accessibilityManager/hooks.ts +20 -13
  4. package/appUtils/accessibilityManager/index.ts +28 -1
  5. package/appUtils/accessibilityManager/utils.ts +36 -5
  6. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +1 -0
  7. package/appUtils/focusManager/index.ios.ts +18 -1
  8. package/appUtils/focusManagerAux/utils/index.ts +18 -0
  9. package/appUtils/focusManagerAux/utils/utils.ios.ts +35 -0
  10. package/appUtils/platform/platformUtils.ts +116 -17
  11. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +94 -4
  12. package/appUtils/playerManager/OverlayObserver/utils.ts +32 -20
  13. package/appUtils/playerManager/conts.ts +21 -0
  14. package/appUtils/playerManager/player.ts +4 -0
  15. package/appUtils/playerManager/usePlayerState.tsx +14 -2
  16. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  17. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  18. package/arrayUtils/index.ts +5 -0
  19. package/cellUtils/index.ts +32 -0
  20. package/manifestUtils/defaultManifestConfigurations/player.js +59 -1
  21. package/manifestUtils/keys.js +21 -0
  22. package/manifestUtils/sharedConfiguration/screenPicker/utils.js +1 -0
  23. package/navigationUtils/index.ts +19 -16
  24. package/package.json +2 -2
  25. package/playerUtils/usePlayerTTS.ts +8 -3
  26. package/reactHooks/feed/useBatchLoading.ts +7 -1
  27. package/reactHooks/feed/usePipesCacheReset.ts +3 -1
  28. package/reactHooks/layout/index.ts +1 -1
  29. package/reactHooks/player/TVSeekControlller/TVSeekController.ts +27 -10
  30. package/utils/__tests__/mapAccum.test.ts +73 -0
  31. package/utils/index.ts +7 -0
  32. package/utils/mapAccum.ts +23 -0
  33. package/zappFrameworkUtils/HookCallback/callbackNavigationAction.ts +15 -1
@@ -35,8 +35,11 @@ export class AnalyticPlayerListener
35
35
  this.handleAnalyticEvent(PLAYBACK_EVENT.complete);
36
36
  };
37
37
 
38
- onError = (err: Error) => {
39
- this.handleAnalyticEvent(PLAYBACK_EVENT.error, err); // TODO: Check error format
38
+ onError = (err: QuickBrickPlayer.PlayerErrorI) => {
39
+ this.handleAnalyticEvent(
40
+ PLAYBACK_EVENT.error,
41
+ err.toObject?.() || { message: err.message }
42
+ );
40
43
  };
41
44
 
42
45
  onPlayerPause = (event) => {
@@ -31,6 +31,10 @@ export const BUTTON_ACCESSIBILITY_KEYS = {
31
31
  hint: "accessibility_close_mini_hint",
32
32
  },
33
33
  },
34
+ back_to_live: {
35
+ label: "back_to_live_label",
36
+ hint: "",
37
+ },
34
38
  maximize: {
35
39
  label: "accessibility_maximize_label",
36
40
  hint: "accessibility_maximize_hint",
@@ -23,19 +23,6 @@ export const useAccessibilityManager = (
23
23
  }
24
24
  }, [pluginConfiguration, accessibilityManager]);
25
25
 
26
- useEffect(() => {
27
- const subscription = accessibilityManager.getStateAsObservable().subscribe({
28
- next: () => {
29
- // TODO: handle accessibility states
30
- // screenReaderEnabled: false
31
- // reduceMotionEnabled: false
32
- // boldTextEnabled: false
33
- },
34
- });
35
-
36
- return () => subscription.unsubscribe();
37
- }, [accessibilityManager]);
38
-
39
26
  return accessibilityManager;
40
27
  };
41
28
 
@@ -72,3 +59,23 @@ export const useAnnouncementActive = (
72
59
 
73
60
  return isActive;
74
61
  };
62
+
63
+ export const useAccessibilityState = (
64
+ pluginConfiguration: Record<string, any> = {}
65
+ ) => {
66
+ const accessibilityManager = useAccessibilityManager(pluginConfiguration);
67
+
68
+ const [state, setState] = useState<AccessibilityState>(
69
+ accessibilityManager.getState()
70
+ );
71
+
72
+ useEffect(() => {
73
+ const subscription = accessibilityManager
74
+ .getStateAsObservable()
75
+ .subscribe(setState);
76
+
77
+ return () => subscription.unsubscribe();
78
+ }, [accessibilityManager]);
79
+
80
+ return state;
81
+ };
@@ -36,7 +36,20 @@ export class AccessibilityManager {
36
36
  false
37
37
  );
38
38
 
39
- private constructor() {}
39
+ private constructor() {
40
+ this.ttsManager
41
+ .getScreenReaderEnabledAsObservable()
42
+ .subscribe((enabled) => {
43
+ const state = this.state$.getValue();
44
+
45
+ if (state.screenReaderEnabled !== enabled) {
46
+ this.state$.next({
47
+ ...state,
48
+ screenReaderEnabled: enabled,
49
+ });
50
+ }
51
+ });
52
+ }
40
53
 
41
54
  public static getInstance(): AccessibilityManager {
42
55
  if (!AccessibilityManager._instance) {
@@ -92,8 +105,15 @@ export class AccessibilityManager {
92
105
  /**
93
106
  * Adds a heading to the queue, headings will be read before the next text
94
107
  * Each heading will be read once and removed from the queue
108
+ * Does nothing if screen reader is not enabled
95
109
  */
96
110
  public addHeading(heading: string) {
111
+ const state = this.state$.getValue();
112
+
113
+ if (!state.screenReaderEnabled) {
114
+ return;
115
+ }
116
+
97
117
  if (!this.pendingFocusId) {
98
118
  this.pendingFocusId = Date.now().toString();
99
119
  }
@@ -108,6 +128,7 @@ export class AccessibilityManager {
108
128
  *
109
129
  * Implements a delay mechanism to reduce noise during rapid navigation.
110
130
  * Only the most recent announcement will be read after the delay period.
131
+ * Does nothing if screen reader is not enabled
111
132
  */
112
133
  public readText({
113
134
  text,
@@ -116,6 +137,12 @@ export class AccessibilityManager {
116
137
  text: string;
117
138
  keyOfLocalizedText?: string;
118
139
  }) {
140
+ const state = this.state$.getValue();
141
+
142
+ if (!state.screenReaderEnabled) {
143
+ return;
144
+ }
145
+
119
146
  let textToRead = text;
120
147
 
121
148
  if (keyOfLocalizedText) {
@@ -12,13 +12,44 @@ export function calculateReadingTime(
12
12
  minimumPause: number = 500,
13
13
  announcementDelay: number = 700
14
14
  ): number {
15
- const words = text
16
- .trim()
15
+ const trimmed = text.trim();
16
+
17
+ // Count words (split on whitespace and punctuation, keep alnum boundaries)
18
+ const words = trimmed
17
19
  .split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
18
20
  .filter(Boolean).length;
19
21
 
20
- return (
21
- Math.max(minimumPause, (words / wordsPerMinute) * 60 * 1000) +
22
- announcementDelay
22
+ // Count spaces - multiple consecutive spaces add extra pause time
23
+ const spaceMatches: string[] = trimmed.match(/\s+/g) || [];
24
+
25
+ const totalSpaces = spaceMatches.reduce(
26
+ (sum: number, match: string) => sum + match.length,
27
+ 0
23
28
  );
29
+
30
+ const extraSpaces = Math.max(0, totalSpaces - (words - 1)); // words-1 is normal spacing
31
+
32
+ // Heuristic: punctuation increases TTS duration beyond word-based WPM.
33
+ // Commas typically introduce short pauses, sentence terminators longer ones.
34
+ const commaCount = (trimmed.match(/,/g) || []).length;
35
+ const semicolonCount = (trimmed.match(/;/g) || []).length;
36
+ const colonCount = (trimmed.match(/:/g) || []).length;
37
+ const dashCount = (trimmed.match(/\u2013|\u2014|-/g) || []).length; // – — -
38
+ const sentenceEndCount = (trimmed.match(/[.!?](?!\d)/g) || []).length;
39
+
40
+ const commaPauseMs = 220; // short prosody pause for ","
41
+ const midPauseMs = 260; // for ";", ":", dashes
42
+ const sentenceEndPauseMs = 420; // for ".", "!", "?"
43
+ const extraSpacePauseMs = 50; // per extra space beyond normal spacing
44
+
45
+ const punctuationPause =
46
+ commaCount * commaPauseMs +
47
+ (semicolonCount + colonCount + dashCount) * midPauseMs +
48
+ sentenceEndCount * sentenceEndPauseMs +
49
+ extraSpaces * extraSpacePauseMs;
50
+
51
+ const baseByWordsMs = (words / wordsPerMinute) * 60 * 1000;
52
+ const estimatedMs = Math.max(minimumPause, baseByWordsMs + punctuationPause);
53
+
54
+ return estimatedMs + announcementDelay;
24
55
  }
@@ -69,6 +69,7 @@ exports[`focusManagerIOS should be defined 1`] = `
69
69
  "getGroupRootById": [Function],
70
70
  "getPreferredFocusChild": [Function],
71
71
  "invokeHandler": [Function],
72
+ "isChildOf": [Function],
72
73
  "isFocusOn": [Function],
73
74
  "isGroupItemFocused": [Function],
74
75
  "moveFocus": [Function],
@@ -1,12 +1,20 @@
1
1
  import { NativeModules } from "react-native";
2
2
  import * as R from "ramda";
3
3
 
4
- import { isCurrentFocusOn } from "../focusManagerAux/utils";
4
+ import {
5
+ isCurrentFocusOn,
6
+ isChildOf as isChildOfUtils,
7
+ } from "../focusManagerAux/utils";
5
8
  import { Tree } from "./treeDataStructure/Tree";
6
9
  import { findFocusableNode } from "./treeDataStructure/Utils";
7
10
  import { subscriber } from "../../functionUtils";
8
11
  import { findChild } from "./utils";
9
12
 
13
+ import {
14
+ emitRegistered,
15
+ emitUnregistered,
16
+ } from "../focusManagerAux/utils/utils.ios";
17
+
10
18
  const { FocusableManagerModule } = NativeModules;
11
19
 
12
20
  /**
@@ -180,10 +188,14 @@ export const focusManager = (function () {
180
188
  function register({ id, component }) {
181
189
  const { isGroup = false } = component;
182
190
 
191
+ emitRegistered(id);
192
+
183
193
  return isGroup ? registerGroup(id, component) : registerItem(id, component);
184
194
  }
185
195
 
186
196
  function unregister(id, { group = false } = {}) {
197
+ emitUnregistered(id);
198
+
187
199
  group ? unregisterGroup(id) : unregisterItem(id);
188
200
  }
189
201
 
@@ -400,6 +412,10 @@ export const focusManager = (function () {
400
412
  return id && isCurrentFocusOn(id, currentFocusNode);
401
413
  }
402
414
 
415
+ function isChildOf(childId, parentId): boolean {
416
+ return isChildOfUtils(focusableTree, childId, parentId);
417
+ }
418
+
403
419
  return {
404
420
  on,
405
421
  invokeHandler,
@@ -422,5 +438,6 @@ export const focusManager = (function () {
422
438
  isGroupItemFocused,
423
439
  getPreferredFocusChild,
424
440
  isFocusOn,
441
+ isChildOf,
425
442
  };
426
443
  })();
@@ -190,3 +190,21 @@ export const isCurrentFocusOn = (id, node) => {
190
190
 
191
191
  return isCurrentFocusOn(id, node.parent);
192
192
  };
193
+
194
+ export const isChildOf = (focusableTree, childId, parentId) => {
195
+ if (isNil(childId) || isNil(parentId)) {
196
+ return false;
197
+ }
198
+
199
+ const childNode = focusableTree.findInTree(childId);
200
+
201
+ if (isNil(childNode)) {
202
+ return false;
203
+ }
204
+
205
+ if (childNode.parent?.id === parentId) {
206
+ return true;
207
+ }
208
+
209
+ return isChildOf(focusableTree, childNode.parent?.id, parentId);
210
+ };
@@ -0,0 +1,35 @@
1
+ import { ReplaySubject } from "rxjs";
2
+ import { filter } from "rxjs/operators";
3
+ import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
4
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
5
+
6
+ type FocusableID = string;
7
+ type RegistrationEvent = {
8
+ id: FocusableID;
9
+ registered: boolean;
10
+ };
11
+
12
+ const isFocusableButton = (id: Option<FocusableID>): boolean =>
13
+ id && id.includes?.(BUTTON_PREFIX);
14
+
15
+ const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
16
+
17
+ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
18
+ registeredSubject$.pipe(
19
+ filter(
20
+ (value) =>
21
+ value.registered && focusManager.isChildOf(value.id, focusableGroupId)
22
+ )
23
+ );
24
+
25
+ export const emitRegistered = (id: Option<FocusableID>): void => {
26
+ if (isFocusableButton(id)) {
27
+ registeredSubject$.next({ id, registered: true });
28
+ }
29
+ };
30
+
31
+ export const emitUnregistered = (id: Option<FocusableID>): void => {
32
+ if (isFocusableButton(id)) {
33
+ registeredSubject$.next({ id, registered: false });
34
+ }
35
+ };
@@ -170,7 +170,9 @@ export const getClosedCaptionState = () => {
170
170
  */
171
171
  export class TTSManager {
172
172
  private ttsState$ = new BehaviorSubject<boolean>(false);
173
+ private screenReaderEnabled$ = new BehaviorSubject<boolean>(false);
173
174
  private static ttsManagerInstance: TTSManager;
175
+ private samsungListenerId: number | null = null;
174
176
 
175
177
  private constructor() {
176
178
  this.initialize();
@@ -185,23 +187,116 @@ export class TTSManager {
185
187
  }
186
188
 
187
189
  async initialize() {
188
- if (!isVizioPlatform()) return;
190
+ if (isVizioPlatform()) {
191
+ document.addEventListener(
192
+ "VIZIO_TTS_ENABLED",
193
+ () => {
194
+ log_debug("Vizio screen reader enabled");
195
+ this.screenReaderEnabled$.next(true);
196
+ },
197
+ false
198
+ );
189
199
 
190
- document.addEventListener(
191
- "VIZIO_TTS_ENABLED",
192
- () => {
193
- this.ttsState$.next(true);
194
- },
195
- false
196
- );
200
+ document.addEventListener(
201
+ "VIZIO_TTS_DISABLED",
202
+ () => {
203
+ log_debug("Vizio screen reader disabled");
204
+ this.screenReaderEnabled$.next(false);
205
+ },
206
+ false
207
+ );
208
+ }
197
209
 
198
- document.addEventListener(
199
- "VIZIO_TTS_DISABLED",
200
- () => {
201
- this.ttsState$.next(false);
202
- },
203
- false
204
- );
210
+ if (isLgPlatform() && window.webOS?.service) {
211
+ try {
212
+ // https://webostv.developer.lge.com/develop/references/settings-service
213
+ window.webOS.service.request("luna://com.webos.settingsservice", {
214
+ method: "getSystemSettings",
215
+ parameters: {
216
+ category: "option",
217
+ keys: ["audioGuidance"],
218
+ subscribe: true, // Request a subscription to changes
219
+ },
220
+ onSuccess: (response: any) => {
221
+ const isEnabled = response?.settings?.audioGuidance === "on";
222
+
223
+ log_debug("LG Audio Guidance status changed", {
224
+ isEnabled,
225
+ response,
226
+ });
227
+
228
+ this.screenReaderEnabled$.next(isEnabled);
229
+ },
230
+ onFailure: (error: any) => {
231
+ log_debug("webOS settings subscription failed", { error });
232
+ this.screenReaderEnabled$.next(false);
233
+ },
234
+ });
235
+ } catch (error) {
236
+ log_debug("webOS settings service request error", { error });
237
+ // Fallback to false if the service is not available
238
+ this.screenReaderEnabled$.next(false);
239
+ }
240
+ }
241
+
242
+ if (isSamsungPlatform() && typeof window.webapis !== "undefined") {
243
+ try {
244
+ if (
245
+ window.webapis?.tvinfo &&
246
+ typeof window.webapis.tvinfo.getMenuValue === "function" &&
247
+ typeof window.webapis.tvinfo.addCaptionChangeListener === "function"
248
+ ) {
249
+ // Get initial Voice Guide status
250
+ const initialStatus = window.webapis.tvinfo.getMenuValue(
251
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY
252
+ );
253
+
254
+ const isEnabled =
255
+ initialStatus === window.webapis.tvinfo.TvInfoMenuValue.ON;
256
+
257
+ log_debug("Samsung Voice Guide initial status", {
258
+ isEnabled,
259
+ initialStatus,
260
+ });
261
+
262
+ this.screenReaderEnabled$.next(isEnabled);
263
+
264
+ // Listen for Voice Guide status changes
265
+ const onChange = () => {
266
+ const currentStatus = window.webapis.tvinfo.getMenuValue(
267
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY
268
+ );
269
+
270
+ const enabled =
271
+ currentStatus === window.webapis.tvinfo.TvInfoMenuValue.ON;
272
+
273
+ log_debug("Samsung Voice Guide status changed", {
274
+ enabled,
275
+ currentStatus,
276
+ });
277
+
278
+ this.screenReaderEnabled$.next(enabled);
279
+ };
280
+
281
+ this.samsungListenerId =
282
+ window.webapis.tvinfo.addCaptionChangeListener(
283
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY,
284
+ onChange
285
+ );
286
+
287
+ log_debug("Samsung Voice Guide listener registered", {
288
+ listenerId: this.samsungListenerId,
289
+ });
290
+ } else {
291
+ log_debug("Samsung TvInfo API not available");
292
+ this.screenReaderEnabled$.next(false);
293
+ }
294
+ } catch (error) {
295
+ log_debug("Samsung Voice Guide listener error", { error });
296
+ // Fallback to false if the service is not available
297
+ this.screenReaderEnabled$.next(false);
298
+ }
299
+ }
205
300
  }
206
301
 
207
302
  getCurrentState(): boolean {
@@ -212,6 +307,10 @@ export class TTSManager {
212
307
  return this.ttsState$.asObservable();
213
308
  }
214
309
 
310
+ getScreenReaderEnabledAsObservable() {
311
+ return this.screenReaderEnabled$.asObservable();
312
+ }
313
+
215
314
  readText(text: string) {
216
315
  this.ttsState$.next(true);
217
316
 
@@ -229,14 +328,14 @@ export class TTSManager {
229
328
  try {
230
329
  window.webOS.service.request("luna://com.webos.service.tts", {
231
330
  method: "speak",
232
- onFailure(error: any) {
331
+ onFailure: (error: any) => {
233
332
  log_debug("There was a failure setting up webOS TTS service", {
234
333
  error,
235
334
  });
236
335
 
237
336
  this.ttsState$.next(false);
238
337
  },
239
- onSuccess(response: any) {
338
+ onSuccess: (response: any) => {
240
339
  log_debug("webOS TTS service is configured successfully", {
241
340
  response,
242
341
  });
@@ -5,6 +5,7 @@ import { createLogger, utilsLogger } from "../../../logger";
5
5
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
6
6
  import {
7
7
  findPluginByIdentifier,
8
+ loadFeedEntry,
8
9
  loadFeedAndPrefetchThumbnailImage,
9
10
  parseTimeToSeconds,
10
11
  retrieveFeedUrl,
@@ -26,6 +27,19 @@ type ChapterMarkerOriginal = {
26
27
  actions: ActionChapter[];
27
28
  };
28
29
 
30
+ export type LiveMetadataEvent = {
31
+ programId: string;
32
+ assetId: string;
33
+ title: string;
34
+ programStartTime: string;
35
+ programEndTime: string;
36
+ };
37
+
38
+ export type LiveMetadataConfig = {
39
+ updateUrl: string;
40
+ updateInterval: number;
41
+ };
42
+
29
43
  export type TitleSummaryEvent = {
30
44
  title: string | number;
31
45
  summary: string | number;
@@ -50,11 +64,13 @@ export type PlayNextState = PlayNextConfig & {
50
64
  export class OverlaysObserver {
51
65
  readonly chapterSubject: BehaviorSubject<ChapterMarkerEvent>;
52
66
  private playNextSubject: BehaviorSubject<PlayNextState>;
67
+ private liveMetadataSubject: BehaviorSubject<LiveMetadataEvent>;
53
68
  private titleSummarySubject: BehaviorSubject<TitleSummaryEvent>;
54
69
  private feedUrl: string;
55
70
  private reloadData: () => void;
56
71
  private updateTitleAndDescription: (data: any) => void;
57
72
  private feedDataInterval: any;
73
+ private liveMetadataUpdateInterval: any;
58
74
  private releasePlayerObserver?: () => void;
59
75
  readonly entry: ZappEntry;
60
76
  private chapterMarkerEvents: ChapterMarkerEvent[];
@@ -66,13 +82,17 @@ export class OverlaysObserver {
66
82
  this.chapterSubject = new BehaviorSubject(null);
67
83
  this.playNextSubject = new BehaviorSubject(null);
68
84
 
85
+ this.player = player;
86
+ this.entry = player.getEntry();
87
+
69
88
  this.titleSummarySubject = new BehaviorSubject<TitleSummaryEvent>({
70
- title: player.getEntry()?.title || "",
71
- summary: player.getEntry()?.summary || "",
89
+ title: this.entry?.title || "",
90
+ summary: this.entry?.summary || "",
72
91
  });
73
92
 
74
- this.entry = player.getEntry();
75
- this.player = player;
93
+ this.liveMetadataSubject = new BehaviorSubject<LiveMetadataEvent>(
94
+ this.entry?.extensions?.liveMetadata || null
95
+ );
76
96
 
77
97
  this.chapterMarkerEvents = this.prepareChapterMarkers();
78
98
  this.releasePlayerObserver = this.subscribeToPlayerEvents();
@@ -80,7 +100,9 @@ export class OverlaysObserver {
80
100
  this.reloadData = () => {};
81
101
  this.updateTitleAndDescription = () => {};
82
102
  this.feedDataInterval = null;
103
+ this.liveMetadataUpdateInterval = null;
83
104
  void this.preparePlayNext();
105
+ void this.prepareLiveMetadata();
84
106
  }
85
107
 
86
108
  private setupFeedDataInterval(interval: number) {
@@ -98,6 +120,13 @@ export class OverlaysObserver {
98
120
  }
99
121
  }
100
122
 
123
+ public clearLiveMetadataUpdateInterval() {
124
+ if (this.liveMetadataUpdateInterval) {
125
+ clearInterval(this.liveMetadataUpdateInterval);
126
+ this.liveMetadataUpdateInterval = null;
127
+ }
128
+ }
129
+
101
130
  public setFeedDataHandlers(
102
131
  feedUrl: string,
103
132
  reloadData: () => void,
@@ -197,6 +226,48 @@ export class OverlaysObserver {
197
226
  }
198
227
  };
199
228
 
229
+ prepareLiveMetadata = async () => {
230
+ if (!this.player?.isLive?.()) {
231
+ log_debug("prepareLiveMetadata: Player is not live. Skipping...");
232
+
233
+ return;
234
+ }
235
+
236
+ const config: LiveMetadataConfig =
237
+ this.entry?.extensions?.liveMetadataConfig;
238
+
239
+ if (!config?.updateUrl || !config?.updateInterval) {
240
+ log_debug(
241
+ "prepareLiveMetadata: Live metadata configuration is not available. Skipping...",
242
+ { config }
243
+ );
244
+
245
+ return;
246
+ }
247
+
248
+ const reloadData = async () => {
249
+ try {
250
+ const entry = await loadFeedEntry(config.updateUrl, this.entry);
251
+
252
+ this.onLiveMetadataUpdated(entry?.extensions?.liveMetadata || null);
253
+ } catch (error) {
254
+ log_error("prepareLiveMetadata: Metadata fetching failed", {
255
+ error,
256
+ config,
257
+ });
258
+ }
259
+ };
260
+
261
+ log_debug("prepareLiveMetadata: Setting up live metadata observer update", {
262
+ config,
263
+ });
264
+
265
+ const interval = Number(config?.updateInterval) || 60;
266
+
267
+ this.clearLiveMetadataUpdateInterval();
268
+ this.liveMetadataUpdateInterval = setInterval(reloadData, interval * 1000);
269
+ };
270
+
200
271
  // TODO: Hack for video end, will be replaced with playlist prev/next in the future
201
272
  getPlayNextEntry = () =>
202
273
  !this.isCanceledByUser ? this.playNextConfig?.entry : null;
@@ -317,9 +388,11 @@ export class OverlaysObserver {
317
388
  onPlayerClose = () => {
318
389
  this.chapterSubject.complete();
319
390
  this.playNextSubject.complete();
391
+ this.liveMetadataSubject.complete();
320
392
  this.titleSummarySubject.complete();
321
393
  this.releasePlayerObserver?.();
322
394
  this.clearFeedDataInterval();
395
+ this.clearLiveMetadataUpdateInterval();
323
396
  this.releasePlayerObserver = null;
324
397
  };
325
398
 
@@ -352,4 +425,21 @@ export class OverlaysObserver {
352
425
  (prev, curr) => prev?.triggerTime === curr?.triggerTime
353
426
  )
354
427
  );
428
+
429
+ private onLiveMetadataUpdated = (liveMetadataEvent: LiveMetadataEvent) => {
430
+ this.liveMetadataSubject.next(liveMetadataEvent);
431
+ };
432
+
433
+ public getLiveMetadataObservable = (): Observable<LiveMetadataEvent> => {
434
+ return this.liveMetadataSubject.pipe(
435
+ distinctUntilChanged(
436
+ (prev, curr) =>
437
+ prev?.programId === curr?.programId && prev?.assetId === curr?.assetId
438
+ )
439
+ );
440
+ };
441
+
442
+ public getLiveMetadataValue = (): LiveMetadataEvent => {
443
+ return this.liveMetadataSubject.value;
444
+ };
355
445
  }