bitmovin-player-react-native 1.0.0 → 1.2.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.
Files changed (37) hide show
  1. package/.prettierignore +1 -0
  2. package/AGENTS.md +91 -0
  3. package/CHANGELOG.md +25 -2
  4. package/CONTRIBUTING.md +5 -8
  5. package/android/build.gradle +3 -3
  6. package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +4 -2
  7. package/android/src/main/java/com/bitmovin/player/reactnative/util/NonFiniteSanitizer.kt +36 -0
  8. package/build/hooks/useProxy.d.ts.map +1 -1
  9. package/build/hooks/useProxy.js +3 -1
  10. package/build/hooks/useProxy.js.map +1 -1
  11. package/build/utils/normalizeNonFinite.d.ts +2 -0
  12. package/build/utils/normalizeNonFinite.d.ts.map +1 -0
  13. package/build/utils/normalizeNonFinite.js +22 -0
  14. package/build/utils/normalizeNonFinite.js.map +1 -0
  15. package/ios/NonFiniteSanitizer.swift +61 -0
  16. package/ios/PlayerModule.swift +3 -3
  17. package/ios/RCTConvert+BitmovinPlayer.swift +1 -1
  18. package/ios/RNBitmovinPlayer.podspec +3 -3
  19. package/ios/RNPlayerView.swift +1 -1
  20. package/package.json +2 -2
  21. package/plugin/build/index.d.ts +1 -4
  22. package/plugin/build/withAppGradleDependencies.js +26 -0
  23. package/plugin/build/withBitmovinAndroidConfig.d.ts +2 -5
  24. package/plugin/build/withBitmovinAndroidConfig.js +5 -2
  25. package/plugin/build/withBitmovinConfig.d.ts +6 -5
  26. package/plugin/build/withBitmovinConfig.js +7 -6
  27. package/plugin/build/withBitmovinIosConfig.d.ts +2 -5
  28. package/plugin/build/withBitmovinIosConfig.js +5 -2
  29. package/plugin/src/withAppGradleDependencies.ts +35 -0
  30. package/plugin/src/withBitmovinAndroidConfig.ts +10 -10
  31. package/plugin/src/withBitmovinConfig.ts +13 -10
  32. package/plugin/src/withBitmovinIosConfig.ts +6 -6
  33. package/scripts/check-dependencies.js +7 -8
  34. package/scripts/setup-hooks.sh +7 -9
  35. package/src/hooks/useProxy.ts +3 -1
  36. package/src/utils/normalizeNonFinite.ts +16 -0
  37. package/android/src/main/AndroidManifestNew.xml +0 -2
package/.prettierignore CHANGED
@@ -9,3 +9,4 @@ integration_test/android/
9
9
  integration_test/ios/
10
10
  integration_test/ios/Pods/
