@applicaster/zapp-react-native-utils 15.1.7 → 15.1.9-alpha.3547421944

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.
@@ -1,8 +1,21 @@
1
1
  import * as R from "ramda";
2
- import { BehaviorSubject } from "rxjs";
2
+ import { BehaviorSubject, Observable } from "rxjs";
3
+ import { distinctUntilChanged } from "rxjs/operators";
3
4
  import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
4
5
  import { createLogger, utilsLogger } from "../../logger";
5
- import { getBoolFromConfigValue } from "../../configurationUtils";
6
+ import {
7
+ getBoolFromConfigValue,
8
+ imageSrcFromMediaItem,
9
+ } from "../../configurationUtils";
10
+ // Imported from the leaf modules rather than the `playerUtils` barrel: that
11
+ // barrel imports `playerManager`, so going through it would create a require
12
+ // cycle (playerManager/index -> player -> playerUtils/index -> playerManager).
13
+ import {
14
+ resolvePlayerTitleSubtitle,
15
+ getPlayerContentDataKeys,
16
+ } from "../../playerUtils/resolvePlayerTitleSubtitle";
17
+ import { isAudioItem } from "../../playerUtils/isAudioItem";
18
+ import { isTV } from "../../reactUtils";
6
19
  import { PlayerRole, SharedPlayerCallBacksKeys } from "./const";
7
20
  import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
8
21
  import { TVSeekController } from "../../reactHooks/player/TVSeekControlller/TVSeekController";
@@ -19,6 +32,23 @@ export declare type PlayerControllerListenerData = {
19
32
  id: string;
20
33
  listener: QuickBrickPlayer.SharedPlayerCallBacks;
21
34
  };
