@applicaster/zapp-react-native-ui-components 16.0.0-rc.121 → 16.0.0-rc.123

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.
@@ -3,7 +3,10 @@ import {
3
3
  PlayerLifecycleListener,
4
4
  playerManager,
5
5
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
6
- import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
6
+ import {
7
+ getUserCellPlayerMutedPreference,
8
+ setUserCellPlayerMutedPreference,
9
+ } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
7
10
  import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
8
11
  import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
9
12
  import { loggerLiveImageManager } from "./loggerHelper";
@@ -21,13 +24,15 @@ export type LiveImageManagerEvent =
21
24
  | "onEnd"
22
25
  | "onStart"
23
26
  | "onError"
24
- | "onDestroy";
27
+ | "onDestroy"
28
+ | "onRegister"
29
+ | "onPlaybackChange"
30
+ | "onMuteChange";
25
31
 
26
32
  export type LiveImageManagerListenerData = {
27
33
  id: string;
28
- listener: Record<
29
- LiveImageManagerEvent,
30
- (event?: Record<string, any>) => void
34
+ listener: Partial<
35
+ Record<LiveImageManagerEvent, (event?: Record<string, any>) => void>
31
36
  >;
32
37
  };
33
38
 
@@ -54,6 +59,7 @@ type PlayerFactoryConfig = {
54
59
  };
55
60
 
56
61
  type LiveImageProps = {
62
+ audioMutedByDefault?: boolean;
57
63
  playerId: string;
58
64
  setMode?: (type: LiveImageType) => void;
59
65
  preloadHooks?: PreloadHookConfig[];
@@ -81,9 +87,11 @@ export class LiveImageManager implements PlayerLifecycleListener {
81
87
 
82
88
  protected listeners: Record<
83
89
  string,
84
- Record<LiveImageManagerEvent, (event?: Record<string, any>) => void>
90
+ LiveImageManagerListenerData["listener"]
85
91
  > = {};
86
92
 
93
+ private mutedSubscriptions = new Map<LiveImage, () => void>();
94
+
87
95
  constructor() {
88
96
  playerManager.addLifecycleListener(this);
89
97
  this.listeners = {};
@@ -96,8 +104,25 @@ export class LiveImageManager implements PlayerLifecycleListener {
96
104
  }
97
105
 
98
106
  public register = (item: LiveImage): (() => void) => {
107
+ if (!isTV()) {
108
+ const muted = !!getUserCellPlayerMutedPreference(item.getMuted());
109
+
110
+ if (muted !== item.getMuted()) {
111
+ muted ? item.mute() : item.unmute();
112
+ }
113
+ }
114
+
99
115
  this.items.push(item);
116
+
117
+ this.mutedSubscriptions.set(
118
+ item,
119
+ item.subscribeMuted(() =>
120
+ this.invokeListenersUpdate({ callbackName: "onMuteChange" })
121
+ )
122
+ );
123
+
100
124
  log_debug(`register: live image ${item.playerId} - ${item.tag}`);
125
+ this.invokeListenersUpdate({ callbackName: "onRegister" });
101
126
 
102
127
  // TV only Start playing video once registered
103
128
  if (isTV()) {
@@ -109,6 +134,8 @@ export class LiveImageManager implements PlayerLifecycleListener {
109
134
 
110
135
  public unregister = (item: LiveImage) => {
111
136
  log_debug(`unregister: live-image ${item.playerId} - ${item.tag}`);
137
+ this.mutedSubscriptions.get(item)?.();
138
+ this.mutedSubscriptions.delete(item);
112
139
 
113
140
  if (this.currentlyPlaying === item) {
114
141
  this.currentlyPlaying = null;
@@ -172,6 +199,25 @@ export class LiveImageManager implements PlayerLifecycleListener {
172
199
  return this.currentlyPlaying;
173
200
  };
174
201
 
202
+ public getEntryMuted = (entryId: string): boolean | undefined => {
203
+ const matches = (item: LiveImage) => String(item.getEntryId()) === entryId;
204
+
205
+ const item =
206
+ this.currentlyPlaying && matches(this.currentlyPlaying)
207
+ ? this.currentlyPlaying
208
+ : this.items.find(matches);
209
+
210
+ return item?.getMuted();
211
+ };
212
+
213
+ public getPlayingCellId = (entryId: string): string | undefined => {
214
+ const item = this.currentlyPlaying;
215
+
216
+ return item && String(item.getEntryId()) === entryId && item.isPlaying()
217
+ ? item.playerId
218
+ : undefined;
219
+ };
220
+
175
221
  private findNextPlayableItem = () => {
176
222
  if (isTV()) {
177
223
  return this.items[this.items.length - 1];
@@ -302,6 +348,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
302
348
  setUserCellPlayerMutedPreference(true);
303
349
 
304
350
  this.items.forEach((liveImage) => liveImage.mute());
351
+ this.invokeListenersUpdate({ callbackName: "onMuteChange" });
305
352
  };
306
353
 
307
354
  public unmuteAll = () => {
@@ -310,6 +357,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
310
357
  setUserCellPlayerMutedPreference(false);
311
358
 
312
359
  this.items.forEach((liveImage) => liveImage.unmute());
360
+ this.invokeListenersUpdate({ callbackName: "onMuteChange" });
313
361
  };
314
362
 
315
363
  public onViewPositionChanged = (item: LiveImage) => {
@@ -579,20 +627,67 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
579
627
  left: 0,
580
628
  };
581
629
 
582
- // Keeps muted state in sync: updates the initial value for deferred
583
- // player creation and forwards to the player if it already exists.
584
- public initiallyMuted: boolean = true;
630
+ private muted: boolean;
631
+ private mutedListeners = new Set<() => void>();
632
+ private playbackEnded = false;
633
+
634
+ getMuted = (): boolean => this.muted;
635
+
636
+ getEntryId = () => this.factoryConfig.entry.id;
637
+
638
+ isPlaying = (): boolean =>
639
+ !this.playbackEnded &&
640
+ !!this.player?.isReadyToPlay() &&
641
+ this.player.isPlaying();
642
+
643
+ subscribeMuted = (listener: () => void): (() => void) => {
644
+ this.mutedListeners.add(listener);
645
+
646
+ return () => this.mutedListeners.delete(listener);
647
+ };
648
+
649
+ private setMuted = (muted: boolean) => {
650
+ if (this.muted === muted) return;
651
+
652
+ this.muted = muted;
653
+
654
+ this.mutedListeners.forEach((listener) => {
655
+ try {
656
+ listener();
657
+ } catch (error) {
658
+ log_error(`Mute listener failed for ${this.playerId}`, { error });
659
+ }
660
+ });
661
+ };
585
662
 
586
663
  mute = () => {
587
- this.initiallyMuted = true;
664
+ this.setMuted(true);
588
665
  this.player?.mute();
589
666
  };
590
667
 
591
668
  unmute = () => {
592
- this.initiallyMuted = false;
669
+ this.setMuted(false);
593
670
  this.player?.unmute();
594
671
  };
595
672
 
673
+ onPlayerMute = () => this.setMuted(true);
674
+
675
+ onPlayerUnmute = () => this.setMuted(false);
676
+
677
+ onPlayerResume = () => {
678
+ this.playbackEnded = false;
679
+
680
+ LiveImageManager.instance.invokeListenersUpdate({
681
+ callbackName: "onPlaybackChange",
682
+ });
683
+ };
684
+
685
+ onPlayerPause = () => {
686
+ LiveImageManager.instance.invokeListenersUpdate({
687
+ callbackName: "onPlaybackChange",
688
+ });
689
+ };
690
+
596
691
  positionToString() {
597
692
  if (!this.position) {
598
693
  return "position not set";
@@ -617,6 +712,10 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
617
712
  this.preloadHooks = props.preloadHooks;
618
713
  this.factoryConfig = props.factoryConfig;
619
714
  this.tag = props.tag || "untagged";
715
+
716
+ this.muted = isTV()
717
+ ? false
718
+ : !!getUserCellPlayerMutedPreference(props.audioMutedByDefault);
620
719
  }
621
720
 
622
721
  async prepareForPlayback(): Promise<boolean> {
@@ -670,7 +769,7 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
670
769
  playerId: this.factoryConfig.playerId,
671
770
  autoplay: false,
672
771
  entry,
673
- muted: this.initiallyMuted,
772
+ muted: this.muted,
674
773
  playerPluginId: this.factoryConfig.playerPluginId,
675
774
  screenConfig: this.factoryConfig.screenConfig,
676
775
  playerRole: PlayerRole.Cell,
@@ -686,6 +785,11 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
686
785
  this.component = factoryItem.Component;
687
786
  this.player.addListener({ id: "live-image", listener: this });
688
787
 
788
+ // A user toggle may have arrived while the async factory was pending.
789
+ if (this.player.getIsMuted() !== this.muted) {
790
+ this.muted ? this.player.mute() : this.player.unmute();
791
+ }
792
+
689
793
  return true;
690
794
  }
691
795
 
@@ -694,6 +798,8 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
694
798
  };
695
799
 
696
800
  onLoad = (_event) => {
801
+ this.playbackEnded = false;
802
+
697
803
  log_info(
698
804
  `onLoad: live-image, video loaded, switching to video, ${playerInfo(
699
805
  this.player
@@ -704,6 +810,8 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
704
810
  };
705
811
 
706
812
  onError = (error: Error) => {
813
+ this.playbackEnded = true;
814
+
707
815
  log_error(
708
816
  `onError: live-image, fail to play, switching back to image. error: ${
709
817
  error?.message
@@ -714,6 +822,8 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
714
822
  };
715
823
 
716
824
  onVideoError = (error: Error) => {
825
+ this.playbackEnded = true;
826
+
717
827
  log_error(
718
828
  `onVideoError: live-image, fail to play, switching back to image. error: ${
719
829
  error?.message
@@ -724,6 +834,8 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
724
834
  };
725
835
 
726
836
  onVideoEnd = (_event) => {
837
+ this.playbackEnded = true;
838
+
727
839
  log_info(
728
840
  `onVideoEnd: live-image, video completed, switching back to image, ${playerInfo(
729
841
  this.player
@@ -734,6 +846,8 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
734
846
  };
735
847
 
736
848
  onPlayerClose = () => {
849
+ this.playbackEnded = true;
850
+
737
851
  log_info(
738
852
  `onPlayerClose: live-image, player closed, switching back to image, ${playerInfo(
739
853
  this.player
@@ -0,0 +1,135 @@
1
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
2
+ import { getUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
3
+ import {
4
+ LiveImageManager,
5
+ type LiveImageManagerEvent,
6
+ } from "./LiveImageManager";
7
+
8
+ const { log_error } = createLogger({
9
+ category: "MuteUnmuteController",
10
+ subsystem: "MuteUnmuteController",
11
+ });
12
+
13
+ export const MUTE_UNMUTE_ACTION_IDENTIFIER = "mute_unmute_action";
14
+
15
+ export type MuteUnmuteEntryState = {
16
+ muted: boolean;
17
+ activeCellId?: string;
18
+ };
19
+
20
+ export type MuteUnmuteListener = (state: MuteUnmuteEntryState) => void;
21
+
22
+ type StateOptions = { audioMutedByDefault?: boolean };
23
+
24
+ type MuteUnmuteManager = {
25
+ getEntryMuted: (entryId: string) => boolean | undefined;
26
+ getPlayingCellId: (entryId: string) => string | undefined;
27
+ addListener: (data: {
28
+ id: string;
29
+ listener: Record<LiveImageManagerEvent, () => void>;
30
+ }) => () => void;
31
+ muteAll: () => void;
32
+ unmuteAll: () => void;
33
+ };
34
+
35
+ type MuteUnmuteDependencies = {
36
+ manager?: MuteUnmuteManager;
37
+ getMutedPreference?: (audioMutedByDefault?: boolean) => boolean | unknown;
38
+ };
39
+
40
+ const MANAGER_EVENTS: LiveImageManagerEvent[] = [
41
+ "onStart",
42
+ "onEnd",
43
+ "onError",
44
+ "onDestroy",
45
+ "onRegister",
46
+ "onMuteChange",
47
+ "onPlaybackChange",
48
+ ];
49
+
50
+ const sameState = (a: MuteUnmuteEntryState, b: MuteUnmuteEntryState) =>
51
+ a.muted === b.muted && a.activeCellId === b.activeCellId;
52
+
53
+ export class MuteUnmuteController {
54
+ private readonly manager: MuteUnmuteManager;
55
+ private readonly getMutedPreference: NonNullable<
56
+ MuteUnmuteDependencies["getMutedPreference"]
57
+ >;
58
+
59
+ private subscriptionCounter = 0;
60
+
61
+ constructor({ manager, getMutedPreference }: MuteUnmuteDependencies = {}) {
62
+ this.manager = manager ?? LiveImageManager.instance;
63
+
64
+ this.getMutedPreference =
65
+ getMutedPreference ?? getUserCellPlayerMutedPreference;
66
+ }
67
+
68
+ public getEntryState = (
69
+ entryId: string,
70
+ options?: StateOptions
71
+ ): MuteUnmuteEntryState => {
72
+ return {
73
+ activeCellId: this.manager.getPlayingCellId(entryId),
74
+ muted:
75
+ this.manager.getEntryMuted(entryId) ??
76
+ !!this.getMutedPreference(options?.audioMutedByDefault),
77
+ };
78
+ };
79
+
80
+ public toggle = (entryId: string): void => {
81
+ if (this.getEntryState(entryId).muted) {
82
+ this.manager.unmuteAll();
83
+ } else {
84
+ this.manager.muteAll();
85
+ }
86
+ };
87
+
88
+ public subscribe = (
89
+ entryId: string,
90
+ listener: MuteUnmuteListener,
91
+ options?: StateOptions
92
+ ): (() => void) => {
93
+ const id = `mute-unmute-controller:${entryId}:${++this.subscriptionCounter}`;
94
+
95
+ let lastState = this.getEntryState(entryId, options);
96
+ let active = true;
97
+
98
+ const check = () => {
99
+ if (!active) return;
100
+
101
+ const next = this.getEntryState(entryId, options);
102
+
103
+ if (sameState(next, lastState)) return;
104
+
105
+ lastState = next;
106
+
107
+ try {
108
+ listener(next);
109
+ } catch (error) {
110
+ log_error(
111
+ `subscribe: listener for entry ${entryId} threw: ${error?.message}`,
112
+ { error }
113
+ );
114
+ }
115
+ };
116
+
117
+ const managerListeners = Object.fromEntries(
118
+ MANAGER_EVENTS.map((name) => [name, check])
119
+ ) as Record<LiveImageManagerEvent, () => void>;
120
+
121
+ const removeManagerListener = this.manager.addListener({
122
+ id,
123
+ listener: managerListeners,
124
+ });
125
+
126
+ return () => {
127
+ if (!active) return;
128
+
129
+ active = false;
130
+ removeManagerListener();
131
+ };
132
+ };
133
+ }
134
+
135
+ export const muteUnmuteController = new MuteUnmuteController();
@@ -7,12 +7,8 @@ import { AppState, AppStateStatus, View } from "react-native";
7
7
  import { TrackedView } from "../TrackedView";
8
8
  import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks/layout";
9
9
 
10
- import {
11
- isApplePlatform,
12
- isTV,
13
- } from "@applicaster/zapp-react-native-utils/reactUtils";
10
+ import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
14
11
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
15
- import { getUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
16
12
  import Image from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/Image";
17
13
  import { LiveImage, LiveImageManager, LiveImageType } from "./LiveImageManager";
18
14
 
@@ -57,10 +53,6 @@ const PlayerLiveImageComponent = (props: Props) => {
57
53
  const [mode, setMode] = React.useState(LiveImageType.Image);
58
54
  const [isReadyToPlay, setIsReady] = React.useState(false);
59
55
 
60
- const [muted, setMuted] = React.useState(
61
- isTV() ? false : getUserCellPlayerMutedPreference(props.audioMutedByDefault)
62
- );
63
-
64
56
  const ref = React.useRef(null);
65
57
  const trackViewRef = React.useRef(null);
66
58
 
@@ -121,6 +113,7 @@ const PlayerLiveImageComponent = (props: Props) => {
121
113
 
122
114
  const liveImageItem: LiveImage = React.useMemo(() => {
123
115
  return new LiveImage({
116
+ audioMutedByDefault: props.audioMutedByDefault,
124
117
  playerId,
125
118
  preloadHooks,
126
119
  factoryConfig: {
@@ -132,10 +125,19 @@ const PlayerLiveImageComponent = (props: Props) => {
132
125
  },
133
126
  tag: item.title?.toString(),
134
127
  });
135
- }, [playerId, preloadHooks, playerPluginId, screenConfig, item]);
136
-
137
- // Keep the muted state in sync with user preference and player state
138
- liveImageItem.initiallyMuted = muted;
128
+ }, [
129
+ playerId,
130
+ preloadHooks,
131
+ playerPluginId,
132
+ screenConfig,
133
+ item,
134
+ props.audioMutedByDefault,
135
+ ]);
136
+
137
+ const muted = React.useSyncExternalStore(
138
+ liveImageItem.subscribeMuted,
139
+ liveImageItem.getMuted
140
+ );
139
141
 
140
142
  React.useEffect(() => {
141
143
  liveImageItem.setMode = setModeDebounced;
@@ -202,12 +204,6 @@ const PlayerLiveImageComponent = (props: Props) => {
202
204
  setIsReady(false);
203
205
  }
204
206
  },
205
- onPlayerMute: () => {
206
- setMuted(true);
207
- },
208
- onPlayerUnmute: () => {
209
- setMuted(false);
210
- },
211
207
  onPlayerClose: () => {
212
208
  onLiveImageCompleted();
213
209
  setIsReady(false);