11
11
  .github/*.md
12
+ CLAUDE.md
package/AGENTS.md ADDED
@@ -0,0 +1,91 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+
5
+ - `src/`: TypeScript API surface and React components (library code).
6
+ - `ios/`, `android/`: Native bridges (Swift/Kotlin) used by the Expo Module.
7
+ - `plugin/`: Expo Config Plugin code used during prebuild.
8
+ - `example/`: Runnable sample app (Expo) to develop and validate changes.
9
+ - `integration_test/`: Expo app + Cavy-based integration tests.
10
+ - `scripts/`: Repo tooling (lint/format hooks, helpers). `build/`: transpiled output.
11
+
12
+ ## Build, Test, and Development Commands
13
+
14
+ - `yarn bootstrap` — install deps, prebuild example, set up pods.
15
+ - `yarn build` (`build:module`, `build:plugin`) — build the library and plugin to `build/`.
16
+ - `yarn example start | ios | android` — run Metro or launch the example on simulators/emulators.
17
+ - `yarn integration-test test:ios | test:android | test` — run Cavy tests.
18
+ - `yarn lint` (`lint:all`), `yarn typecheck` (`typecheck:all`), `yarn format:all` — quality gates.
19
+ - `yarn docs` — generate TypeDoc documentation.
20
+
21
+ ## Coding Style & Naming Conventions
22
+
23
+ - TypeScript: ESLint + Prettier (2 spaces, single quotes, trailing commas `es5`).
24
+ - Names: types/interfaces PascalCase; variables/functions camelCase; React components PascalCase.
25
+ - Files in `src/` use camelCase (e.g., `playerConfig.ts`).
26
+ - iOS: SwiftLint via `yarn lint:ios`; format with `yarn format:ios`.
27
+ - Android: ktlint via `yarn lint:android`; format with `yarn format:android`.
28
+
29
+ ## Testing Guidelines
30
+
31
+ - Framework: Cavy; tests live under `integration_test/tests`.
32
+ - Setup: `cp integration_test/.env.example integration_test/.env` and set `EXPO_PUBLIC_BITMOVIN_PLAYER_LICENSE_KEY`.
33
+ - Run: `yarn integration-test test:ios` or `test:android` (simulator/emulator only).
34
+ - Add per‑feature tests (e.g., `Playback.test.ts`) covering happy‑path and error events.
35
+
36
+ ## Commit & Pull Request Guidelines
37
+
38
+ - Commits: concise, imperative; do not use prefixes such as `fix:`, `chore:`, etc.
39
+ - PRs: follow `.github/PULL_REQUEST_TEMPLATE.md`; link issues; add screenshots for UI-facing changes.
40
+ - Required before review: `yarn lint:all`, `yarn typecheck:all`, build the library, and run the example on at least one platform.
41
+ - Changelog: add a `CHANGELOG.md` entry for user‑visible behavior changes.
42
+
43
+ ## Security & Configuration Tips
44
+
45
+ - Never commit secrets. Keep license keys only in `integration_test/.env` (gitignored).
46
+
47
+ ## Docs Index
48
+
49
+ - CONTRIBUTING.md — Development workflow, Development setup, TypeScript Code Style, Linting (pre-commit hooks), Kotlin, Swift, Testing, Scripts.
50
+ - example/README.md — Getting started, Development Setup (.env and license keys), Running the application (iOS/Android/tvOS/Android TV), Bundler only, Architecture, Troubleshooting.
51
+ - integration_test/README.md — Setup, Environment Configuration (.env), Running the tests, Architecture, Test Coverage, Platform Support.
52
+ - README.md — “Sample Application” section links to `example/` README and official docs; use as entry point.
53
+
54
+ ## Developer Essentials
55
+
56
+ - Environment
57
+ - Node + Yarn (use Yarn across workspaces); macOS: Xcode + CocoaPods; Android: Android Studio + JDK 17+ (Gradle 8.2 wrapper).
58
+ - Platforms per README: Expo 53+, React Native 0.79+, React 17+.
59
+ - Tooling & Hooks
60
+ - Install hooks with `yarn setup-hooks`; verify with `yarn lint:all` and `yarn typecheck:all` before PRs.
61
+ - SwiftLint/ktlint required for native; use `yarn format:ios` / `yarn format:android` for auto-fixes.
62
+ - Workflow
63
+ - Bootstrap from repo root: `yarn bootstrap`. Re-run prebuilds after env/native/config changes (`yarn example prebuild`, `yarn integration-test prebuild`).
64
+ - Public API: export via `src/index.ts`; keep native module/view names identical to TS wrappers; maintain event parity with TS types.
65
+ - Instance routing uses stable `nativeId` values; ensure native registries clean up on module destruction.
66
+ - Secrets
67
+ - Never commit keys. Store license keys only in `.env` under `example/` and `integration_test/` as documented.
68
+ - Validation
69
+ - Use the example app for manual checks and the integration tests on simulators/emulators for feature coverage.
70
+
71
+ ## Routing Public API to React Native
72
+
73
+ - Source of truth: export public surface from `src/index.ts:1-29` (ensure new modules are re‑exported).
74
+ - Use typed Expo wrappers in `src/modules/`:
75
+ - Define `declare class <Feature>Module extends NativeModule<Events> { ... }` and load with `requireNativeModule('<Feature>Module')` (example: `src/modules/PlayerModule.ts:1,251`).
76
+ - Match native names exactly on both platforms:
77
+ - iOS: `Name("PlayerModule")` in `ios/PlayerModule.swift:7`; Android: `Name("PlayerModule")` in `android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt:23`.
78
+ - Bridge views via view managers:
79
+ - TS host component: `requireNativeViewManager('RNPlayerViewManager')` in `src/components/PlayerView/native.ts:33-35`.
80
+ - iOS manager: `ios/RNPlayerViewManager.swift:6`; Android manager: `android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt:12`.
81
+ - Events flow native → JS via Expo `Events(...)` lists; keep parity with TS props:
82
+ - Android events list: `android/.../RNPlayerViewManager.kt:50-112`; iOS: `ios/RNPlayerViewManager.swift:33-60`.
83
+ - TS event typings live in `src/components/PlayerView/nativeEvents.ts:72-160` (extend as needed).
84
+ - Instance routing uses `nativeId` across modules; keep IDs stable and registry-backed on native:
85
+ - Example registry and async functions in `android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt:18-24,41-49`.
86
+ - Adding a new API (happy path):
87
+ - TS: add method on a `src/modules/<Feature>Module.ts` wrapper, export from `src/index.ts`.
88
+ - iOS/Android: add matching `AsyncFunction("methodName")` to Expo `ModuleDefinition`; emit events if needed.
89
+ - Views: add `Prop(...)`/`Events(...)` in native managers and extend `NativePlayerViewProps`.
90
+ - Tests: extend integration tests; validate on at least one platform.
91
+ - Naming: keep method/event names identical across TS, iOS, Android.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2025-10-17
4
+
5
+ ### Changed
6
+
7
+ - Update Bitmovin's native iOS SDK version to `3.97.2`
8
+ - Update Bitmovin's native Android SDK version to `3.128.0+jason`
9
+ - Update IMA SDK dependency on iOS to `3.26.1`
10
+ - Update IMA SDK dependency on tvOS to `4.15.1`
11
+ - Update IMA SDK dependency on Android to `3.37.0`
12
+
13
+ ## [1.1.0] - 2025-09-03
14
+
15
+ ### Changed
16
+
17
+ - Update Bitmovin's native Android SDK version to `3.123.0`
18
+ - Update Bitmovin's native iOS SDK version to `3.94.1`
19
+
20
+ ### Fixed
21
+
22
+ - Crash when using `react-native-reanimated` along with `bitmovin-player-react-native` and playing a live stream
23
+ - Expo config plugin feature configurations taking no effect
24
+ - Example application crash on tvOS simulator
25
+
3
26
  ## [1.0.0] - 2025-08-04
4
27
 
5
28
  ### Breaking Change
@@ -14,8 +37,8 @@
14
37
 
15
38
  ### Changed
16
39
 
17
- - Minimum iOS/tvOS version is now 15.1+ (was 14.0+).
18
- - Minimum Android SDK version is now 24 (was 21).
40
+ - Minimum iOS/tvOS version is now 15.1+ (was 14.0+). Due to a transient React Native minimum [iOS/tvOS version change](https://github.com/react-native-community/discussions-and-proposals/discussions/812).
41
+ - Minimum Android SDK version is now 24 (was 21). Due to a transient React Native minimum [Android version change](https://github.com/react-native-community/discussions-and-proposals/discussions/802).
19
42
  - Native setup is now automated through Expo SDK - manual configuration is no longer required for v1.0.0+.
20
43
 
21
44
  ## [0.44.0] - 2025-07-25
package/CONTRIBUTING.md CHANGED
@@ -41,17 +41,14 @@ To build and run the example app on iOS:
41
41
  yarn example ios
42
42
  ```
43
43
 
44
- To edit the Swift/Objective-C files, open `example/ios/BitmovinPlayerReactNativeExample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > RNBitmovinPlayer`.
44
+ To edit the Swift/Objective-C files, open Xcode via `yarn example open:ios` and find the source files at `Pods > Development Pods > RNBitmovinPlayer`.
45
45
 
46
- To edit the Kotlin files, open `example/android` in Android Studio and find the source files at `bitmovin-player-react-native` under `Android`.
46
+ To edit the Kotlin files, open Android Studio via `yarn example open:android` and find the source files at `bitmovin-player-react-native` under `Android`.
47
47
 
48
- ## For iOS/tvOS on-device development
48
+ ## Development setup
49
49
 
50
- To build the example project for an iOS or tvOS device, you need to create a file at `example/ios/Developer.xcconfig`. In this file, add your development team like this:
51
-
52
- ```yml
53
- DEVELOPMENT_TEAM = YOUR_TEAM_ID
54
- ```
50
+ - For the Example app, see the relevant [`example/README`](example/README.md#development-setup) section.
51
+ - For the integration tests, see the relevant [`integration_test/README`](README.md#2-environment-configuration) section.
55
52
 
56
53
  ## TypeScript Code Style
57
54
 
@@ -104,10 +104,10 @@ dependencies {
104
104
  implementation "androidx.concurrent:concurrent-futures-ktx:1.1.0"
105
105
 
106
106
  // Google IMA
107
- implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.35.1'
107
+ implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.37.0'
108
108
 
109
109
  // Bitmovin
110
110
  implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
111
- implementation 'com.bitmovin.player:player:3.119.0+jason'
112
- implementation 'com.bitmovin.player:player-media-session:3.119.0+jason'
111
+ implementation 'com.bitmovin.player:player:3.128.0+jason'
112
+ implementation 'com.bitmovin.player:player-media-session:3.128.0+jason'
113
113
  }
@@ -22,6 +22,7 @@ import com.bitmovin.player.api.ui.UiConfig
22
22
  import com.bitmovin.player.reactnative.converter.toJson
23
23
  import com.bitmovin.player.reactnative.converter.toUserInterfaceType
24
24
  import com.bitmovin.player.reactnative.ui.RNPictureInPictureHandler
25
+ import com.bitmovin.player.reactnative.util.NonFiniteSanitizer
25
26
  import expo.modules.kotlin.AppContext
26
27
  import expo.modules.kotlin.viewevent.EventDispatcher
27
28
  import expo.modules.kotlin.viewevent.ViewEventCallback
@@ -615,8 +616,9 @@ class RNPlayerView(context: Context, appContext: AppContext) : ExpoView(context,
615
616
  }
616
617
 
617
618
  private fun onEvent(dispatcher: ViewEventCallback<Map<String, Any>>, eventData: Map<String, Any>) {
618
- dispatcher(eventData)
619
- onBmpEvent(eventData)
619
+ val sanitized = NonFiniteSanitizer.sanitizeEventData(eventData)
620
+ dispatcher(sanitized)
621
+ onBmpEvent(sanitized)
620
622
  }
621
623
 
622
624
  fun setFullscreen(isFullscreen: Boolean) {
@@ -0,0 +1,36 @@
1
+ package com.bitmovin.player.reactnative.util
2
+
3
+ object NonFiniteSanitizer {
4
+ /**
5
+ * Type-safe method specifically for event data maps.
6
+ * Sanitizes event data while preserving type safety and handling null filtering.
7
+ */
8
+ fun sanitizeEventData(
9
+ eventData: Map<String, Any>,
10
+ ): Map<String, Any> = eventData.mapValues { sanitize(it.value) ?: it.value }
11
+
12
+ private fun sanitize(value: Any?): Any? = when (value) {
13
+ null -> null
14
+ is Double -> if (value.isFinite()) value else value.toSentinel()
15
+ is Float -> if (value.isFinite()) value else value.toSentinel()
16
+ is Map<*, *> -> value.mapValues { sanitize(it.value) }
17
+ is List<*> -> value.mapNotNull { sanitize(it) }
18
+ is Array<*> -> value.mapNotNull { sanitize(it) }
19
+ is Number -> value // Int/Long/Short/Byte
20
+ else -> value
21
+ }
22
+ }
23
+
24
+ private const val SENTINEL_PREFIX = "BMP_"
25
+
26
+ private fun Double.toSentinel(): String = when (this) {
27
+ Double.POSITIVE_INFINITY -> "${SENTINEL_PREFIX}Infinity"
28
+ Double.NEGATIVE_INFINITY -> "${SENTINEL_PREFIX}-Infinity"
29
+ else -> "${SENTINEL_PREFIX}NaN"
30
+ }
31
+
32
+ private fun Float.toSentinel(): String = when (this) {
33
+ Float.POSITIVE_INFINITY -> "${SENTINEL_PREFIX}Infinity"
34
+ Float.NEGATIVE_INFINITY -> "${SENTINEL_PREFIX}-Infinity"
35
+ else -> "${SENTINEL_PREFIX}NaN"
36
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"useProxy.d.ts","sourceRoot":"","sources":["../../src/hooks/useProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAe,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGlC;;GAEG;AACH,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAEtC;;GAEG;AACH,KAAK,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE;IAAE,WAAW,EAAE,CAAC,CAAA;CAAE,KAAK,IAAI,CAAC;AAE7D;;GAEG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,GACtB,CAAC,CAAC,SAAS,KAAK,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,CAAC,CAahE"}
1
+ {"version":3,"file":"useProxy.d.ts","sourceRoot":"","sources":["../../src/hooks/useProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAe,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAIlC;;GAEG;AACH,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAEtC;;GAEG;AACH,KAAK,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE;IAAE,WAAW,EAAE,CAAC,CAAA;CAAE,KAAK,IAAI,CAAC;AAE7D;;GAEG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,GACtB,CAAC,CAAC,SAAS,KAAK,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,CAAC,CAchE"}
@@ -1,5 +1,6 @@
1
1
  import { useCallback } from 'react';
2
2
  import { findNodeHandle } from 'react-native';
3
+ import { normalizeNonFinite } from '../utils/normalizeNonFinite';
3
4
  /**
4
5
  * Create a proxy function that unwraps native events.
5
6
  */
@@ -10,7 +11,8 @@ export function useProxy(viewRef) {
10
11
  return;
11
12
  }
12
13
  const { target, ...eventWithoutTarget } = event.nativeEvent;
13
- callback?.(eventWithoutTarget);
14
+ const sanitized = normalizeNonFinite(eventWithoutTarget);
15
+ callback?.(sanitized);
14
16
  }, [viewRef]);