35
+
36
+ /**
37
+ * The player's resolved Title/Subtitle text, published by the player the same
38
+ * way duration and progress are. Consumers render these values as-is rather
39
+ * than reading `entry` or the plugin configuration themselves.
40
+ */
41
+ export type PlayerContent = {
42
+ title: Option<string | number>;
43
+ summary: Option<string | number>;
44
+ /**
45
+ * Cover art for the content currently playing. Published alongside the text
46
+ * (rather than read from `entry` by whoever needs it) so it follows the same
47
+ * entry: on live audio that means the programme's own artwork, refreshed
48
+ * with every feed poll, instead of the channel's.
49
+ */
50
+ imageUrl: Option<string>;
51
+ };
22
52
  /**
23
53
  * Player Instance Controller
24
54
  * Instance exposes utility methods that will help set or get state
@@ -45,6 +75,7 @@ export class Player {
45
75
  readonly playerRole: PlayerRole = PlayerRole.Unspecified;
46
76
  private overlaysObserver?: OverlaysObserver | null;
47
77
  private entryObservable?: BehaviorSubject<ZappEntry> | null;
78
+ private contentSubject?: BehaviorSubject<PlayerContent> | null;
48
79
 
49
80
  protected listeners: {
50
81
  [key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
@@ -58,7 +89,17 @@ export class Player {
58
89
 
59
90
  set entry(newEntry: ZappEntry | null) {
60
91
  this._entry = newEntry;
92
+ // The live entry is scoped to the entry it overlays - see
93
+ // `OverlaysObserver.clearLiveEntry`. No-op when there is no observer,
94
+ // which is every non-live player.
95
+ this.overlaysObserver?.clearLiveEntry();
61
96
  this.entryObservable?.next(newEntry);
97
+ // Optional chaining short-circuits the whole call expression, so
98
+ // `getContent()` is NOT evaluated while no subscriber exists. That
99
+ // matters: `PlayerNative` assigns `this.entry` in its constructor, before
100
+ // subclass arrow-function properties such as `getPluginConfiguration`
101
+ // have been assigned.
102
+ this.contentSubject?.next(this.getContent());
62
103
  }
63
104
 
64
105
  constructor(props) {
@@ -117,6 +158,77 @@ export class Player {
117
158
  return this.entryObservable;
118
159
  };
119
160
 
161
+ /**
162
+ * The player's resolved Title/Subtitle.
163
+ *
164
+ * Resolved on demand rather than cached: the plugin configuration arrives
165
+ * asynchronously on both the native and cast paths, so a value computed once
166
+ * at entry-set time would be stale for whichever arrives first.
167
+ *
168
+ * Every fallback (no config, empty/whitespace key, unresolvable key, key
169
+ * resolving to an empty string or to a non-string/non-number) lives in
170
+ * `resolvePlayerContentText` and is never reimplemented here.
171
+ */
172
+ getContent = (): PlayerContent => {
173
+ // Live audio keeps playing one channel while the programme on air
174
+ // changes, so the observer's freshly-loaded programme entry wins when
175
+ // there is one. Read through `?.` so this never constructs an observer:
176
+ // a VOD or cast player has none and falls straight through to its entry.
177
+ const entry = this.overlaysObserver?.liveEntry ?? this.entry;
178
+
179
+ return {
180
+ ...resolvePlayerTitleSubtitle({
181
+ entry,
182
+ pluginConfiguration: this.getPluginConfiguration(),
183
+ dataKeys: getPlayerContentDataKeys({
184
+ isTV: isTV(),
185
+ isAudioContent: isAudioItem(entry),
186
+ }),
187
+ }),
188
+ // Same selection Android's `EntryHelpers.extractCoverImage` makes:
189
+ // prefer the `image_base` media item, otherwise the first one available.
190
+ // Reused rather than reimplemented so the two platforms cannot drift.
191
+ imageUrl: entry ? imageSrcFromMediaItem(entry, []) : undefined,
192
+ };
193
+ };
194
+
195
+ /**
196
+ * Created lazily, like `entryObservable`. An eagerly created subject would
197
+ * resolve during `PlayerNative`'s constructor and call a not-yet-assigned
198
+ * `getPluginConfiguration`.
199
+ *
200
+ * The seed is produced by calling the resolver, never from raw entry fields:
201
+ * a `BehaviorSubject` replays its seed synchronously on subscribe, so a raw
202
+ * seed would overwrite correctly-resolved text at mount.
203
+ */
204
+ getContentObservable = (): Observable<PlayerContent> => {
205
+ if (!this.contentSubject) {
206
+ this.contentSubject = new BehaviorSubject<PlayerContent>(
207
+ this.getContent()
208
+ );
209
+ }
210
+
211
+ return this.contentSubject.asObservable().pipe(
212
+ // `resolvePlayerTitleSubtitle` returns a fresh object every call, so
213
+ // reference equality would never dedupe. Compare by value.
214
+ distinctUntilChanged(
215
+ (previous, next) =>
216
+ previous.title === next.title &&
217
+ previous.summary === next.summary &&
218
+ previous.imageUrl === next.imageUrl
219
+ )
220
+ );
221
+ };
222
+
223
+ /**
224
+ * Re-resolves and publishes the current content. Called when something the
225
+ * resolution depends on changed without `entry` itself being reassigned -
226
+ * today that is the observer swapping in a newly-loaded live programme.
227
+ */
228
+ publishContent = () => {
229
+ this.contentSubject?.next(this.getContent());
230
+ };
231
+
120
232
  /**
121
233
  * ---------------------------------------- PLAYER SETTERS GETTERS ----------------------------------------
122
234
  * These methods let you get information about the content / ad playing
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { Player, PlayerContent } from "./player";
3
+ import { usePlayer } from "./usePlayer";
4
+
5
+ const EMPTY_CONTENT: PlayerContent = {
6
+ title: null,
7
+ summary: null,
8
+ imageUrl: null,
9
+ };
10
+
11
+ /**
12
+ * Subscribes to the player's resolved Title/Subtitle, the same way
13
+ * `usePlayerState` subscribes to playback state. Modeled on that hook
14
+ * deliberately: one React bridge pattern for everything the manager publishes.
15
+ *
16
+ * Passing no `playerId` uses the active player, which is the right default -
17
+ * only one player instance is live at a time. `usePlayer` additionally swaps
18
+ * to the casting receiver while casting, so this follows cast handover
19
+ * automatically.
20
+ */
21
+ export const usePlayerContent = (playerId?: string): PlayerContent => {
22
+ const player: Player = usePlayer(playerId);
23
+
24
+ const [content, setContent] = React.useState<PlayerContent>(
25
+ () => player?.getContent() ?? EMPTY_CONTENT
26
+ );
27
+
28
+ React.useEffect(() => {
29
+ if (!player) {
30
+ setContent(EMPTY_CONTENT);
31
+
32
+ return;
33
+ }
34
+
35
+ const subscription = player
36
+ .getContentObservable()
37
+ .subscribe((next) => setContent(next));
38
+
39
+ return () => subscription.unsubscribe();
40
+ }, [player]);
41
+
42
+ return content;
43
+ };
@@ -65,11 +65,38 @@ export const isEmptyOrNil = (value: any): boolean =>
65
65
  */
