bitmovin-player-react-native 1.20.1 → 1.21.0

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.
@@ -9,16 +9,138 @@ export type PluginProps = {
9
9
  dependencies?: string[];
10
10
  };
11
11
 
12
+ const DEFAULT_SPACING = ' ';
13
+
12
14
  const defaultProps: PluginProps = {
13
- spacing: ' ',
15
+ spacing: DEFAULT_SPACING,
14
16
  dependencies: [],
15
17
  };
16
18
 
19
+ const CORE_LIBRARY_DESUGARING_DEPENDENCY =
20
+ "coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'";
21
+ const CORE_LIBRARY_DESUGARING_ARTIFACT = 'com.android.tools:desugar_jdk_libs';
22
+
23
+ const hasCoreLibraryDesugaringEnabled = (contents: string) =>
24
+ /^\s*(?:set)?CoreLibraryDesugaringEnabled\b.*?\btrue\b/m.test(contents);
25
+
26
+ const escapeRegExp = (value: string) =>
27
+ value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+
29
+ const hasGradleDependencyDeclaration = (
30
+ contents: string,
31
+ dependency: string,
32
+ configuration?: string,
33
+ allowVersionSuffix = false
34
+ ) => {
35
+ const dependencyPattern = escapeRegExp(dependency);
36
+ const configurationPattern = configuration
37
+ ? escapeRegExp(configuration)
38
+ : '[A-Za-z_][\\w.-]*';
39
+ const versionSuffixPattern = allowVersionSuffix ? `(?::[^'"]*)?` : '';
40
+ const declarationPattern = new RegExp(
41
+ `^${configurationPattern}\\s*(?:\\(\\s*)?['"]${dependencyPattern}${versionSuffixPattern}['"]`
42
+ );
43
+ let nestedBlockDepth = 0;
44
+
45
+ return contents.split('\n').some((line) => {
46
+ const trimmedLine = line.trim();
47
+ if (trimmedLine.startsWith('//')) {
48
+ return false;
49
+ }
50
+
51
+ if (/^dependencies\s*\{$/.test(trimmedLine)) {
52
+ return false;
53
+ }
54
+
55
+ const isTopLevelDependency =
56
+ nestedBlockDepth === 0 && declarationPattern.test(trimmedLine);
57
+ const openBlockCount = (trimmedLine.match(/\{/g) || []).length;
58
+ const closeBlockCount = (trimmedLine.match(/\}/g) || []).length;
59
+ nestedBlockDepth += openBlockCount - closeBlockCount;
60
+
61
+ return isTopLevelDependency;
62
+ });
63
+ };
64
+
65
+ // Matches any pinned version of the desugaring artifact so an existing
66
+ // declaration is never duplicated, even when it pins a different version.
67
+ const hasCoreLibraryDesugaringDependency = (contents: string) =>
68
+ hasGradleDependencyDeclaration(
69
+ contents,
70
+ CORE_LIBRARY_DESUGARING_ARTIFACT,
71
+ 'coreLibraryDesugaring',
72
+ true
73
+ );
74
+
75
+ const replaceDisabledCoreLibraryDesugaringSettings = (contents: string) =>
76
+ contents
77
+ .replace(
78
+ /^(\s*)setCoreLibraryDesugaringEnabled\s*\(\s*false\s*\)/gm,
79
+ '$1setCoreLibraryDesugaringEnabled(true)'
80
+ )
81
+ .replace(
82
+ /^(\s*)coreLibraryDesugaringEnabled(\s*=\s*)false\b/gm,
83
+ '$1coreLibraryDesugaringEnabled$2true'
84
+ )
85
+ .replace(
86
+ /^(\s*)coreLibraryDesugaringEnabled(\s+)false\b/gm,
87
+ '$1coreLibraryDesugaringEnabled$2true'
88
+ );
89
+
90
+ const ensureCoreLibraryDesugaringCompileOptions = (
91
+ contents: string,
92
+ spacing: string,
93
+ androidBlockStart: number,
94
+ androidPosition: number
95
+ ) => {
96
+ const androidBlock = contents.slice(androidBlockStart, androidPosition);
97
+ const updatedAndroidBlock =
98
+ replaceDisabledCoreLibraryDesugaringSettings(androidBlock);
99
+ const updatedContents = [
100
+ contents.slice(0, androidBlockStart),
101
+ updatedAndroidBlock,
102
+ contents.slice(androidPosition),
103
+ ].join('');
104
+ const updatedAndroidPosition =
105
+ androidPosition + updatedAndroidBlock.length - androidBlock.length;
106
+
107
+ if (hasCoreLibraryDesugaringEnabled(updatedAndroidBlock)) {
108
+ return updatedContents;
109
+ }
110
+
111
+ const compileOptionsRelativeStart = updatedAndroidBlock.search(
112
+ /^[^\S\r\n]*compileOptions\s*\{$/m
113
+ );
114
+ if (compileOptionsRelativeStart === -1) {
115
+ const compileOptions = [
116
+ `${spacing}compileOptions {`,
117
+ `${spacing}${spacing}setCoreLibraryDesugaringEnabled(true)`,
118
+ `${spacing}}`,
119
+ '',
120
+ ].join('\n');
121
+ return [
122
+ updatedContents.slice(0, updatedAndroidPosition),
123
+ compileOptions,
124
+ updatedContents.slice(updatedAndroidPosition),
125
+ ].join('');
126
+ }
127
+
128
+ const compileOptionsStart = androidBlockStart + compileOptionsRelativeStart;
129
+ const compileOptionsLineEnd =
130
+ updatedContents.indexOf('\n', compileOptionsStart) + 1;
131
+ return [
132
+ updatedContents.slice(0, compileOptionsLineEnd),
133
+ `${spacing}${spacing}setCoreLibraryDesugaringEnabled(true)\n`,
134
+ updatedContents.slice(compileOptionsLineEnd),
135
+ ].join('');
136
+ };
137
+
17
138
  const withAppGradleDependencies: ConfigPlugin<PluginProps> = (
18
139
  config,
19
140
  props: PluginProps
20
141
  ) => {
21
142
  const combinedProps = { ...defaultProps, ...(props || {}) };
143
+ const spacing = combinedProps.spacing || DEFAULT_SPACING;
22
144
  config = withAppBuildGradle(config, (config) => {
23
145
  if (config.modResults.language !== 'groovy') {
24
146
  WarningAggregator.addWarningAndroid(
@@ -28,16 +150,6 @@ const withAppGradleDependencies: ConfigPlugin<PluginProps> = (
28
150
  return config;
29
151
  }
30
152
 
31
- const deduplicatedDependencies = Array.from(
32
- new Set(combinedProps.dependencies)
33
- );
34
- const filteredDependencies = deduplicatedDependencies.filter((dep) => {
35
- return config.modResults.contents.indexOf(dep) === -1;
36
- });
37
- if (filteredDependencies.length === 0) {
38
- return config;
39
- }
40
-
41
153
  const androidBlockStart =
42
154
  config.modResults.contents.search(/^android \{$/m);
43
155
  if (androidBlockStart === -1) {
@@ -56,21 +168,6 @@ const withAppGradleDependencies: ConfigPlugin<PluginProps> = (
56
168
  );
57
169
  return config;
58
170
  }
59
- const androidPosition = androidBlockStart + androidBlockEnd;
60
- const compileOptions = [];
61
- compileOptions.push(`${combinedProps.spacing}compileOptions {`);
62
- compileOptions.push('\n');
63
- compileOptions.push(
64
- `${combinedProps.spacing}setCoreLibraryDesugaringEnabled(true)`
65
- );
66
- compileOptions.push('\n');
67
- compileOptions.push(`${combinedProps.spacing}}`);
68
- compileOptions.push('\n');
69
- config.modResults.contents = [
70
- config.modResults.contents.slice(0, androidPosition),
71
- ...compileOptions,
72
- config.modResults.contents.slice(androidPosition),
73
- ].join('');
74
171
 
75
172
  const dependenciesBlockStart =
76
173
  config.modResults.contents.search(/^dependencies \{$/m);
@@ -92,22 +189,43 @@ const withAppGradleDependencies: ConfigPlugin<PluginProps> = (
92
189
  );
93
190
  return config;
94
191
  }
95
- const position = dependenciesBlockStart + dependenciesBlockEnd;
96
- let insertedDependencies: string[] = [];
97
- insertedDependencies.push('\n');
98
- insertedDependencies.push(
99
- `${combinedProps.spacing}coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'`
192
+ const androidPosition = androidBlockStart + androidBlockEnd;
193
+ config.modResults.contents = ensureCoreLibraryDesugaringCompileOptions(
194
+ config.modResults.contents,
195
+ spacing,
196
+ androidBlockStart,
197
+ androidPosition
100
198
  );
101
- insertedDependencies.push('\n');
102
- filteredDependencies.forEach((dependency) => {
103
- insertedDependencies.push(
104
- `${combinedProps.spacing}implementation '${dependency}'\n`
105
- );
106
- });
107
- insertedDependencies.push('\n');
199
+
200
+ const updatedDependenciesBlockStart =
201
+ config.modResults.contents.search(/^dependencies \{$/m);
202
+ const updatedFromDependencies = config.modResults.contents.substring(
203
+ updatedDependenciesBlockStart
204
+ );
205
+ const updatedDependenciesBlockEnd = updatedFromDependencies.search(/^\}$/m);
206
+ const position =
207
+ updatedDependenciesBlockStart + updatedDependenciesBlockEnd;
208
+ const dependenciesBlock = config.modResults.contents.slice(
209
+ updatedDependenciesBlockStart,
210
+ position
211
+ );
212
+ const dependencyDeclarations = [
213
+ ...(!hasCoreLibraryDesugaringDependency(dependenciesBlock)
214
+ ? [`${spacing}${CORE_LIBRARY_DESUGARING_DEPENDENCY}`]
215
+ : []),
216
+ ...Array.from(new Set(combinedProps.dependencies))
217
+ .filter(
218
+ (dependency) =>
219
+ !hasGradleDependencyDeclaration(dependenciesBlock, dependency)
220
+ )
221
+ .map((dependency) => `${spacing}implementation '${dependency}'`),
222
+ ];
223
+ if (dependencyDeclarations.length === 0) {
224
+ return config;
225
+ }
108
226
  config.modResults.contents = [
109
227
  config.modResults.contents.slice(0, position),
110
- ...insertedDependencies,
228
+ `\n${dependencyDeclarations.join('\n')}\n`,
111
229
  config.modResults.contents.slice(position),
112
230
  ].join('');
113
231
  return config;
@@ -0,0 +1,150 @@
1
+ const assert = require('node:assert/strict');
2
+
3
+ const withAppGradleDependencies =
4
+ require('../plugin/build/withAppGradleDependencies').default;
5
+
6
+ const desugaringDependency =
7
+ "coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'";
8
+ const featureDependency = 'com.example:feature:1.0';
9
+
10
+ const countOccurrences = (contents, needle) =>
11
+ contents.split(needle).length - 1;
12
+
13
+ const applyPlugin = async (contents, props = {}) => {
14
+ const config = withAppGradleDependencies(
15
+ { name: 'test-app', slug: 'test-app' },
16
+ props
17
+ );
18
+ const appBuildGradleMod = config.mods?.android?.appBuildGradle;
19
+ assert.equal(typeof appBuildGradleMod, 'function');
20
+
21
+ const result = await appBuildGradleMod({
22
+ ...config,
23
+ modResults: {
24
+ language: 'groovy',
25
+ contents,
26
+ },
27
+ modRequest: {
28
+ projectRoot: process.cwd(),
29
+ platform: 'android',
30
+ modName: 'appBuildGradle',
31
+ introspect: true,
32
+ },
33
+ });
34
+
35
+ return result.modResults.contents;
36
+ };
37
+
38
+ const gradle = ({
39
+ android = " namespace 'com.example'",
40
+ dependencies = '',
41
+ }) => `plugins {
42
+ id 'com.android.application'
43
+ }
44
+
45
+ android {
46
+ ${android}
47
+ }
48
+
49
+ dependencies {
50
+ implementation 'com.facebook.react:react-android'
51
+ ${dependencies}
52
+ }
53
+ `;
54
+
55
+ const tests = [];
56
+
57
+ const test = (name, run) => {
58
+ tests.push({ name, run });
59
+ };
60
+
61
+ test('adds desugaring when no feature dependencies are requested', async () => {
62
+ const result = await applyPlugin(gradle({}));
63
+
64
+ assert.match(result, /setCoreLibraryDesugaringEnabled\(true\)/);
65
+ assert.ok(result.includes(desugaringDependency));
66
+ });
67
+
68
+ test('inserts desugaring inside existing compileOptions with surrounding whitespace', async () => {
69
+ const result = await applyPlugin(
70
+ gradle({
71
+ android: ` namespace 'com.example'
72
+
73
+ compileOptions {
74
+ sourceCompatibility JavaVersion.VERSION_17
75
+ }`,
76
+ })
77
+ );
78
+
79
+ assert.equal(countOccurrences(result, 'compileOptions {'), 1);
80
+ assert.match(
81
+ result,
82
+ /compileOptions \{\n setCoreLibraryDesugaringEnabled\(true\)\n sourceCompatibility/
83
+ );
84
+ assert.doesNotMatch(
85
+ result,
86
+ /namespace 'com\.example'\n\n setCoreLibraryDesugaringEnabled/
87
+ );
88
+ });
89
+
90
+ test('does not duplicate existing desugaring dependency', async () => {
91
+ const result = await applyPlugin(
92
+ gradle({
93
+ android: ` namespace 'com.example'
94
+ compileOptions {
95
+ setCoreLibraryDesugaringEnabled(true)
96
+ }`,
97
+ dependencies:
98
+ " coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'",
99
+ })
100
+ );
101
+
102
+ assert.equal(
103
+ countOccurrences(
104
+ result,
105
+ "coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs"
106
+ ),
107
+ 1
108
+ );
109
+ assert.doesNotMatch(result, /desugar_jdk_libs:2\.1\.5/);
110
+ });
111
+
112
+ test('ignores comments and constraints when checking feature dependencies', async () => {
113
+ const result = await applyPlugin(
114
+ gradle({
115
+ dependencies: ` // ${featureDependency} is added by the plugin
116
+ constraints {
117
+ implementation('${featureDependency}') {
118
+ because 'pins the version when another dependency requests it'
119
+ }
120
+ }`,
121
+ }),
122
+ { dependencies: [featureDependency] }
123
+ );
124
+
125
+ assert.match(result, /implementation 'com\.example:feature:1\.0'/);
126
+ });
127
+
128
+ const run = async () => {
129
+ const failures = [];
130
+
131
+ for (const { name, run } of tests) {
132
+ try {
133
+ await run();
134
+ console.log(`PASS ${name}`);
135
+ } catch (error) {
136
+ failures.push({ name, error });
137
+ console.error(`FAIL ${name}`);
138
+ console.error(error);
139
+ }
140
+ }
141
+
142
+ if (failures.length > 0) {
143
+ process.exitCode = 1;
144
+ }
145
+ };
146
+
147
+ run().catch((error) => {
148
+ console.error(error);
149
+ process.exitCode = 1;
150
+ });
package/src/events.ts CHANGED
@@ -746,6 +746,70 @@ export interface PlaybackSpeedChangedEvent extends Event {
746
746
  to: number;
747
747
  }
748
748
 
749
+ /**
750
+ * The `line` field of a {@link SubtitleCueLayout}.
751
+ *
752
+ * `{ value: 'auto' }` defers placement to the renderer.
753
+ * `{ value: number; unit: 'percent' }` places the cue as a percentage of the viewport.
754
+ * `{ value: number; unit: 'line' }` places the cue by counting rendered text-line slots.
755
+ *
756
+ * @platform Android
757
+ */
758
+ export type SubtitleCueLayoutLine =
759
+ | { value: 'auto' }
760
+ | { value: number; unit: 'line' | 'percent' };
761
+
762
+ /**
763
+ * Normalized cue-box geometry.
764
+ *
765
+ * Values reflect native cue data including platform defaults, not only explicitly authored
766
+ * subtitle settings. Omitted fields were not set or not applicable for this cue.
767
+ *
768
+ * @platform Android
769
+ */
770
+ export interface SubtitleCueLayout {
771
+ /**
772
+ * Cue position on the axis perpendicular to the text flow.
773
+ *
774
+ * For horizontal captions this controls vertical placement:
775
+ * - `{ value: 85, unit: 'percent' }` — 85% down the viewport.
776
+ * - `{ value: 12, unit: 'line' }` — 12 rendered text-line slots from the edge.
777
+ * - `{ value: 'auto' }` — renderer chooses placement.
778
+ *
779
+ * For vertical captions this controls horizontal placement.
780
+ */
781
+ line?: SubtitleCueLayoutLine;
782
+ /**
783
+ * Which edge of the cue box is anchored at `line`.
784
+ *
785
+ * Does not control text alignment inside the box; see `textAlign` for that.
786
+ */
787
+ lineAlign?: 'start' | 'center' | 'end';
788
+ /**
789
+ * Cue box position as a percentage of the viewport on the axis orthogonal to `line`.
790
+ *
791
+ * `'auto'` defers to the renderer.
792
+ */
793
+ position?: number | 'auto';
794
+ /**
795
+ * Which edge of the cue box is anchored at `position`.
796
+ */
797
+ positionAlign?: 'line-left' | 'center' | 'line-right' | 'auto';
798
+ /**
799
+ * Width of the cue box as a percentage of the viewport dimension in the writing direction.
800
+ */
801
+ size?: number;
802
+ /**
803
+ * Text alignment inside the cue box.
804
+ */
805
+ textAlign?: 'start' | 'center' | 'end' | 'left' | 'right';
806
+ /**
807
+ * Writing direction of the cue text; affects how `line`, `position`, and `size` are interpreted.
808
+ * When absent, the cue uses the default horizontal writing direction.
809
+ */
810
+ writingMode?: 'vertical-lr' | 'vertical-rl';
811
+ }
812
+
749
813
  /**
750
814
  * Emitted when a subtitle entry transitions into the active status.
751
815
  */
@@ -759,13 +823,27 @@ export interface CueEnterEvent extends Event {
759
823
  */
760
824
  end: number;
761
825
  /**
762
- * The textual content of this subtitle.
826
+ * Plain-text content of this subtitle.
763
827
  */
764
828
  text?: string;
765
829
  /**
766
- * Data URI for image data of this subtitle.
830
+ * Data URI for image subtitle data.
767
831
  */
768
832
  image?: string;
833
+ /**
834
+ * HTML cue text, which may include styling generated by the native SDK.
835
+ *
836
+ * Treat as renderer input, not plain text.
837
+ *
838
+ * @platform Android
839
+ */
840
+ html?: string;
841
+ /**
842
+ * Normalized cue-box geometry.
843
+ *
844
+ * @platform Android
845
+ */
846
+ layout?: SubtitleCueLayout;
769
847
  }
770
848
 
771
849
  /**
@@ -781,13 +859,27 @@ export interface CueExitEvent extends Event {
781
859
  */
782
860
  end: number;
783
861
  /**
784
- * The textual content of this subtitle.
862
+ * Plain-text content of this subtitle.
785
863
  */
786
864
  text?: string;
787
865
  /**
788
- * Data URI for image data of this subtitle.
866
+ * Data URI for image subtitle data.
789
867
  */
790
868
  image?: string;
869
+ /**
870
+ * HTML cue text, which may include styling generated by the native SDK.
871
+ *
872
+ * Treat as renderer input, not plain text.
873
+ *
874
+ * @platform Android
875
+ */
876
+ html?: string;
877
+ /**
878
+ * Normalized cue-box geometry.
879
+ *
880
+ * @platform Android
881
+ */
882
+ layout?: SubtitleCueLayout;
791
883
  }
792
884
 
793
885
  /**
@@ -13,8 +13,6 @@ export interface MediaControlConfig {
13
13
  * For a detailed list of the supported features in the **default behavior**,
14
14
  * check the **Default Supported Features** section.
15
15
  *
16
- * @remarks Enabling this flag will automatically treat {@link TweaksConfig.updatesNowPlayingInfoCenter} as `false`.
17
- *
18
16
  * ## Limitations
19
17
  * ---
20
18
  * - Android: If an app creates multiple player instances, the player shown in media controls is the latest one created having media controls enabled.
@@ -152,6 +152,11 @@ declare class PlayerModule extends NativeModule<PlayerModuleEvents> {
152
152
  */
153
153
  isAirPlayAvailable(nativeId: string): Promise<boolean | null>;
154
154
 
155
+ /**
156
+ * Display the AirPlay route selection menu for nativeId's player (iOS only).
157
+ */
158
+ showAirPlayTargetPicker(nativeId: string): Promise<void>;
159
+
155
160
  /**
156
161
  * Resolve nativeId's cast availability state.
157
162
  */
package/src/player.ts CHANGED
@@ -395,6 +395,22 @@ export class Player extends NativeInstance<PlayerConfig> {
395
395
  return (await PlayerModule.isAirPlayAvailable(this.nativeId)) ?? false;
396
396
  };
397
397
 
398
+ /**
399
+ * Displays the system AirPlay route selection menu, allowing the user to pick
400
+ * an AirPlay target for the current playback.
401
+ *
402
+ * @platform iOS
403
+ */
404
+ showAirPlayTargetPicker = () => {
405
+ if (Platform.OS !== 'ios' || Platform.isTV) {
406
+ console.warn(
407
+ `[Player ${this.nativeId}] Method showAirPlayTargetPicker is only available on iOS (not Android/tvOS).`
408
+ );
409
+ return;
410
+ }
411
+ void PlayerModule.showAirPlayTargetPicker(this.nativeId);
412
+ };
413
+
398
414
  /**
399
415
  * @returns The currently selected audio track or `null`.
400
416
  */
@@ -174,19 +174,6 @@ export interface TweaksConfig {
174
174
  * @platform Android
175
175
  */
176
176
  enableDrmLicenseRenewRetry?: boolean;
177
- /**
178
- * Determines whether `AVKit` should update Now Playing information automatically when using System UI.
179
- *
180
- * - If set to `false`, the automatic updates of Now Playing Info sent by `AVKit` are disabled.
181
- * This prevents interference with manual updates you may want to perform.
182
- * - If set to `true`, the default behaviour is maintained, allowing `AVKit` to handle Now Playing updates.
183
- *
184
- * Default is `true`.
185
- *
186
- * @deprecated To enable the Now Playing information use {@link MediaControlConfig.isEnabled}
187
- * @platform iOS
188
- */
189
- updatesNowPlayingInfoCenter?: boolean;
190
177
 
191
178
  /**
192
179
  * When switching between video formats (eg: adapting between video qualities)