15
17
  }
16
18
  //# sourceMappingURL=useProxy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useProxy.js","sourceRoot":"","sources":["../../src/hooks/useProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,WAAW,EAAE,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAY9C;;GAEG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAuB;IAEvB,OAAO,WAAW,CAChB,CAAkB,QAAsB,EAAE,EAAE,CAC1C,CAAC,KAAyB,EAAE,EAAE;QAC5B,MAAM,qBAAqB,GAAY,KAAK,CAAC,WAAmB,CAAC,MAAM,CAAC;QACxE,IAAI,qBAAqB,KAAK,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,OAAO;QACT,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,GAAG,kBAAkB,EAAE,GAAG,KAAK,CAAC,WAAkB,CAAC;QACnE,QAAQ,EAAE,CAAC,kBAAuB,CAAC,CAAC;IACtC,CAAC,EACH,CAAC,OAAO,CAAC,CACV,CAAC;AACJ,CAAC","sourcesContent":["import { RefObject, useCallback } from 'react';\nimport { Event } from '../events';\nimport { findNodeHandle } from 'react-native';\n\n/**\n * A function that takes a generic event as argument.\n */\ntype Callback<E> = (event: E) => void;\n\n/**\n * A function that takes the synthetic version of a generic event as argument.\n */\ntype NativeCallback<E> = (event: { nativeEvent: E }) => void;\n\n/**\n * Create a proxy function that unwraps native events.\n */\nexport function useProxy(\n viewRef: RefObject<any>\n): <E extends Event>(callback?: Callback<E>) => NativeCallback<E> {\n return useCallback(\n <E extends Event>(callback?: Callback<E>) =>\n (event: { nativeEvent: E }) => {\n const eventTargetNodeHandle: number = (event.nativeEvent as any).target;\n if (eventTargetNodeHandle !== findNodeHandle(viewRef.current)) {\n return;\n }\n const { target, ...eventWithoutTarget } = event.nativeEvent as any;\n callback?.(eventWithoutTarget as E);\n },\n [viewRef]\n );\n}\n"]}