66
66
  export const getLabel = (dataKeyValue, customDataKeyValue) => (entry) => {
67
67
  const label = getKeyForLabel(dataKeyValue, customDataKeyValue);
68
- const isPath = label && R.length(label) > 0;
69
- const fallback = isEmptyOrNil(label) ? "" : R.head(label);
68
+ const isPath = label && label.length > 0;
69
+
70
+ // An unresolved key is rendered as-is, because a data key is allowed to be a
71
+ // literal label ("More") rather than a path. Fall back to the whole key
72
+ // instead of its first segment: "extensions.section" reads as an unresolved
73
+ // path, while "extensions" reads as a legitimate label and hides the problem.
74
+ const fallback = isEmptyOrNil(label) ? "" : label.join(".");
70
75
 
71
76
  try {
72
- return isPath ? R.pathOr(fallback, label, entry) : fallback;
77
+ if (!isPath) {
78
+ return fallback;
79
+ }
80
+
81
+ const resolved = label.reduce(
82
+ (value, key) => (value == null ? undefined : value[key]),
83
+ entry
84
+ );
85
+
86
+ if (isNotNil(resolved)) {
87
+ return resolved;
88
+ }
89
+
90
+ // A multi-segment key is unambiguously a path, so failing to resolve it is
91
+ // a misconfiguration rather than someone typing a literal label.
92
+ if (label.length > 1) {
93
+ cellUtilsLogger.warning({
94
+ message: `getLabel: data key "${fallback}" was not found on the entry, rendering the key itself`,
95
+ data: { dataKeyValue, customDataKeyValue, entryId: entry?.id },
96
+ });
97
+ }
98
+
99
+ return fallback;
73
100
  } catch (error) {
74
101
  cellUtilsLogger.warning({
75
102
  data: { error, dataKeyValue, customDataKeyValue },
@@ -1352,6 +1352,35 @@ function getPlayerConfiguration({ platform, version }) {
1352
1352
  type: "switch",
1353
1353
  label_tooltip: "Select whether you want to display title in player",
1354
1354
  },
1355
+ {
1356
+ section: "title",
1357
+ key: "title_data_key_source",
1358
+ label: "Title data key",
1359
+ initial_value: "title",
1360
+ type: "select",
1361
+ options: [
1362
+ { text: "Title", value: "title" },
1363
+ { text: "Other", value: "other" },
1364
+ ],
1365
+ label_tooltip:
1366
+ "Select which feed data key provides the player content title. Keep 'Title' to use the entry title",
1367
+ },
1368
+ {
1369
+ section: "title",
1370
+ key: "title_custom_data_key",
1371
+ label: "Custom title data key",
1372
+ initial_value: "",
1373
+ type: "text_input",
1374
+ rules: "conditional",
1375
+ conditional_fields: [
1376
+ {
1377
+ key: "styles/title_data_key_source",
1378
+ condition_value: "other",
1379
+ },
1380
+ ],
1381
+ label_tooltip:
1382
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_name. Falls back to the entry title if the key is empty or missing",
1383
+ },
1355
1384
  {
1356
1385
  section: "title",
1357
1386
  key: "title_font_android",
@@ -1759,6 +1788,35 @@ function getPlayerConfiguration({ platform, version }) {
1759
1788
  label_tooltip:
1760
1789
  "Select whether you want to display subtitle in player",
1761
1790
  },
1791
+ {
1792
+ section: "subtitle",
1793
+ key: "subtitle_data_key_source",
1794
+ label: "Subtitle data key",
1795
+ initial_value: "summary",
1796
+ type: "select",
1797
+ options: [
1798
+ { text: "Subtitle", value: "summary" },
1799
+ { text: "Other", value: "other" },
1800
+ ],
1801
+ label_tooltip:
1802
+ "Select which feed data key provides the player content subtitle. Keep 'Subtitle' to use the entry summary",
1803
+ },
1804
+ {
1805
+ section: "subtitle",
1806
+ key: "subtitle_custom_data_key",
1807
+ label: "Custom subtitle data key",
1808
+ initial_value: "",
1809
+ type: "text_input",
1810
+ rules: "conditional",
1811
+ conditional_fields: [
1812
+ {
1813
+ key: "styles/subtitle_data_key_source",
1814
+ condition_value: "other",
1815
+ },
1816
+ ],
1817
+ label_tooltip:
1818
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_subtitle. Falls back to the entry summary if the key is empty or missing",
1819
+ },
1762
1820
  {
1763
1821
  section: "subtitle",
1764
1822
  key: "subtitle_font_android",
@@ -2923,6 +2981,33 @@ function getPlayerConfiguration({ platform, version }) {
2923
2981
  "Video Player - Title Text",
2924
2982
  "Video Player Title Text Label Styles",
2925
2983
  [
2984
+ {
2985
+ key: "video_player_title_data_key_source",
2986
+ label: "Video title data key",
2987
+ initial_value: "title",
2988
+ type: "select",
2989
+ options: [
2990
+ { text: "Title", value: "title" },
2991
+ { text: "Other", value: "other" },
2992
+ ],
2993
+ label_tooltip:
2994
+ "Select which feed data key provides the video player title. Keep 'Title' to use the entry title",
2995
+ },
2996
+ {
2997
+ key: "video_player_title_custom_data_key",
2998
+ label: "Custom video title data key",
2999
+ initial_value: "",
3000
+ type: "text_input",
3001
+ rules: "conditional",
3002
+ conditional_fields: [
3003
+ {
3004
+ key: "styles/video_player_title_data_key_source",
3005
+ condition_value: "other",
3006
+ },
3007
+ ],
3008
+ label_tooltip:
3009
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_name. Falls back to the entry title if the key is empty or missing",
3010
+ },
2926
3011
  {
2927
3012
  key: "player_title_color",
2928
3013
  label: "Font color",
@@ -3008,6 +3093,33 @@ function getPlayerConfiguration({ platform, version }) {
3008
3093
  "Video Player - Summary Text",
3009
3094
  "Video Player Summary Text Label Styles",
3010
3095
  [
3096
+ {
3097
+ key: "video_player_summary_data_key_source",
3098
+ label: "Video summary data key",
3099
+ initial_value: "summary",
3100
+ type: "select",
3101
+ options: [
3102
+ { text: "Summary", value: "summary" },
3103
+ { text: "Other", value: "other" },
3104
+ ],
3105
+ label_tooltip:
3106
+ "Select which feed data key provides the video player summary. Keep 'Summary' to use the entry summary",
3107
+ },
3108
+ {
3109
+ key: "video_player_summary_custom_data_key",
3110
+ label: "Custom video summary data key",
3111
+ initial_value: "",
3112
+ type: "text_input",
3113
+ rules: "conditional",
3114
+ conditional_fields: [
3115
+ {
3116
+ key: "styles/video_player_summary_data_key_source",
3117
+ condition_value: "other",
3118
+ },
3119
+ ],
3120
+ label_tooltip:
3121
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_subtitle. Falls back to the entry summary if the key is empty or missing",
3122
+ },
3011
3123
  {
3012
3124
  key: "player_summary_color",
3013
3125
  label: "Font color",
@@ -3093,6 +3205,33 @@ function getPlayerConfiguration({ platform, version }) {
3093
3205
  "Audio Player - Title Text",
3094
3206
  "Audio Player Title Text Label Styles",
3095
3207
  [
3208
+ {
3209
+ key: "audio_player_title_data_key_source",
3210
+ label: "Audio title data key",
3211
+ initial_value: "title",
3212
+ type: "select",
3213
+ options: [
3214
+ { text: "Title", value: "title" },
3215
+ { text: "Other", value: "other" },
3216
+ ],
3217
+ label_tooltip:
3218
+ "Select which feed data key provides the audio player title. Keep 'Title' to use the entry title",
3219
+ },
3220
+ {
3221
+ key: "audio_player_title_custom_data_key",
3222
+ label: "Custom audio title data key",
3223
+ initial_value: "",
3224
+ type: "text_input",
3225
+ rules: "conditional",
3226
+ conditional_fields: [
3227
+ {
3228
+ key: "styles/audio_player_title_data_key_source",
3229
+ condition_value: "other",
3230
+ },
3231
+ ],
3232
+ label_tooltip:
3233
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_name. Falls back to the entry title if the key is empty or missing",
3234
+ },
3096
3235
  {
3097
3236
  key: "audio_player_title_color",
3098
3237
  label: "Audio player title color",
@@ -3178,6 +3317,33 @@ function getPlayerConfiguration({ platform, version }) {
3178
3317
  "Audio Player - Summary Text",
3179
3318
  "Audio Player Summary Text Label Styles",
3180
3319
  [
3320
+ {
3321
+ key: "audio_player_summary_data_key_source",
3322
+ label: "Audio summary data key",
3323
+ initial_value: "summary",
3324
+ type: "select",
3325
+ options: [
3326
+ { text: "Summary", value: "summary" },
3327
+ { text: "Other", value: "other" },
3328
+ ],
3329
+ label_tooltip:
3330
+ "Select which feed data key provides the audio player summary. Keep 'Summary' to use the entry summary",
3331
+ },
3332
+ {
3333
+ key: "audio_player_summary_custom_data_key",
3334
+ label: "Custom audio summary data key",
3335
+ initial_value: "",
3336
+ type: "text_input",
3337
+ rules: "conditional",
3338
+ conditional_fields: [
3339
+ {
3340
+ key: "styles/audio_player_summary_data_key_source",
3341
+ condition_value: "other",
3342
+ },
3343
+ ],
3344
+ label_tooltip:
3345
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_subtitle. Falls back to the entry summary if the key is empty or missing",
3346
+ },
3181
3347
  {
3182
3348
  key: "audio_player_summary_color",
3183
3349
  label: "Audio player summary color",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "15.1.7",
3
+ "version": "15.1.9-alpha.3547421944",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "15.1.7",
30
+ "@applicaster/applicaster-types": "15.1.9-alpha.3547421944",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",