1
+ {"version":3,"file":"useProxy.js","sourceRoot":"","sources":["../../src/hooks/useProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,WAAW,EAAE,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAYjE;;GAEG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAuB;IAEvB,OAAO,WAAW,CAChB,CAAkB,QAAsB,EAAE,EAAE,CAC1C,CAAC,KAAyB,EAAE,EAAE;QAC5B,MAAM,qBAAqB,GAAY,KAAK,CAAC,WAAmB,CAAC,MAAM,CAAC;QACxE,IAAI,qBAAqB,KAAK,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,OAAO;QACT,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,GAAG,kBAAkB,EAAE,GAAG,KAAK,CAAC,WAAkB,CAAC;QACnE,MAAM,SAAS,GAAG,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;QACzD,QAAQ,EAAE,CAAC,SAAc,CAAC,CAAC;IAC7B,CAAC,EACH,CAAC,OAAO,CAAC,CACV,CAAC;AACJ,CAAC","sourcesContent":["import { RefObject, useCallback } from 'react';\nimport { Event } from '../events';\nimport { findNodeHandle } from 'react-native';\nimport { normalizeNonFinite } from '../utils/normalizeNonFinite';\n\n/**\n * A function that takes a generic event as argument.\n */\ntype Callback<E> = (event: E) => void;\n\n/**\n * A function that takes the synthetic version of a generic event as argument.\n */\ntype NativeCallback<E> = (event: { nativeEvent: E }) => void;\n\n/**\n * Create a proxy function that unwraps native events.\n */\nexport function useProxy(\n viewRef: RefObject<any>\n): <E extends Event>(callback?: Callback<E>) => NativeCallback<E> {\n return useCallback(\n <E extends Event>(callback?: Callback<E>) =>\n (event: { nativeEvent: E }) => {\n const eventTargetNodeHandle: number = (event.nativeEvent as any).target;\n if (eventTargetNodeHandle !== findNodeHandle(viewRef.current)) {\n return;\n }\n const { target, ...eventWithoutTarget } = event.nativeEvent as any;\n const sanitized = normalizeNonFinite(eventWithoutTarget);\n callback?.(sanitized as E);\n },\n [viewRef]\n );\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function normalizeNonFinite<T>(input: T): T;
2
+ //# sourceMappingURL=normalizeNonFinite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeNonFinite.d.ts","sourceRoot":"","sources":["../../src/utils/normalizeNonFinite.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAejD"}
@@ -0,0 +1,22 @@
1
+ export function normalizeNonFinite(input) {
2
+ const sentinelPrefix = 'BMP_';
3
+ function walk(v) {
4
+ if (v === `${sentinelPrefix}Infinity`)
5
+ return Infinity;
6
+ if (v === `${sentinelPrefix}-Infinity`)
7
+ return -Infinity;
8
+ if (v === `${sentinelPrefix}NaN`)
9
+ return NaN;
10
+ if (Array.isArray(v))
11
+ return v.map(walk);
12
+ if (v && typeof v === 'object') {
13
+ const out = {};
14
+ for (const k of Object.keys(v))
15
+ out[k] = walk(v[k]);
16
+ return out;
17
+ }
18
+ return v;
19
+ }
20
+ return walk(input);
21
+ }
22
+ //# sourceMappingURL=normalizeNonFinite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeNonFinite.js","sourceRoot":"","sources":["../../src/utils/normalizeNonFinite.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,kBAAkB,CAAI,KAAQ;IAC5C,MAAM,cAAc,GAAG,MAAM,CAAC;IAC9B,SAAS,IAAI,CAAC,CAAM;QAClB,IAAI,CAAC,KAAK,GAAG,cAAc,UAAU;YAAE,OAAO,QAAQ,CAAC;QACvD,IAAI,CAAC,KAAK,GAAG,cAAc,WAAW;YAAE,OAAO,CAAC,QAAQ,CAAC;QACzD,IAAI,CAAC,KAAK,GAAG,cAAc,KAAK;YAAE,OAAO,GAAG,CAAC;QAC7C,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAQ,EAAE,CAAC;YACpB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAE,CAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7D,OAAO,GAAG,CAAC;QACb,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC","sourcesContent":["export function normalizeNonFinite<T>(input: T): T {\n const sentinelPrefix = 'BMP_';\n function walk(v: any): any {\n if (v === `${sentinelPrefix}Infinity`) return Infinity;\n if (v === `${sentinelPrefix}-Infinity`) return -Infinity;\n if (v === `${sentinelPrefix}NaN`) return NaN;\n if (Array.isArray(v)) return v.map(walk);\n if (v && typeof v === 'object') {\n const out: any = {};\n for (const k of Object.keys(v)) out[k] = walk((v as any)[k]);\n return out;\n }\n return v;\n }\n return walk(input);\n}\n"]}
@@ -0,0 +1,61 @@
1
+ import Foundation
2
+
3
+ internal enum NonFiniteSanitizer {
4
+ /// Type-safe method specifically for event data dictionaries.
5
+ /// Sanitizes event data while preserving type safety.
6
+ static func sanitizeEventData(_ eventData: [String: Any]) -> [String: Any] {
7
+ eventData.reduce(into: [:]) {
8
+ $0[$1.key] = sanitize($1.value)
9
+ }
10
+ }
11
+
12
+ // Keep existing generic method for backward compatibility
13
+ private static func sanitize(_ value: Any) -> Any {
14
+ switch value {
15
+ case let doubleValue as Double:
16
+ guard doubleValue.isInfinite else { return doubleValue }
17
+ return doubleValue.toSentinel()
18
+ case let floatValue as Float:
19
+ guard floatValue.isInfinite else { return floatValue }
20
+ return floatValue.toSentinel()
21
+ case let dict as [AnyHashable: Any]:
22
+ return dict.reduce(into: [:]) {
23
+ $0[$1.key] = sanitize($1.value)
24
+ }
25
+ case let array as [Any]:
26
+ return array.map { sanitize($0) }
27
+ default:
28
+ return value
29
+ }
30
+ }
31
+ }
32
+
33
+ private let sentinelPrefix = "BMP_"
34
+
35
+ private extension Float {
36
+ // Helper to convert non-finite doubles to sentinel strings
37
+ func toSentinel() -> String {
38
+ switch self {
39
+ case .infinity:
40
+ return "\(sentinelPrefix)Infinity"
41
+ case -.infinity:
42
+ return "\(sentinelPrefix)-Infinity"
43
+ default:
44
+ return "\(sentinelPrefix)NaN"
45
+ }
46
+ }
47
+ }
48
+
49
+ private extension Double {
50
+ // Helper to convert non-finite doubles to sentinel strings
51
+ func toSentinel() -> String {
52
+ switch self {
53
+ case .infinity:
54
+ return "\(sentinelPrefix)Infinity"
55
+ case -.infinity:
56
+ return "\(sentinelPrefix)-Infinity"
57
+ default:
58
+ return "\(sentinelPrefix)NaN"
59
+ }
60
+ }
61
+ }
@@ -188,7 +188,7 @@ public class PlayerModule: Module {
188
188
  ) { [weak self] (nativeId: NativeId, analyticsConfig: [String: Any]?, config: [String: Any]?, networkNativeId: NativeId?, _: String?) in // swiftlint:disable:this line_length
189
189
  guard !PlayerRegistry.hasPlayer(nativeId: nativeId),
190
190
  let playerConfig = RCTConvert.playerConfig(config),
191
- let analyticsConfig = RCTConvert.analyticsConfig(analyticsConfig) else { return } // swiftlint:disable:this line_length
191
+ let analyticsConfig = RCTConvert.analyticsConfig(analyticsConfig) else { return }
192
192
  #if os(iOS)
193
193
  self?.setupRemoteControlConfig(playerConfig.remoteControlConfig)
194
194
  #endif
@@ -205,7 +205,7 @@ public class PlayerModule: Module {
205
205
  }.runOnQueue(.main)
206
206
  AsyncFunction("loadSource") { [weak self] (nativeId: NativeId, sourceNativeId: NativeId) in
207
207
  guard let player = PlayerRegistry.getPlayer(nativeId: nativeId),
208
- let sourceModule = self?.appContext?.moduleRegistry.get(SourceModule.self), // swiftlint:disable:this line_length
208
+ let sourceModule = self?.appContext?.moduleRegistry.get(SourceModule.self),
209
209
  let source = sourceModule.retrieve(sourceNativeId) else { return }
210
210
  player.load(source: source)
211
211
  }.runOnQueue(.main)
@@ -219,7 +219,7 @@ public class PlayerModule: Module {
219
219
 
220
220
  private func setupRemoteControlConfig(_ remoteControlConfig: RemoteControlConfig) {
221
221
  remoteControlConfig.prepareSource = { [weak self] _, sourceConfig in
222
- guard let sourceModule = self?.appContext?.moduleRegistry.get(SourceModule.self), // swiftlint:disable:this line_length
222
+ guard let sourceModule = self?.appContext?.moduleRegistry.get(SourceModule.self),
223
223
  let sourceNativeId = sourceModule.nativeId(where: { $0.sourceConfig === sourceConfig }),
224
224
  let castSourceConfig = sourceModule.retrieveCastSourceConfig(sourceNativeId) else {
225
225
  return nil
@@ -196,7 +196,7 @@ extension RCTConvert {
196
196
  return nil
197
197
  }
198
198
  var request = HttpRequest(url: url, method: method)
199
- request.headers = NSMutableDictionary(dictionary: headers)
199
+ request.headers = headers
200
200
 
201
201
  if let bodyBase64EncodedString = json["body"] as? String {
202
202
  request.body = Data(base64Encoded: bodyBase64EncodedString)
@@ -28,9 +28,9 @@ Pod::Spec.new do |s|
28
28
  s.static_framework = true
29
29
 
30
30
  s.dependency 'ExpoModulesCore'
31
- s.dependency "BitmovinPlayer", "3.91.0"
32
- s.ios.dependency "GoogleAds-IMA-iOS-SDK", "3.23.0"
33
- s.tvos.dependency "GoogleAds-IMA-tvOS-SDK", "4.13.0"
31
+ s.dependency "BitmovinPlayer", "3.97.2"
32
+ s.ios.dependency "GoogleAds-IMA-iOS-SDK", "3.26.1"
33
+ s.tvos.dependency "GoogleAds-IMA-tvOS-SDK", "4.15.1"
34
34
 
35
35
  if podfile_properties['BITMOVIN_GOOGLE_CAST_SDK_VERSION'].to_s != ''
36
36
  s.ios.dependency "google-cast-sdk", podfile_properties['BITMOVIN_GOOGLE_CAST_SDK_VERSION'].to_s
@@ -237,7 +237,7 @@ private extension Event {
237
237
  result[key] = pair.value
238
238
  }
239
239
  }
240
- return stringKeyedPayload
240
+ return NonFiniteSanitizer.sanitizeEventData(stringKeyedPayload)
241
241
  }
242
242
  }
243
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitmovin-player-react-native",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Official React Native bindings for Bitmovin's mobile Player SDKs.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -94,6 +94,6 @@
94
94
  "./scripts/format-android.sh",
95
95
  "./scripts/lint-android.sh"
96
96
  ],
97
- "*.(ts|tsx|js|jsx|md|json|yml|yaml)": "prettier --write"
97
+ "*.(ts|tsx|js|jsx|md|json|yml|yaml)": "prettier --write --no-error-on-unmatched-pattern"
98
98
  }
99
99
  }
@@ -1,7 +1,4 @@
1
1
  import Features from './Features';
2
2
  export { Features as FeatureFlags };
3
- declare const _default: import("expo/config-plugins").ConfigPlugin<{
4
- playerLicenseKey: string;
5
- featureFlags: Features;
6
- }>;
3
+ declare const _default: import("expo/config-plugins").ConfigPlugin<import("./withBitmovinConfig").BitmovinConfigOptions>;
7
4
  export default _default;
@@ -19,6 +19,30 @@ const withAppGradleDependencies = (config, props) => {
19
19
  if (filteredDependencies.length === 0) {
20
20
  return config;
21
21
  }
22
+ const androidBlockStart = config.modResults.contents.search(/^android \{$/m);
23
+ if (androidBlockStart === -1) {
24
+ config_plugins_1.WarningAggregator.addWarningAndroid('withAppGradleDependencies', `Cannot configure app/build.gradle as no android block start was found`);
25
+ return config;
26
+ }
27
+ const fromAndroid = config.modResults.contents.substring(androidBlockStart);
28
+ const androidBlockEnd = fromAndroid.search(/^\}$/m);
29
+ if (androidBlockEnd === -1) {
30
+ config_plugins_1.WarningAggregator.addWarningAndroid('withAppGradleDependencies', `Cannot configure app/build.gradle as no android block end was found`);
31
+ return config;
32
+ }
33
+ const androidPosition = androidBlockStart + androidBlockEnd;
34
+ const compileOptions = [];
35
+ compileOptions.push(`${combinedProps.spacing}compileOptions {`);
36
+ compileOptions.push('\n');
37
+ compileOptions.push(`${combinedProps.spacing}setCoreLibraryDesugaringEnabled(true)`);
38
+ compileOptions.push('\n');
39
+ compileOptions.push(`${combinedProps.spacing}}`);
40
+ compileOptions.push('\n');
41
+ config.modResults.contents = [
42
+ config.modResults.contents.slice(0, androidPosition),
43
+ ...compileOptions,
44
+ config.modResults.contents.slice(androidPosition),
45
+ ].join('');
22
46
  const dependenciesBlockStart = config.modResults.contents.search(/^dependencies \{$/m);
23
47
  if (dependenciesBlockStart === -1) {
24
48
  config_plugins_1.WarningAggregator.addWarningAndroid('withAppGradleDependencies', `Cannot configure app/build.gradle as no dependency block start was found`);
@@ -33,6 +57,8 @@ const withAppGradleDependencies = (config, props) => {
33
57
  const position = dependenciesBlockStart + dependenciesBlockEnd;
34
58
  let insertedDependencies = [];
35
59
  insertedDependencies.push('\n');
60
+ insertedDependencies.push(`${combinedProps.spacing}coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'`);
61
+ insertedDependencies.push('\n');
36
62
  filteredDependencies.forEach((dependency) => {
37
63
  insertedDependencies.push(`${combinedProps.spacing}implementation '${dependency}'\n`);
38
64
  });
@@ -1,7 +1,4 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
- import Features from './Features';
3
- declare const withBitmovinAndroidConfig: ConfigPlugin<{
4
- playerLicenseKey: string;
5
- features: Features;
6
- }>;
2
+ import { BitmovinConfigOptions } from './withBitmovinConfig';
3
+ declare const withBitmovinAndroidConfig: ConfigPlugin<BitmovinConfigOptions>;
7
4
  export default withBitmovinAndroidConfig;
@@ -5,7 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const config_plugins_1 = require("expo/config-plugins");
7
7
  const withAppGradleDependencies_1 = __importDefault(require("./withAppGradleDependencies"));
8
- const withBitmovinAndroidConfig = (config, { playerLicenseKey, features }) => {
8
+ const withBitmovinAndroidConfig = (config, options) => {
9
+ const { playerLicenseKey = '', features = {} } = options || {};
9
10
  const offlineFeatureConfig = typeof features.offline === 'object'
10
11
  ? features.offline
11
12
  : {
@@ -36,7 +37,9 @@ const withBitmovinAndroidConfig = (config, { playerLicenseKey, features }) => {
36
37
  }
37
38
  config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
38
39
  const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
39
- config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, 'BITMOVIN_PLAYER_LICENSE_KEY', playerLicenseKey);
40
+ if (playerLicenseKey) {
41
+ config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, 'BITMOVIN_PLAYER_LICENSE_KEY', playerLicenseKey);
42
+ }
40
43
  config.modResults.manifest['uses-permission'] =
41
44
  config.modResults.manifest['uses-permission'] || [];
42
45
  // Configure Picture-in-Picture support
@@ -1,5 +1,9 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
2
  import Features from './Features';
3
+ export interface BitmovinConfigOptions {
4
+ playerLicenseKey?: string;
5
+ features?: Features;
6
+ }
3
7
  /**
4
8
  * Expo Config Plugin for Bitmovin Player.
5
9
  * This plugin configures the Bitmovin Player for both iOS and Android platforms.
@@ -16,7 +20,7 @@ import Features from './Features';
16
20
  * 'bitmovin-player-react-native',
17
21
  * {
18
22
  * playerLicenseKey: 'YOUR_BITMOVIN_PLAYER_LICENSE_KEY',
19
- * featureFlags: {
23
+ * features: {
20
24
  * airPlay: true,
21
25
  * backgroundPlayback: true,
22
26
  * googleCastSDK: { android: '21.3.0', ios: '4.8.1.2' },
@@ -28,8 +32,5 @@ import Features from './Features';
28
32
  * ]
29
33
  * };
30
34
  */
31
- declare const withBitmovinConfig: ConfigPlugin<{
32
- playerLicenseKey: string;
33
- featureFlags: Features;
34
- }>;
35
+ declare const withBitmovinConfig: ConfigPlugin<BitmovinConfigOptions>;
35
36
  export default withBitmovinConfig;
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const withBitmovinIosConfig_1 = __importDefault(require("./withBitmovinIosConfig"));
7
7
  const withBitmovinAndroidConfig_1 = __importDefault(require("./withBitmovinAndroidConfig"));
8
- const defaultFeatureFlags = {
8
+ const defaultFeatures = {
9
9
  airPlay: false,
10
10
  backgroundPlayback: false,
11
11
  googleCastSDK: undefined,
@@ -28,7 +28,7 @@ const defaultFeatureFlags = {
28
28
  * 'bitmovin-player-react-native',
29
29
  * {
30
30
  * playerLicenseKey: 'YOUR_BITMOVIN_PLAYER_LICENSE_KEY',
31
- * featureFlags: {
31
+ * features: {
32
32
  * airPlay: true,
33
33
  * backgroundPlayback: true,
34
34
  * googleCastSDK: { android: '21.3.0', ios: '4.8.1.2' },
@@ -40,10 +40,11 @@ const defaultFeatureFlags = {
40
40
  * ]
41
41
  * };
42
42
  */
43
- const withBitmovinConfig = (config, { playerLicenseKey, featureFlags }) => {
44
- const features = { ...defaultFeatureFlags, ...(featureFlags || {}) };
45
- config = (0, withBitmovinIosConfig_1.default)(config, { playerLicenseKey, features });
46
- config = (0, withBitmovinAndroidConfig_1.default)(config, { playerLicenseKey, features });
43
+ const withBitmovinConfig = (config, options) => {
44
+ const { playerLicenseKey, features } = options;
45
+ const mergedFeatures = { ...defaultFeatures, ...(features || {}) };
46
+ config = (0, withBitmovinIosConfig_1.default)(config, { playerLicenseKey, features: mergedFeatures });
47
+ config = (0, withBitmovinAndroidConfig_1.default)(config, { playerLicenseKey, features: mergedFeatures });
47
48
  return config;
48
49
  };
49
50
  exports.default = withBitmovinConfig;
@@ -1,7 +1,4 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
- import Features from './Features';
3
- declare const withBitmovinIosConfig: ConfigPlugin<{
4
- playerLicenseKey: string;
5
- features: Features;
6
- }>;
2
+ import { BitmovinConfigOptions } from './withBitmovinConfig';
3
+ declare const withBitmovinIosConfig: ConfigPlugin<BitmovinConfigOptions>;
7
4
  export default withBitmovinIosConfig;
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const config_plugins_1 = require("expo/config-plugins");
4
4
  const isTV = !!process.env.EXPO_TV;
5
- const withBitmovinIosConfig = (config, { playerLicenseKey, features }) => {
5
+ const withBitmovinIosConfig = (config, options) => {
6
+ const { playerLicenseKey = '', features = {} } = options || {};
6
7
  const offlineFeatureConfig = typeof features.offline === 'object'
7
8
  ? features.offline
8
9
  : {
@@ -18,7 +19,9 @@ const withBitmovinIosConfig = (config, { playerLicenseKey, features }) => {
18
19
  : features.googleCastSDK.ios
19
20
  : null;
20
21
  config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
21
- config.modResults['BitmovinPlayerLicenseKey'] = playerLicenseKey;
22
+ if (playerLicenseKey) {
23
+ config.modResults['BitmovinPlayerLicenseKey'] = playerLicenseKey;
24
+ }
22
25
  if (features.backgroundPlayback ||
23
26
  features.airPlay ||
24
27
  features.pictureInPicture) {
@@ -38,6 +38,39 @@ const withAppGradleDependencies: ConfigPlugin<PluginProps> = (
38
38
  return config;
39
39
  }
40
40
 
41
+ const androidBlockStart = config.modResults.contents.search(/^android \{$/m);
42
+ if (androidBlockStart === -1) {
43
+ WarningAggregator.addWarningAndroid(
44
+ 'withAppGradleDependencies',
45
+ `Cannot configure app/build.gradle as no android block start was found`
46
+ );
47
+ return config;
48
+ }
49
+ const fromAndroid = config.modResults.contents.substring(
50
+ androidBlockStart
51
+ );
52
+ const androidBlockEnd = fromAndroid.search(/^\}$/m);
53
+ if (androidBlockEnd === -1) {
54
+ WarningAggregator.addWarningAndroid(
55
+ 'withAppGradleDependencies',
56
+ `Cannot configure app/build.gradle as no android block end was found`
57
+ );
58
+ return config;
59
+ }
60
+ const androidPosition = androidBlockStart + androidBlockEnd;
61
+ const compileOptions = []
62
+ compileOptions.push(`${combinedProps.spacing}compileOptions {`)
63
+ compileOptions.push('\n');
64
+ compileOptions.push(`${combinedProps.spacing}setCoreLibraryDesugaringEnabled(true)`)
65
+ compileOptions.push('\n');
66
+ compileOptions.push(`${combinedProps.spacing}}`)
67
+ compileOptions.push('\n');
68
+ config.modResults.contents = [
69
+ config.modResults.contents.slice(0, androidPosition),
70
+ ... compileOptions,
71
+ config.modResults.contents.slice(androidPosition),
72
+ ].join('');
73
+
41
74
  const dependenciesBlockStart =
42
75
  config.modResults.contents.search(/^dependencies \{$/m);
43
76
  if (dependenciesBlockStart === -1) {
@@ -61,6 +94,8 @@ const withAppGradleDependencies: ConfigPlugin<PluginProps> = (
61
94
  const position = dependenciesBlockStart + dependenciesBlockEnd;
62
95
  let insertedDependencies: string[] = [];
63
96
  insertedDependencies.push('\n');
97
+ insertedDependencies.push(`${combinedProps.spacing}coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'`)
98
+ insertedDependencies.push('\n');
64
99
  filteredDependencies.forEach((dependency) => {
65
100
  insertedDependencies.push(
66
101
  `${combinedProps.spacing}implementation '${dependency}'\n`
@@ -4,15 +4,13 @@ import {
4
4
  withAndroidManifest,
5
5
  withGradleProperties,
6
6
  } from 'expo/config-plugins';
7
- import Features from './Features';
8
7
  import withAppGradleDependencies from './withAppGradleDependencies';
8
+ import { BitmovinConfigOptions } from './withBitmovinConfig';
9
9
 
10
10
  type ManifestActivity = AndroidConfig.Manifest.ManifestActivity;
11
11
 
12
- const withBitmovinAndroidConfig: ConfigPlugin<{
13
- playerLicenseKey: string;
14
- features: Features;
15
- }> = (config, { playerLicenseKey, features }) => {
12
+ const withBitmovinAndroidConfig: ConfigPlugin<BitmovinConfigOptions> = (config, options) => {
13
+ const { playerLicenseKey = '', features = {} } = options || {};
16
14
  const offlineFeatureConfig =
17
15
  typeof features.offline === 'object'
18
16
  ? features.offline
@@ -50,11 +48,13 @@ const withBitmovinAndroidConfig: ConfigPlugin<{
50
48
  config.modResults
51
49
  );
52
50
 
53
- AndroidConfig.Manifest.addMetaDataItemToMainApplication(
54
- mainApplication,
55
- 'BITMOVIN_PLAYER_LICENSE_KEY',
56
- playerLicenseKey
57
- );
51
+ if (playerLicenseKey) {
52
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
53
+ mainApplication,
54
+ 'BITMOVIN_PLAYER_LICENSE_KEY',
55
+ playerLicenseKey
56
+ );
57
+ }
58
58
 
59
59
  config.modResults.manifest['uses-permission'] =
60
60
  config.modResults.manifest['uses-permission'] || [];
@@ -3,7 +3,7 @@ import withBitmovinIosConfig from './withBitmovinIosConfig';
3
3
  import Features from './Features';
4
4
  import withBitmovinAndroidConfig from './withBitmovinAndroidConfig';
5
5
 
6
- const defaultFeatureFlags: Features = {
6
+ const defaultFeatures: Features = {
7
7
  airPlay: false,
8
8
  backgroundPlayback: false,
9
9
  googleCastSDK: undefined,
@@ -11,6 +11,11 @@ const defaultFeatureFlags: Features = {
11
11
  pictureInPicture: false,
12
12
  };
13
13
 
14
+ export interface BitmovinConfigOptions {
15
+ playerLicenseKey?: string;
16
+ features?: Features;
17
+ }
18
+
14
19
  /**
15
20
  * Expo Config Plugin for Bitmovin Player.
16
21
  * This plugin configures the Bitmovin Player for both iOS and Android platforms.
@@ -18,7 +23,7 @@ const defaultFeatureFlags: Features = {
18
23
  * @param config - The Expo config object.
19
24
  * @param options - An object containing the player license key and feature flags.
20
25
  * @returns The modified Expo config object with Bitmovin Player configurations.
21
- *
26
+ *
22
27
  * @example
23
28
  * // app.config.js
24
29
  * module.exports = {
@@ -27,7 +32,7 @@ const defaultFeatureFlags: Features = {
27
32
  * 'bitmovin-player-react-native',
28
33
  * {
29
34
  * playerLicenseKey: 'YOUR_BITMOVIN_PLAYER_LICENSE_KEY',
30
- * featureFlags: {
35
+ * features: {
31
36
  * airPlay: true,
32
37
  * backgroundPlayback: true,
33
38
  * googleCastSDK: { android: '21.3.0', ios: '4.8.1.2' },
@@ -39,13 +44,11 @@ const defaultFeatureFlags: Features = {
39
44
  * ]
40
45
  * };
41
46
  */
42
- const withBitmovinConfig: ConfigPlugin<{
43
- playerLicenseKey: string;
44
- featureFlags: Features;
45
- }> = (config, { playerLicenseKey, featureFlags }) => {
46
- const features = { ...defaultFeatureFlags, ...(featureFlags || {}) };
47
- config = withBitmovinIosConfig(config, { playerLicenseKey, features });
48
- config = withBitmovinAndroidConfig(config, { playerLicenseKey, features });
47
+ const withBitmovinConfig: ConfigPlugin<BitmovinConfigOptions> = (config, options) => {
48
+ const { playerLicenseKey, features } = options;
49
+ const mergedFeatures = { ...defaultFeatures, ...(features || {}) };
50
+ config = withBitmovinIosConfig(config, { playerLicenseKey, features: mergedFeatures });
51
+ config = withBitmovinAndroidConfig(config, { playerLicenseKey, features: mergedFeatures });
49
52
  return config;
50
53
  };
51
54
 
@@ -3,14 +3,12 @@ import {
3
3
  withInfoPlist,
4
4
  withPodfileProperties,
5
5
  } from 'expo/config-plugins';
6
- import Features from './Features';
6
+ import { BitmovinConfigOptions } from './withBitmovinConfig';
7
7
 
8
8
  const isTV = !!process.env.EXPO_TV;
9
9
 
10
- const withBitmovinIosConfig: ConfigPlugin<{
11
- playerLicenseKey: string;
12
- features: Features;
13
- }> = (config, { playerLicenseKey, features }) => {
10
+ const withBitmovinIosConfig: ConfigPlugin<BitmovinConfigOptions> = (config, options) => {
11
+ const { playerLicenseKey = '', features = {} } = options || {};
14
12
  const offlineFeatureConfig =
15
13
  typeof features.offline === 'object'
16
14
  ? features.offline
@@ -28,7 +26,9 @@ const withBitmovinIosConfig: ConfigPlugin<{
28
26
  : null;
29
27
 
30
28
  config = withInfoPlist(config, (config) => {
31
- config.modResults['BitmovinPlayerLicenseKey'] = playerLicenseKey;
29
+ if (playerLicenseKey) {
30
+ config.modResults['BitmovinPlayerLicenseKey'] = playerLicenseKey;
31
+ }
32
32
  if (
33
33
  features.backgroundPlayback ||
34
34
  features.airPlay ||
@@ -8,19 +8,18 @@
8
8
  // Fail install if required app-level deps are missing
9
9
  const required = ['expo', 'expo-crypto'];
10
10
  const has = (name) => {
11
- try {
12
- require.resolve(`${name}/package.json`, { paths: [process.cwd()] });
13
- return true;
14
- }
15
- catch {
16
- return false;
11
+ try {
12
+ require.resolve(`${name}/package.json`, { paths: [process.cwd()] });
13
+ return true;
14
+ } catch {
15
+ return false;
17
16
  }
18
17
  };
19
18
  const missing = required.filter((r) => !has(r));
20
19
  if (missing.length) {
21
20
  console.error(
22
21
  `\n[bitmovin-player-react-native] Missing required deps in your app: ${missing.join(', ')}\n` +
23
- `Install: npx expo install ${missing.join(' ')}\n`
22
+ `Install: npx expo install ${missing.join(' ')}\n`
24
23
  );
25
24
  process.exit(1);
26
- }
25
+ }
@@ -1,18 +1,16 @@
1
1
  #!/bin/bash
2
2
 
3
+ set -euo pipefail
4
+
3
5
  # Setup git pre-commit hooks for the project
4
6
  echo "Setting up pre-commit hooks..."
5
7
 
6
- # Check if .git directory exists
7
- if [ ! -d ".git" ]; then
8
- echo "Error: Not a git repository. Please run this from the root of the project."
9
- exit 1
10
- fi
8
+ HOOK_DIR=$(git rev-parse --git-path hooks)
11
9
 
12
10
  # Check if hooks directory exists
13
- if [ ! -d ".git/hooks" ]; then
14
- echo "Creating .git/hooks directory..."
15
- mkdir -p .git/hooks
11
+ if [ ! -d "$HOOK_DIR" ]; then
12
+ echo "Creating $HOOK_DIR directory..."
13
+ mkdir -p "$HOOK_DIR"
16
14
  fi
17
15
 
18
16
  # Get the directory where this script is located
@@ -26,7 +24,7 @@ if [ ! -f "$PRE_COMMIT_SOURCE" ]; then
26
24
  fi
27
25
 
28
26
  # Check if pre-commit hook already exists and compare content
29
- HOOK_PATH=".git/hooks/pre-commit"
27
+ HOOK_PATH="$HOOK_DIR/pre-commit"
30
28
  NEEDS_UPDATE=true
31
29
 
32
30
  if [ -f "$HOOK_PATH" ]; then
@@ -1,6 +1,7 @@
1
1
  import { RefObject, useCallback } from 'react';
2
2
  import { Event } from '../events';
3
3
  import { findNodeHandle } from 'react-native';
4
+ import { normalizeNonFinite } from '../utils/normalizeNonFinite';
4
5
 
5
6
  /**
6
7
  * A function that takes a generic event as argument.
@@ -26,7 +27,8 @@ export function useProxy(
26
27
  return;
27
28
  }
28
29
  const { target, ...eventWithoutTarget } = event.nativeEvent as any;
29
- callback?.(eventWithoutTarget as E);
30
+ const sanitized = normalizeNonFinite(eventWithoutTarget);
31
+ callback?.(sanitized as E);
30
32
  },
31
33
  [viewRef]
32
34
  );
@@ -0,0 +1,16 @@
1
+ export function normalizeNonFinite<T>(input: T): T {
2
+ const sentinelPrefix = 'BMP_';
3
+ function walk(v: any): any {
4
+ if (v === `${sentinelPrefix}Infinity`) return Infinity;
5
+ if (v === `${sentinelPrefix}-Infinity`) return -Infinity;
6
+ if (v === `${sentinelPrefix}NaN`) return NaN;
7
+ if (Array.isArray(v)) return v.map(walk);
8
+ if (v && typeof v === 'object') {
9
+ const out: any = {};
10
+ for (const k of Object.keys(v)) out[k] = walk((v as any)[k]);
11
+ return out;
12
+ }
13
+ return v;
14
+ }
15
+ return walk(input);
16
+ }
@@ -1,2 +0,0 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- </manifest>