From 48fb788e7861ab06fdd7d5c741569876c9eb69fe Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Wed, 23 Aug 2023 16:45:14 +0200 Subject: [PATCH 01/85] feat: partially migrate analytics API from v2 to the bundled variant using the v3 collector --- .../player/reactnative/AnalyticsModule.kt | 41 ++++++++++ .../player/reactnative/PlayerModule.kt | 32 ++++++++ .../reactnative/converter/JsonConverter.kt | 79 ++++++++++--------- example/src/screens/BasicAnalytics.tsx | 14 ++++ src/analytics/collector.ts | 16 ++++ src/analytics/config.ts | 26 +----- src/player.ts | 11 ++- 7 files changed, 153 insertions(+), 66 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt index fa297c7f..8545df0f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt @@ -90,6 +90,7 @@ class AnalyticsModule(private val context: ReactApplicationContext) : ReactConte * @param nativeId Native Id of the collector instance. * @param json Custom data config json. */ + @Deprecated("Confusing API naming", replaceWith = ReplaceWith("sendCustomDataEvent(nativeId, json)")) @ReactMethod fun setCustomDataOnce(nativeId: NativeId, json: ReadableMap?) { uiManager()?.addUIBlock { _ -> @@ -99,6 +100,22 @@ class AnalyticsModule(private val context: ReactApplicationContext) : ReactConte } } + /** + * Sends a sample with the provided custom data. + * Does not change the configured custom data of the collector or source. + * @param nativeId Native Id of the collector instance. + * @param json Custom data config json. + */ + @ReactMethod + fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?) { + uiManager()?.addUIBlock { _ -> + JsonConverter.toAnalyticsCustomData(json)?.let { + collectors[nativeId]?.sendCustomDataEvent(it) + } + } + } + + /** * Sets the custom data config for a `BitmovinPlayerCollector` instance. * @param nativeId Native Id of the collector instance. @@ -168,6 +185,30 @@ class AnalyticsModule(private val context: ReactApplicationContext) : ReactConte } } + /** + * Sets the source metadata for the current active source of the player associated to `playerId`. + */ + @ReactMethod + fun setSourceMetadata(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { + uiManager()?.addUIBlock { _ -> + val source = playerModule()?.getPlayer(playerId)?.source + val collector = collectors[nativeId] + val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(json) + when { + source == null -> Log.d( + "[AnalyticsModule]", "Could not find source for player ($playerId)" + ) + collector == null -> Log.d( + "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" + ) + sourceMetadata == null -> Log.d( + "[AnalyticsModule]", "Could not convert source metadata, thus they are not applied to the collector ($nativeId)" + ) + else -> collector.setSourceMetadata(source, sourceMetadata) + } + } + } + /** * Gets the current user Id for a `BitmovinPlayerCollector` instance. * @param nativeId Native Id of the the collector instance. diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index dc26730b..33ed4afc 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -1,6 +1,9 @@ package com.bitmovin.player.reactnative +import android.util.Log +import com.bitmovin.analytics.api.DefaultMetadata import com.bitmovin.player.api.Player +import com.bitmovin.player.api.analytics.create import com.bitmovin.player.reactnative.converter.JsonConverter import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule @@ -47,6 +50,35 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB } } + /** + * Creates a new `Player` instance inside the internal players using the provided `playerConfig` and `analyticsConfig`. + * @param playerConfigJson `PlayerConfig` object received from JS. + * @param analyticsConfigJson `AnalyticsConfig` object received from JS. + */ + @ReactMethod + fun initWithConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { + uiManager()?.addUIBlock { + if (players.containsKey(nativeId)) { + Log.d("[PlayerModule]", "Duplicate player creation for id $nativeId") + return@addUIBlock + } + val playerConfig = JsonConverter.toPlayerConfig(playerConfigJson) + val analyticsConfig = JsonConverter.toAnalyticsConfig(analyticsConfigJson) + val defaultMetadata = JsonConverter.toAnalyticsDefaultMetadata(analyticsConfigJson) + + players[nativeId] = if (analyticsConfig == null) { + Player.create(context, playerConfig) + } else { + Player.create( + context = context, + playerConfig = playerConfig, + analyticsConfig = analyticsConfig, + defaultMetadata = defaultMetadata ?: DefaultMetadata(), + ) + } + } + } + /** * Load the source of the given `nativeId` with `config` options from JS. * @param nativeId Target player. diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index d52e1496..9afad696 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -1,7 +1,9 @@ package com.bitmovin.player.reactnative.converter import com.bitmovin.analytics.BitmovinAnalyticsConfig +import com.bitmovin.analytics.api.AnalyticsConfig import com.bitmovin.analytics.api.CustomData +import com.bitmovin.analytics.api.DefaultMetadata import com.bitmovin.analytics.api.SourceMetadata import com.bitmovin.player.api.DeviceDescription.DeviceName import com.bitmovin.player.api.DeviceDescription.ModelName @@ -30,6 +32,7 @@ import com.bitmovin.player.api.source.SourceConfig import com.bitmovin.player.api.source.SourceType import com.bitmovin.player.api.ui.ScalingMode import com.bitmovin.player.api.ui.StyleConfig +import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getName import com.bitmovin.player.reactnative.extensions.getProperty import com.bitmovin.player.reactnative.extensions.putDouble @@ -741,46 +744,44 @@ class JsonConverter { * @return The produced `BitmovinAnalyticsConfig` or null. */ @JvmStatic - fun toAnalyticsConfig(json: ReadableMap?): BitmovinAnalyticsConfig? = json?.let { - var config: BitmovinAnalyticsConfig? = null - it.getString("key")?.let { key -> - config = it.getString("playerKey") - ?.let { playerKey -> BitmovinAnalyticsConfig(key, playerKey) } - ?: BitmovinAnalyticsConfig(key) - } - it.getString("cdnProvider")?.let { cdnProvider -> - config?.cdnProvider = cdnProvider - } - it.getString("customUserId")?.let { customUserId -> - config?.customUserId = customUserId - } - it.getString("experimentName")?.let { experimentName -> - config?.experimentName = experimentName - } - it.getString("videoId")?.let { videoId -> - config?.videoId = videoId - } - it.getString("title")?.let { title -> - config?.title = title - } - it.getString("path")?.let { path -> - config?.path = path - } - if (it.hasKey("isLive")) { - config?.isLive = it.getBoolean("isLive") - } - if (it.hasKey("ads")) { - config?.ads = it.getBoolean("ads") - } - if (it.hasKey("randomizeUserId")) { - config?.randomizeUserId = it.getBoolean("randomizeUserId") - } - for (n in 1..30) { - it.getString("customData${n}")?.let { customDataN -> - config?.setProperty("customData${n}", customDataN) + fun toAnalyticsConfig(json: ReadableMap?): AnalyticsConfig? = json?.let { + val licenseKey = it.getString("licenseKey") ?: return null + + return AnalyticsConfig.Builder(licenseKey).apply { + it.getBooleanOrNull("adTrackingDisabled")?.let { adTrackingDisabled -> + setAdTrackingDisabled(adTrackingDisabled) } - } - config + it.getBooleanOrNull("randomizeUserId")?.let { randomizeUserId -> + setRandomizeUserId(randomizeUserId) + } + for (n in 1..30) { + it.getString("customData${n}")?.let { customDataN -> + setProperty("customData${n}", customDataN) + } + } + }.build() + } + + /** + * Converts an arbitrary json object into an analytics `DefaultMetadata`. + * @param json JS object representing the `CustomData`. + * @return The produced `CustomData` or null. + */ + @JvmStatic + fun toAnalyticsDefaultMetadata(json: ReadableMap?): DefaultMetadata? { + if (json == null) return null + + return DefaultMetadata.Builder().apply { + toAnalyticsCustomData(json)?.let { + setCustomData(it) + } + json.getString("cdnProvider")?.let { cdnProvider -> + setCdnProvider(cdnProvider) + } + json.getString("customUserId")?.let { customUserId -> + setCustomUserId(customUserId) + } + }.build() } /** diff --git a/example/src/screens/BasicAnalytics.tsx b/example/src/screens/BasicAnalytics.tsx index c3bad553..9db9b48d 100644 --- a/example/src/screens/BasicAnalytics.tsx +++ b/example/src/screens/BasicAnalytics.tsx @@ -45,12 +45,26 @@ export default function BasicAds() { poster: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', }); + player.analyticsCollector?.setCustomData({ + experimentName: 'setCustomData', + customData1: `customized data 1`, + }); return () => { player.destroy(); }; }, [player]) ); + setTimeout(() => { + player.load({ + url: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8', + type: SourceType.HLS, + title: 'Art of Motion HLS', + poster: + 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', + }); + }, 10000); + return ( diff --git a/src/analytics/collector.ts b/src/analytics/collector.ts index 6a25d829..9f0b31f1 100644 --- a/src/analytics/collector.ts +++ b/src/analytics/collector.ts @@ -71,6 +71,7 @@ export class AnalyticsCollector extends NativeInstance { * to update your custom data during runtime. * * @param customData - Analytics custom data config. + * @deprecated */ setCustomDataOnce = (customData: CustomDataConfig) => { AnalyticsModule.setCustomDataOnce(this.nativeId, customData); @@ -103,11 +104,26 @@ export class AnalyticsCollector extends NativeInstance { return AnalyticsModule.getUserId(this.nativeId); }; + /** + * Sets the source metadata for the current source loaded into the player. + * This method should be called every time a new source is loaded into the player to ensure + * that the analytics data is correct. + * @param sourceMetadata - Source metadata to set. + */ + setSourceMetadata = (sourceMetadata: SourceMetadata) => { + return AnalyticsModule.setSourceMetadata( + this.nativeId, + this.playerId, + sourceMetadata + ); + }; + /** * Adds source metadata for the current source loaded into the player. * This method should be called every time a new source is loaded into the player to ensure * that the analytics data is correct. * @param sourceMetadata - Source metadata to set. + * @deprecated */ addSourceMetadata = (sourceMetadata: SourceMetadata) => { return AnalyticsModule.addSourceMetadata( diff --git a/src/analytics/config.ts b/src/analytics/config.ts index c1f74239..71b83914 100644 --- a/src/analytics/config.ts +++ b/src/analytics/config.ts @@ -28,33 +28,13 @@ export interface AnalyticsConfig */ customUserId?: string; /** - * ID of the video in the CMS system. + * The analytics license key */ - videoId?: string; - /** - * Human readable title of the video asset currently playing. - */ - title?: string; - /** - * Analytics key. - */ - key: string; - /** - * Player key. - */ - playerKey?: string; - /** - * Breadcrumb path to show where in the app the user is. - */ - path?: string; - /** - * Flag to see if stream is live before stream metadata is available (default: false). - */ - isLive?: boolean; + licenseKey: string; /** * Flag to enable Ad tracking (default: false). */ - ads?: boolean; + adTrackingDisabled?: boolean; /** * Flag to use randomised userId not depending on device specific values (default: false). */ diff --git a/src/player.ts b/src/player.ts index ff05dba2..1500c5aa 100644 --- a/src/player.ts +++ b/src/player.ts @@ -164,12 +164,15 @@ export class Player extends NativeInstance { */ initialize = () => { if (!this.isInitialized) { - PlayerModule.initWithConfig(this.nativeId, this.config); const analyticsConfig = this.config?.analyticsConfig; if (analyticsConfig) { - this.analyticsCollector = new AnalyticsCollector(analyticsConfig); - this.analyticsCollector?.initialize(); - this.analyticsCollector?.attach(this.nativeId); + PlayerModule.initWithConfig( + this.nativeId, + this.config, + analyticsConfig + ); + } else { + PlayerModule.initWithConfig(this.nativeId, this.config); } this.isInitialized = true; } From 9994cab8f8f7cf407342d6aa6bdfbed2958a8b5c Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 8 Sep 2023 15:56:52 +0200 Subject: [PATCH 02/85] chore: use pod-install NPM package to install CocoaPods tools and perform pod install --- example/package.json | 3 +++ example/yarn.lock | 5 +++++ package.json | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/example/package.json b/example/package.json index 0372525c..28cb457d 100644 --- a/example/package.json +++ b/example/package.json @@ -20,5 +20,8 @@ "react-native-safe-area-context": "4.3.1", "react-native-screens": "3.15.0", "react-native-system-navigation-bar": "^2.6.1" + }, + "devDependencies": { + "pod-install": "^0.1.39" } } diff --git a/example/yarn.lock b/example/yarn.lock index 7cef16a9..050dae4e 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -3353,6 +3353,11 @@ plist@^3.0.2: base64-js "^1.5.1" xmlbuilder "^15.1.1" +pod-install@^0.1.39: + version "0.1.39" + resolved "https://registry.yarnpkg.com/pod-install/-/pod-install-0.1.39.tgz#853a0585bafbd332c2ca6543854fd4919958cfb3" + integrity sha512-0kVvdLYe0CtfJEr+ISvTMxAEB0UF4JMRToPjuu9xAAq1mEqA2Ql5u7uLWX1m45BMM+7NfU4LnBbnfNjmQE9GCw== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" diff --git a/package.json b/package.json index 87259c40..900332e3 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "format": "prettier --write .", "build": "tsup ./src/index.ts --dts --target es2020 --format cjs,esm -d lib", "example": "yarn --cwd example", - "pods": "yarn pods-install || yarn pods-install --repo-update || yarn pods-update", - "pods-install": "cd example/ios && bundle install && bundle exec pod install --silent", - "pods-update": "cd example/ios && bundle install && bundle exec pod update --silent", + "pods": "yarn pods-install || yarn pods-update", + "pods-install": "cd example && yarn pod-install", + "pods-update": "cd example/ios && bundle install && pod update --silent", "bootstrap": "yarn && yarn example && yarn pods", "prepare": "husky install" }, From e42ecca466e6f4c132d975bf81223e94667cbbff Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 8 Sep 2023 16:10:27 +0200 Subject: [PATCH 03/85] chore: improve pods-update command to not fail on non-mac machines --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 900332e3..81086c68 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "example": "yarn --cwd example", "pods": "yarn pods-install || yarn pods-update", "pods-install": "cd example && yarn pod-install", - "pods-update": "cd example/ios && bundle install && pod update --silent", + "pods-update": "! [[ \"$OSTYPE\" =~ ^darwin ]] && echo 'CocoaPods is only supported on darwin machines' || (cd example/ios && pod update --silent)", "bootstrap": "yarn && yarn example && yarn pods", "prepare": "husky install" }, From 6e7d3b541dd8350a30da0ca6922a875542227c9c Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 11 Sep 2023 09:36:46 +0200 Subject: [PATCH 04/85] chore: remove ruby bundler references --- .github/workflows/ci.yml | 20 ++--- .github/workflows/create-sdk-update-pr.yml | 3 +- .gitignore | 1 - example/ios/.ruby-version | 1 - example/ios/Gemfile | 3 - example/ios/Gemfile.lock | 97 ---------------------- example/ios/Podfile.lock | 2 +- 7 files changed, 8 insertions(+), 119 deletions(-) delete mode 100644 example/ios/.ruby-version delete mode 100644 example/ios/Gemfile delete mode 100644 example/ios/Gemfile.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 414efba1..6737d22b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,15 +108,11 @@ jobs: key: pods-${{ hashFiles('example/ios/Podfile.lock') }} restore-keys: pods- - - name: Install Bundle dependencies - run: bundle install - working-directory: example/ios - - name: Install pods run: | - bundle exec pod repo remove trunk - bundle exec pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || bundle exec pod repo update bitmovin - bundle exec pod install --repo-update + pod repo remove trunk + pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || pod repo update bitmovin + pod install --repo-update working-directory: example/ios env: CP_HOME_DIR: ${{ github.workspace }}/.cocoapods-cache @@ -171,15 +167,11 @@ jobs: key: pods-${{ hashFiles('example/ios/Podfile.lock') }} restore-keys: pods- - - name: Install Bundle dependencies - run: bundle install - working-directory: example/ios - - name: Install pods run: | - bundle exec pod repo remove trunk - bundle exec pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || bundle exec pod repo update bitmovin - bundle exec pod install --repo-update + pod repo remove trunk + pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || pod repo update bitmovin + pod install --repo-update working-directory: example/ios env: CP_HOME_DIR: ${{ github.workspace }}/.cocoapods-cache diff --git a/.github/workflows/create-sdk-update-pr.yml b/.github/workflows/create-sdk-update-pr.yml index 98983dd0..2f71b111 100644 --- a/.github/workflows/create-sdk-update-pr.yml +++ b/.github/workflows/create-sdk-update-pr.yml @@ -55,9 +55,8 @@ jobs: sed -i '' 's/s.dependency "BitmovinPlayer", ".*/s.dependency "BitmovinPlayer", "${{ inputs.version_number }}"/g' RNBitmovinPlayer.podspec yarn install --frozen-lockfile --cwd example cd example/ios - bundle install pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git - bundle exec pod update BitmovinPlayer + pod update BitmovinPlayer - name: Bump Android player SDK version if: ${{ inputs.sdk_name == 'android' }} diff --git a/.gitignore b/.gitignore index cc24f9e8..fced6c19 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,6 @@ android.iml # Cocoapods # example/ios/Pods/ -example/vendor/bundle/ # node.js # diff --git a/example/ios/.ruby-version b/example/ios/.ruby-version deleted file mode 100644 index 1f7da99d..00000000 --- a/example/ios/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.7.7 diff --git a/example/ios/Gemfile b/example/ios/Gemfile deleted file mode 100644 index 0341aea2..00000000 --- a/example/ios/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source "https://rubygems.org" - -gem "cocoapods", "1.11.3" diff --git a/example/ios/Gemfile.lock b/example/ios/Gemfile.lock deleted file mode 100644 index 3cf58dd2..00000000 --- a/example/ios/Gemfile.lock +++ /dev/null @@ -1,97 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.6) - rexml - activesupport (6.1.7.6) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - claide (1.1.0) - cocoapods (1.11.3) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.3) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.3) - activesupport (>= 5.0, < 7) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.2.2) - escape (0.0.4) - ethon (0.16.0) - ffi (>= 1.15.0) - ffi (1.15.5) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (1.14.1) - concurrent-ruby (~> 1.0) - json (2.6.3) - minitest (5.19.0) - molinillo (0.8.0) - nanaimo (0.3.0) - nap (1.1.0) - netrc (0.11.0) - public_suffix (4.0.7) - rexml (3.2.5) - ruby-macho (2.5.1) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - xcodeproj (1.22.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - zeitwerk (2.6.11) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods (= 1.11.3) - -BUNDLED WITH - 2.1.4 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e07787dc..70e49728 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -572,4 +572,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: ae31af0f37bdf647197475f3f0d4a5a3e93df60c -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 From 7205ab1dc8c77c8edc46ad0bb6093f661e2c3b60 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 11 Sep 2023 11:02:25 +0200 Subject: [PATCH 05/85] chore: fix creating GitHub release in finish release workflow --- .github/workflows/finish-release-train.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/finish-release-train.yml b/.github/workflows/finish-release-train.yml index ad2cba6a..6fc51e78 100644 --- a/.github/workflows/finish-release-train.yml +++ b/.github/workflows/finish-release-train.yml @@ -85,5 +85,6 @@ jobs: - name: Create GitHub release uses: ncipollo/release-action@v1 with: + tag: v${{ needs.create_pr.outputs.version_number }} token: ${{ secrets.GITHUB_TOKEN }} body: ${{ steps.changelog.outputs.release_notes }} From 0c7e29d3100029fa87504a3172d9d28a0b7c2f4f Mon Sep 17 00:00:00 2001 From: Update Bot Date: Mon, 11 Sep 2023 09:20:32 +0000 Subject: [PATCH 06/85] chore(ios): update ios player version to 3.44.2 --- RNBitmovinPlayer.podspec | 2 +- example/ios/Podfile.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/RNBitmovinPlayer.podspec b/RNBitmovinPlayer.podspec index dfb3f475..3313223a 100644 --- a/RNBitmovinPlayer.podspec +++ b/RNBitmovinPlayer.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "React-Core" - s.dependency "BitmovinPlayer", "3.44.1" + s.dependency "BitmovinPlayer", "3.44.2" s.ios.dependency "GoogleAds-IMA-iOS-SDK", "3.18.4" s.tvos.dependency "GoogleAds-IMA-tvOS-SDK", "4.8.2" end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e07787dc..09b2fba3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,10 +3,10 @@ PODS: - BitmovinAnalyticsCollector/Core - BitmovinPlayerCore (~> 3.41) - BitmovinAnalyticsCollector/Core (3.1.1) - - BitmovinPlayer (3.44.1): + - BitmovinPlayer (3.44.2): - BitmovinAnalyticsCollector/BitmovinPlayer (~> 3.0) - - BitmovinPlayerCore (= 3.44.1) - - BitmovinPlayerCore (3.44.1) + - BitmovinPlayerCore (= 3.44.2) + - BitmovinPlayerCore (3.44.2) - boost (1.76.0) - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) @@ -340,7 +340,7 @@ PODS: - React-logger (= 0.69.6-2) - React-perflogger (= 0.69.6-2) - RNBitmovinPlayer (0.10.0): - - BitmovinPlayer (= 3.44.1) + - BitmovinPlayer (= 3.44.2) - GoogleAds-IMA-iOS-SDK (= 3.18.4) - GoogleAds-IMA-tvOS-SDK (= 4.8.2) - React-Core @@ -515,8 +515,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BitmovinAnalyticsCollector: 6aa2e995e325d95d1145bcad3d733d2f8c301c00 - BitmovinPlayer: 0237ded5f8ccde045757872bc55a4c6f78d4c39b - BitmovinPlayerCore: fbb73591382c497e889be9e92792f4ebb86cc8fb + BitmovinPlayer: e2809771a4f07cc51bb571889d005835b007c2b9 + BitmovinPlayerCore: 1e216be104179368396c1d401f1a664a91ddc093 boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 234abba95e31cc2aada0cf3b97cdb11bc5b90575 @@ -563,7 +563,7 @@ SPEC CHECKSUMS: React-RCTText: f72442f7436fd8624494963af4906000a5465ce6 React-runtimeexecutor: f1383f6460ea3d66ed122b4defb0b5ba664ee441 ReactCommon: 7857ab475239c5ba044b7ed946ba564f2e7f1626 - RNBitmovinPlayer: f3686a5df04654ca05b39260676daaf6f0aeee29 + RNBitmovinPlayer: 402ac86b81ad94cbfb4c95cd494d5dfbf5e1bd8e RNCPicker: 0250e95ad170569a96f5b0555cdd5e65b9084dca RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 From ff48f0e9044fcbcd22a612cfac2f201ab01a08de Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Tue, 12 Sep 2023 14:16:47 +0200 Subject: [PATCH 07/85] chore(android): add writeable map extension --- .../bitmovin/player/reactnative/extensions/WritableMap.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/WritableMap.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/WritableMap.kt index fec095eb..6f17860b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/WritableMap.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/WritableMap.kt @@ -17,3 +17,11 @@ fun WritableMap.putDouble(key: String, d: Double?) { putDouble(key, d) } } + +fun WritableMap.putBoolean(key: String, b: Boolean?) { + if (b == null) { + putNull(key) + } else { + putBoolean(key, b) + } +} From 30f6e6f050d9b849db993125489d1ae419657b0d Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Tue, 12 Sep 2023 14:19:25 +0200 Subject: [PATCH 08/85] feat: upgrade analytics api v3 and the bundle analytics-player --- CHANGELOG.md | 9 + .../player/reactnative/AnalyticsModule.kt | 191 +----------------- .../player/reactnative/PlayerModule.kt | 4 +- .../player/reactnative/SourceModule.kt | 62 ++++-- .../reactnative/converter/JsonConverter.kt | 22 +- example/src/screens/BasicAnalytics.tsx | 52 ++--- ios/AnalyticsModule.m | 9 +- ios/AnalyticsModule.swift | 173 +--------------- ios/PlayerModule.m | 1 + ios/PlayerModule.swift | 25 +++ ios/RCTConvert+BitmovinPlayer.swift | 94 +++------ ios/SourceModule.m | 4 +- ios/SourceModule.swift | 21 +- src/analytics/collector.ts | 135 ------------- src/analytics/config.ts | 102 +++++----- src/analytics/index.ts | 2 +- src/analytics/player.ts | 36 ++++ src/player.ts | 17 +- src/source.ts | 21 +- 19 files changed, 300 insertions(+), 680 deletions(-) delete mode 100644 src/analytics/collector.ts create mode 100644 src/analytics/player.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fd652829..f32657c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,19 @@ - `Player.getAudioTrack` and `Player.getSubtitleTrack` APIs to get currently selected audio and subtitle tracks - `SourceConfig.description` property to allow setting a description for the source - `Player.getThumbnail` and `Source.getThumbnail` APIs to get thumbnail images +- `DefaultMetadata` for configuration of the bundled analytics collector +- `Player.analytics` to access the `AnalyticsApi` and interact with the bundled analytics collector +- `SourceConfig.analyticsSourceMetadata` for extended configuration of the bundled analytics collector ### Changed - Update Bitmovin's native Android SDK version to `3.43.0` +- `AnalyticsConfig` properties to match the bitmovin analytics v3 API + +### Removed + +- `AnalyticsCollector` in favor of the bundled analytics functionality +- `CdnProvider`, as the property on the `AnalyticsConfig` is now a `string` ## [0.10.0] (2023-09-04) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt index 8545df0f..23c9eeb4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt @@ -1,7 +1,6 @@ package com.bitmovin.player.reactnative -import android.util.Log -import com.bitmovin.analytics.bitmovin.player.api.IBitmovinPlayerCollector +import com.bitmovin.player.api.analytics.AnalyticsApi.Companion.analytics import com.bitmovin.player.reactnative.converter.JsonConverter import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule @@ -11,95 +10,11 @@ private const val MODULE_NAME = "AnalyticsModule" @ReactModule(name = MODULE_NAME) class AnalyticsModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { - /** - * In-memory mapping from `nativeId`s to `BitmovinPlayerCollector` instances. - */ - private val collectors: Registry = mutableMapOf() - /** * JS exported module name. */ override fun getName() = MODULE_NAME - /** - * Fetches the `BitmovinPlayerCollector` instance associated with `nativeId` from the internal registry. - * @param nativeId `BitmovinPlayerCollector` instance ID. - * @return The associated `BitmovinPlayerCollector` instance or `null`. - */ - fun getCollector(nativeId: NativeId?): IBitmovinPlayerCollector? { - if (nativeId == null) { - return null - } - return collectors[nativeId] - } - - /** - * Creates a new `BitmovinPlayerCollector` instance inside the internal registry using the provided `config` object. - * @param config `BitmovinAnalyticsConfig` object received from JS. - */ - @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - JsonConverter.toAnalyticsConfig(config)?.let { - collectors[nativeId] = IBitmovinPlayerCollector.create(it, context) - } - } - } - - /** - * Detaches and removes the given `BitmovinPlayerCollector` from the internal registry. - * @param nativeId Native Id of the collector instance. - */ - @ReactMethod - fun destroy(nativeId: NativeId) { - uiManager()?.addUIBlock { - if (collectors.containsKey(nativeId)) { - collectors[nativeId]?.detachPlayer() - collectors.remove(nativeId) - } - } - } - - /** - * Attaches a `BitmovinPlayerCollector` to the `Player` instance with native Id equal to `playerId`. - * @param nativeId Native Id of the collector instance. - * @param playerId Native Id of the player instance. - */ - @ReactMethod - fun attach(nativeId: NativeId, playerId: NativeId) { - uiManager()?.addUIBlock { _ -> - playerModule()?.getPlayer(playerId)?.let { - collectors[nativeId]?.attachPlayer(it) - } - } - } - - /** - * Detaches the player object from a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the collector instance. - */ - @ReactMethod - fun detach(nativeId: NativeId) { - uiManager()?.addUIBlock { _ -> - collectors[nativeId]?.detachPlayer() - } - } - - /** - * Updates the custom data config for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the collector instance. - * @param json Custom data config json. - */ - @Deprecated("Confusing API naming", replaceWith = ReplaceWith("sendCustomDataEvent(nativeId, json)")) - @ReactMethod - fun setCustomDataOnce(nativeId: NativeId, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - JsonConverter.toAnalyticsCustomData(json)?.let { - collectors[nativeId]?.setCustomDataOnce(it) - } - } - } - /** * Sends a sample with the provided custom data. * Does not change the configured custom data of the collector or source. @@ -110,114 +25,20 @@ class AnalyticsModule(private val context: ReactApplicationContext) : ReactConte fun sendCustomDataEvent(nativeId: NativeId, json: ReadableMap?) { uiManager()?.addUIBlock { _ -> JsonConverter.toAnalyticsCustomData(json)?.let { - collectors[nativeId]?.sendCustomDataEvent(it) - } - } - } - - - /** - * Sets the custom data config for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the collector instance. - * @param json Custom data config json. - */ - @ReactMethod - fun setCustomData(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - val customData = JsonConverter.toAnalyticsCustomData(json) - when { - source == null -> Log.d( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> Log.d( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - customData == null -> Log.d( - "[AnalyticsModule]", "Could not convert custom data, thus they are not applied to the active source for the player ($playerId) with the collector ($nativeId)" - ) - else -> collector.setCustomData(source, customData) - } - } - } - - /** - * Gets the current custom data config for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the the collector instance. - * @param promise JS promise object. - */ - @ReactMethod - fun getCustomData(nativeId: NativeId, playerId: NativeId?, promise: Promise) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - when { - source == null -> promise.reject( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> promise.reject( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - else -> promise.resolve(JsonConverter.fromAnalyticsCustomData(collector.getCustomData(source))) - } - } - } - - @ReactMethod - fun addSourceMetadata(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(json) - when { - source == null -> Log.d( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> Log.d( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - sourceMetadata == null -> Log.d( - "[AnalyticsModule]", "Could not convert source metadata, thus they are not applied to the collector ($nativeId)" - ) - else -> collector.addSourceMetadata(source, sourceMetadata) - } - } - } - - /** - * Sets the source metadata for the current active source of the player associated to `playerId`. - */ - @ReactMethod - fun setSourceMetadata(nativeId: NativeId, playerId: NativeId?, json: ReadableMap?) { - uiManager()?.addUIBlock { _ -> - val source = playerModule()?.getPlayer(playerId)?.source - val collector = collectors[nativeId] - val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(json) - when { - source == null -> Log.d( - "[AnalyticsModule]", "Could not find source for player ($playerId)" - ) - collector == null -> Log.d( - "[AnalyticsModule]", "Could not find analytics collector ($nativeId)" - ) - sourceMetadata == null -> Log.d( - "[AnalyticsModule]", "Could not convert source metadata, thus they are not applied to the collector ($nativeId)" - ) - else -> collector.setSourceMetadata(source, sourceMetadata) + playerModule()?.getPlayer(nativeId)?.analytics?.sendCustomDataEvent(it) } } } /** - * Gets the current user Id for a `BitmovinPlayerCollector` instance. - * @param nativeId Native Id of the the collector instance. + * Gets the current user Id for a player instance with analytics. + * @param nativeId Native Id of the the player instance. * @param promise JS promise object. */ @ReactMethod - fun getUserId(nativeId: NativeId, promise: Promise) { + fun getUserId(playerId: NativeId, promise: Promise) { uiManager()?.addUIBlock { _ -> - collectors[nativeId]?.let { + playerModule()?.getPlayer(playerId)?.analytics?.let { promise.resolve(it.userId) } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index 79017c2d..c403745b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -56,7 +56,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB * @param analyticsConfigJson `AnalyticsConfig` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { + fun initWithAnalyticsConfig(nativeId: NativeId, playerConfigJson: ReadableMap?, analyticsConfigJson: ReadableMap?) { uiManager()?.addUIBlock { if (players.containsKey(nativeId)) { Log.d("[PlayerModule]", "Duplicate player creation for id $nativeId") @@ -64,7 +64,7 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB } val playerConfig = JsonConverter.toPlayerConfig(playerConfigJson) val analyticsConfig = JsonConverter.toAnalyticsConfig(analyticsConfigJson) - val defaultMetadata = JsonConverter.toAnalyticsDefaultMetadata(analyticsConfigJson) + val defaultMetadata = JsonConverter.toAnalyticsDefaultMetadata(analyticsConfigJson?.getMap("defaultMetadata")) players[nativeId] = if (analyticsConfig == null) { Player.create(context, playerConfig) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt index 04d3a6f5..6079091f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -1,8 +1,16 @@ package com.bitmovin.player.reactnative +import android.util.Log +import com.bitmovin.analytics.api.SourceMetadata +import com.bitmovin.player.api.analytics.create import com.bitmovin.player.api.source.Source +import com.bitmovin.player.api.source.SourceConfig import com.bitmovin.player.reactnative.converter.JsonConverter -import com.facebook.react.bridge.* +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.UIManagerModule @@ -33,17 +41,24 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB } /** - * Creates a new `Source` instance inside the internal sources using the provided `config` object. + * Creates a new `Source` instance inside the internal sources using the provided + * `config` and `analyticsSourceMetadata` object as well as an initialized DRM configuration ID. * @param nativeId ID to be associated with the `Source` instance. + * @param drmNativeId ID of the DRM config to use. * @param config `SourceConfig` object received from JS. + * @param analyticsSourceMetadata `SourceMetadata` object received from JS. */ @ReactMethod - fun initWithConfig(nativeId: NativeId, config: ReadableMap?) { + fun initWithAnalyticsConfig( + nativeId: NativeId, + drmNativeId: NativeId?, + config: ReadableMap?, + analyticsSourceMetadata: ReadableMap? + ) { uiManager()?.addUIBlock { - if (!sources.containsKey(nativeId)) { - JsonConverter.toSourceConfig(config)?.let { - sources[nativeId] = Source.create(it) - } + val sourceMetadata = JsonConverter.toAnalyticsSourceMetadata(analyticsSourceMetadata) ?: SourceMetadata() + initializeSource(nativeId, drmNativeId, config) { sourceConfig -> + Source.create(sourceConfig, sourceMetadata) } } } @@ -56,15 +71,36 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param config `SourceConfig` object received from JS. */ @ReactMethod - fun initWithDrmConfig(nativeId: NativeId, drmNativeId: NativeId, config: ReadableMap?) { + fun initWithConfig( + nativeId: NativeId, + drmNativeId: NativeId?, + config: ReadableMap?, + ) { uiManager()?.addUIBlock { - val drmConfig = drmModule()?.getConfig(drmNativeId) - if (!sources.containsKey(nativeId) && drmConfig != null) { - JsonConverter.toSourceConfig(config)?.let { - it.drmConfig = drmConfig - sources[nativeId] = Source.create(it) + initializeSource(nativeId, drmNativeId, config) { sourceConfig -> + Source.create(sourceConfig) + } + } + } + + private fun initializeSource( + nativeId: NativeId, + drmNativeId: NativeId?, + config: ReadableMap?, + action: (SourceConfig) -> Source + ) { + val drmConfig = drmNativeId?.let { drmModule()?.getConfig(it) } + if (!sources.containsKey(nativeId)) { + val sourceConfig = JsonConverter.toSourceConfig(config)?.apply { + if (drmConfig != null) { + this.drmConfig = drmConfig } } + if (sourceConfig == null) { + Log.d("[SourceModule]", "Could not parse SourceConfig") + } else { + sources[nativeId] = action(sourceConfig) + } } } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index e988986d..f200effb 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -1,6 +1,5 @@ package com.bitmovin.player.reactnative.converter -import com.bitmovin.analytics.BitmovinAnalyticsConfig import com.bitmovin.analytics.api.AnalyticsConfig import com.bitmovin.analytics.api.CustomData import com.bitmovin.analytics.api.DefaultMetadata @@ -41,6 +40,7 @@ import com.bitmovin.player.api.ui.StyleConfig import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getName import com.bitmovin.player.reactnative.extensions.getProperty +import com.bitmovin.player.reactnative.extensions.putBoolean import com.bitmovin.player.reactnative.extensions.putDouble import com.bitmovin.player.reactnative.extensions.putInt import com.bitmovin.player.reactnative.extensions.setProperty @@ -808,11 +808,6 @@ class JsonConverter { it.getBooleanOrNull("randomizeUserId")?.let { randomizeUserId -> setRandomizeUserId(randomizeUserId) } - for (n in 1..30) { - it.getString("customData${n}")?.let { customDataN -> - setProperty("customData${n}", customDataN) - } - } }.build() } @@ -866,7 +861,7 @@ class JsonConverter { * @return The produced JS value or null. */ @JvmStatic - fun fromAnalyticsCustomData(customData: CustomData?): ReadableMap? = customData?.let { + fun fromAnalyticsCustomData(customData: CustomData?): WritableMap? = customData?.let { val json = Arguments.createMap() for (n in 1..30) { it.getProperty("customData${n}")?.let { customDataN -> @@ -892,6 +887,19 @@ class JsonConverter { ) } + @JvmStatic + fun fromAnalyticsSourceMetadata(sourceMetadata: SourceMetadata?): ReadableMap? { + if (sourceMetadata == null) return null + + return fromAnalyticsCustomData(sourceMetadata.customData)?.apply { + putString("title", sourceMetadata.title) + putString("videoId", sourceMetadata.videoId) + putString("cdnProvider", sourceMetadata.cdnProvider) + putString("path", sourceMetadata.path) + putBoolean("isLive", sourceMetadata.isLive) + } + } + /** * Converts any `VideoQuality` value into its json representation. * @param videoQuality `VideoQuality` value. diff --git a/example/src/screens/BasicAnalytics.tsx b/example/src/screens/BasicAnalytics.tsx index 9db9b48d..0f0ffe7c 100644 --- a/example/src/screens/BasicAnalytics.tsx +++ b/example/src/screens/BasicAnalytics.tsx @@ -5,7 +5,6 @@ import { usePlayer, PlayerView, SourceType, - CdnProvider, } from 'bitmovin-player-react-native'; import { useTVGestures } from '../hooks'; @@ -14,22 +13,19 @@ export default function BasicAds() { const player = usePlayer({ analyticsConfig: { - key: '', // `key` is the only required parameter. - playerKey: '', - cdnProvider: CdnProvider.AKAMAI, // Check out `CdnProvider` for more options. - customUserId: 'Custom user ID', - randomizeUserId: false, // Default value is true. - experimentName: 'Experiment name', - videoId: 'MyVideoId', - title: 'Art of Motion', - isLive: false, - ads: false, // Can be changed to `true` in case `advertisingConfig` is also present. - path: '/examples/basic_analytics', - customData1: 'Custom data field 1', - customData2: 'Custom data field 2', - customData3: 'Custom data field 3', - customData4: 'Custom data field 4', - customData5: 'Custom data field 5', + licenseKey: '', // `licenseKey` is the only required parameter. + randomizeUserId: false, + adTrackingDisabled: true, + defaultMetadata: { + cdnProvider: 'akamai', + customUserId: 'Custom user ID from React', + experimentName: 'Experiment name', + customData1: 'Custom data field 1', + customData2: 'Custom data field 2', + customData3: 'Custom data field 3', + customData4: 'Custom data field 4', + customData5: 'Custom data field 5', + }, }, }); @@ -44,10 +40,14 @@ export default function BasicAds() { title: 'Art of Motion', poster: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', - }); - player.analyticsCollector?.setCustomData({ - experimentName: 'setCustomData', - customData1: `customized data 1`, + analyticsSourceMetadata: { + videoId: 'MyVideoId', + title: 'Art of Motion', + isLive: false, + path: '/examples/basic_analytics', + customData1: 'Custom data field 1 from source', + experimentName: 'Experiment Name Override', + }, }); return () => { player.destroy(); @@ -55,16 +55,6 @@ export default function BasicAds() { }, [player]) ); - setTimeout(() => { - player.load({ - url: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8', - type: SourceType.HLS, - title: 'Art of Motion HLS', - poster: - 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', - }); - }, 10000); - return ( diff --git a/ios/AnalyticsModule.m b/ios/AnalyticsModule.m index 9600f763..c3766806 100644 --- a/ios/AnalyticsModule.m +++ b/ios/AnalyticsModule.m @@ -2,14 +2,7 @@ @interface RCT_EXTERN_REMAP_MODULE(AnalyticsModule, AnalyticsModule, NSObject) -RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config) -RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) -RCT_EXTERN_METHOD(attach:(NSString *)nativeId playerId:(NSString *)playerId) -RCT_EXTERN_METHOD(detach:(NSString *)nativeId) -RCT_EXTERN_METHOD(setCustomDataOnce:(NSString *)nativeId json:(nullable id)json) -RCT_EXTERN_METHOD(setCustomData:(NSString *)nativeId playerId:(nullable NSString *)playerId json:(nullable id)json) -RCT_EXTERN_METHOD(getCustomData:(NSString *)nativeId playerId:(nullable NSString *)playerId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(sendCustomDataEvent:(NSString *)nativeId json:(nullable id)json) RCT_EXTERN_METHOD(getUserId:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(addSourceMetadata:(NSString *)nativeId playerId:(nullable NSString *)playerId json:(nullable id)json) @end diff --git a/ios/AnalyticsModule.swift b/ios/AnalyticsModule.swift index 35ba1dd6..fe2ac945 100644 --- a/ios/AnalyticsModule.swift +++ b/ios/AnalyticsModule.swift @@ -11,9 +11,6 @@ class AnalyticsModule: NSObject, RCTBridgeModule { bridge.module(for: PlayerModule.self) as? PlayerModule } - /// In-memory mapping from `nativeId`s to `BitmovinPlayerCollector` instances. - private var collectors: Registry = [:] - /// JS module name. static func moduleName() -> String! { "AnalyticsModule" @@ -30,190 +27,42 @@ class AnalyticsModule: NSObject, RCTBridgeModule { } /** - Retrieves a `BitmovinPlayerCollector` instance from the internal registry for the given `nativeId`. - - Parameter nativeId: Native Id of the collector instance. - - Returns: Collector instance associated with the `nativeId` or `nil`. - */ - @objc func retrieve(_ nativeId: NativeId) -> BitmovinPlayerCollector? { - collectors[nativeId] - } - - /** - Creates a new `BitmovinPlayerCollector` instance inside the internal registry using the provided `config` object. - - Parameter nativeId: ID to associate with the `BitmovinPlayerCollector` instance. - - Parameter config: `BitmovinAnalyticsConfig` object received from JS. - */ - @objc(initWithConfig:config:) - func initWithConfig(_ nativeId: NativeId, config: Any?) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard let analyticsConfig = RCTConvert.analyticsConfig(config) else { - return - } - self?.collectors[nativeId] = BitmovinPlayerCollector(config: analyticsConfig) - } - } - - /** - Detaches and removes the given `BitmovinPlayerCollector` from the internal registry. - - Parameter nativeId: Native Id of the collector instance. - */ - @objc(destroy:) - func destroy(_ nativeId: NativeId) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - self?.collectors[nativeId]?.detachPlayer() - self?.collectors[nativeId] = nil - } - } - - /** - Attaches a `BitmovinPlayerCollector` to the `Player` instance with native Id equal to `playerId`. - - Parameter nativeId: Native Id of the collector instance. + Sends a sample with the provided custom data. + Does not change the configured custom data of the collector or source. - Parameter playerId: Native Id of the player instance. - */ - @objc(attach:playerId:) - func attach(_ nativeId: NativeId, playerId: NativeId) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let player = self?.playerModule?.retrieve(playerId) - else { - return - } - collector.attachPlayer(player: player) - } - } - - /** - Detaches the player object from a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - */ - @objc(detach:) - func detach(_ nativeId: NativeId) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard let collector = self?.collectors[nativeId] else { - return - } - collector.detachPlayer() - } - } - - /** - Updates the custom data config for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - Parameter json: Custom data config json. */ - @objc(setCustomDataOnce:json:) - func setCustomDataOnce(_ nativeId: NativeId, json: Any?) { + @objc(sendCustomDataEvent:json:) + func sendCustomDataEvent(_ playerId: NativeId, json: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in guard - let collector = self?.collectors[nativeId], + let playerAnalytics = self?.playerModule?.retrieve(playerId)?.analytics, let customData = RCTConvert.analyticsCustomData(json) else { return } - collector.setCustomDataOnce(customData: customData) - } - } - - /** - Sets the custom data config for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - - Parameter playerId: Native Id of the player instance. - - Parameter json: Custom data config json. - */ - @objc(setCustomData:playerId:json:) - func setCustomData( - _ nativeId: NativeId, - playerId: NativeId?, - json: Any? - ) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let customData = RCTConvert.analyticsCustomData(json), - let playerId = playerId, - let player = self?.bridge[PlayerModule.self]?.retrieve(playerId), - let source = player.source - else { - return - } - collector.apply(customData: customData, for: source) - } - } - - /** - Gets the current custom data config for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the the collector instance. - - Parameter playerId: Native Id of the player instance. - - Parameter resolver: JS promise resolver. - - Parameter rejecter: JS promise rejecter. - */ - @objc(getCustomData:playerId:resolver:rejecter:) - func getCustomData( - _ nativeId: NativeId, - playerId: NativeId?, - resolver resolve: @escaping RCTPromiseResolveBlock, - rejecter reject: @escaping RCTPromiseRejectBlock - ) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let playerId = playerId, - let player = self?.bridge[PlayerModule.self]?.retrieve(playerId), - let source = player.source, - let customData = RCTConvert.toJson(analyticsCustomData: collector.customData(for: source)) - else { - reject("[AnalyticsModule]", "Could not find analytics collector with ID (\(nativeId))", nil) - return - } - resolve(customData) + playerAnalytics.sendCustomDataEvent(customData: customData) } } /** Gets the current user Id for a `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the the collector instance. + - Parameter playerId: Native Id of the the player instance. - Parameter resolver: JS promise resolver. - Parameter rejecter: JS promise rejecter. */ @objc(getUserId:resolver:rejecter:) func getUserId( - _ nativeId: NativeId, + _ playerId: NativeId, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock ) { bridge.uiManager.addUIBlock { [weak self] _, _ in - guard let collector = self?.collectors[nativeId] else { - reject("[AnalyticsModule]", "Could not find analytics collector with ID (\(nativeId))", nil) - return - } - resolve(collector.getUserId()) - } - } - - /** - Applies the source metadata for the current source via the `BitmovinPlayerCollector` instance. - - Parameter nativeId: Native Id of the collector instance. - - Parameter playerId: Native Id of the player instance. - - Parameter json: Custom data config json. - */ - @objc(addSourceMetadata:playerId:json:) - func addSourceMetadata( - _ nativeId: NativeId, - playerId: NativeId?, - json: Any? - ) { - bridge.uiManager.addUIBlock { [weak self] _, _ in - guard - let collector = self?.collectors[nativeId], - let sourceMetadata = RCTConvert.analyticsSourceMetadata(json), - let playerId = playerId, - let player = self?.bridge[PlayerModule.self]?.retrieve(playerId), - let source = player.source - else { + guard let playerAnalytics = self?.playerModule?.retrieve(playerId)?.analytics else { + reject("[AnalyticsModule]", "Could not find player with ID (\(playerId))", nil) return } - collector.apply(sourceMetadata: sourceMetadata, for: source) + resolve(playerAnalytics.userId) } } } diff --git a/ios/PlayerModule.m b/ios/PlayerModule.m index 1d70a2e9..131923df 100644 --- a/ios/PlayerModule.m +++ b/ios/PlayerModule.m @@ -3,6 +3,7 @@ @interface RCT_EXTERN_REMAP_MODULE(PlayerModule, PlayerModule, NSObject) RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config) +RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId config:(nullable id)config analyticsConfig:(nullable id)analyticsConfig) RCT_EXTERN_METHOD(loadSource:(NSString *)nativeId sourceNativeId:(NSString *)sourceNativeId) RCT_EXTERN_METHOD(loadOfflineContent:(NSString *)nativeId offlineContentManagerBridgeId:(NSString *)offlineContentManagerBridgeId options:(nullable id)options) RCT_EXTERN_METHOD(unload:(NSString *)nativeId) diff --git a/ios/PlayerModule.swift b/ios/PlayerModule.swift index a30f515b..895f7afd 100644 --- a/ios/PlayerModule.swift +++ b/ios/PlayerModule.swift @@ -48,6 +48,31 @@ class PlayerModule: NSObject, RCTBridgeModule { self?.players[nativeId] = PlayerFactory.create(playerConfig: playerConfig) } } + + /** + Creates a new analytics enabled `Player` instance inside the internal players using the provided `config` and `analyticsConfig` object. + - Parameter config: `PlayerConfig` object received from JS. + - Parameter analyticsConfig: `AnalyticsConfig` object received from JS. + */ + @objc(initWithAnalyticsConfig:config:analyticsConfig:) + func initWithAnalyticsConfig(_ nativeId: NativeId, config: Any?, analyticsConfig: Any?) { + bridge.uiManager.addUIBlock { [weak self] _, _ in + let analyticsConfigJson = analyticsConfig + guard + self?.players[nativeId] == nil, + let playerConfig = RCTConvert.playerConfig(config), + let analyticsConfig = RCTConvert.analyticsConfig(analyticsConfig) + else { + return + } + let defaultMetadata = RCTConvert.analyticsDefaultMetadataFromAnalyticsConfig(analyticsConfigJson) + self?.players[nativeId] = PlayerFactory.create( + playerConfig: playerConfig, + analyticsConfig: analyticsConfig, + defaultMetadata: defaultMetadata ?? DefaultMetadata() + ) + } + } /** Loads the given source configuration into `nativeId`'s `Player` object. diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index bf4d08ba..8201ddee 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -636,79 +636,41 @@ extension RCTConvert { - Parameter json: JS object. - Returns: The associated `BitmovinAnalyticsConfig` value or nil. */ - static func analyticsConfig(_ json: Any?) -> BitmovinAnalyticsConfig? { + static func analyticsConfig(_ json: Any?) -> AnalyticsConfig? { guard let json = json as? [String: Any?], - let key = json["key"] as? String + let key = json["licenseKey"] as? String else { return nil } - let config: BitmovinAnalyticsConfig - if let playerKey = json["playerKey"] as? String { - config = BitmovinAnalyticsConfig(key: key, playerKey: playerKey) - } else { - config = BitmovinAnalyticsConfig(key: key) - } - if let cdnProvider = json["cdnProvider"] as? String { - config.cdnProvider = cdnProvider - } - if let customerUserId = json["customUserId"] as? String { - config.customerUserId = customerUserId - } - if let experimentName = json["experimentName"] as? String { - config.experimentName = experimentName - } - if let videoId = json["videoId"] as? String { - config.videoId = videoId - } - if let title = json["title"] as? String { - config.title = title - } - if let path = json["path"] as? String { - config.path = path - } - if let isLive = json["isLive"] as? Bool { - config.isLive = isLive - } - if let ads = json["ads"] as? Bool { - config.ads = ads - } - if let randomizeUserId = json["randomizeUserId"] as? Bool { - config.randomizeUserId = randomizeUserId - } - config.customData1 = json["customData1"] as? String - config.customData2 = json["customData2"] as? String - config.customData3 = json["customData3"] as? String - config.customData4 = json["customData4"] as? String - config.customData5 = json["customData5"] as? String - config.customData6 = json["customData6"] as? String - config.customData7 = json["customData7"] as? String - config.customData8 = json["customData8"] as? String - config.customData9 = json["customData9"] as? String - config.customData10 = json["customData10"] as? String - config.customData11 = json["customData11"] as? String - config.customData12 = json["customData12"] as? String - config.customData13 = json["customData13"] as? String - config.customData14 = json["customData14"] as? String - config.customData15 = json["customData15"] as? String - config.customData16 = json["customData16"] as? String - config.customData17 = json["customData17"] as? String - config.customData18 = json["customData18"] as? String - config.customData19 = json["customData19"] as? String - config.customData20 = json["customData20"] as? String - config.customData21 = json["customData21"] as? String - config.customData22 = json["customData22"] as? String - config.customData23 = json["customData23"] as? String - config.customData24 = json["customData24"] as? String - config.customData25 = json["customData25"] as? String - config.customData26 = json["customData26"] as? String - config.customData27 = json["customData27"] as? String - config.customData28 = json["customData28"] as? String - config.customData29 = json["customData29"] as? String - config.customData30 = json["customData30"] as? String - config.experimentName = json["experimentName"] as? String + let randomizeUserId = json["randomizeUserId"] as? Bool + let adTrackingDisabled = json["adTrackingDisabled"] as? Bool + + let config = AnalyticsConfig( + licenseKey: key, + randomizeUserId: randomizeUserId ?? false, + adTrackingDisabled: adTrackingDisabled ?? false + ) return config } + + static func analyticsDefaultMetadataFromAnalyticsConfig(_ json: Any?) -> DefaultMetadata? { + guard + let analyticsConfigJson = json as? [String: Any?], + let json = analyticsConfigJson["defaultMetadata"] as? [String: Any?] + else { + return nil + } + let cdnProvider = json["cdnProvider"] as? String + let customUserId = json["customUserId"] as? String + let customData = analyticsCustomData(json) + + return DefaultMetadata( + cdnProvider: cdnProvider, + customUserId: customUserId, + customData: customData ?? CustomData() + ) + } /** Utility method to get an analytics `CustomData` value from a JS object. diff --git a/ios/SourceModule.m b/ios/SourceModule.m index a876c77a..0f07e7eb 100644 --- a/ios/SourceModule.m +++ b/ios/SourceModule.m @@ -2,8 +2,8 @@ @interface RCT_EXTERN_REMAP_MODULE(SourceModule, SourceModule, NSObject) -RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config) -RCT_EXTERN_METHOD(initWithDrmConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config) +RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config) +RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) RCT_EXTERN_METHOD( isAttachedToPlayer:(NSString *)nativeId diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 2e4a1990..54737553 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -33,20 +33,25 @@ class SourceModule: NSObject, RCTBridgeModule { } /** - Creates a new `Source` instance inside the internal sources using the provided `config` object. + Creates a new `Source` instance inside the internal sources using the provided `config` and `analyticsSourceMetadata` object and an optionally initialized DRM configuration ID. - Parameter nativeId: ID to be associated with the `Source` instance. + - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. + - Parameter analyticsSourceMetadata: `SourceMetadata` object received from JS. */ - @objc(initWithConfig:config:) - func initWithConfig(_ nativeId: NativeId, config: Any?) { + @objc(initWithAnalyticsConfig:drmNativeId:config:analyticsSourceMetadata:) + func initWithAnalyticsConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, analyticsSourceMetadata: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in + let fairplayConfig = drmNativeId != nil ? self?.getDrmModule()?.retrieve(drmNativeId!) : nil + guard self?.sources[nativeId] == nil, - let sourceConfig = RCTConvert.sourceConfig(config) + let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig), + let sourceMetadata = RCTConvert.analyticsSourceMetadata(analyticsSourceMetadata) else { return } - self?.sources[nativeId] = SourceFactory.create(from: sourceConfig) + self?.sources[nativeId] = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) } } @@ -56,12 +61,12 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. */ - @objc(initWithDrmConfig:drmNativeId:config:) - func initWithDrmConfig(_ nativeId: NativeId, drmNativeId: NativeId, config: Any?) { + @objc(initWithConfig:drmNativeId:config:) + func initWithConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in guard self?.sources[nativeId] == nil, - let fairplayConfig = self?.getDrmModule()?.retrieve(drmNativeId), + let fairplayConfig = drmNativeId != nil ? self?.getDrmModule()?.retrieve(drmNativeId!) : nil, let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig) else { return diff --git a/src/analytics/collector.ts b/src/analytics/collector.ts deleted file mode 100644 index 9f0b31f1..00000000 --- a/src/analytics/collector.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NativeModules } from 'react-native'; -import NativeInstance from '../nativeInstance'; -import { AnalyticsConfig, CustomDataConfig, SourceMetadata } from './config'; - -const AnalyticsModule = NativeModules.AnalyticsModule; - -/** - * Analytics collector that can be attached to a player object in order to collect and send - * its analytics information. - */ -export class AnalyticsCollector extends NativeInstance { - /** - * Whether the native `AnalyticsCollector` object has been created. - */ - isInitialized = false; - - /** - * The native player id that this analytics collector is attached to. - */ - playerId?: string; - - /** - * Whether the native `AnalyticsCollector` object has been disposed. - */ - isDestroyed = false; - - /** - * Initializes a native `BitmovinPlayerCollector` object. - */ - initialize = () => { - if (!this.isInitialized) { - AnalyticsModule.initWithConfig(this.nativeId, this.config); - this.isInitialized = true; - } - }; - - /** - * Disposes the native `BitmovinPlayerCollector` object that has been created - * during initialization. - */ - destroy = () => { - if (!this.isDestroyed) { - AnalyticsModule.destroy(this.nativeId); - this.isDestroyed = true; - this.playerId = undefined; - } - }; - - /** - * Attach a player instance to this analytics plugin. After this is completed, BitmovinAnalytics - * will start monitoring and sending analytics data based on the attached player instance. - * - * @param playerId - Native Id of the player to attach this collector instance. - */ - attach = (playerId: string): void => { - this.playerId = playerId; - AnalyticsModule.attach(this.nativeId, playerId); - }; - - /** - * Detach a player instance from this analytics plugin if there's any attached. If no player is attached, - * nothing happens. - */ - detach = (): void => { - this.playerId = undefined; - AnalyticsModule.detach(this.nativeId); - }; - - /** - * Dynamically updates analytics custom data information. Use this method - * to update your custom data during runtime. - * - * @param customData - Analytics custom data config. - * @deprecated - */ - setCustomDataOnce = (customData: CustomDataConfig) => { - AnalyticsModule.setCustomDataOnce(this.nativeId, customData); - }; - - /** - * Sets the internal analytics custom data state. - * - * @param customData - Analytics custom data config. - */ - setCustomData = (customData: CustomDataConfig) => { - AnalyticsModule.setCustomData(this.nativeId, this.playerId, customData); - }; - - /** - * Gets the current custom data config from the native `BitmovinPlayerCollector` instance. - * - * @returns The current custom data config. - */ - getCustomData = async (): Promise => { - return AnalyticsModule.getCustomData(this.nativeId, this.playerId); - }; - - /** - * Gets the current user id used by the native `BitmovinPlayerCollector` instance. - * - * @returns The current user id. - */ - getUserId = async (): Promise => { - return AnalyticsModule.getUserId(this.nativeId); - }; - - /** - * Sets the source metadata for the current source loaded into the player. - * This method should be called every time a new source is loaded into the player to ensure - * that the analytics data is correct. - * @param sourceMetadata - Source metadata to set. - */ - setSourceMetadata = (sourceMetadata: SourceMetadata) => { - return AnalyticsModule.setSourceMetadata( - this.nativeId, - this.playerId, - sourceMetadata - ); - }; - - /** - * Adds source metadata for the current source loaded into the player. - * This method should be called every time a new source is loaded into the player to ensure - * that the analytics data is correct. - * @param sourceMetadata - Source metadata to set. - * @deprecated - */ - addSourceMetadata = (sourceMetadata: SourceMetadata) => { - return AnalyticsModule.addSourceMetadata( - this.nativeId, - this.playerId, - sourceMetadata - ); - }; -} diff --git a/src/analytics/config.ts b/src/analytics/config.ts index 71b83914..14000a96 100644 --- a/src/analytics/config.ts +++ b/src/analytics/config.ts @@ -1,46 +1,77 @@ -import { NativeInstanceConfig } from '../nativeInstance'; - /** - * Available cdn provider options for AnalyticsConfig. + * Object used to configure the build-in analytics collector. */ -export enum CdnProvider { - BITMOVIN = 'bitmovin', - AKAMAI = 'akamai', - FASTLY = 'fastly', - MAXCDN = 'maxcdn', - CLOUDFRONT = 'cloudfront', - CHINACACHE = 'chinacache', - BITGRAVITY = 'bitgravity', +export interface AnalyticsConfig { + /** + * The analytics license key + */ + licenseKey: string; + /** + * Flag to enable Ad tracking (default: false). + */ + adTrackingDisabled?: boolean; + /** + * Flag to use randomised userId not depending on device specific values (default: false). + */ + randomizeUserId?: boolean; + /** + * Default metadata to be sent with events. + * Fields of the `SourceMetadata` are prioritized over the default metadata. + */ + defaultMetadata?: DefaultMetadata; } /** - * Object used to configure a new `AnalyticsCollector` instance. + * DefaultMetadata that can be used to enrich the analytics data. + * DefaultMetadata is not bound to a specific source and can be used to set fields for the lifecycle of the collector. + * If fields are specified in `SourceMetadata` and `DefaultMetadata`, `SourceMetadata` takes precedence. */ -export interface AnalyticsConfig - extends NativeInstanceConfig, - CustomDataConfig { +export interface DefaultMetadata extends CustomDataConfig { /** * CDN Provide that the video playback session is using. */ - cdnProvider?: CdnProvider; + cdnProvider?: string; /** * User ID of the customer. */ customUserId?: string; +} + +/** + * `SourceMetadata` that can be used to enrich the analytics data. + */ +export interface SourceMetadata extends CustomDataConfig { /** - * The analytics license key + * ID of the video in the CMS system */ - licenseKey: string; + videoId?: String; + /** - * Flag to enable Ad tracking (default: false). + * Human readable title of the video asset currently playing */ - adTrackingDisabled?: boolean; + title?: String; + /** - * Flag to use randomised userId not depending on device specific values (default: false). + * Breadcrumb path to show where in the app the user is */ - randomizeUserId?: boolean; + path?: String; + + /** + * Flag to see if stream is live before stream metadata is available + */ + isLive?: boolean; + + /** + * CDN Provider that the video playback session is using + */ + cdnProvider?: String; } +/** + * Free-form data that can be used to enrich the analytics data + * If customData is specified in `SourceMetadata` and `DefaultMetadata` + * data is merged on a field basis with `SourceMetadata` taking precedence. + */ export interface CustomDataConfig { /** * Optional free-form custom data @@ -197,30 +228,3 @@ export interface CustomDataConfig { */ experimentName?: string; } - -export interface SourceMetadata extends CustomDataConfig { - /** - * ID of the video in the CMS system - */ - videoId?: String; - - /** - * Human readable title of the video asset currently playing - */ - title?: String; - - /** - * Breadcrumb path to show where in the app the user is - */ - path?: String; - - /** - * Flag to see if stream is live before stream metadata is available - */ - isLive?: boolean; - - /** - * CDN Provider that the video playback session is using - */ - cdnProvider?: String; -} diff --git a/src/analytics/index.ts b/src/analytics/index.ts index de864ce2..08b0a11b 100644 --- a/src/analytics/index.ts +++ b/src/analytics/index.ts @@ -1,2 +1,2 @@ export * from './config'; -export * from './collector'; +export * from './player'; diff --git a/src/analytics/player.ts b/src/analytics/player.ts new file mode 100644 index 00000000..d20b7152 --- /dev/null +++ b/src/analytics/player.ts @@ -0,0 +1,36 @@ +import { NativeModules } from 'react-native'; +import { CustomDataConfig } from './config'; + +const AnalyticsModule = NativeModules.AnalyticsModule; + +/** + * Provides the means to control the analytics collected by a `Player`. + * Use the `Player.analytics` property to access a `Player`'s `AnalyticsApi`. + */ +export class AnalyticsApi { + /** + * The native player id that this analytics api is attached to. + */ + playerId: string; + + constructor(playerId: string) { + this.playerId = playerId; + } + + /** + * Sends a sample with the provided custom data. + * Does not change the configured custom data of the collector or source. + */ + sendCustomDataEvent = (customData: CustomDataConfig) => { + AnalyticsModule.sendCustomDataEvent(this.playerId, customData); + }; + + /** + * Gets the current user id used by the bundled analytics instance. + * + * @returns The current user id. + */ + getUserId = async (): Promise => { + return AnalyticsModule.getUserId(this.playerId); + }; +} diff --git a/src/player.ts b/src/player.ts index d865f1d3..7b9223fe 100644 --- a/src/player.ts +++ b/src/player.ts @@ -1,6 +1,6 @@ import { NativeModules, Platform } from 'react-native'; import { AdItem, AdvertisingConfig } from './advertising'; -import { AnalyticsCollector, AnalyticsConfig } from './analytics'; +import { AnalyticsConfig } from './analytics'; import NativeInstance, { NativeInstanceConfig } from './nativeInstance'; import { Source, SourceConfig } from './source'; import { AudioTrack } from './audioTrack'; @@ -10,6 +10,7 @@ import { TweaksConfig } from './tweaksConfig'; import { AdaptationConfig } from './adaptationConfig'; import { OfflineContentManager, OfflineSourceOptions } from './offline'; import { Thumbnail } from './thumbnail'; +import { AnalyticsApi } from './analytics/player'; const PlayerModule = NativeModules.PlayerModule; @@ -153,10 +154,6 @@ export class Player extends NativeInstance { * Currently active source, or `null` if none is active. */ source?: Source; - /** - * Analytics collector currently attached to this player instance. - */ - analyticsCollector?: AnalyticsCollector; /** * Whether the native `Player` object has been created. */ @@ -165,6 +162,12 @@ export class Player extends NativeInstance { * Whether the native `Player` object has been disposed. */ isDestroyed = false; + /** + * The `AnalyticsApi` for interactions regarding the `Player`'s analytics. + * + * `undefined` if the player was created without analytics support. + */ + analytics?: AnalyticsApi = undefined; /** * Allocates the native `Player` instance and its resources natively. @@ -173,11 +176,12 @@ export class Player extends NativeInstance { if (!this.isInitialized) { const analyticsConfig = this.config?.analyticsConfig; if (analyticsConfig) { - PlayerModule.initWithConfig( + PlayerModule.initWithAnalyticsConfig( this.nativeId, this.config, analyticsConfig ); + this.analytics = new AnalyticsApi(this.nativeId); } else { PlayerModule.initWithConfig(this.nativeId, this.config); } @@ -192,7 +196,6 @@ export class Player extends NativeInstance { if (!this.isDestroyed) { PlayerModule.destroy(this.nativeId); this.source?.destroy(); - this.analyticsCollector?.destroy(); this.isDestroyed = true; } }; diff --git a/src/source.ts b/src/source.ts index 81dd86dc..156ade8f 100644 --- a/src/source.ts +++ b/src/source.ts @@ -3,6 +3,7 @@ import { Drm, DrmConfig } from './drm'; import NativeInstance, { NativeInstanceConfig } from './nativeInstance'; import { SideLoadedSubtitleTrack } from './subtitleTrack'; import { Thumbnail } from './thumbnail'; +import { SourceMetadata } from './analytics'; const SourceModule = NativeModules.SourceModule; @@ -129,6 +130,10 @@ export interface SourceConfig extends NativeInstanceConfig { * The `SourceOptions` for this configuration. */ options?: SourceOptions; + /** + * The `SourceMetadata` for the `Source` to setup custom analytics tracking + */ + analyticsSourceMetadata?: SourceMetadata; } /** @@ -153,16 +158,24 @@ export class Source extends NativeInstance { */ initialize = () => { if (!this.isInitialized) { + const sourceMetadata = this.config?.analyticsSourceMetadata; if (this.config?.drmConfig) { this.drm = new Drm(this.config.drmConfig); this.drm.initialize(); - SourceModule.initWithDrmConfig( + } + if (sourceMetadata) { + SourceModule.initWithAnalyticsConfig( this.nativeId, - this.drm.nativeId, - this.config + this.drm?.nativeId, + this.config, + sourceMetadata ); } else { - SourceModule.initWithConfig(this.nativeId, this.config); + SourceModule.initWithConfig( + this.nativeId, + this.drm?.nativeId, + this.config + ); } this.isInitialized = true; } From 30c8ba768d1e84ae7c6195bbf8b4c2cce6c7d0e0 Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Tue, 12 Sep 2023 16:56:30 +0200 Subject: [PATCH 09/85] chore: fix changelog --- CHANGELOG.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a4bee2..865951bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,15 @@ # Changelog -## [0.11.0] (2023-09-11) +## [Unreleased] ### Added -- `Player.getAudioTrack` and `Player.getSubtitleTrack` APIs to get currently selected audio and subtitle tracks -- `SourceConfig.description` property to allow setting a description for the source -- `Player.getThumbnail` and `Source.getThumbnail` APIs to get thumbnail images - `DefaultMetadata` for configuration of the bundled analytics collector - `Player.analytics` to access the `AnalyticsApi` and interact with the bundled analytics collector - `SourceConfig.analyticsSourceMetadata` for extended configuration of the bundled analytics collector ### Changed -- Update Bitmovin's native Android SDK version to `3.43.0` - `AnalyticsConfig` properties to match the bitmovin analytics v3 API ### Removed @@ -21,6 +17,18 @@ - `AnalyticsCollector` in favor of the bundled analytics functionality - `CdnProvider`, as the property on the `AnalyticsConfig` is now a `string` +## [0.11.0] (2023-09-11) + +### Added + +- `Player.getAudioTrack` and `Player.getSubtitleTrack` APIs to get currently selected audio and subtitle tracks +- `SourceConfig.description` property to allow setting a description for the source +- `Player.getThumbnail` and `Source.getThumbnail` APIs to get thumbnail images + +### Changed + +- Update Bitmovin's native Android SDK version to `3.43.0` + ## [0.10.0] (2023-09-04) ### Added From 7f666cc17bdf56fc437e0f1c640829b61c236d52 Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Wed, 13 Sep 2023 10:12:52 +0200 Subject: [PATCH 10/85] feat(android): use jason build of the android player --- CHANGELOG.md | 6 ++++++ android/build.gradle | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd652829..857dfc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Changed + +- Use `jason` build of Bitmovin's native Android SDK + +## [Unreleased] + ### Added - `Player.getAudioTrack` and `Player.getSubtitleTrack` APIs to get currently selected audio and subtitle tracks diff --git a/android/build.gradle b/android/build.gradle index 9a5210b7..f4ec8f65 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1' - implementation 'com.bitmovin.player:player:3.43.0' + implementation 'com.bitmovin.player:player:3.43.0+jason' //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules } From 84c6b0d2c91cb300cf3a8916e66ba14db6603c6f Mon Sep 17 00:00:00 2001 From: Matthias Tamegger Date: Wed, 13 Sep 2023 12:03:05 +0200 Subject: [PATCH 11/85] chore: rename AnalyticsModule to PlayerAnalyticsModule --- .../{AnalyticsModule.kt => PlayerAnalyticsModule.kt} | 4 ++-- .../bitmovin/player/reactnative/RNPlayerViewPackage.kt | 2 +- ios/{AnalyticsModule.m => PlayerAnalyticsModule.m} | 2 +- ...{AnalyticsModule.swift => PlayerAnalyticsModule.swift} | 8 ++++---- src/analytics/player.ts | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename android/src/main/java/com/bitmovin/player/reactnative/{AnalyticsModule.kt => PlayerAnalyticsModule.kt} (91%) rename ios/{AnalyticsModule.m => PlayerAnalyticsModule.m} (73%) rename ios/{AnalyticsModule.swift => PlayerAnalyticsModule.swift} (90%) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt similarity index 91% rename from android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt rename to android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt index 23c9eeb4..f7e5f384 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerAnalyticsModule.kt @@ -6,10 +6,10 @@ import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.UIManagerModule -private const val MODULE_NAME = "AnalyticsModule" +private const val MODULE_NAME = "PlayerAnalyticsModule" @ReactModule(name = MODULE_NAME) -class AnalyticsModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { +class PlayerAnalyticsModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { /** * JS exported module name. */ diff --git a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt index 8378f5ae..fad7dec4 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt @@ -24,7 +24,7 @@ class RNPlayerViewPackage : ReactPackage { PlayerModule(reactContext), SourceModule(reactContext), DrmModule(reactContext), - AnalyticsModule(reactContext), + PlayerAnalyticsModule(reactContext), RNPlayerViewManager(reactContext), FullscreenHandlerModule(reactContext), CustomMessageHandlerModule(reactContext) diff --git a/ios/AnalyticsModule.m b/ios/PlayerAnalyticsModule.m similarity index 73% rename from ios/AnalyticsModule.m rename to ios/PlayerAnalyticsModule.m index c3766806..013472a2 100644 --- a/ios/AnalyticsModule.m +++ b/ios/PlayerAnalyticsModule.m @@ -1,6 +1,6 @@ #import -@interface RCT_EXTERN_REMAP_MODULE(AnalyticsModule, AnalyticsModule, NSObject) +@interface RCT_EXTERN_REMAP_MODULE(PlayerAnalyticsModule, PlayerAnalyticsModule, NSObject) RCT_EXTERN_METHOD(sendCustomDataEvent:(NSString *)nativeId json:(nullable id)json) RCT_EXTERN_METHOD(getUserId:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) diff --git a/ios/AnalyticsModule.swift b/ios/PlayerAnalyticsModule.swift similarity index 90% rename from ios/AnalyticsModule.swift rename to ios/PlayerAnalyticsModule.swift index fe2ac945..34960e76 100644 --- a/ios/AnalyticsModule.swift +++ b/ios/PlayerAnalyticsModule.swift @@ -1,8 +1,8 @@ import BitmovinPlayer import BitmovinCollector -@objc(AnalyticsModule) -class AnalyticsModule: NSObject, RCTBridgeModule { +@objc(PlayerAnalyticsModule) +class PlayerAnalyticsModule: NSObject, RCTBridgeModule { /// React bridge reference. @objc var bridge: RCTBridge! @@ -13,7 +13,7 @@ class AnalyticsModule: NSObject, RCTBridgeModule { /// JS module name. static func moduleName() -> String! { - "AnalyticsModule" + "PlayerAnalyticsModule" } /// Module requires main thread initialization. @@ -59,7 +59,7 @@ class AnalyticsModule: NSObject, RCTBridgeModule { ) { bridge.uiManager.addUIBlock { [weak self] _, _ in guard let playerAnalytics = self?.playerModule?.retrieve(playerId)?.analytics else { - reject("[AnalyticsModule]", "Could not find player with ID (\(playerId))", nil) + reject("[PlayerAnalyticsModule]", "Could not find player with ID (\(playerId))", nil) return } resolve(playerAnalytics.userId) diff --git a/src/analytics/player.ts b/src/analytics/player.ts index d20b7152..29d4a6be 100644 --- a/src/analytics/player.ts +++ b/src/analytics/player.ts @@ -1,7 +1,7 @@ import { NativeModules } from 'react-native'; import { CustomDataConfig } from './config'; -const AnalyticsModule = NativeModules.AnalyticsModule; +const PlayerAnalyticsModule = NativeModules.PlayerAnalyticsModule; /** * Provides the means to control the analytics collected by a `Player`. @@ -22,7 +22,7 @@ export class AnalyticsApi { * Does not change the configured custom data of the collector or source. */ sendCustomDataEvent = (customData: CustomDataConfig) => { - AnalyticsModule.sendCustomDataEvent(this.playerId, customData); + PlayerAnalyticsModule.sendCustomDataEvent(this.playerId, customData); }; /** @@ -31,6 +31,6 @@ export class AnalyticsApi { * @returns The current user id. */ getUserId = async (): Promise => { - return AnalyticsModule.getUserId(this.playerId); + return PlayerAnalyticsModule.getUserId(this.playerId); }; } From 5a36b832fa8df623b821661d92bcc6ae41712e72 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 18 Sep 2023 14:27:48 +0200 Subject: [PATCH 12/85] chore: remove cd call for pods-install yarn command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 537cc0b4..82827a48 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "build": "tsup ./src/index.ts --dts --target es2020 --format cjs,esm -d lib", "example": "yarn --cwd example", "pods": "yarn pods-install || yarn pods-update", - "pods-install": "cd example && yarn pod-install", + "pods-install": "yarn example pod-install", "pods-update": "! [[ \"$OSTYPE\" =~ ^darwin ]] && echo 'CocoaPods is only supported on darwin machines' || (cd example/ios && pod update --silent)", "bootstrap": "yarn && yarn example && yarn pods", "prepare": "husky install" From 2e88d6e586a7e2bc6b67521a7b4c664831cef5f9 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 18 Sep 2023 14:31:21 +0200 Subject: [PATCH 13/85] chore: remove Darwin check for pod install --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 82827a48..12aa1c66 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "example": "yarn --cwd example", "pods": "yarn pods-install || yarn pods-update", "pods-install": "yarn example pod-install", - "pods-update": "! [[ \"$OSTYPE\" =~ ^darwin ]] && echo 'CocoaPods is only supported on darwin machines' || (cd example/ios && pod update --silent)", + "pods-update": "cd example/ios && pod update --silent", "bootstrap": "yarn && yarn example && yarn pods", "prepare": "husky install" }, From db122bf413f8005b693f2313ce8f5454d2f2e9e1 Mon Sep 17 00:00:00 2001 From: Update Bot Date: Mon, 18 Sep 2023 13:44:20 +0000 Subject: [PATCH 14/85] chore(android): update android player version to 3.44.0+jason --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index f4ec8f65..e48185ee 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.29.0' implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1' - implementation 'com.bitmovin.player:player:3.43.0+jason' + implementation 'com.bitmovin.player:player:3.44.0+jason' //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules } From 7e801f558540ad1bd9a6494923a4429dbfbbaed4 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 18 Sep 2023 16:56:22 +0200 Subject: [PATCH 15/85] feat(casting): define public API for player API, events and remoteControlConfig for casting --- src/events.ts | 74 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/player.ts | 35 ++++++++++++++++++ src/remoteControlConfig.ts | 37 +++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 src/remoteControlConfig.ts diff --git a/src/events.ts b/src/events.ts index 4e866d1e..708f93c4 100644 --- a/src/events.ts +++ b/src/events.ts @@ -554,3 +554,77 @@ export interface VideoPlaybackQualityChangedEvent extends Event { */ oldVideoQuality: VideoQuality; } + +/** + * Emitted when casting to a cast-compatible device is available. + */ +export interface CastAvailableEvent extends Event {} + +/** + * Emitted when the playback on a cast-compatible device was paused. + */ +export interface CastPaused extends Event {} + +/** + * Emitted when the playback on a cast-compatible device has finished. + */ +export interface CastPlaybackFinished extends Event {} + +/** + * Emitted when playback on a cast-compatible device has started. + */ +export interface CastPlaying extends Event {} + +/** + * Emitted when the cast app is launched successfully. + */ +export interface CastStarted extends Event { + /** + * The name of the cast device on which the app was launched. + */ + deviceName: string | null; +} + +/** + * Emitted when casting is initiated, but the user still needs to choose which device should be used. + */ +export interface CastStart extends Event {} + +/** + * Emitted when casting to a cast-compatible device is stopped. + */ +export interface CastStopped extends Event {} + +/** + * Emitted when the time update from the currently used cast-compatible device is received. + */ +export interface CastTimeUpdated extends Event {} + +/** + * Contains information for the `CastWaitingForDevice` event. + */ +export interface CastPayload { + /** + * The current time in seconds. + */ + currentTime: number; + /** + * The name of the chosen cast device. + */ + deviceName: string; + /** + * The type of the payload (always "cast"). + */ + type: string; +} + +/** + * Emitted when a cast-compatible device has been chosen and the player is waiting for the device to get ready for + * playback. + */ +export interface CastWaitingForDevice extends Event { + /** + * The [CastPayload] object for the event + */ + castPayload: CastPayload; +} diff --git a/src/index.ts b/src/index.ts index 9cbb0829..28d9b370 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,3 +13,4 @@ export * from './styleConfig'; export * from './ui'; export * from './offline'; export * from './thumbnail'; +export * from './remoteControlConfig'; diff --git a/src/player.ts b/src/player.ts index 7b9223fe..fddfb96b 100644 --- a/src/player.ts +++ b/src/player.ts @@ -11,6 +11,7 @@ import { AdaptationConfig } from './adaptationConfig'; import { OfflineContentManager, OfflineSourceOptions } from './offline'; import { Thumbnail } from './thumbnail'; import { AnalyticsApi } from './analytics/player'; +import { RemoteControlConfig } from './remoteControlConfig'; const PlayerModule = NativeModules.PlayerModule; @@ -61,6 +62,10 @@ export interface PlayerConfig extends NativeInstanceConfig { * Configures adaptation logic. */ adaptationConfig?: AdaptationConfig; + /** + * Configures remote playback functionality. + */ + remoteControlConfig?: RemoteControlConfig; } /** @@ -491,4 +496,34 @@ export class Player extends NativeInstance { getThumbnail = async (time: number): Promise => { return PlayerModule.getThumbnail(this.nativeId, time); }; + + /** + * Whether casting to a cast-compatible remote device is available. `CastAvailableEvent` signals when + * casting becomes available. + */ + isCastAvailable = async (): Promise => { + return PlayerModule.isCastAvailable(); + }; + + /** + * Whether video is currently being casted to a remote device and not played locally. + */ + isCasting = async (): Promise => { + return PlayerModule.isCasting(this.nativeId); + }; + + /** + * Initiates casting the current video to a cast-compatible remote device. The user has to choose to which device it + * should be sent. + */ + castVideo = () => { + PlayerModule.castVideo(this.nativeId); + }; + + /** + * Stops casting the current video. Has no effect if [isCasting] is false. + */ + castStop = () => { + PlayerModule.castStop(this.nativeId); + }; } diff --git a/src/remoteControlConfig.ts b/src/remoteControlConfig.ts new file mode 100644 index 00000000..7d02064d --- /dev/null +++ b/src/remoteControlConfig.ts @@ -0,0 +1,37 @@ +/** + * Configures remote playback behavior. + */ +export interface RemoteControlConfig { + /** + * A URL to a CSS file the receiver app loads to style the receiver app. + * Default value is `null`, indicating that the default CSS of the receiver app will be used. + */ + receiverStylesheetUrl?: string | null; + /** + * A Map containing custom configuration values that are sent to the remote control receiver. + * Default value is an empty map. + */ + customReceiverConfig?: Record; + /** + * Whether casting is enabled. + * Default value is `true`. + * + * Has no effect if the BitmovinCastManager is not initialized in the app's context. + */ + isCastEnabled?: boolean; + /** + * Indicates whether cookies and credentials will be sent along manifest requests on the cast receiver + * Default value is `false`. + */ + sendManifestRequestsWithCredentials?: boolean; + /** + * Indicates whether cookies and credentials will be sent along segment requests on the cast receiver + * Default value is `false`. + */ + sendSegmentRequestsWithCredentials?: boolean; + /** + * Indicates whether cookies and credentials will be sent along DRM licence requests on the cast receiver + * Default value is `false`. + */ + sendDrmLicenseRequestsWithCredentials?: boolean; +} From a4971c13611420da3303ade795fc3709f231d73d Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 19 Sep 2023 08:00:06 +0200 Subject: [PATCH 16/85] feat(casting): add RemoteControlConfig deserializer --- .../reactnative/converter/JsonConverter.kt | 60 +++++++++++++++++++ .../extensions/ReadableMapExtension.kt | 18 ++++++ 2 files changed, 78 insertions(+) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index f200effb..504af690 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -18,6 +18,7 @@ import com.bitmovin.player.api.advertising.AdQuartile import com.bitmovin.player.api.advertising.AdSource import com.bitmovin.player.api.advertising.AdSourceType import com.bitmovin.player.api.advertising.AdvertisingConfig +import com.bitmovin.player.api.casting.RemoteControlConfig import com.bitmovin.player.api.drm.WidevineConfig import com.bitmovin.player.api.event.PlayerEvent import com.bitmovin.player.api.event.SourceEvent @@ -39,6 +40,7 @@ import com.bitmovin.player.api.ui.ScalingMode import com.bitmovin.player.api.ui.StyleConfig import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getName +import com.bitmovin.player.reactnative.extensions.getOrDefault import com.bitmovin.player.reactnative.extensions.getProperty import com.bitmovin.player.reactnative.extensions.putBoolean import com.bitmovin.player.reactnative.extensions.putDouble @@ -96,9 +98,67 @@ class JsonConverter { playerConfig.adaptationConfig = it } } + if (json.hasKey("remoteControlConfig")) { + toRemoteControlConfig(json.getMap("remoteControlConfig"))?.let { + playerConfig.remoteControlConfig = it + } + } return playerConfig } + /** + * Converts an arbitrary [ReadableMap] to a [RemoteControlConfig]. + * + * @param json JS object representing the [RemoteControlConfig]. + * @return The generated [RemoteControlConfig]. + */ + private fun toRemoteControlConfig(json: ReadableMap?): RemoteControlConfig? { + if (json == null) return null + val defaultRemoteControlConfig = RemoteControlConfig() + + val receiverStylesheetUrl = json.getOrDefault( + "receiverStylesheetUrl", + defaultRemoteControlConfig.receiverStylesheetUrl + ) + + var customReceiverConfig = defaultRemoteControlConfig.customReceiverConfig + if (json.hasKey("customReceiverConfig")) { + customReceiverConfig = json.getMap("customReceiverConfig") + ?.toHashMap() + ?.mapValues { entry -> entry.value as? String? } ?: emptyMap() + } + + val isCastEnabled = json.getOrDefault( + "isCastEnabled", + defaultRemoteControlConfig.isCastEnabled + ) + + val sendManifestRequestsWithCredentials = json.getOrDefault( + "sendManifestRequestsWithCredentials", + defaultRemoteControlConfig.sendManifestRequestsWithCredentials + ) + + val sendSegmentRequestsWithCredentials = json.getOrDefault( + "sendSegmentRequestsWithCredentials", + defaultRemoteControlConfig.sendSegmentRequestsWithCredentials + ) + + val sendDrmLicenseRequestsWithCredentials = json.getOrDefault( + "sendDrmLicenseRequestsWithCredentials", + defaultRemoteControlConfig.sendDrmLicenseRequestsWithCredentials + ) + + + return RemoteControlConfig( + receiverStylesheetUrl = receiverStylesheetUrl, + customReceiverConfig = customReceiverConfig, + isCastEnabled = isCastEnabled, + sendManifestRequestsWithCredentials = sendManifestRequestsWithCredentials, + sendSegmentRequestsWithCredentials = sendSegmentRequestsWithCredentials, + sendDrmLicenseRequestsWithCredentials = sendDrmLicenseRequestsWithCredentials, + ) + } + /** * Converts an arbitrary `json` to `SourceOptions`. * @param json JS object representing the `SourceOptions`. diff --git a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt index 7c482229..8560eb3b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableMapExtension.kt @@ -5,3 +5,21 @@ import com.facebook.react.bridge.ReadableMap fun ReadableMap.getBooleanOrNull( key: String ): Boolean? = takeIf { hasKey(key) }?.getBoolean(key) + +/** + * Reads the [Boolean] value from the given [ReadableMap] if the [key] is present. + * Returns the [default] value otherwise. + */ +fun ReadableMap.getOrDefault( + key: String, + default: Boolean, +) = if (hasKey(key)) getBoolean(key) else default + +/** + * Reads the [String] value from the given [ReadableMap] if the [key] is present. + * Returns the [default] value otherwise. + */ +fun ReadableMap.getOrDefault( + key: String, + default: String?, +) = if (hasKey(key)) getString(key) else default From 3c6f38d535f884845d431a864750cd76a4c8e909 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 19 Sep 2023 08:13:37 +0200 Subject: [PATCH 17/85] refactor: use when in event serialization --- .../reactnative/converter/JsonConverter.kt | 267 ++++++++++-------- 1 file changed, 154 insertions(+), 113 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index c38f9ef4..2cc0cb96 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -396,39 +396,54 @@ class JsonConverter { val json = Arguments.createMap() json.putString("name", event.getName()) json.putDouble("timestamp", event.timestamp.toDouble()) - if (event is SourceEvent.Load) { - json.putMap("source", fromSource(event.source)) - } - if (event is SourceEvent.Loaded) { - json.putMap("source", fromSource(event.source)) - } - if (event is SourceEvent.Error) { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } - if (event is SourceEvent.Warning) { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } - if (event is SourceEvent.AudioTrackAdded) { - json.putMap("audioTrack", fromAudioTrack(event.audioTrack)) - } - if (event is SourceEvent.AudioTrackChanged) { - json.putMap("oldAudioTrack", fromAudioTrack(event.oldAudioTrack)) - json.putMap("newAudioTrack", fromAudioTrack(event.newAudioTrack)) - } - if (event is SourceEvent.AudioTrackRemoved) { - json.putMap("audioTrack", fromAudioTrack(event.audioTrack)) - } - if (event is SourceEvent.SubtitleTrackAdded) { - json.putMap("subtitleTrack", fromSubtitleTrack(event.subtitleTrack)) - } - if (event is SourceEvent.SubtitleTrackRemoved) { - json.putMap("subtitleTrack", fromSubtitleTrack(event.subtitleTrack)) - } - if (event is SourceEvent.SubtitleTrackChanged) { - json.putMap("oldSubtitleTrack", fromSubtitleTrack(event.oldSubtitleTrack)) - json.putMap("newSubtitleTrack", fromSubtitleTrack(event.newSubtitleTrack)) + when (event) { + is SourceEvent.Load -> { + json.putMap("source", fromSource(event.source)) + } + + is SourceEvent.Loaded -> { + json.putMap("source", fromSource(event.source)) + } + + is SourceEvent.Error -> { + json.putInt("code", event.code.value) + json.putString("message", event.message) + } + + is SourceEvent.Warning -> { + json.putInt("code", event.code.value) + json.putString("message", event.message) + } + + is SourceEvent.AudioTrackAdded -> { + json.putMap("audioTrack", fromAudioTrack(event.audioTrack)) + } + + is SourceEvent.AudioTrackChanged -> { + json.putMap("oldAudioTrack", fromAudioTrack(event.oldAudioTrack)) + json.putMap("newAudioTrack", fromAudioTrack(event.newAudioTrack)) + } + + is SourceEvent.AudioTrackRemoved -> { + json.putMap("audioTrack", fromAudioTrack(event.audioTrack)) + } + + is SourceEvent.SubtitleTrackAdded -> { + json.putMap("subtitleTrack", fromSubtitleTrack(event.subtitleTrack)) + } + + is SourceEvent.SubtitleTrackRemoved -> { + json.putMap("subtitleTrack", fromSubtitleTrack(event.subtitleTrack)) + } + + is SourceEvent.SubtitleTrackChanged -> { + json.putMap("oldSubtitleTrack", fromSubtitleTrack(event.oldSubtitleTrack)) + json.putMap("newSubtitleTrack", fromSubtitleTrack(event.newSubtitleTrack)) + } + + else -> { + // Event is not supported yet + } } return json } @@ -443,86 +458,112 @@ class JsonConverter { val json = Arguments.createMap() json.putString("name", event.getName()) json.putDouble("timestamp", event.timestamp.toDouble()) - if (event is PlayerEvent.Error) { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } - if (event is PlayerEvent.Warning) { - json.putInt("code", event.code.value) - json.putString("message", event.message) - } - if (event is PlayerEvent.Play) { - json.putDouble("time", event.time) - } - if (event is PlayerEvent.Playing) { - json.putDouble("time", event.time) - } - if (event is PlayerEvent.Paused) { - json.putDouble("time", event.time) - } - if (event is PlayerEvent.TimeChanged) { - json.putDouble("currentTime", event.time) - } - if (event is PlayerEvent.Seek) { - json.putMap("from", fromSeekPosition(event.from)) - json.putMap("to", fromSeekPosition(event.to)) - } - if (event is PlayerEvent.TimeShift) { - json.putDouble("position", event.position) - json.putDouble("targetPosition", event.target) - } - if (event is PlayerEvent.PictureInPictureAvailabilityChanged) { - json.putBoolean("isPictureInPictureAvailable", event.isPictureInPictureAvailable) - } - if (event is PlayerEvent.AdBreakFinished) { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - } - if (event is PlayerEvent.AdBreakStarted) { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - } - if (event is PlayerEvent.AdClicked) { - json.putString("clickThroughUrl", event.clickThroughUrl) - } - if (event is PlayerEvent.AdError) { - json.putInt("code", event.code) - json.putString("message", event.message) - json.putMap("adConfig", fromAdConfig(event.adConfig)) - json.putMap("adItem", fromAdItem(event.adItem)) - } - if (event is PlayerEvent.AdFinished) { - json.putMap("ad", fromAd(event.ad)) - } - if (event is PlayerEvent.AdManifestLoad) { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - json.putMap("adConfig", fromAdConfig(event.adConfig)) - } - if (event is PlayerEvent.AdManifestLoaded) { - json.putMap("adBreak", fromAdBreak(event.adBreak)) - json.putMap("adConfig", fromAdConfig(event.adConfig)) - json.putDouble("downloadTime", event.downloadTime.toDouble()) - } - if (event is PlayerEvent.AdQuartile) { - json.putString("quartile", fromAdQuartile(event.quartile)) - } - if (event is PlayerEvent.AdScheduled) { - json.putInt("numberOfAds", event.numberOfAds) - } - if (event is PlayerEvent.AdSkipped) { - json.putMap("ad", fromAd(event.ad)) - } - if (event is PlayerEvent.AdStarted) { - json.putMap("ad", fromAd(event.ad)) - json.putString("clickThroughUrl", event.clickThroughUrl) - json.putString("clientType", fromAdSourceType(event.clientType)) - json.putDouble("duration", event.duration) - json.putInt("indexInQueue", event.indexInQueue) - json.putString("position", event.position) - json.putDouble("skipOffset", event.skipOffset) - json.putDouble("timeOffset", event.timeOffset) - } - if (event is PlayerEvent.VideoPlaybackQualityChanged) { - json.putMap("newVideoQuality", fromVideoQuality(event.newVideoQuality)) - json.putMap("oldVideoQuality", fromVideoQuality(event.oldVideoQuality)) + when (event) { + is PlayerEvent.Error -> { + json.putInt("code", event.code.value) + json.putString("message", event.message) + } + + is PlayerEvent.Warning -> { + json.putInt("code", event.code.value) + json.putString("message", event.message) + } + + is PlayerEvent.Play -> { + json.putDouble("time", event.time) + } + + is PlayerEvent.Playing -> { + json.putDouble("time", event.time) + } + + is PlayerEvent.Paused -> { + json.putDouble("time", event.time) + } + + is PlayerEvent.TimeChanged -> { + json.putDouble("currentTime", event.time) + } + + is PlayerEvent.Seek -> { + json.putMap("from", fromSeekPosition(event.from)) + json.putMap("to", fromSeekPosition(event.to)) + } + + is PlayerEvent.TimeShift -> { + json.putDouble("position", event.position) + json.putDouble("targetPosition", event.target) + } + + is PlayerEvent.PictureInPictureAvailabilityChanged -> { + json.putBoolean("isPictureInPictureAvailable", event.isPictureInPictureAvailable) + } + + is PlayerEvent.AdBreakFinished -> { + json.putMap("adBreak", fromAdBreak(event.adBreak)) + } + + is PlayerEvent.AdBreakStarted -> { + json.putMap("adBreak", fromAdBreak(event.adBreak)) + } + + is PlayerEvent.AdClicked -> { + json.putString("clickThroughUrl", event.clickThroughUrl) + } + + is PlayerEvent.AdError -> { + json.putInt("code", event.code) + json.putString("message", event.message) + json.putMap("adConfig", fromAdConfig(event.adConfig)) + json.putMap("adItem", fromAdItem(event.adItem)) + } + + is PlayerEvent.AdFinished -> { + json.putMap("ad", fromAd(event.ad)) + } + + is PlayerEvent.AdManifestLoad -> { + json.putMap("adBreak", fromAdBreak(event.adBreak)) + json.putMap("adConfig", fromAdConfig(event.adConfig)) + } + + is PlayerEvent.AdManifestLoaded -> { + json.putMap("adBreak", fromAdBreak(event.adBreak)) + json.putMap("adConfig", fromAdConfig(event.adConfig)) + json.putDouble("downloadTime", event.downloadTime.toDouble()) + } + + is PlayerEvent.AdQuartile -> { + json.putString("quartile", fromAdQuartile(event.quartile)) + } + + is PlayerEvent.AdScheduled -> { + json.putInt("numberOfAds", event.numberOfAds) + } + + is PlayerEvent.AdSkipped -> { + json.putMap("ad", fromAd(event.ad)) + } + + is PlayerEvent.AdStarted -> { + json.putMap("ad", fromAd(event.ad)) + json.putString("clickThroughUrl", event.clickThroughUrl) + json.putString("clientType", fromAdSourceType(event.clientType)) + json.putDouble("duration", event.duration) + json.putInt("indexInQueue", event.indexInQueue) + json.putString("position", event.position) + json.putDouble("skipOffset", event.skipOffset) + json.putDouble("timeOffset", event.timeOffset) + } + + is PlayerEvent.VideoPlaybackQualityChanged -> { + json.putMap("newVideoQuality", fromVideoQuality(event.newVideoQuality)) + json.putMap("oldVideoQuality", fromVideoQuality(event.oldVideoQuality)) + } + + else -> { + // Event is not supported yet + } } return json } From 8924c069f5d207414bb30086b4ab6f98e90ffa25 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 19 Sep 2023 08:31:32 +0200 Subject: [PATCH 18/85] docs: improve documentation --- .../bitmovin/player/reactnative/converter/JsonConverter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index 2cc0cb96..0d7de6aa 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -442,7 +442,7 @@ class JsonConverter { } else -> { - // Event is not supported yet + // Event is not supported yet or does not have any additional data } } return json @@ -562,7 +562,7 @@ class JsonConverter { } else -> { - // Event is not supported yet + // Event is not supported yet or does not have any additional data } } return json From 089a40d971c23cb89387f8f55702caa2416823ad Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 09:19:25 +0200 Subject: [PATCH 19/85] feat(casting): add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c000e116..7565d3f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - `DefaultMetadata` for configuration of the bundled analytics collector - `Player.analytics` to access the `AnalyticsApi` and interact with the bundled analytics collector - `SourceConfig.analyticsSourceMetadata` for extended configuration of the bundled analytics collector +- Google Cast SDK support for Android and iOS ### Changed From 5b8405b94975699f3184ca7d06de4818ad6bb8da Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 09:22:06 +0200 Subject: [PATCH 20/85] feat(casting): add platform mentions to player API --- src/player.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/player.ts b/src/player.ts index fddfb96b..afdf69b0 100644 --- a/src/player.ts +++ b/src/player.ts @@ -500,6 +500,8 @@ export class Player extends NativeInstance { /** * Whether casting to a cast-compatible remote device is available. `CastAvailableEvent` signals when * casting becomes available. + * + * @platform iOS, Android */ isCastAvailable = async (): Promise => { return PlayerModule.isCastAvailable(); @@ -507,6 +509,8 @@ export class Player extends NativeInstance { /** * Whether video is currently being casted to a remote device and not played locally. + * + * @platform iOS, Android */ isCasting = async (): Promise => { return PlayerModule.isCasting(this.nativeId); @@ -515,6 +519,8 @@ export class Player extends NativeInstance { /** * Initiates casting the current video to a cast-compatible remote device. The user has to choose to which device it * should be sent. + * + * @platform iOS, Android */ castVideo = () => { PlayerModule.castVideo(this.nativeId); @@ -522,6 +528,8 @@ export class Player extends NativeInstance { /** * Stops casting the current video. Has no effect if [isCasting] is false. + * + * @platform iOS, Android */ castStop = () => { PlayerModule.castStop(this.nativeId); From 4b56c08ccdd6fb25ec7fd1940677d85fbb32f251 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 09:27:52 +0200 Subject: [PATCH 21/85] feat(casting): add documentation about regular Android platback events during casting --- src/events.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/events.ts b/src/events.ts index 708f93c4..f20fbee9 100644 --- a/src/events.ts +++ b/src/events.ts @@ -562,16 +562,22 @@ export interface CastAvailableEvent extends Event {} /** * Emitted when the playback on a cast-compatible device was paused. + * + * On Android `PausedEvent` is also emitted while casting. */ export interface CastPaused extends Event {} /** * Emitted when the playback on a cast-compatible device has finished. + * + * On Android `PlaybackFinishedEvent` is also emitted while casting. */ export interface CastPlaybackFinished extends Event {} /** * Emitted when playback on a cast-compatible device has started. + * + * On Android `PlayingEvent` is also emitted while casting. */ export interface CastPlaying extends Event {} From 82b056b0d7a9d9c391dd5fa2c740755434288975 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 09:29:31 +0200 Subject: [PATCH 22/85] feat(casting): fix optionality in CastPayload --- src/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events.ts b/src/events.ts index f20fbee9..75211868 100644 --- a/src/events.ts +++ b/src/events.ts @@ -617,7 +617,7 @@ export interface CastPayload { /** * The name of the chosen cast device. */ - deviceName: string; + deviceName: string | null; /** * The type of the payload (always "cast"). */ From 56ee8d3617c7f091ee7eb6eb69a3b385ccc78dbf Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 10:35:07 +0200 Subject: [PATCH 23/85] feat(casting): fix casting event names --- src/events.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/events.ts b/src/events.ts index 75211868..cb7f1a6a 100644 --- a/src/events.ts +++ b/src/events.ts @@ -565,26 +565,26 @@ export interface CastAvailableEvent extends Event {} * * On Android `PausedEvent` is also emitted while casting. */ -export interface CastPaused extends Event {} +export interface CastPausedEvent extends Event {} /** * Emitted when the playback on a cast-compatible device has finished. * * On Android `PlaybackFinishedEvent` is also emitted while casting. */ -export interface CastPlaybackFinished extends Event {} +export interface CastPlaybackFinishedEvent extends Event {} /** * Emitted when playback on a cast-compatible device has started. * * On Android `PlayingEvent` is also emitted while casting. */ -export interface CastPlaying extends Event {} +export interface CastPlayingEvent extends Event {} /** * Emitted when the cast app is launched successfully. */ -export interface CastStarted extends Event { +export interface CastStartedEvent extends Event { /** * The name of the cast device on which the app was launched. */ @@ -594,20 +594,20 @@ export interface CastStarted extends Event { /** * Emitted when casting is initiated, but the user still needs to choose which device should be used. */ -export interface CastStart extends Event {} +export interface CastStartEvent extends Event {} /** * Emitted when casting to a cast-compatible device is stopped. */ -export interface CastStopped extends Event {} +export interface CastStoppedEvent extends Event {} /** * Emitted when the time update from the currently used cast-compatible device is received. */ -export interface CastTimeUpdated extends Event {} +export interface CastTimeUpdatedEvent extends Event {} /** - * Contains information for the `CastWaitingForDevice` event. + * Contains information for the `CastWaitingForDeviceEvent`. */ export interface CastPayload { /** @@ -628,7 +628,7 @@ export interface CastPayload { * Emitted when a cast-compatible device has been chosen and the player is waiting for the device to get ready for * playback. */ -export interface CastWaitingForDevice extends Event { +export interface CastWaitingForDeviceEvent extends Event { /** * The [CastPayload] object for the event */ From 47a80714d6b7993bd4a91ebd4176049aa8177d24 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 10:36:10 +0200 Subject: [PATCH 24/85] feat(casting): fix API documentation --- src/player.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index afdf69b0..effa17f2 100644 --- a/src/player.ts +++ b/src/player.ts @@ -527,7 +527,7 @@ export class Player extends NativeInstance { }; /** - * Stops casting the current video. Has no effect if [isCasting] is false. + * Stops casting the current video. Has no effect if `isCasting` is false. * * @platform iOS, Android */ From 1d77a803f97eaf9c98de04870b64f55834d53456 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 10:38:30 +0200 Subject: [PATCH 25/85] feat(casting): improve API documentation --- src/remoteControlConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remoteControlConfig.ts b/src/remoteControlConfig.ts index 7d02064d..74d738b9 100644 --- a/src/remoteControlConfig.ts +++ b/src/remoteControlConfig.ts @@ -16,7 +16,7 @@ export interface RemoteControlConfig { * Whether casting is enabled. * Default value is `true`. * - * Has no effect if the BitmovinCastManager is not initialized in the app's context. + * Has no effect if the `BitmovinCastManager` is not initialized before the `Player` is created with this configuration. */ isCastEnabled?: boolean; /** From 5562685e0a1d96e55a1b4b1ce11219de2086ca21 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 19 Sep 2023 10:44:06 +0200 Subject: [PATCH 26/85] feat(casting): add player module android casting API --- .../player/reactnative/PlayerModule.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt index c403745b..b144b34b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -4,6 +4,7 @@ import android.util.Log import com.bitmovin.analytics.api.DefaultMetadata import com.bitmovin.player.api.Player import com.bitmovin.player.api.analytics.create +import com.bitmovin.player.api.event.PlayerEvent import com.bitmovin.player.reactnative.converter.JsonConverter import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule @@ -496,6 +497,48 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB } } + /** + * Initiates casting the current video to a cast-compatible remote device. The user has to choose to which device it + * should be sent. + */ + @ReactMethod + fun castVideo(nativeId: NativeId) { + uiManager()?.addUIBlock { + players[nativeId]?.castVideo() + } + } + + /** + * Stops casting the current video. Has no effect if [isCasting] is false. + */ + @ReactMethod + fun castStop(nativeId: NativeId) { + uiManager()?.addUIBlock { + players[nativeId]?.castStop() + } + } + + /** + * Whether casting to a cast-compatible remote device is available. [PlayerEvent.CastAvailable] signals when + * casting becomes available. + */ + @ReactMethod + fun isCastAvailable(nativeId: NativeId, promise: Promise) { + uiManager()?.addUIBlock { + promise.resolve(players[nativeId]?.isCastAvailable) + } + } + + /** + * Whether video is currently being casted to a remote device and not played locally. + */ + @ReactMethod + fun isCasting(nativeId: NativeId, promise: Promise) { + uiManager()?.addUIBlock { + promise.resolve(players[nativeId]?.isCasting) + } + } + /** * Helper function that returns the initialized `UIManager` instance. */ From c347115a74676cbab250cf76e595bdac05734f0e Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 10:48:13 +0200 Subject: [PATCH 27/85] feat(casting): define BitmovinCastManager public API --- src/bitmovinCastManager.ts | 84 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + 2 files changed, 85 insertions(+) create mode 100644 src/bitmovinCastManager.ts diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts new file mode 100644 index 00000000..703b23bd --- /dev/null +++ b/src/bitmovinCastManager.ts @@ -0,0 +1,84 @@ +import { NativeModules, Platform } from 'react-native'; + +const BitmovinCastManagerModule = NativeModules.BitmovinCastManagerModule; + +/** + * The options to be used for initializing `BitmovinCastManager` + * @platform Android, iOS + */ +export interface BitmovinCastManagerOptions { + /** + * ID of receiver application. + * Using `nil` value will result in using the default application ID + */ + applicationId?: string | null; + /** + * A custom message namespace to be used for communication between sender and receiver. + * Using `nil` value will result in using the default message namespace + */ + messageNamespace?: string | null; +} + +/** + * Singleton providing access to GoogleCast related features. + * The `BitmovinCastManager` needs to be initialized by calling `BitmovinCastManager.initialize` + * before `Player` creation to enable casting features. + * + * @platform Android, iOS + */ +export const BitmovinCastManager = { + /** + * Returns whether the `BitmovinCastManager` is initialized. + * @returns A promise that resolves with a boolean indicating whether the `BitmovinCastManager` is initialized + */ + isInitialized: async (): Promise => { + if (Platform.OS === 'ios' && Platform.isTV) { + return false; + } + return BitmovinCastManagerModule.isInitialized(); + }, + + /** + * Initialize `BitmovinCastManager` based on the provided `BitmovinCastManagerOptions`. + * This method needs to be called before `Player` creation to enable casting features. + * If no options are provided, the default options will be used. + * + * @param options The options to be used for initializing `BitmovinCastManager` + * @returns A promise that resolves when the `BitmovinCastManager` was initialized successfully + */ + initialize: async ( + options: BitmovinCastManagerOptions | null = null + ): Promise => { + if (Platform.OS === 'ios' && Platform.isTV) { + return Promise.resolve(); + } + return BitmovinCastManagerModule.initialize(options); + }, + + /** + * Sends the given message to the cast receiver. + * + * @param message The message to be sent + * @param messageNamespace The message namespace to be used, in case of null the default message namespace will be used + * @returns A promise that resolves when the message was sent successfully + */ + sendMessage: (message: String, messageNamespace: String | null = null) => { + if (Platform.OS === 'ios' && Platform.isTV) { + return Promise.resolve(); + } + return BitmovinCastManagerModule.sendMessage(message, messageNamespace); + }, + + /** + * Sends the given metadata wrapped in a metadata message object to the cast receiver on the configured message namespace. + * The provided metadata must be JSON serializable. + * @param metadata The metadata to be sent + * @returns A promise that resolves when the metadata was sent successfully + */ + sendMetadata: (metadata: Record) => { + if (Platform.OS === 'ios' && Platform.isTV) { + return Promise.resolve(); + } + return BitmovinCastManagerModule.sendMetadata(metadata); + }, +}; diff --git a/src/index.ts b/src/index.ts index 28d9b370..540f5089 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,3 +14,4 @@ export * from './ui'; export * from './offline'; export * from './thumbnail'; export * from './remoteControlConfig'; +export * from './bitmovinCastManager'; From a313fd97465110764a9dd2bd04d9b73001af8b47 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Tue, 19 Sep 2023 10:48:38 +0200 Subject: [PATCH 28/85] feat(casting): add android cast event serialization --- .../reactnative/converter/JsonConverter.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index e6147c43..62e63446 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -21,6 +21,7 @@ import com.bitmovin.player.api.advertising.AdvertisingConfig import com.bitmovin.player.api.drm.WidevineConfig import com.bitmovin.player.api.event.PlayerEvent import com.bitmovin.player.api.event.SourceEvent +import com.bitmovin.player.api.event.data.CastPayload import com.bitmovin.player.api.event.data.SeekPosition import com.bitmovin.player.api.media.AdaptationConfig import com.bitmovin.player.api.media.audio.AudioTrack @@ -564,6 +565,14 @@ class JsonConverter { json.putMap("oldVideoQuality", fromVideoQuality(event.oldVideoQuality)) } + is PlayerEvent.CastWaitingForDevice -> { + json.putMap("castPayload", fromCastPayload(event.castPayload)) + } + + is PlayerEvent.CastStarted -> { + json.putString("deviceName", event.deviceName) + } + else -> { // Event is not supported yet or does not have any additional data } @@ -1008,3 +1017,12 @@ class JsonConverter { } } } + +/** + * Converts a [CastPayload] object into its JS representation. + */ +private fun fromCastPayload(castPayload: CastPayload) = Arguments.createMap().apply { + putDouble("currentTime", castPayload.currentTime) + putString("deviceName", castPayload.deviceName) + putString("type", castPayload.type) +} From 3ee1aef924ddb8c725715f1e39122986bb60c1c3 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 11:20:50 +0200 Subject: [PATCH 29/85] feat(casting): fix API documentation --- src/bitmovinCastManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts index 703b23bd..ef4e22ae 100644 --- a/src/bitmovinCastManager.ts +++ b/src/bitmovinCastManager.ts @@ -9,12 +9,12 @@ const BitmovinCastManagerModule = NativeModules.BitmovinCastManagerModule; export interface BitmovinCastManagerOptions { /** * ID of receiver application. - * Using `nil` value will result in using the default application ID + * Using `null` value will result in using the default application ID */ applicationId?: string | null; /** * A custom message namespace to be used for communication between sender and receiver. - * Using `nil` value will result in using the default message namespace + * Using `null` value will result in using the default message namespace */ messageNamespace?: string | null; } From bcbaf4d057425b0c3171ba1954015b81394fc21a Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 11:24:40 +0200 Subject: [PATCH 30/85] feat(casting): remove sendMetadata from BitmovinCastManager --- src/bitmovinCastManager.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts index ef4e22ae..728bd294 100644 --- a/src/bitmovinCastManager.ts +++ b/src/bitmovinCastManager.ts @@ -68,17 +68,4 @@ export const BitmovinCastManager = { } return BitmovinCastManagerModule.sendMessage(message, messageNamespace); }, - - /** - * Sends the given metadata wrapped in a metadata message object to the cast receiver on the configured message namespace. - * The provided metadata must be JSON serializable. - * @param metadata The metadata to be sent - * @returns A promise that resolves when the metadata was sent successfully - */ - sendMetadata: (metadata: Record) => { - if (Platform.OS === 'ios' && Platform.isTV) { - return Promise.resolve(); - } - return BitmovinCastManagerModule.sendMetadata(metadata); - }, }; From 21ac37ab5b35772f12b61399797fb24d52b4d3b4 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 11:55:12 +0200 Subject: [PATCH 31/85] feat(casting): implement casting APIs on Player for iOS --- ios/PlayerModule.m | 10 ++++++++ ios/PlayerModule.swift | 55 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/ios/PlayerModule.m b/ios/PlayerModule.m index 131923df..575ee1d8 100644 --- a/ios/PlayerModule.m +++ b/ios/PlayerModule.m @@ -98,5 +98,15 @@ @interface RCT_EXTERN_REMAP_MODULE(PlayerModule, PlayerModule, NSObject) time:(nonnull NSNumber *)time resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD( + isCastAvailable:(NSString *)nativeId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD( + isCasting:(NSString *)nativeId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(castVideo:(NSString *)nativeId) +RCT_EXTERN_METHOD(castStop:(NSString *)nativeId) @end diff --git a/ios/PlayerModule.swift b/ios/PlayerModule.swift index 895f7afd..c93eac48 100644 --- a/ios/PlayerModule.swift +++ b/ios/PlayerModule.swift @@ -607,4 +607,59 @@ class PlayerModule: NSObject, RCTBridgeModule { resolve(RCTConvert.toJson(thumbnail: self?.players[nativeId]?.thumbnail(forTime: time.doubleValue))) } } + + /** + Returns `true` if casting to another device (such as a ChromeCast) is available, otherwise false. + - Parameter nativeId: Target player id. + - Parameter resolver: JS promise resolver. + - Parameter rejecter: JS promise rejecter. + */ + @objc(isCastAvailable:resolver:rejecter:) + func isCastAvailable( + _ nativeId: NativeId, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + bridge.uiManager.addUIBlock { [weak self] _, _ in + resolve(self?.players[nativeId]?.isCastAvailable) + } + } + + /** + Returns `true` if the video is currently casted to a device and not played locally, or `false` if the video is played locally. + - Parameter nativeId: Target player id. + - Parameter resolver: JS promise resolver. + - Parameter rejecter: JS promise rejecter. + */ + @objc(isCasting:resolver:rejecter:) + func isCasting( + _ nativeId: NativeId, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + bridge.uiManager.addUIBlock { [weak self] _, _ in + resolve(self?.players[nativeId]?.isCasting) + } + } + + /** + Initiates casting the current video to a cast-compatible device. The user has to choose to which device it should be sent. + */ + @objc(castVideo:) + func castVideo(_ nativeId: NativeId) { + bridge.uiManager.addUIBlock { [weak self] _, _ in + self?.players[nativeId]?.castVideo() + } + } + + /** + Stops casting the current video if it is casting at the moment (i.e. `isCasting` returns `true`). + Has no effect if `isCasting` returns `false`. + */ + @objc(castStop:) + func castStop(_ nativeId: NativeId) { + bridge.uiManager.addUIBlock { [weak self] _, _ in + self?.players[nativeId]?.castStop() + } + } } From 861c5c150d99f900a98917a2eca201d64c1772a3 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 12:11:37 +0200 Subject: [PATCH 32/85] feat(casting): add API for listening to casting events on the RN side --- src/components/PlayerView/events.ts | 18 ++++++++++++++++++ src/components/PlayerView/index.tsx | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/src/components/PlayerView/events.ts b/src/components/PlayerView/events.ts index 2ebcb930..d8024da5 100644 --- a/src/components/PlayerView/events.ts +++ b/src/components/PlayerView/events.ts @@ -11,6 +11,15 @@ import { AdScheduledEvent, AdSkippedEvent, AdStartedEvent, + CastAvailableEvent, + CastPausedEvent, + CastPlaybackFinishedEvent, + CastPlayingEvent, + CastStartedEvent, + CastStartEvent, + CastStoppedEvent, + CastTimeUpdatedEvent, + CastWaitingForDeviceEvent, DestroyEvent, Event, FullscreenEnabledEvent, @@ -69,6 +78,15 @@ interface EventProps { onAdScheduled: AdScheduledEvent; onAdSkipped: AdSkippedEvent; onAdStarted: AdStartedEvent; + onCastAvailable: CastAvailableEvent; + onCastPaused: CastPausedEvent; + onCastPlaybackFinished: CastPlaybackFinishedEvent; + onCastPlaying: CastPlayingEvent; + onCastStarted: CastStartedEvent; + onCastStart: CastStartEvent; + onCastStopped: CastStoppedEvent; + onCastTimeUpdated: CastTimeUpdatedEvent; + onCastWaitingForDevice: CastWaitingForDeviceEvent; onDestroy: DestroyEvent; onEvent: Event; onFullscreenEnabled: FullscreenEnabledEvent; diff --git a/src/components/PlayerView/index.tsx b/src/components/PlayerView/index.tsx index 5fa509e3..f6519daf 100644 --- a/src/components/PlayerView/index.tsx +++ b/src/components/PlayerView/index.tsx @@ -185,6 +185,15 @@ export function PlayerView({ onAdScheduled={proxy(props.onAdScheduled)} onAdSkipped={proxy(props.onAdSkipped)} onAdStarted={proxy(props.onAdStarted)} + onCastAvailable={proxy(props.onCastAvailable)} + onCastPaused={proxy(props.onCastPaused)} + onCastPlaybackFinished={proxy(props.onCastPlaybackFinished)} + onCastPlaying={proxy(props.onCastPlaying)} + onCastStarted={proxy(props.onCastStarted)} + onCastStart={proxy(props.onCastStart)} + onCastStopped={proxy(props.onCastStopped)} + onCastTimeUpdated={proxy(props.onCastTimeUpdated)} + onCastWaitingForDevice={proxy(props.onCastWaitingForDevice)} onDestroy={proxy(props.onDestroy)} onEvent={proxy(props.onEvent)} onFullscreenEnabled={proxy(props.onFullscreenEnabled)} From 34b564816714eb716a5ca62258782cb9984ef96b Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 13:10:08 +0200 Subject: [PATCH 33/85] feat(casting): implement RemoteControlConfig parsing --- ios/RCTConvert+BitmovinPlayer.swift | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index 8201ddee..d12fef75 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -31,6 +31,9 @@ extension RCTConvert { if let adaptationConfig = RCTConvert.adaptationConfig(json["adaptationConfig"]) { playerConfig.adaptationConfig = adaptationConfig } + if let remoteControlConfig = RCTConvert.remoteControlConfig(json["remoteControlConfig"]) { + playerConfig.remoteControlConfig = remoteControlConfig + } return playerConfig } @@ -919,4 +922,35 @@ extension RCTConvert { "height": thumbnail.height, ] } + + /** + Utility method to instantiate a `RemoteControlConfig` from a JS object. + - Parameter json: JS object + - Returns: The produced `RemoteControlConfig` object + */ + static func remoteControlConfig(_ json: Any?) -> RemoteControlConfig? { + guard let json = json as? [String: Any?] else { + return nil + } + let remoteControlConfig = RemoteControlConfig() + if let receiverStylesheetUrl = RCTConvert.nsurl(json["receiverStylesheetUrl"]) { + remoteControlConfig.receiverStylesheetUrl = receiverStylesheetUrl + } + if let customReceiverConfig = json["customReceiverConfig"] as? [String: String] { + remoteControlConfig.customReceiverConfig = customReceiverConfig + } + if let isCastEnabled = json["isCastEnabled"] as? Bool { + remoteControlConfig.isCastEnabled = isCastEnabled + } + if let sendManifestRequestsWithCredentials = json["sendManifestRequestsWithCredentials"] as? Bool { + remoteControlConfig.sendManifestRequestsWithCredentials = sendManifestRequestsWithCredentials + } + if let sendSegmentRequestsWithCredentials = json["sendSegmentRequestsWithCredentials"] as? Bool { + remoteControlConfig.sendSegmentRequestsWithCredentials = sendSegmentRequestsWithCredentials + } + if let sendDrmLicenseRequestsWithCredentials = json["sendDrmLicenseRequestsWithCredentials"] as? Bool { + remoteControlConfig.sendDrmLicenseRequestsWithCredentials = sendDrmLicenseRequestsWithCredentials + } + return remoteControlConfig + } } From 379252d7287a250e9128991a9352aaa072a89de9 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 13:25:32 +0200 Subject: [PATCH 34/85] feat(casting): change customReceiverConfig to String to String map in RemoteControlConfig --- .../com/bitmovin/player/reactnative/converter/JsonConverter.kt | 3 ++- src/remoteControlConfig.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index dc09855c..577c7a1f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -126,7 +126,8 @@ class JsonConverter { if (json.hasKey("customReceiverConfig")) { customReceiverConfig = json.getMap("customReceiverConfig") ?.toHashMap() - ?.mapValues { entry -> entry.value as? String? } ?: emptyMap() + ?.filterValues { entry -> entry is String } + ?.mapValues { entry -> entry.value as String } ?: emptyMap() } val isCastEnabled = json.getOrDefault( diff --git a/src/remoteControlConfig.ts b/src/remoteControlConfig.ts index 74d738b9..4e337f45 100644 --- a/src/remoteControlConfig.ts +++ b/src/remoteControlConfig.ts @@ -11,7 +11,7 @@ export interface RemoteControlConfig { * A Map containing custom configuration values that are sent to the remote control receiver. * Default value is an empty map. */ - customReceiverConfig?: Record; + customReceiverConfig?: Record; /** * Whether casting is enabled. * Default value is `true`. From 85fafcfa8b93124ad48c0a832a8938764210a5ad Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 14:24:32 +0200 Subject: [PATCH 35/85] feat(casting): introduce BitmovinCastManagerModule for iOS --- ios/BitmovinCastManagerModule.m | 14 ++++++ ios/BitmovinCastManagerModule.swift | 75 +++++++++++++++++++++++++++++ ios/RCTConvert+BitmovinPlayer.swift | 11 +++++ src/bitmovinCastManager.ts | 2 +- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 ios/BitmovinCastManagerModule.m create mode 100644 ios/BitmovinCastManagerModule.swift diff --git a/ios/BitmovinCastManagerModule.m b/ios/BitmovinCastManagerModule.m new file mode 100644 index 00000000..1bcc488d --- /dev/null +++ b/ios/BitmovinCastManagerModule.m @@ -0,0 +1,14 @@ +#import + +@interface RCT_EXTERN_REMAP_MODULE(BitmovinCastManagerModule, BitmovinCastManagerModule, NSObject) + +RCT_EXTERN_METHOD( + isInitialized:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD( + initializeCasting:(nullable id)config + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(sendMessage:(NSString *)message messageNamespace:(nullable NSString *)messageNamespace) + +@end diff --git a/ios/BitmovinCastManagerModule.swift b/ios/BitmovinCastManagerModule.swift new file mode 100644 index 00000000..235078dc --- /dev/null +++ b/ios/BitmovinCastManagerModule.swift @@ -0,0 +1,75 @@ +import BitmovinPlayer + +@objc(BitmovinCastManagerModule) +class BitmovinCastManagerModule: NSObject, RCTBridgeModule { + /// React bridge reference. + @objc var bridge: RCTBridge! + + /// JS module name. + static func moduleName() -> String! { + "BitmovinCastManagerModule" + } + + /// Module requires main thread initialization. + static func requiresMainQueueSetup() -> Bool { + true + } + + /// Since most `BitmovinCastManagerModule` operations are UI related and need to be executed on the main thread, they are scheduled with `UIManager.addBlock`. + var methodQueue: DispatchQueue! { + bridge.uiManager.methodQueue + } + + /** + Initializes the BitmovinCastManager with the given options or with no options when none given. + */ + @objc(initializeCasting:resolver:rejecter:) + func initializeCasting( + _ config: Any?, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + bridge.uiManager.addUIBlock { _, _ in + if let config = config { + guard let options = RCTConvert.castManagerOptions(config) else { + reject("BitmovinCastManagerModule", "Could not deserialize BitmovinCastManagerOptions", nil) + return + } + BitmovinCastManager.initializeCasting(options: options) + resolve(nil) + } else { + BitmovinCastManager.initializeCasting() + resolve(nil) + } + } + } + + /** + Returns true if casting is already initialized. + - Parameter resolver: JS promise resolver. + - Parameter rejecter: JS promise rejecter. + */ + @objc(isInitialized:rejecter:) + func isInitialized( + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + bridge.uiManager.addUIBlock { _, _ in + resolve(BitmovinCastManager.isInitialized()) + } + } + + /** + Sends the given message to the cast receiver on the provided namespace. + If no namespace is provided, the one returned by defaultChannel.protocolNamespace is used. + */ + @objc(sendMessage:messageNamespace:) + func sendMessage( + _ message: String, + messageNamespace: String? + ) { + bridge.uiManager.addUIBlock { _, _ in + BitmovinCastManager.sharedInstance().sendMessage(message, withNamespace: messageNamespace) + } + } +} diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index 8201ddee..a6b77726 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -919,4 +919,15 @@ extension RCTConvert { "height": thumbnail.height, ] } + + static func castManagerOptions(_ json: Any?) -> BitmovinCastManagerOptions? { + guard let json = json as? [String: Any?] else { + return nil + } + + let options = BitmovinCastManagerOptions() + options.applicationId = json["applicationId"] as? String + options.messageNamespace = json["messageNamespace"] as? String + return options + } } diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts index 728bd294..30e642f6 100644 --- a/src/bitmovinCastManager.ts +++ b/src/bitmovinCastManager.ts @@ -52,7 +52,7 @@ export const BitmovinCastManager = { if (Platform.OS === 'ios' && Platform.isTV) { return Promise.resolve(); } - return BitmovinCastManagerModule.initialize(options); + return BitmovinCastManagerModule.initializeCasting(options); }, /** From cda908ac1b5cffa794052db34e2f0d9846cb0cc5 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 14:28:06 +0200 Subject: [PATCH 36/85] feat(casting): add missing API docs --- ios/RCTConvert+BitmovinPlayer.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index 521ae355..ea915fb6 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -954,6 +954,11 @@ extension RCTConvert { return remoteControlConfig } + /** + Utility method to instantiate a `BitmovinCastManagerOptions` from a JS object. + - Parameter json: JS object + - Returns: The produced `BitmovinCastManagerOptions` object + */ static func castManagerOptions(_ json: Any?) -> BitmovinCastManagerOptions? { guard let json = json as? [String: Any?] else { return nil From 4a0dd5e1dd1ae5d09acb5dd402e47da95565c73b Mon Sep 17 00:00:00 2001 From: bitbalint <99177189+bitbalint@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:49:40 +0200 Subject: [PATCH 37/85] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index da2e5c74..73d4ece8 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,3 +3,7 @@ contact_links: - name: Feature Requests url: https://community.bitmovin.com/t/how-to-submit-a-feature-request-to-us/1463 about: Learn how to suggest new features for our Player SDKs. +contact_links: + - name: Report a security vulnerability + url: https://bitmovin.atlassian.net/wiki/external/1502085332/YTYwODMwZjQyNjkwNGQ0ODg5MTgwM2NhMDliNjRmODE + about: Report a security vulnerability. From 51d96d7b0e3080aa61de1b7fab344ddf50b2c992 Mon Sep 17 00:00:00 2001 From: bitbalint <99177189+bitbalint@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:50:38 +0200 Subject: [PATCH 38/85] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 73d4ece8..f7cb1496 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,7 +3,6 @@ contact_links: - name: Feature Requests url: https://community.bitmovin.com/t/how-to-submit-a-feature-request-to-us/1463 about: Learn how to suggest new features for our Player SDKs. -contact_links: - name: Report a security vulnerability url: https://bitmovin.atlassian.net/wiki/external/1502085332/YTYwODMwZjQyNjkwNGQ0ODg5MTgwM2NhMDliNjRmODE about: Report a security vulnerability. From 546400bfe7963229ea697c768bf0fb976ec1791a Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 15:41:45 +0200 Subject: [PATCH 39/85] fix: fix non-DRM playback on iOS --- ios/SourceModule.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 54737553..1bd3b735 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -42,8 +42,13 @@ class SourceModule: NSObject, RCTBridgeModule { @objc(initWithAnalyticsConfig:drmNativeId:config:analyticsSourceMetadata:) func initWithAnalyticsConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, analyticsSourceMetadata: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in - let fairplayConfig = drmNativeId != nil ? self?.getDrmModule()?.retrieve(drmNativeId!) : nil - + let fairplayConfig: FairplayConfig? + if let drmNativeId = drmNativeId { + fairplayConfig = self?.getDrmModule()?.retrieve(drmNativeId) + } else { + fairplayConfig = nil + } + guard self?.sources[nativeId] == nil, let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig), @@ -64,9 +69,15 @@ class SourceModule: NSObject, RCTBridgeModule { @objc(initWithConfig:drmNativeId:config:) func initWithConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?) { bridge.uiManager.addUIBlock { [weak self] _, _ in + let fairplayConfig: FairplayConfig? + if let drmNativeId = drmNativeId { + fairplayConfig = self?.getDrmModule()?.retrieve(drmNativeId) + } else { + fairplayConfig = nil + } + guard self?.sources[nativeId] == nil, - let fairplayConfig = drmNativeId != nil ? self?.getDrmModule()?.retrieve(drmNativeId!) : nil, let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig) else { return From 4315ea966d6bd8d933e5276812837ec2b8440b30 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 15:59:11 +0200 Subject: [PATCH 40/85] feat(casting): add note about calling initialize only when Cast SDK is available --- src/bitmovinCastManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts index 30e642f6..c2cb2b02 100644 --- a/src/bitmovinCastManager.ts +++ b/src/bitmovinCastManager.ts @@ -43,6 +43,8 @@ export const BitmovinCastManager = { * This method needs to be called before `Player` creation to enable casting features. * If no options are provided, the default options will be used. * + * IMPORTANT: This should only be called when the Google Cast SDK is available in the application. + * * @param options The options to be used for initializing `BitmovinCastManager` * @returns A promise that resolves when the `BitmovinCastManager` was initialized successfully */ From 733af0eb7984c3a7cc947307d481672482930343 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Wed, 20 Sep 2023 07:32:10 +0200 Subject: [PATCH 41/85] chore(casting): add missing native id --- src/player.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/player.ts b/src/player.ts index effa17f2..0411a47b 100644 --- a/src/player.ts +++ b/src/player.ts @@ -504,7 +504,7 @@ export class Player extends NativeInstance { * @platform iOS, Android */ isCastAvailable = async (): Promise => { - return PlayerModule.isCastAvailable(); + return PlayerModule.isCastAvailable(this.nativeId); }; /** From 57381f0ded63684b745cdccc4b1ea4ff03e51f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20K=C3=A1konyi?= Date: Wed, 20 Sep 2023 08:55:12 +0200 Subject: [PATCH 42/85] feat(casting): remove unnecessary filter Co-authored-by: Lukas Knoch-Girstmair --- .../com/bitmovin/player/reactnative/converter/JsonConverter.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index 577c7a1f..391eca7c 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -126,7 +126,6 @@ class JsonConverter { if (json.hasKey("customReceiverConfig")) { customReceiverConfig = json.getMap("customReceiverConfig") ?.toHashMap() - ?.filterValues { entry -> entry is String } ?.mapValues { entry -> entry.value as String } ?: emptyMap() } From c5fe1d7ff615880e2dbdf369a8defc3307bb561e Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 20 Sep 2023 11:38:18 +0200 Subject: [PATCH 43/85] feat(casting): implement player events for casting --- ios/Event+JSON.swift | 20 +++++++++++++++ ios/RCTConvert+BitmovinPlayer.swift | 13 ++++++++++ ios/RNPlayerView+PlayerListener.swift | 36 +++++++++++++++++++++++++++ ios/RNPlayerView.swift | 9 +++++++ ios/RNPlayerViewManager.m | 9 +++++++ 5 files changed, 87 insertions(+) diff --git a/ios/Event+JSON.swift b/ios/Event+JSON.swift index 204929f0..e53fee40 100644 --- a/ios/Event+JSON.swift +++ b/ios/Event+JSON.swift @@ -340,3 +340,23 @@ extension VideoDownloadQualityChangedEvent { ] } } + +extension CastStartedEvent { + func toJSON() -> [AnyHashable: Any] { + [ + "name": name, + "timestamp": timestamp, + "deviceName": deviceName + ] + } +} + +extension CastWaitingForDeviceEvent { + func toJSON() -> [AnyHashable: Any] { + [ + "name": name, + "timestamp": timestamp, + "castPayload": RCTConvert.toJson(castPayload: castPayload) + ] + } +} diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index ea915fb6..59d0148b 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -969,4 +969,17 @@ extension RCTConvert { options.messageNamespace = json["messageNamespace"] as? String return options } + + /** + Utility method to compute a JS value from an `CastPayload` object. + - Parameter castPayload `CastPayload` object to be converted. + - Returns: The produced JS object. + */ + static func toJson(castPayload: CastPayload) -> [String: Any?] { + return [ + "currentTime": castPayload.currentTime, + "deviceName": castPayload.deviceName, + "type": castPayload.type, + ] + } } diff --git a/ios/RNPlayerView+PlayerListener.swift b/ios/RNPlayerView+PlayerListener.swift index 6e485bb6..928774fa 100644 --- a/ios/RNPlayerView+PlayerListener.swift +++ b/ios/RNPlayerView+PlayerListener.swift @@ -168,4 +168,40 @@ extension RNPlayerView: PlayerListener { func onVideoPlaybackQualityChanged(_ event: VideoDownloadQualityChangedEvent, player: Player) { onVideoPlaybackQualityChanged?(event.toJSON()) } + + func onCastAvailable(_ event: CastAvailableEvent, player: Player) { + onCastAvailable?(event.toJSON()) + } + + func onCastPaused(_ event: CastPausedEvent, player: Player) { + onCastPaused?(event.toJSON()) + } + + func onCastPlaybackFinished(_ event: CastPlaybackFinishedEvent, player: Player) { + onCastPlaybackFinished?(event.toJSON()) + } + + func onCastPlaying(_ event: CastPlayingEvent, player: Player) { + onCastPlaying?(event.toJSON()) + } + + func onCastStarted(_ event: CastStartedEvent, player: Player) { + onCastStarted?(event.toJSON()) + } + + func onCastStart(_ event: CastStartEvent, player: Player) { + onCastStart?(event.toJSON()) + } + + func onCastStopped(_ event: CastStoppedEvent, player: Player) { + onCastStopped?(event.toJSON()) + } + + func onCastTimeUpdated(_ event: CastTimeUpdatedEvent, player: Player) { + onCastTimeUpdated?(event.toJSON()) + } + + func onCastWaitingForDevice(_ event: CastWaitingForDeviceEvent, player: Player) { + onCastWaitingForDevice?(event.toJSON()) + } } diff --git a/ios/RNPlayerView.swift b/ios/RNPlayerView.swift index dc1d4b94..2112c33d 100644 --- a/ios/RNPlayerView.swift +++ b/ios/RNPlayerView.swift @@ -53,6 +53,15 @@ class RNPlayerView: UIView { @objc var onFullscreenDisabled: RCTBubblingEventBlock? @objc var onFullscreenEnter: RCTBubblingEventBlock? @objc var onFullscreenExit: RCTBubblingEventBlock? + @objc var onCastAvailable: RCTBubblingEventBlock? + @objc var onCastPaused: RCTBubblingEventBlock? + @objc var onCastPlaybackFinished: RCTBubblingEventBlock? + @objc var onCastPlaying: RCTBubblingEventBlock? + @objc var onCastStarted: RCTBubblingEventBlock? + @objc var onCastStart: RCTBubblingEventBlock? + @objc var onCastStopped: RCTBubblingEventBlock? + @objc var onCastTimeUpdated: RCTBubblingEventBlock? + @objc var onCastWaitingForDevice: RCTBubblingEventBlock? /// The `PlayerView` subview. var playerView: PlayerView? { diff --git a/ios/RNPlayerViewManager.m b/ios/RNPlayerViewManager.m index 18f8f109..3fec7f83 100644 --- a/ios/RNPlayerViewManager.m +++ b/ios/RNPlayerViewManager.m @@ -52,6 +52,15 @@ @interface RCT_EXTERN_REMAP_MODULE(NativePlayerView, RNPlayerViewManager, RCTVie RCT_EXPORT_VIEW_PROPERTY(onFullscreenDisabled, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onFullscreenEnter, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onFullscreenExit, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastAvailable, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastPaused, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastPlaybackFinished, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastPlaying, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastStarted, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastStart, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastStopped, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastTimeUpdated, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCastWaitingForDevice, RCTBubblingEventBlock) RCT_EXTERN_METHOD(attachPlayer:(nonnull NSNumber *)viewId playerId:(NSString *)playerId playerConfig:(nullable NSDictionary *)playerConfig) RCT_EXTERN_METHOD(attachFullscreenBridge:(nonnull NSNumber *)viewId fullscreenBridgeId:(NSString *)fullscreenBridgeId) From e5228412bf31d4f7645c99f741a3a400ed3242a5 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 20 Sep 2023 11:38:39 +0200 Subject: [PATCH 44/85] feat(casting): fix documentation --- src/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events.ts b/src/events.ts index cb7f1a6a..51a85072 100644 --- a/src/events.ts +++ b/src/events.ts @@ -630,7 +630,7 @@ export interface CastPayload { */ export interface CastWaitingForDeviceEvent extends Event { /** - * The [CastPayload] object for the event + * The `CastPayload` object for the event */ castPayload: CastPayload; } From 865a993e9169e1ab01ba92dc2dab480eb75b5b0d Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 20 Sep 2023 12:07:19 +0200 Subject: [PATCH 45/85] feat(casting): rename initializeCasting to initializeCastManager for better naming --- ios/BitmovinCastManagerModule.m | 2 +- ios/BitmovinCastManagerModule.swift | 4 ++-- src/bitmovinCastManager.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/BitmovinCastManagerModule.m b/ios/BitmovinCastManagerModule.m index 1bcc488d..eaa02c22 100644 --- a/ios/BitmovinCastManagerModule.m +++ b/ios/BitmovinCastManagerModule.m @@ -6,7 +6,7 @@ @interface RCT_EXTERN_REMAP_MODULE(BitmovinCastManagerModule, BitmovinCastManage isInitialized:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD( - initializeCasting:(nullable id)config + initializeCastManager:(nullable id)config resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(sendMessage:(NSString *)message messageNamespace:(nullable NSString *)messageNamespace) diff --git a/ios/BitmovinCastManagerModule.swift b/ios/BitmovinCastManagerModule.swift index 235078dc..07c0b5e0 100644 --- a/ios/BitmovinCastManagerModule.swift +++ b/ios/BitmovinCastManagerModule.swift @@ -23,8 +23,8 @@ class BitmovinCastManagerModule: NSObject, RCTBridgeModule { /** Initializes the BitmovinCastManager with the given options or with no options when none given. */ - @objc(initializeCasting:resolver:rejecter:) - func initializeCasting( + @objc(initializeCastManager:resolver:rejecter:) + func initializeCastManager( _ config: Any?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts index c2cb2b02..fe494d61 100644 --- a/src/bitmovinCastManager.ts +++ b/src/bitmovinCastManager.ts @@ -54,7 +54,7 @@ export const BitmovinCastManager = { if (Platform.OS === 'ios' && Platform.isTV) { return Promise.resolve(); } - return BitmovinCastManagerModule.initializeCasting(options); + return BitmovinCastManagerModule.initializeCastManager(options); }, /** From e0b142e929e2680261193db1fac9b1445eaf53fc Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Wed, 20 Sep 2023 12:44:05 +0200 Subject: [PATCH 46/85] feat(casting): implement Android BitmovinCastManagerModule and add add updateContext function --- .../reactnative/BitmovinCastManagerModule.kt | 74 +++++++++++++++++++ .../player/reactnative/RNPlayerViewPackage.kt | 3 +- .../reactnative/converter/JsonConverter.kt | 16 +++- src/bitmovinCastManager.ts | 14 ++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt new file mode 100644 index 00000000..d7c8094d --- /dev/null +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -0,0 +1,74 @@ +package com.bitmovin.player.reactnative + +import com.bitmovin.player.casting.BitmovinCastManager +import com.bitmovin.player.reactnative.converter.JsonConverter +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.UIManagerModule + +private const val MODULE_NAME = "BitmovinCastManagerModule" + +@ReactModule(name = MODULE_NAME) +class BitmovinCastManagerModule( + private val context: ReactApplicationContext, +) : ReactContextBaseJavaModule(context) { + override fun getName() = MODULE_NAME + + /** + * Returns whether the [BitmovinCastManager] is initialized. + */ + @ReactMethod + fun isInitialized() = BitmovinCastManager.isInitialized() + + /** + * Initializes the [BitmovinCastManager] with the given options. + */ + @ReactMethod + fun initializeCastManager(options: ReadableMap?, promise: Promise) { + val castOptions = JsonConverter.toCastOptions(options) + uiManager?.addUIBlock { + BitmovinCastManager.initialize( + castOptions?.applicationId, + castOptions?.messageNamespace + ) + promise.resolve(null) + } + } + + /** + * Sends a message to the receiver. + */ + @ReactMethod + fun sendMessage(message: String, messageNamespace: String?, promise: Promise) { + uiManager?.addUIBlock { + BitmovinCastManager.getInstance().sendMessage(message, messageNamespace) + promise.resolve(null) + } + } + + /** + * Updates the context of the [BitmovinCastManager] to the current activity. + */ + @ReactMethod + fun updateContext(promise: Promise) { + uiManager?.addUIBlock { + BitmovinCastManager.getInstance().updateContext(currentActivity) + promise.resolve(null) + } + } + + private val uiManager: UIManagerModule? + get() = context.getNativeModule(UIManagerModule::class.java) +} + +/** + * Represents configuration options for the [BitmovinCastManager]. + */ +data class BitmovinCastManagerOptions( + val applicationId: String? = null, + val messageNamespace: String? = null, +) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt index fad7dec4..f5327e13 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt @@ -27,7 +27,8 @@ class RNPlayerViewPackage : ReactPackage { PlayerAnalyticsModule(reactContext), RNPlayerViewManager(reactContext), FullscreenHandlerModule(reactContext), - CustomMessageHandlerModule(reactContext) + CustomMessageHandlerModule(reactContext), + BitmovinCastManagerModule(reactContext), ) } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt index 391eca7c..ba00af60 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt @@ -39,6 +39,7 @@ import com.bitmovin.player.api.source.SourceType import com.bitmovin.player.api.source.TimelineReferencePoint import com.bitmovin.player.api.ui.ScalingMode import com.bitmovin.player.api.ui.StyleConfig +import com.bitmovin.player.reactnative.BitmovinCastManagerOptions import com.bitmovin.player.reactnative.extensions.getBooleanOrNull import com.bitmovin.player.reactnative.extensions.getName import com.bitmovin.player.reactnative.extensions.getOrDefault @@ -184,7 +185,7 @@ class JsonConverter { "end" -> TimelineReferencePoint.End else -> null } - + /** * Converts an arbitrary `json` to `AdaptationConfig`. * @param json JS object representing the `AdaptationConfig`. @@ -640,6 +641,19 @@ class JsonConverter { return json } + /** + * Converts an arbitrary `json` into [BitmovinCastManagerOptions]. + * @param json JS object representing the [BitmovinCastManagerOptions]. + * @return The generated [BitmovinCastManagerOptions] if successful, `null` otherwise. + */ + fun toCastOptions(json: ReadableMap?): BitmovinCastManagerOptions? { + if (json == null) return null + return BitmovinCastManagerOptions( + json.getOrDefault("applicationId", null), + json.getOrDefault("messageNamespace", null) + ) + } + /** * Converts an arbitrary `json` to `WidevineConfig`. * @param json JS object representing the `WidevineConfig`. diff --git a/src/bitmovinCastManager.ts b/src/bitmovinCastManager.ts index fe494d61..196821c7 100644 --- a/src/bitmovinCastManager.ts +++ b/src/bitmovinCastManager.ts @@ -57,6 +57,20 @@ export const BitmovinCastManager = { return BitmovinCastManagerModule.initializeCastManager(options); }, + /** + * Must be called in every Android Activity to update the context to the current one. + * Make sure to call this method on every Android Activity switch. + * + * @returns A promise that resolves when the context was updated successfully + * @platform Android + */ + updateContext: async (): Promise => { + if (Platform.OS === 'ios') { + return Promise.resolve(); + } + return BitmovinCastManagerModule.updateContext(); + }, + /** * Sends the given message to the cast receiver. * From 4f0e67a6bf56e7714de64e5fdf29d5cbb5f03e5d Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 20 Sep 2023 13:18:14 +0200 Subject: [PATCH 47/85] feat(casting): disable casting on other screens --- example/src/screens/BasicAds.tsx | 6 +++++- example/src/screens/BasicAnalytics.tsx | 3 +++ example/src/screens/BasicDrmPlayback.tsx | 6 +++++- example/src/screens/BasicFullscreenHandling.tsx | 6 +++++- example/src/screens/BasicPictureInPicture.tsx | 3 +++ example/src/screens/BasicPlayback.tsx | 6 +++++- example/src/screens/CustomHtmlUi.tsx | 3 +++ example/src/screens/CustomPlayback.tsx | 3 +++ example/src/screens/LandscapeFullscreenHandling.tsx | 3 +++ example/src/screens/OfflinePlayback.tsx | 6 +++++- example/src/screens/ProgrammaticTrackSelection.tsx | 6 +++++- example/src/screens/SubtitlePlayback.tsx | 6 +++++- example/src/screens/SystemUi.tsx | 3 +++ 13 files changed, 53 insertions(+), 7 deletions(-) diff --git a/example/src/screens/BasicAds.tsx b/example/src/screens/BasicAds.tsx index 8d7086f1..a2d1dfb0 100644 --- a/example/src/screens/BasicAds.tsx +++ b/example/src/screens/BasicAds.tsx @@ -54,10 +54,14 @@ const advertisingConfig = { ], }; +const remoteControlConfig = { + isCastEnabled: false, +}; + export default function BasicAds() { useTVGestures(); - const player = usePlayer({ advertisingConfig }); + const player = usePlayer({ advertisingConfig, remoteControlConfig }); useFocusEffect( useCallback(() => { diff --git a/example/src/screens/BasicAnalytics.tsx b/example/src/screens/BasicAnalytics.tsx index 0f0ffe7c..fb6bebf3 100644 --- a/example/src/screens/BasicAnalytics.tsx +++ b/example/src/screens/BasicAnalytics.tsx @@ -27,6 +27,9 @@ export default function BasicAds() { customData5: 'Custom data field 5', }, }, + remoteControlConfig: { + isCastEnabled: false, + }, }); useFocusEffect( diff --git a/example/src/screens/BasicDrmPlayback.tsx b/example/src/screens/BasicDrmPlayback.tsx index a9634377..c441d386 100644 --- a/example/src/screens/BasicDrmPlayback.tsx +++ b/example/src/screens/BasicDrmPlayback.tsx @@ -111,7 +111,11 @@ const source: SourceConfig = { export default function BasicDrmPlayback() { useTVGestures(); - const player = usePlayer(); + const player = usePlayer({ + remoteControlConfig: { + isCastEnabled: false, + }, + }); useFocusEffect( useCallback(() => { diff --git a/example/src/screens/BasicFullscreenHandling.tsx b/example/src/screens/BasicFullscreenHandling.tsx index 88cc3ecc..a87d1e5e 100644 --- a/example/src/screens/BasicFullscreenHandling.tsx +++ b/example/src/screens/BasicFullscreenHandling.tsx @@ -66,7 +66,11 @@ export default function BasicFullscreenHandling({ }: BasicFullscreenHandlingProps) { useTVGestures(); - const player = usePlayer(); + const player = usePlayer({ + remoteControlConfig: { + isCastEnabled: false, + }, + }); const [fullscreenMode, setFullscreenMode] = useState(false); const fullscreenHandler = useRef( diff --git a/example/src/screens/BasicPictureInPicture.tsx b/example/src/screens/BasicPictureInPicture.tsx index 7496d68e..d4d69b56 100644 --- a/example/src/screens/BasicPictureInPicture.tsx +++ b/example/src/screens/BasicPictureInPicture.tsx @@ -22,6 +22,9 @@ export default function BasicPictureInPicture() { // Enable picture in picture UI option on player controls. isPictureInPictureEnabled: true, }, + remoteControlConfig: { + isCastEnabled: false, + }, }); useFocusEffect( diff --git a/example/src/screens/BasicPlayback.tsx b/example/src/screens/BasicPlayback.tsx index f7ec3304..027bad63 100644 --- a/example/src/screens/BasicPlayback.tsx +++ b/example/src/screens/BasicPlayback.tsx @@ -16,7 +16,11 @@ function prettyPrint(header: string, obj: any) { export default function BasicPlayback() { useTVGestures(); - const player = usePlayer(); + const player = usePlayer({ + remoteControlConfig: { + isCastEnabled: false, + }, + }); useFocusEffect( useCallback(() => { diff --git a/example/src/screens/CustomHtmlUi.tsx b/example/src/screens/CustomHtmlUi.tsx index 70cd6756..ee948312 100644 --- a/example/src/screens/CustomHtmlUi.tsx +++ b/example/src/screens/CustomHtmlUi.tsx @@ -55,6 +55,9 @@ export default function CustomHtmlUi({ navigation }: CustomHtmlUiProps) { supplementalPlayerUiCss: 'https://cdn.bitmovin.com/player/ui/ui-customized-sample.css', }, + remoteControlConfig: { + isCastEnabled: false, + }, }); useFocusEffect( diff --git a/example/src/screens/CustomPlayback.tsx b/example/src/screens/CustomPlayback.tsx index 5b4ffb15..3c9bed6a 100644 --- a/example/src/screens/CustomPlayback.tsx +++ b/example/src/screens/CustomPlayback.tsx @@ -16,6 +16,9 @@ const CustomPlayback: React.FC = ({ route }) => { const player = usePlayer({ licenseKey: route.params?.licenseKey, + remoteControlConfig: { + isCastEnabled: false, + }, }); useFocusEffect( diff --git a/example/src/screens/LandscapeFullscreenHandling.tsx b/example/src/screens/LandscapeFullscreenHandling.tsx index 9b185bcb..5e027e3d 100644 --- a/example/src/screens/LandscapeFullscreenHandling.tsx +++ b/example/src/screens/LandscapeFullscreenHandling.tsx @@ -74,6 +74,9 @@ export default function LandscapeFullscreenHandling({ playbackConfig: { isAutoplayEnabled: true, }, + remoteControlConfig: { + isCastEnabled: false, + }, }); const [fullscreenMode, setFullscreenMode] = useState(true); diff --git a/example/src/screens/OfflinePlayback.tsx b/example/src/screens/OfflinePlayback.tsx index c3f0ae7f..3a4ba235 100644 --- a/example/src/screens/OfflinePlayback.tsx +++ b/example/src/screens/OfflinePlayback.tsx @@ -88,7 +88,11 @@ export default function OfflinePlayback() { prettyPrint(`EVENT`, event); }, []); - const player = usePlayer(); + const player = usePlayer({ + remoteControlConfig: { + isCastEnabled: false, + }, + }); useFocusEffect(useCallback(() => () => player.destroy(), [player])); diff --git a/example/src/screens/ProgrammaticTrackSelection.tsx b/example/src/screens/ProgrammaticTrackSelection.tsx index b1389d06..02976b10 100644 --- a/example/src/screens/ProgrammaticTrackSelection.tsx +++ b/example/src/screens/ProgrammaticTrackSelection.tsx @@ -32,7 +32,11 @@ const Separator = () => ; export default function ProgrammaticTrackSelection() { useTVGestures(); - const player = usePlayer(); + const player = usePlayer({ + remoteControlConfig: { + isCastEnabled: false, + }, + }); const [tracks, setTracks] = useState([]); useFocusEffect( diff --git a/example/src/screens/SubtitlePlayback.tsx b/example/src/screens/SubtitlePlayback.tsx index 6cc15889..bff9b0e8 100644 --- a/example/src/screens/SubtitlePlayback.tsx +++ b/example/src/screens/SubtitlePlayback.tsx @@ -17,7 +17,11 @@ function prettyPrint(header: string, obj: any) { export default function SubtitlePlayback() { useTVGestures(); - const player = usePlayer(); + const player = usePlayer({ + remoteControlConfig: { + isCastEnabled: false, + }, + }); useFocusEffect( useCallback(() => { diff --git a/example/src/screens/SystemUi.tsx b/example/src/screens/SystemUi.tsx index 4d50532b..30cbc2a6 100644 --- a/example/src/screens/SystemUi.tsx +++ b/example/src/screens/SystemUi.tsx @@ -22,6 +22,9 @@ export default function SystemUI() { styleConfig: { userInterfaceType: UserInterfaceType.System, }, + remoteControlConfig: { + isCastEnabled: false, + }, }); useFocusEffect( From 64998bef11a25e4358879f7a05c0e23ac1649b1d Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Wed, 20 Sep 2023 14:06:59 +0200 Subject: [PATCH 48/85] feat(casting): use promise --- .../bitmovin/player/reactnative/BitmovinCastManagerModule.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt index d7c8094d..a70f5ba3 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/BitmovinCastManagerModule.kt @@ -22,7 +22,9 @@ class BitmovinCastManagerModule( * Returns whether the [BitmovinCastManager] is initialized. */ @ReactMethod - fun isInitialized() = BitmovinCastManager.isInitialized() + fun isInitialized(promise: Promise) = uiManager?.addUIBlock { + promise.resolve(BitmovinCastManager.isInitialized()) + } /** * Initializes the [BitmovinCastManager] with the given options. From 5ab3abce0854a662bcbdcf5f601c2b3736dfee4f Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 20 Sep 2023 15:49:28 +0200 Subject: [PATCH 49/85] feat(casting): add sample screen for casting --- example/src/App.tsx | 13 ++++++ example/src/screens/Casting.tsx | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 example/src/screens/Casting.tsx diff --git a/example/src/App.tsx b/example/src/App.tsx index 3e0d79f5..97e2bda2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -20,6 +20,7 @@ import BasicFullscreenHandling from './screens/BasicFullscreenHandling'; import LandscapeFullscreenHandling from './screens/LandscapeFullscreenHandling'; import SystemUI from './screens/SystemUi'; import OfflinePlayback from './screens/OfflinePlayback'; +import Casting from './screens/Casting'; export type RootStackParamsList = { ExamplesList: { @@ -53,6 +54,7 @@ export type RootStackParamsList = { CustomHtmlUi: { navigation: NativeStackNavigationProp; }; + Casting: undefined; }; const RootStack = createNativeStackNavigator(); @@ -114,6 +116,10 @@ export default function App() { title: 'Landscape Fullscreen handling', routeName: 'LandscapeFullscreenHandling', }); + stackParams.data.push({ + title: 'Casting', + routeName: 'Casting', + }); } if (Platform.OS === 'ios' && !Platform.isTV) { @@ -228,6 +234,13 @@ export default function App() { options={{ title: 'System UI' }} /> )} + {!Platform.isTV && ( + + )} ); diff --git a/example/src/screens/Casting.tsx b/example/src/screens/Casting.tsx new file mode 100644 index 00000000..0cff58ce --- /dev/null +++ b/example/src/screens/Casting.tsx @@ -0,0 +1,77 @@ +import React, { useCallback } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; +import { + Event, + usePlayer, + PlayerView, + SourceType, + BitmovinCastManager, +} from 'bitmovin-player-react-native'; + +function prettyPrint(header: string, obj: any) { + console.log(header, JSON.stringify(obj, null, 2)); +} + +export default function Casting() { + BitmovinCastManager.initialize(); + + const player = usePlayer(); + + useFocusEffect( + useCallback(() => { + player.load({ + url: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8', + type: SourceType.HLS, + title: 'BipBop - Apple sample stream', + }); + return () => { + player.destroy(); + }; + }, [player]) + ); + + const onReady = useCallback((event: Event) => { + prettyPrint(`EVENT [${event.name}]`, event); + }, []); + + const onEvent = useCallback((event: Event) => { + prettyPrint(`EVENT [${event.name}]`, event); + }, []); + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'black', + }, + player: { + flex: 1, + }, +}); From 00a46037d8f6108d029545ec9297a5541de6d23b Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Wed, 20 Sep 2023 16:03:59 +0200 Subject: [PATCH 50/85] feat(casting): add Google Cast SDK to example app for iOS --- example/ios/Podfile | 1 + example/ios/Podfile.lock | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index 9115ae26..ded5ffc8 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -38,6 +38,7 @@ end target 'BitmovinPlayerReactNativeExample' do setup :ios + pod 'google-cast-sdk', '4.8.0' end target 'BitmovinPlayerReactNativeExample-tvOS' do diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3b6de35b..78d7f03e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -80,10 +80,16 @@ PODS: - FlipperKit/FlipperKitNetworkPlugin - fmt (7.1.3) - glog (0.3.5) + - google-cast-sdk (4.8.0): + - google-cast-sdk/Core (= 4.8.0) + - Protobuf (~> 3.13) + - google-cast-sdk/Core (4.8.0): + - Protobuf (~> 3.13) - GoogleAds-IMA-iOS-SDK (3.18.4) - GoogleAds-IMA-tvOS-SDK (4.8.2) - libevent (2.1.12.1) - OpenSSL-Universal (1.1.1100) + - Protobuf (3.24.3) - RCT-Folly (2021.06.28.00-v2): - boost - DoubleConversion @@ -382,6 +388,7 @@ DEPENDENCIES: - FlipperKit/SKIOSNetworkPlugin (= 0.151.0) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - google-cast-sdk (= 4.8.0) - OpenSSL-Universal (= 1.1.1100) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) @@ -433,9 +440,11 @@ SPEC REPOS: - Flipper-Glog - Flipper-RSocket - FlipperKit + - google-cast-sdk - GoogleAds-IMA-iOS-SDK - GoogleAds-IMA-tvOS-SDK - OpenSSL-Universal + - Protobuf - SocketRocket - YogaKit @@ -533,10 +542,12 @@ SPEC CHECKSUMS: FlipperKit: feec58b1283d3560c703adc670c45ecf400c48d9 fmt: 135c0c55547979f715b56dfa54037ececa96d07a glog: bac6d5aa2990176cc22d0432fb3e28805d580aeb + google-cast-sdk: afeb1aac0744b1bc4f70bc3db8468e33fabbff38 GoogleAds-IMA-iOS-SDK: b01284e3bf3d64ba948de6692ffda531452c3713 GoogleAds-IMA-tvOS-SDK: 2dda9d3b34c43003222d3417315fecec22b698a1 libevent: a6d75fcd7be07cbc5070300ea8dbc8d55dfab88e OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c + Protobuf: 970f7ee93a3a08e3cf64859b8efd95ee32b4f87f RCT-Folly: 6955e7728b76277c5df5a5aba37ee1ff1dd99976 RCTRequired: 4a18f06da2ea5eb7e1b7334643f988ef76159c9e RCTTypeSafety: 3006f0658ff73d9e81a5fde4dbf9e9260a679ab0 @@ -570,6 +581,6 @@ SPEC CHECKSUMS: Yoga: be4fede0339d862989f5b064f8939c7429ad92c9 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: ae31af0f37bdf647197475f3f0d4a5a3e93df60c +PODFILE CHECKSUM: c212c97d06bc742b7a8cc429dd03ec4c059fe021 COCOAPODS: 1.12.1 From 7d7648b21e7423439eb21c29a23239ecf36ae6d4 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Wed, 20 Sep 2023 16:20:05 +0200 Subject: [PATCH 51/85] feat(casting): add play-services-cast-framework dependency and options provider to sample app --- example/android/app/build.gradle | 2 ++ example/android/app/src/main/AndroidManifest.xml | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index eee992a7..f08ccc23 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -269,6 +269,8 @@ dependencies { } implementation project(':bitmovin-player-react-native') + implementation("com.google.android.gms:play-services-cast-framework:21.3.0") + if (enableHermes) { def hermesPath = "../../node_modules/hermes-engine/android/"; debugImplementation files(hermesPath + "hermes-debug.aar") diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 9788447d..93444417 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -29,5 +29,10 @@ + + + From 93412c706ddb0b66d878920bb85a10900d6b0392 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Wed, 20 Sep 2023 16:24:48 +0200 Subject: [PATCH 52/85] feat(casting): add mediarouter dependency to sample app --- example/android/app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index f08ccc23..37e85de9 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -270,6 +270,7 @@ dependencies { implementation project(':bitmovin-player-react-native') implementation("com.google.android.gms:play-services-cast-framework:21.3.0") + implementation("androidx.mediarouter:mediarouter:1.3.1") if (enableHermes) { def hermesPath = "../../node_modules/hermes-engine/android/"; From 94feff2748111257232fc187d201a51efe615c6a Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Thu, 21 Sep 2023 06:33:19 +0200 Subject: [PATCH 53/85] feat(casting): attach cast events to Android player --- .../com/bitmovin/player/reactnative/RNPlayerView.kt | 12 ++++++++++++ .../player/reactnative/RNPlayerViewManager.kt | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt index 4ca7beb5..02823aa5 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt @@ -62,6 +62,18 @@ private val EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING = mapOf( PlayerEvent.AdSkipped::class to "adSkipped", PlayerEvent.AdStarted::class to "adStarted", PlayerEvent.VideoPlaybackQualityChanged::class to "videoPlaybackQualityChanged", + PlayerEvent.CastStart::class to "castStart", + @Suppress("DEPRECATION") + PlayerEvent.CastPlaybackFinished::class to "castPlaybackFinished", + @Suppress("DEPRECATION") + PlayerEvent.CastPaused::class to "castPaused", + @Suppress("DEPRECATION") + PlayerEvent.CastPlaying::class to "castPlaying", + PlayerEvent.CastStarted::class to "castStarted", + PlayerEvent.CastAvailable::class to "castAvailable", + PlayerEvent.CastStopped::class to "castStopped", + PlayerEvent.CastWaitingForDevice::class to "castWaitingForDevice", + PlayerEvent.CastTimeUpdated::class to "castTimeUpdated", ) private val EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING_UI = mapOf, String>( diff --git a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt index a3597eba..6df0d82b 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -113,6 +113,15 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple "fullscreenDisabled" to "onFullscreenDisabled", "fullscreenEnter" to "onFullscreenEnter", "fullscreenExit" to "onFullscreenExit", + "castStart" to "onCastStart", + "castPlaybackFinished" to "onCastPlaybackFinished", + "castPaused" to "onCastPaused", + "castPlaying" to "onCastPlaying", + "castStarted" to "onCastStarted", + "castAvailable" to "onCastAvailable", + "castStopped" to "onCastStopped", + "castWaitingForDevice" to "onCastWaitingForDevice", + "castTimeUpdated" to "onCastTimeUpdated", ) /** From 432b8fe0b10d0e86e65d9e9b80ffcd5c9fae78a8 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Thu, 21 Sep 2023 08:48:06 +0200 Subject: [PATCH 54/85] feat(casting): move dependencies to a more sensible location --- example/android/app/build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 37e85de9..4183f6b5 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -255,6 +255,10 @@ dependencies { // Only needed if the offline feature is used implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0" + // only needed if the casting feature is used + implementation("com.google.android.gms:play-services-cast-framework:21.3.0") + implementation("androidx.mediarouter:mediarouter:1.3.1") + implementation "com.facebook.react:react-native:+" // From node_modules implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" debugImplementation("com.facebook.flipper:flipper:$FLIPPER_VERSION") { @@ -269,9 +273,6 @@ dependencies { } implementation project(':bitmovin-player-react-native') - implementation("com.google.android.gms:play-services-cast-framework:21.3.0") - implementation("androidx.mediarouter:mediarouter:1.3.1") - if (enableHermes) { def hermesPath = "../../node_modules/hermes-engine/android/"; debugImplementation files(hermesPath + "hermes-debug.aar") From ec4d23b034742c56b41bf2b92dbad45277517554 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Thu, 21 Sep 2023 08:48:50 +0200 Subject: [PATCH 55/85] feat(casting): add expanded controller activity to manifest --- example/android/app/src/main/AndroidManifest.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 93444417..8beff2b6 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,20 @@ + + + + + + + From d328d42f0a3110e68375e6c7f538b46546f62336 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Thu, 21 Sep 2023 08:54:55 +0200 Subject: [PATCH 56/85] feat(casting): eagerly initialize cast context --- .../bitmovin/player/reactnative/example/MainActivity.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example/android/app/src/main/java/com/bitmovin/player/reactnative/example/MainActivity.java b/example/android/app/src/main/java/com/bitmovin/player/reactnative/example/MainActivity.java index b9c1b4ae..7bdb45b9 100644 --- a/example/android/app/src/main/java/com/bitmovin/player/reactnative/example/MainActivity.java +++ b/example/android/app/src/main/java/com/bitmovin/player/reactnative/example/MainActivity.java @@ -5,11 +5,19 @@ import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactRootView; +import com.google.android.gms.cast.framework.CastContext; public class MainActivity extends ReactActivity { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(null); + try { + // Load Google Cast context eagerly in order to ensure that + // the cast state is updated correctly. + CastContext.getSharedInstance(this, Runnable::run); + } catch (Exception e) { + // cast framework not supported + } } /** From 23f8834c04602067e15d6354ca2f01cb6bee4fda Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Thu, 21 Sep 2023 08:55:44 +0200 Subject: [PATCH 57/85] feat(casting): call `updateContext` on Android --- example/src/screens/Casting.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/example/src/screens/Casting.tsx b/example/src/screens/Casting.tsx index 0cff58ce..cff76039 100644 --- a/example/src/screens/Casting.tsx +++ b/example/src/screens/Casting.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { View, StyleSheet } from 'react-native'; +import { View, StyleSheet, Platform } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; import { Event, @@ -16,6 +16,11 @@ function prettyPrint(header: string, obj: any) { export default function Casting() { BitmovinCastManager.initialize(); + if (Platform.OS === 'android') { + // Must be called in every activity on Android + BitmovinCastManager.updateContext(); + } + const player = usePlayer(); useFocusEffect( From 98d40e50f51dc9b4beb3d5bc6085b6566495c961 Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Thu, 21 Sep 2023 09:01:32 +0200 Subject: [PATCH 58/85] feat(casting): add explaining comment to manifest and fix formatting --- .../android/app/src/main/AndroidManifest.xml | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 8beff2b6..da2c0e41 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -30,19 +30,20 @@ - - - - - - + + + + + + + Date: Thu, 21 Sep 2023 12:34:58 +0200 Subject: [PATCH 59/85] docs(casting): add casting sample to example readme --- example/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/example/README.md b/example/README.md index df7c8266..221b6a65 100644 --- a/example/README.md +++ b/example/README.md @@ -14,6 +14,7 @@ inside the [`src/screens/`](https://github.com/bitmovin/bitmovin-player-react-na - [Basic Analytics](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/BasicAnalytics.tsx) - [Basic Offline Playback](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/OfflinePlayback.tsx) (iOS and Android only) - [System UI](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/SystemUi.tsx) (iOS and tvOS only) +- [Casting](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/Casting.tsx) (iOS and Android only) ### Custom asset playback From 1b35dd71f966f7a6e22bd6e2930abf07c4e6cc8b Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 13:53:02 +0200 Subject: [PATCH 60/85] feat(casting): enable casting different source on iOS --- .../player/reactnative/SourceModule.kt | 2 + ios/PlayerModule.swift | 27 +++++++++- ios/RCTConvert+BitmovinPlayer.swift | 10 ++++ ios/SourceModule.m | 11 +++- ios/SourceModule.swift | 51 ++++++++++++++++--- src/source.ts | 26 +++++++++- 6 files changed, 116 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt index 6079091f..8bbe0ce0 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -53,6 +53,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB nativeId: NativeId, drmNativeId: NativeId?, config: ReadableMap?, + remotePlaybackJson: ReadableMap?, analyticsSourceMetadata: ReadableMap? ) { uiManager()?.addUIBlock { @@ -75,6 +76,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB nativeId: NativeId, drmNativeId: NativeId?, config: ReadableMap?, + remotePlaybackJson: ReadableMap? ) { uiManager()?.addUIBlock { initializeSource(nativeId, drmNativeId, config) { sourceConfig -> diff --git a/ios/PlayerModule.swift b/ios/PlayerModule.swift index c93eac48..4fcc63a4 100644 --- a/ios/PlayerModule.swift +++ b/ios/PlayerModule.swift @@ -45,7 +45,18 @@ class PlayerModule: NSObject, RCTBridgeModule { else { return } - self?.players[nativeId] = PlayerFactory.create(playerConfig: playerConfig) + let player = PlayerFactory.create(playerConfig: playerConfig) + self?.players[nativeId] = player + + playerConfig.remoteControlConfig.prepareSource = { [weak self] type, sourceConfig in + guard let sourceModule = self?.bridge[SourceModule.self], + let sourceNativeId = sourceModule.nativeId(where: { $0.sourceConfig === sourceConfig }), + let castSourceConfig = sourceModule.retrieveCastSourceConfig(sourceNativeId) else { + return nil + } + + return castSourceConfig + } } } @@ -65,12 +76,24 @@ class PlayerModule: NSObject, RCTBridgeModule { else { return } + let defaultMetadata = RCTConvert.analyticsDefaultMetadataFromAnalyticsConfig(analyticsConfigJson) - self?.players[nativeId] = PlayerFactory.create( + let player = PlayerFactory.create( playerConfig: playerConfig, analyticsConfig: analyticsConfig, defaultMetadata: defaultMetadata ?? DefaultMetadata() ) + self?.players[nativeId] = player + + playerConfig.remoteControlConfig.prepareSource = { [weak self] type, sourceConfig in + guard let sourceModule = self?.bridge[SourceModule.self], + let sourceNativeId = sourceModule.nativeId(where: { $0.sourceConfig === sourceConfig }), + let castSourceConfig = sourceModule.retrieveCastSourceConfig(sourceNativeId) else { + return nil + } + + return castSourceConfig + } } } diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index 59d0148b..abf3770e 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -982,4 +982,14 @@ extension RCTConvert { "type": castPayload.type, ] } + + static func sourceRemotePlaybackConfig(_ json: Any?) -> SourceRemotePlaybackConfig? { + guard let json = json as? [String: Any?] else { + return nil + } + + return SourceRemotePlaybackConfig( + castSourceConfig: RCTConvert.sourceConfig(json["castSourceConfig"]) + ) + } } diff --git a/ios/SourceModule.m b/ios/SourceModule.m index 0f07e7eb..cf7e36c5 100644 --- a/ios/SourceModule.m +++ b/ios/SourceModule.m @@ -2,8 +2,15 @@ @interface RCT_EXTERN_REMAP_MODULE(SourceModule, SourceModule, NSObject) -RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config) -RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) +RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId + drmNativeId:(NSString *)drmNativeId + config:(nullable id)config + sourceRemotePlaybackConfig:(id)remotePlayback) +RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId + drmNativeId:(NSString *)drmNativeId + config:(nullable id)config + sourceRemotePlaybackConfig:(id)remotePlayback + analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) RCT_EXTERN_METHOD( isAttachedToPlayer:(NSString *)nativeId diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 1bd3b735..f4f54c79 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -8,6 +8,9 @@ class SourceModule: NSObject, RCTBridgeModule { /// In-memory mapping from `nativeId`s to `Source` instances. private var sources: Registry = [:] + /// In-memory mapping from `nativeId`s to `SourceConfig` instances for casting. + private var castSourceConfigs: Registry = [:] + /// JS module name. static func moduleName() -> String! { "SourceModule" @@ -32,6 +35,18 @@ class SourceModule: NSObject, RCTBridgeModule { sources[nativeId] } + func nativeId(where predicate: (Source) -> Bool) -> NativeId? { + sources + .first { key, value in + predicate(value) + }? + .key + } + + func retrieveCastSourceConfig(_ nativeId: NativeId) -> SourceConfig? { + castSourceConfigs[nativeId] + } + /** Creates a new `Source` instance inside the internal sources using the provided `config` and `analyticsSourceMetadata` object and an optionally initialized DRM configuration ID. - Parameter nativeId: ID to be associated with the `Source` instance. @@ -39,8 +54,14 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter config: `SourceConfig` object received from JS. - Parameter analyticsSourceMetadata: `SourceMetadata` object received from JS. */ - @objc(initWithAnalyticsConfig:drmNativeId:config:analyticsSourceMetadata:) - func initWithAnalyticsConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, analyticsSourceMetadata: Any?) { + @objc(initWithAnalyticsConfig:drmNativeId:config:sourceRemotePlaybackConfig:analyticsSourceMetadata:) + func initWithAnalyticsConfig( + _ nativeId: NativeId, + drmNativeId: NativeId?, + config: Any?, + sourceRemotePlaybackConfig: Any, + analyticsSourceMetadata: Any? + ) { bridge.uiManager.addUIBlock { [weak self] _, _ in let fairplayConfig: FairplayConfig? if let drmNativeId = drmNativeId { @@ -56,7 +77,11 @@ class SourceModule: NSObject, RCTBridgeModule { else { return } - self?.sources[nativeId] = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) + let source = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) + self?.sources[nativeId] = source + if let remoteConfig = RCTConvert.sourceRemotePlaybackConfig(sourceRemotePlaybackConfig){ + self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig + } } } @@ -66,8 +91,13 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. */ - @objc(initWithConfig:drmNativeId:config:) - func initWithConfig(_ nativeId: NativeId, drmNativeId: NativeId?, config: Any?) { + @objc(initWithConfig:drmNativeId:config:sourceRemotePlaybackConfig:) + func initWithConfig( + _ nativeId: NativeId, + drmNativeId: NativeId?, + config: Any?, + sourceRemotePlaybackConfig: Any + ) { bridge.uiManager.addUIBlock { [weak self] _, _ in let fairplayConfig: FairplayConfig? if let drmNativeId = drmNativeId { @@ -82,7 +112,11 @@ class SourceModule: NSObject, RCTBridgeModule { else { return } - self?.sources[nativeId] = SourceFactory.create(from: sourceConfig) + let source = SourceFactory.create(from: sourceConfig) + self?.sources[nativeId] = source + if let remoteConfig = RCTConvert.sourceRemotePlaybackConfig(sourceRemotePlaybackConfig) { + self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig + } } } @@ -98,6 +132,7 @@ class SourceModule: NSObject, RCTBridgeModule { @objc(destroy:) func destroy(_ nativeId: NativeId) { sources.removeValue(forKey: nativeId) + castSourceConfigs.removeValue(forKey: nativeId) } /** @@ -219,3 +254,7 @@ class SourceModule: NSObject, RCTBridgeModule { } } } + +internal struct SourceRemotePlaybackConfig { + let castSourceConfig: SourceConfig? +} diff --git a/src/source.ts b/src/source.ts index 156ade8f..535c8bf7 100644 --- a/src/source.ts +++ b/src/source.ts @@ -136,6 +136,21 @@ export interface SourceConfig extends NativeInstanceConfig { analyticsSourceMetadata?: SourceMetadata; } +/** + * The remote playback config for a source. + * @platform iOS + */ +export interface SourceRemotePlaybackConfig { + /** + * The `SourceConfig` for casting. + * Enables to play different content when casting. + * This can be useful when the remote playback device supports different streaming formats, + * DRM systems, etc. than the local device. + * If not set, the local source config will be used for casting. + */ + castSourceConfig?: SourceConfig | null; +} + /** * Represents audio and video content that can be loaded into a player. */ @@ -144,6 +159,13 @@ export class Source extends NativeInstance { * The native DRM config reference of this source. */ drm?: Drm; + /** + * The remote playback config for this source. + * This is only used on iOS. + * + * @platform iOS + */ + remotePlayback: SourceRemotePlaybackConfig | null = null; /** * Whether the native `Source` object has been created. */ @@ -168,13 +190,15 @@ export class Source extends NativeInstance { this.nativeId, this.drm?.nativeId, this.config, + this.remotePlayback, sourceMetadata ); } else { SourceModule.initWithConfig( this.nativeId, this.drm?.nativeId, - this.config + this.config, + this.remotePlayback ); } this.isInitialized = true; From 2c57d12af5584640aecd7b19b1977580466fb886 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 14:04:08 +0200 Subject: [PATCH 61/85] feat(casting): cast DASH source from iOS --- example/src/screens/Casting.tsx | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/example/src/screens/Casting.tsx b/example/src/screens/Casting.tsx index 0cff58ce..b9bd94dc 100644 --- a/example/src/screens/Casting.tsx +++ b/example/src/screens/Casting.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { View, StyleSheet } from 'react-native'; +import { View, StyleSheet, Platform } from 'react-native'; import { useFocusEffect } from '@react-navigation/native'; import { Event, @@ -7,6 +7,7 @@ import { PlayerView, SourceType, BitmovinCastManager, + Source, } from 'bitmovin-player-react-native'; function prettyPrint(header: string, obj: any) { @@ -20,11 +21,29 @@ export default function Casting() { useFocusEffect( useCallback(() => { - player.load({ - url: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8', - type: SourceType.HLS, - title: 'BipBop - Apple sample stream', + const source = new Source({ + url: + Platform.OS === 'ios' + ? 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8' + : 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd', + type: Platform.OS === 'ios' ? SourceType.HLS : SourceType.DASH, + title: 'Art of Motion', + poster: + 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/poster.jpg', + thumbnailTrack: + 'https://cdn.bitmovin.com/content/assets/art-of-motion-dash-hls-progressive/thumbnails/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.vtt', + metadata: { platform: Platform.OS }, }); + + // Configure playing DASH source on Chromecast, even when casting from iOS. + source.remotePlayback = { + castSourceConfig: { + url: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd', + type: SourceType.DASH, + title: 'Art of Motion', + }, + }; + player.loadSource(source); return () => { player.destroy(); }; From 69da4fa1270de9e7474cf4a1fc1f24c1b1834473 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 14:07:58 +0200 Subject: [PATCH 62/85] feat(casting): fix android parameter names --- .../main/java/com/bitmovin/player/reactnative/SourceModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt index 8bbe0ce0..7bfeb2cb 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -53,7 +53,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB nativeId: NativeId, drmNativeId: NativeId?, config: ReadableMap?, - remotePlaybackJson: ReadableMap?, + sourceRemotePlaybackConfig: ReadableMap?, analyticsSourceMetadata: ReadableMap? ) { uiManager()?.addUIBlock { @@ -76,7 +76,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB nativeId: NativeId, drmNativeId: NativeId?, config: ReadableMap?, - remotePlaybackJson: ReadableMap? + sourceRemotePlaybackConfig: ReadableMap? ) { uiManager()?.addUIBlock { initializeSource(nativeId, drmNativeId, config) { sourceConfig -> From 45c33acf0102b373b627c29deeccb14e67f22f2a Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 14:14:10 +0200 Subject: [PATCH 63/85] feat(casting): improve swift code style --- ios/SourceModule.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index f4f54c79..b4b3ba24 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -36,11 +36,9 @@ class SourceModule: NSObject, RCTBridgeModule { } func nativeId(where predicate: (Source) -> Bool) -> NativeId? { - sources - .first { key, value in - predicate(value) - }? - .key + sources.first { _, value in + predicate(value) + }?.key } func retrieveCastSourceConfig(_ nativeId: NativeId) -> SourceConfig? { From 7cda7eb72305c7b9b6f695acae2cd17dc0331287 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 14:23:13 +0200 Subject: [PATCH 64/85] feat(casting): add missing documentation --- ios/SourceModule.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index b4b3ba24..81b5f2b3 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -35,12 +35,14 @@ class SourceModule: NSObject, RCTBridgeModule { sources[nativeId] } + // Finds `NativeId` based on predicate ran on `Source` instances func nativeId(where predicate: (Source) -> Bool) -> NativeId? { sources.first { _, value in predicate(value) }?.key } + // Fetches cast-specific `SourceConfig` by `NativeId` if exists func retrieveCastSourceConfig(_ nativeId: NativeId) -> SourceConfig? { castSourceConfigs[nativeId] } From 0669011e46994903dedbec58dba2f75a514205f8 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 14:24:09 +0200 Subject: [PATCH 65/85] feat(casting): improve swift code style --- ios/SourceModule.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 81b5f2b3..18024c06 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -77,8 +77,7 @@ class SourceModule: NSObject, RCTBridgeModule { else { return } - let source = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) - self?.sources[nativeId] = source + self?.sources[nativeId] = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) if let remoteConfig = RCTConvert.sourceRemotePlaybackConfig(sourceRemotePlaybackConfig){ self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig } @@ -112,8 +111,7 @@ class SourceModule: NSObject, RCTBridgeModule { else { return } - let source = SourceFactory.create(from: sourceConfig) - self?.sources[nativeId] = source + self?.sources[nativeId] = SourceFactory.create(from: sourceConfig) if let remoteConfig = RCTConvert.sourceRemotePlaybackConfig(sourceRemotePlaybackConfig) { self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig } From ca1d83ca8a405c63bf5fedc3f041e9557e1f2e18 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 16:07:26 +0200 Subject: [PATCH 66/85] feat(casting): refactor prepareSource wrapper --- ios/PlayerModule.swift | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/ios/PlayerModule.swift b/ios/PlayerModule.swift index 4fcc63a4..79e45b64 100644 --- a/ios/PlayerModule.swift +++ b/ios/PlayerModule.swift @@ -45,21 +45,12 @@ class PlayerModule: NSObject, RCTBridgeModule { else { return } - let player = PlayerFactory.create(playerConfig: playerConfig) - self?.players[nativeId] = player - playerConfig.remoteControlConfig.prepareSource = { [weak self] type, sourceConfig in - guard let sourceModule = self?.bridge[SourceModule.self], - let sourceNativeId = sourceModule.nativeId(where: { $0.sourceConfig === sourceConfig }), - let castSourceConfig = sourceModule.retrieveCastSourceConfig(sourceNativeId) else { - return nil - } - - return castSourceConfig - } + self?.setupRemoteControlConfig(playerConfig.remoteControlConfig) + self?.players[nativeId] = PlayerFactory.create(playerConfig: playerConfig) } } - + /** Creates a new analytics enabled `Player` instance inside the internal players using the provided `config` and `analyticsConfig` object. - Parameter config: `PlayerConfig` object received from JS. @@ -77,23 +68,13 @@ class PlayerModule: NSObject, RCTBridgeModule { return } + self?.setupRemoteControlConfig(playerConfig.remoteControlConfig) let defaultMetadata = RCTConvert.analyticsDefaultMetadataFromAnalyticsConfig(analyticsConfigJson) - let player = PlayerFactory.create( + self?.players[nativeId] = PlayerFactory.create( playerConfig: playerConfig, analyticsConfig: analyticsConfig, defaultMetadata: defaultMetadata ?? DefaultMetadata() ) - self?.players[nativeId] = player - - playerConfig.remoteControlConfig.prepareSource = { [weak self] type, sourceConfig in - guard let sourceModule = self?.bridge[SourceModule.self], - let sourceNativeId = sourceModule.nativeId(where: { $0.sourceConfig === sourceConfig }), - let castSourceConfig = sourceModule.retrieveCastSourceConfig(sourceNativeId) else { - return nil - } - - return castSourceConfig - } } } @@ -685,4 +666,16 @@ class PlayerModule: NSObject, RCTBridgeModule { self?.players[nativeId]?.castStop() } } + + private func setupRemoteControlConfig(_ remoteControlConfig: RemoteControlConfig) { + remoteControlConfig.prepareSource = { [weak self] type, sourceConfig in + guard let sourceModule = self?.bridge[SourceModule.self], + let sourceNativeId = sourceModule.nativeId(where: { $0.sourceConfig === sourceConfig }), + let castSourceConfig = sourceModule.retrieveCastSourceConfig(sourceNativeId) else { + return nil + } + + return castSourceConfig + } + } } From b3e5461564fc9709c662eb9ff2aa08f3e7a41018 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 22:13:37 +0200 Subject: [PATCH 67/85] feat(casting): enable DRM playback for casting --- ios/DrmModule.swift | 44 +++++++++++----------- ios/RCTConvert+BitmovinPlayer.swift | 57 ++++++++++++++++++++++++----- ios/SourceModule.swift | 16 ++++---- src/drm/widevineConfig.ts | 4 +- 4 files changed, 79 insertions(+), 42 deletions(-) diff --git a/ios/DrmModule.swift b/ios/DrmModule.swift index 0cef2fd2..68cb6c8f 100644 --- a/ios/DrmModule.swift +++ b/ios/DrmModule.swift @@ -6,7 +6,7 @@ class DrmModule: NSObject, RCTBridgeModule { @objc var bridge: RCTBridge! /// In-memory mapping from `nativeId`s to `FairplayConfig` instances. - private var drmConfigs: Registry = [:] + private var drmConfigs: Registry = [:] /// JS module name. static func moduleName() -> String! { @@ -28,7 +28,7 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId: ID to associate with the `FairplayConfig` instance. - Returns: The associated `FairplayConfig` instance or `nil`. */ - @objc func retrieve(_ nativeId: NativeId) -> FairplayConfig? { + @objc func retrieve(_ nativeId: NativeId) -> DrmConfig? { drmConfigs[nativeId] } @@ -42,11 +42,11 @@ class DrmModule: NSObject, RCTBridgeModule { bridge.uiManager.addUIBlock { [weak self] _, _ in guard self?.drmConfigs[nativeId] == nil, - let fairplayConfig = RCTConvert.fairplayConfig(config) + let drmConfig = RCTConvert.drmConfig(config) else { return } - self?.drmConfigs[nativeId] = fairplayConfig + self?.drmConfigs[nativeId] = drmConfig self?.initConfigBlocks(nativeId, config) } } @@ -181,12 +181,12 @@ class DrmModule: NSObject, RCTBridgeModule { */ private func initConfigBlocks(_ nativeId: NativeId, _ config: Any?) { if let json = config as? [String: Any], let fairplayJson = json["fairplay"] as? [String: Any] { - initPrepareCertificate(nativeId, fairplayJson: fairplayJson) - initPrepareMessage(nativeId, fairplayJson: fairplayJson) - initPrepareSyncMessage(nativeId, fairplayJson: fairplayJson) - initPrepareLicense(nativeId, fairplayJson: fairplayJson) - initPrepareLicenseServerUrl(nativeId, fairplayJson: fairplayJson) - initPrepareContentId(nativeId, fairplayJson: fairplayJson) + initFairplayPrepareCertificate(nativeId, fairplayJson: fairplayJson) + initFairplayPrepareMessage(nativeId, fairplayJson: fairplayJson) + initFairplayPrepareSyncMessage(nativeId, fairplayJson: fairplayJson) + initFairplayPrepareLicense(nativeId, fairplayJson: fairplayJson) + initFairplayPrepareLicenseServerUrl(nativeId, fairplayJson: fairplayJson) + initFairplayPrepareContentId(nativeId, fairplayJson: fairplayJson) } } @@ -196,8 +196,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initPrepareCertificate(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] else { + private func initFairplayPrepareCertificate(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { return } if fairplayJson["prepareCertificate"] != nil { @@ -213,8 +213,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initPrepareMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] else { + private func initFairplayPrepareMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { return } if fairplayJson["prepareMessage"] != nil { @@ -230,8 +230,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initPrepareSyncMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] else { + private func initFairplayPrepareSyncMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { return } if fairplayJson["prepareSyncMessage"] != nil { @@ -247,8 +247,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initPrepareLicense(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] else { + private func initFairplayPrepareLicense(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { return } if fairplayJson["prepareLicense"] != nil { @@ -264,8 +264,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initPrepareLicenseServerUrl(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] else { + private func initFairplayPrepareLicenseServerUrl(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { return } if fairplayJson["prepareLicenseServerUrl"] != nil { @@ -281,8 +281,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initPrepareContentId(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] else { + private func initFairplayPrepareContentId(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { return } if fairplayJson["prepareContentId"] != nil { diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index abf3770e..8a028488 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -228,7 +228,7 @@ extension RCTConvert { - Parameter json: JS object - Returns: The produced `SourceConfig` object */ - static func sourceConfig(_ json: Any?, drmConfig: FairplayConfig? = nil) -> SourceConfig? { + static func sourceConfig(_ json: Any?, drmConfig: DrmConfig? = nil) -> SourceConfig? { guard let json = json as? [String: Any?] else { return nil } @@ -335,33 +335,65 @@ extension RCTConvert { } } + /** + Utility method to get a `DrmConfig` from a JS object. + - Parameter json: JS object + - Returns: The generated `DrmConfig` object + */ + static func drmConfig(_ json: Any?) -> DrmConfig? { + guard let json = json as? [String: Any?] else { + return nil + } + if let fairplayConfig = RCTConvert.fairplayConfig(json["fairplay"]) { + return fairplayConfig + } + if let widevineConfig = RCTConvert.widevineConfig(json["widevine"]) { + return widevineConfig + } + return nil + } + /** Utility method to get a `FairplayConfig` from a JS object. - Parameter json: JS object - Returns: The generated `FairplayConfig` object */ static func fairplayConfig(_ json: Any?) -> FairplayConfig? { - guard - let json = json as? [String: Any?], - let fairplayJson = json["fairplay"] as? [String: Any?], - let licenseURL = fairplayJson["licenseUrl"] as? String, - let certificateURL = fairplayJson["certificateUrl"] as? String - else { + guard let json = json as? [String: Any?], + let licenseURL = json["licenseUrl"] as? String, + let certificateURL = json["certificateUrl"] as? String else { return nil } let fairplayConfig = FairplayConfig( license: URL(string: licenseURL), certificateURL: URL(string: certificateURL)! ) - if let licenseRequestHeaders = fairplayJson["licenseRequestHeaders"] as? [String: String] { + if let licenseRequestHeaders = json["licenseRequestHeaders"] as? [String: String] { fairplayConfig.licenseRequestHeaders = licenseRequestHeaders } - if let certificateRequestHeaders = fairplayJson["certificateRequestHeaders"] as? [String: String] { + if let certificateRequestHeaders = json["certificateRequestHeaders"] as? [String: String] { fairplayConfig.certificateRequestHeaders = certificateRequestHeaders } return fairplayConfig } + /** + Utility method to get a `WidevineConfig` from a JS object. + - Parameter json: JS object + - Returns: The generated `WidevineConfig` object + */ + static func widevineConfig(_ json: Any?) -> WidevineConfig? { + guard let json = json as? [String: Any?], + let licenseURL = json["licenseUrl"] as? String else { + return nil + } + let widevineConfig = WidevineConfig(license: URL(string: licenseURL)) + if let licenseRequestHeaders = json["httpHeaders"] as? [String: String] { + widevineConfig.licenseRequestHeaders = licenseRequestHeaders + } + return widevineConfig + } + /** Utility method to get a `ThumbnailTrack` instance from a JS object. - Parameter url: String. @@ -988,8 +1020,13 @@ extension RCTConvert { return nil } + let castSourceConfig = RCTConvert.sourceConfig(json["castSourceConfig"]) + if let castSourceConfigJson = json["castSourceConfig"] as? [String: Any?], + let drmConfig = RCTConvert.drmConfig(castSourceConfigJson["drmConfig"]) { + castSourceConfig?.drmConfig = drmConfig + } return SourceRemotePlaybackConfig( - castSourceConfig: RCTConvert.sourceConfig(json["castSourceConfig"]) + castSourceConfig: castSourceConfig ) } } diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 18024c06..2dcdb7a1 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -63,16 +63,16 @@ class SourceModule: NSObject, RCTBridgeModule { analyticsSourceMetadata: Any? ) { bridge.uiManager.addUIBlock { [weak self] _, _ in - let fairplayConfig: FairplayConfig? + let drmConfig: DrmConfig? if let drmNativeId = drmNativeId { - fairplayConfig = self?.getDrmModule()?.retrieve(drmNativeId) + drmConfig = self?.getDrmModule()?.retrieve(drmNativeId) } else { - fairplayConfig = nil + drmConfig = nil } guard self?.sources[nativeId] == nil, - let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig), + let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: drmConfig), let sourceMetadata = RCTConvert.analyticsSourceMetadata(analyticsSourceMetadata) else { return @@ -98,16 +98,16 @@ class SourceModule: NSObject, RCTBridgeModule { sourceRemotePlaybackConfig: Any ) { bridge.uiManager.addUIBlock { [weak self] _, _ in - let fairplayConfig: FairplayConfig? + let drmConfig: DrmConfig? if let drmNativeId = drmNativeId { - fairplayConfig = self?.getDrmModule()?.retrieve(drmNativeId) + drmConfig = self?.getDrmModule()?.retrieve(drmNativeId) } else { - fairplayConfig = nil + drmConfig = nil } guard self?.sources[nativeId] == nil, - let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: fairplayConfig) + let sourceConfig = RCTConvert.sourceConfig(config, drmConfig: drmConfig) else { return } diff --git a/src/drm/widevineConfig.ts b/src/drm/widevineConfig.ts index efdf3f30..a6f4206d 100644 --- a/src/drm/widevineConfig.ts +++ b/src/drm/widevineConfig.ts @@ -10,7 +10,7 @@ export interface WidevineConfig { /** * A map containing the HTTP request headers, or null. */ - httpHeaders: Record; + httpHeaders?: Record; /** * A block to prepare the data which is sent as the body of the POST license request. * As many DRM providers expect different, vendor-specific messages, this can be done using @@ -45,5 +45,5 @@ export interface WidevineConfig { * as the same DRM scheme information. * Default: `false` */ - shouldKeepDrmSessionsAlive: boolean; + shouldKeepDrmSessionsAlive?: boolean; } From d4ecac66f3a47e578712e05e9c85ee3ec69eb901 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 22:16:14 +0200 Subject: [PATCH 68/85] feat(casting): update documentation --- src/drm/index.ts | 8 ++++++-- src/drm/widevineConfig.ts | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/drm/index.ts b/src/drm/index.ts index 6b054e3d..723ea01f 100644 --- a/src/drm/index.ts +++ b/src/drm/index.ts @@ -14,11 +14,15 @@ const DrmModule = NativeModules.DrmModule; */ export interface DrmConfig extends NativeInstanceConfig { /** - * FairPlay specific configuration. Only applicable for iOS. + * FairPlay specific configuration. + * + * @platform iOS */ fairplay?: FairplayConfig; /** - * Widevine specific configuration. Only applicable for Android. + * Widevine specific configuration. + * + * @platform Android, iOS (only for casting). */ widevine?: WidevineConfig; } diff --git a/src/drm/widevineConfig.ts b/src/drm/widevineConfig.ts index a6f4206d..5b409e6e 100644 --- a/src/drm/widevineConfig.ts +++ b/src/drm/widevineConfig.ts @@ -1,6 +1,6 @@ /** * Represents a Widevine Streaming DRM config. - * Android only. + * @platform Android, iOS (only for casting). */ export interface WidevineConfig { /** @@ -19,6 +19,8 @@ export interface WidevineConfig { * Note that both the passed `message` data and this block return value should be a Base64 string. * So use whatever solution suits you best to handle Base64 in React Native. * + * @platform Android + * * @param message - Base64 encoded message data. * @returns The processed Base64 encoded message. */ @@ -31,12 +33,16 @@ export interface WidevineConfig { * Note that both the passed `license` data and this block return value should be a Base64 string. * So use whatever solution suits you best to handle Base64 in React Native. * + * @platform Android + * * @param license - Base64 encoded license data. * @returns The processed Base64 encoded license. */ prepareLicense?: (license: string) => string; /** * Set widevine's preferred security level. + * + * @platform Android */ preferredSecurityLevel?: string; /** @@ -44,6 +50,8 @@ export interface WidevineConfig { * This allows DRM sessions to be reused over several different source items with the same DRM configuration as well * as the same DRM scheme information. * Default: `false` + * + * @platform Android */ shouldKeepDrmSessionsAlive?: boolean; } From 8fe2a9aaa36d189ec8584243dbd1eeeb8247efce Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 22:22:36 +0200 Subject: [PATCH 69/85] feat(casting): improve code style --- ios/RCTConvert+BitmovinPlayer.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index 8a028488..9a377967 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -1016,17 +1016,12 @@ extension RCTConvert { } static func sourceRemotePlaybackConfig(_ json: Any?) -> SourceRemotePlaybackConfig? { - guard let json = json as? [String: Any?] else { + guard let json = json as? [String: Any?], + let castSourceConfigJson = json["castSourceConfig"] as? [String: Any?], + let castSourceConfig = RCTConvert.sourceConfig(json["castSourceConfig"]) else { return nil } - - let castSourceConfig = RCTConvert.sourceConfig(json["castSourceConfig"]) - if let castSourceConfigJson = json["castSourceConfig"] as? [String: Any?], - let drmConfig = RCTConvert.drmConfig(castSourceConfigJson["drmConfig"]) { - castSourceConfig?.drmConfig = drmConfig - } - return SourceRemotePlaybackConfig( - castSourceConfig: castSourceConfig - ) + castSourceConfig.drmConfig = RCTConvert.drmConfig(castSourceConfigJson["drmConfig"]) + return SourceRemotePlaybackConfig(castSourceConfig: castSourceConfig) } } From f2c2c1bfc6fbf514ffbb5ff34c1b34f6dba6ce8c Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Thu, 21 Sep 2023 22:23:31 +0200 Subject: [PATCH 70/85] feat(casting): add missing documentation --- ios/RCTConvert+BitmovinPlayer.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index abf3770e..7435762a 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -983,6 +983,11 @@ extension RCTConvert { ] } + /** + Utility method to instantiate a `SourceRemotePlaybackConfig` from a JS object. + - Parameter json: JS object + - Returns: The produced `SourceRemotePlaybackConfig` object + */ static func sourceRemotePlaybackConfig(_ json: Any?) -> SourceRemotePlaybackConfig? { guard let json = json as? [String: Any?] else { return nil From dfda37f0d8696b449266824dd1bbf2b3d3c831f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20K=C3=A1konyi?= Date: Fri, 22 Sep 2023 09:06:34 +0200 Subject: [PATCH 71/85] feat(casting): improve API docs Co-authored-by: Lukas Knoch-Girstmair --- src/source.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source.ts b/src/source.ts index 535c8bf7..b0a89f62 100644 --- a/src/source.ts +++ b/src/source.ts @@ -161,7 +161,7 @@ export class Source extends NativeInstance { drm?: Drm; /** * The remote playback config for this source. - * This is only used on iOS. + * This is only supported on iOS. * * @platform iOS */ From 852d26fd1e05b53e968b89e7a34b9e8503a2d6d8 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 09:12:36 +0200 Subject: [PATCH 72/85] feat(casting): rename SourceRemotePlaybackConfig to SourceRemoteControlConfig --- .../bitmovin/player/reactnative/SourceModule.kt | 6 ++++-- ios/RCTConvert+BitmovinPlayer.swift | 8 ++++---- ios/SourceModule.m | 4 ++-- ios/SourceModule.swift | 14 +++++++------- src/source.ts | 4 ++-- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt index 7bfeb2cb..c95c3c56 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -46,6 +46,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId ID to be associated with the `Source` instance. * @param drmNativeId ID of the DRM config to use. * @param config `SourceConfig` object received from JS. + * @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. * @param analyticsSourceMetadata `SourceMetadata` object received from JS. */ @ReactMethod @@ -53,7 +54,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB nativeId: NativeId, drmNativeId: NativeId?, config: ReadableMap?, - sourceRemotePlaybackConfig: ReadableMap?, + sourceRemoteControlConfig: ReadableMap?, analyticsSourceMetadata: ReadableMap? ) { uiManager()?.addUIBlock { @@ -70,13 +71,14 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId ID to be associated with the `Source` instance. * @param drmNativeId ID of the DRM config to use. * @param config `SourceConfig` object received from JS. + * @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. */ @ReactMethod fun initWithConfig( nativeId: NativeId, drmNativeId: NativeId?, config: ReadableMap?, - sourceRemotePlaybackConfig: ReadableMap? + sourceRemoteControlConfig: ReadableMap? ) { uiManager()?.addUIBlock { initializeSource(nativeId, drmNativeId, config) { sourceConfig -> diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index 7435762a..f63a01fd 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -984,16 +984,16 @@ extension RCTConvert { } /** - Utility method to instantiate a `SourceRemotePlaybackConfig` from a JS object. + Utility method to instantiate a `SourceRemoteControlConfig` from a JS object. - Parameter json: JS object - - Returns: The produced `SourceRemotePlaybackConfig` object + - Returns: The produced `SourceRemoteControlConfig` object */ - static func sourceRemotePlaybackConfig(_ json: Any?) -> SourceRemotePlaybackConfig? { + static func sourceRemoteControlConfig(_ json: Any?) -> SourceRemoteControlConfig? { guard let json = json as? [String: Any?] else { return nil } - return SourceRemotePlaybackConfig( + return SourceRemoteControlConfig( castSourceConfig: RCTConvert.sourceConfig(json["castSourceConfig"]) ) } diff --git a/ios/SourceModule.m b/ios/SourceModule.m index cf7e36c5..89c24758 100644 --- a/ios/SourceModule.m +++ b/ios/SourceModule.m @@ -5,11 +5,11 @@ @interface RCT_EXTERN_REMAP_MODULE(SourceModule, SourceModule, NSObject) RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config - sourceRemotePlaybackConfig:(id)remotePlayback) + sourceRemoteControlConfig:(id)remotePlayback) RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId drmNativeId:(NSString *)drmNativeId config:(nullable id)config - sourceRemotePlaybackConfig:(id)remotePlayback + sourceRemoteControlConfig:(id)remotePlayback analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) RCT_EXTERN_METHOD( diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 18024c06..8049a361 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -54,12 +54,12 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter config: `SourceConfig` object received from JS. - Parameter analyticsSourceMetadata: `SourceMetadata` object received from JS. */ - @objc(initWithAnalyticsConfig:drmNativeId:config:sourceRemotePlaybackConfig:analyticsSourceMetadata:) + @objc(initWithAnalyticsConfig:drmNativeId:config:sourceRemoteControlConfig:analyticsSourceMetadata:) func initWithAnalyticsConfig( _ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, - sourceRemotePlaybackConfig: Any, + sourceRemoteControlConfig: Any, analyticsSourceMetadata: Any? ) { bridge.uiManager.addUIBlock { [weak self] _, _ in @@ -78,7 +78,7 @@ class SourceModule: NSObject, RCTBridgeModule { return } self?.sources[nativeId] = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) - if let remoteConfig = RCTConvert.sourceRemotePlaybackConfig(sourceRemotePlaybackConfig){ + if let remoteConfig = RCTConvert.sourceRemoteControlConfig(sourceRemoteControlConfig){ self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig } } @@ -90,12 +90,12 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. */ - @objc(initWithConfig:drmNativeId:config:sourceRemotePlaybackConfig:) + @objc(initWithConfig:drmNativeId:config:sourceRemoteControlConfig:) func initWithConfig( _ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, - sourceRemotePlaybackConfig: Any + sourceRemoteControlConfig: Any ) { bridge.uiManager.addUIBlock { [weak self] _, _ in let fairplayConfig: FairplayConfig? @@ -112,7 +112,7 @@ class SourceModule: NSObject, RCTBridgeModule { return } self?.sources[nativeId] = SourceFactory.create(from: sourceConfig) - if let remoteConfig = RCTConvert.sourceRemotePlaybackConfig(sourceRemotePlaybackConfig) { + if let remoteConfig = RCTConvert.sourceRemoteControlConfig(sourceRemoteControlConfig) { self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig } } @@ -253,6 +253,6 @@ class SourceModule: NSObject, RCTBridgeModule { } } -internal struct SourceRemotePlaybackConfig { +internal struct SourceRemoteControlConfig { let castSourceConfig: SourceConfig? } diff --git a/src/source.ts b/src/source.ts index b0a89f62..06d5cc3e 100644 --- a/src/source.ts +++ b/src/source.ts @@ -140,7 +140,7 @@ export interface SourceConfig extends NativeInstanceConfig { * The remote playback config for a source. * @platform iOS */ -export interface SourceRemotePlaybackConfig { +export interface SourceRemoteControlConfig { /** * The `SourceConfig` for casting. * Enables to play different content when casting. @@ -165,7 +165,7 @@ export class Source extends NativeInstance { * * @platform iOS */ - remotePlayback: SourceRemotePlaybackConfig | null = null; + remotePlayback: SourceRemoteControlConfig | null = null; /** * Whether the native `Source` object has been created. */ From ddff29718e6ca3d349e186a9ca066d907703f17f Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 09:14:17 +0200 Subject: [PATCH 73/85] feat(casting): fix property name on source for remote control config --- src/source.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/source.ts b/src/source.ts index 06d5cc3e..bfc46cc6 100644 --- a/src/source.ts +++ b/src/source.ts @@ -165,7 +165,7 @@ export class Source extends NativeInstance { * * @platform iOS */ - remotePlayback: SourceRemoteControlConfig | null = null; + remoteControl: SourceRemoteControlConfig | null = null; /** * Whether the native `Source` object has been created. */ @@ -190,7 +190,7 @@ export class Source extends NativeInstance { this.nativeId, this.drm?.nativeId, this.config, - this.remotePlayback, + this.remoteControl, sourceMetadata ); } else { @@ -198,7 +198,7 @@ export class Source extends NativeInstance { this.nativeId, this.drm?.nativeId, this.config, - this.remotePlayback + this.remoteControl ); } this.isInitialized = true; From 85c58e781a68f459db5d640ccaea2bcd48c2985c Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 09:15:41 +0200 Subject: [PATCH 74/85] feat(casting): fix indentation --- ios/SourceModule.m | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/SourceModule.m b/ios/SourceModule.m index 89c24758..7d5d4024 100644 --- a/ios/SourceModule.m +++ b/ios/SourceModule.m @@ -3,14 +3,14 @@ @interface RCT_EXTERN_REMAP_MODULE(SourceModule, SourceModule, NSObject) RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId - drmNativeId:(NSString *)drmNativeId - config:(nullable id)config - sourceRemoteControlConfig:(id)remotePlayback) + drmNativeId:(NSString *)drmNativeId + config:(nullable id)config + sourceRemoteControlConfig:(id)remotePlayback) RCT_EXTERN_METHOD(initWithAnalyticsConfig:(NSString *)nativeId - drmNativeId:(NSString *)drmNativeId - config:(nullable id)config - sourceRemoteControlConfig:(id)remotePlayback - analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) + drmNativeId:(NSString *)drmNativeId + config:(nullable id)config + sourceRemoteControlConfig:(id)remotePlayback + analyticsSourceMetadata:(nullable id)analyticsSourceMetadata) RCT_EXTERN_METHOD(destroy:(NSString *)nativeId) RCT_EXTERN_METHOD( isAttachedToPlayer:(NSString *)nativeId From ee6f953940116202e9a527f56c1abe1a57d0f27a Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 09:17:28 +0200 Subject: [PATCH 75/85] feat(casting): fix optionality --- ios/SourceModule.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 8049a361..35a67e24 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -59,7 +59,7 @@ class SourceModule: NSObject, RCTBridgeModule { _ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, - sourceRemoteControlConfig: Any, + sourceRemoteControlConfig: Any?, analyticsSourceMetadata: Any? ) { bridge.uiManager.addUIBlock { [weak self] _, _ in @@ -95,7 +95,7 @@ class SourceModule: NSObject, RCTBridgeModule { _ nativeId: NativeId, drmNativeId: NativeId?, config: Any?, - sourceRemoteControlConfig: Any + sourceRemoteControlConfig: Any? ) { bridge.uiManager.addUIBlock { [weak self] _, _ in let fairplayConfig: FairplayConfig? From 454fbbaf0b22ccf05129d714ef3928c63f5eaea0 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 09:47:01 +0200 Subject: [PATCH 76/85] feat(casting): improve DRM handling --- ios/DrmModule.swift | 44 ++++++++++++++--------------- ios/RCTConvert+BitmovinPlayer.swift | 26 ++++++++--------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/ios/DrmModule.swift b/ios/DrmModule.swift index 68cb6c8f..9229eabc 100644 --- a/ios/DrmModule.swift +++ b/ios/DrmModule.swift @@ -6,7 +6,7 @@ class DrmModule: NSObject, RCTBridgeModule { @objc var bridge: RCTBridge! /// In-memory mapping from `nativeId`s to `FairplayConfig` instances. - private var drmConfigs: Registry = [:] + private var drmConfigs: Registry = [:] /// JS module name. static func moduleName() -> String! { @@ -28,7 +28,7 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId: ID to associate with the `FairplayConfig` instance. - Returns: The associated `FairplayConfig` instance or `nil`. */ - @objc func retrieve(_ nativeId: NativeId) -> DrmConfig? { + @objc func retrieve(_ nativeId: NativeId) -> FairplayConfig? { drmConfigs[nativeId] } @@ -42,11 +42,11 @@ class DrmModule: NSObject, RCTBridgeModule { bridge.uiManager.addUIBlock { [weak self] _, _ in guard self?.drmConfigs[nativeId] == nil, - let drmConfig = RCTConvert.drmConfig(config) + let fairplayConfig = RCTConvert.drmConfig(config).fairplay else { return } - self?.drmConfigs[nativeId] = drmConfig + self?.drmConfigs[nativeId] = fairplayConfig self?.initConfigBlocks(nativeId, config) } } @@ -181,12 +181,12 @@ class DrmModule: NSObject, RCTBridgeModule { */ private func initConfigBlocks(_ nativeId: NativeId, _ config: Any?) { if let json = config as? [String: Any], let fairplayJson = json["fairplay"] as? [String: Any] { - initFairplayPrepareCertificate(nativeId, fairplayJson: fairplayJson) - initFairplayPrepareMessage(nativeId, fairplayJson: fairplayJson) - initFairplayPrepareSyncMessage(nativeId, fairplayJson: fairplayJson) - initFairplayPrepareLicense(nativeId, fairplayJson: fairplayJson) - initFairplayPrepareLicenseServerUrl(nativeId, fairplayJson: fairplayJson) - initFairplayPrepareContentId(nativeId, fairplayJson: fairplayJson) + initPrepareCertificate(nativeId, fairplayJson: fairplayJson) + initPrepareMessage(nativeId, fairplayJson: fairplayJson) + initPrepareSyncMessage(nativeId, fairplayJson: fairplayJson) + initPrepareLicense(nativeId, fairplayJson: fairplayJson) + initPrepareLicenseServerUrl(nativeId, fairplayJson: fairplayJson) + initPrepareContentId(nativeId, fairplayJson: fairplayJson) } } @@ -196,8 +196,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initFairplayPrepareCertificate(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { + private func initPrepareCertificate(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] else { return } if fairplayJson["prepareCertificate"] != nil { @@ -213,8 +213,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initFairplayPrepareMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { + private func initPrepareMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] else { return } if fairplayJson["prepareMessage"] != nil { @@ -230,8 +230,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initFairplayPrepareSyncMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { + private func initPrepareSyncMessage(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] else { return } if fairplayJson["prepareSyncMessage"] != nil { @@ -247,8 +247,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initFairplayPrepareLicense(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { + private func initPrepareLicense(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] else { return } if fairplayJson["prepareLicense"] != nil { @@ -264,8 +264,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initFairplayPrepareLicenseServerUrl(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { + private func initPrepareLicenseServerUrl(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] else { return } if fairplayJson["prepareLicenseServerUrl"] != nil { @@ -281,8 +281,8 @@ class DrmModule: NSObject, RCTBridgeModule { - Parameter nativeId - Instance nativeId. - Parameter config: FairPlay config object sent from JS. */ - private func initFairplayPrepareContentId(_ nativeId: NativeId, fairplayJson: [String: Any]) { - guard let fairplayConfig = drmConfigs[nativeId] as? FairplayConfig else { + private func initPrepareContentId(_ nativeId: NativeId, fairplayJson: [String: Any]) { + guard let fairplayConfig = drmConfigs[nativeId] else { return } if fairplayJson["prepareContentId"] != nil { diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index dcc76586..b42c0d6f 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -340,17 +340,14 @@ extension RCTConvert { - Parameter json: JS object - Returns: The generated `DrmConfig` object */ - static func drmConfig(_ json: Any?) -> DrmConfig? { + static func drmConfig(_ json: Any?) -> (fairplay: FairplayConfig?, widevine: WidevineConfig?) { guard let json = json as? [String: Any?] else { - return nil - } - if let fairplayConfig = RCTConvert.fairplayConfig(json["fairplay"]) { - return fairplayConfig - } - if let widevineConfig = RCTConvert.widevineConfig(json["widevine"]) { - return widevineConfig + return (nil, nil) } - return nil + return ( + fairplay: RCTConvert.fairplayConfig(json["fairplay"]), + widevine: RCTConvert.widevineConfig(json["widevine"]) + ) } /** @@ -1022,11 +1019,14 @@ extension RCTConvert { */ static func sourceRemoteControlConfig(_ json: Any?) -> SourceRemoteControlConfig? { guard let json = json as? [String: Any?], - let castSourceConfigJson = json["castSourceConfig"] as? [String: Any?], - let castSourceConfig = RCTConvert.sourceConfig(json["castSourceConfig"]) else { + let castSourceConfigJson = json["castSourceConfig"] as? [String: Any?] else { return nil } - castSourceConfig.drmConfig = RCTConvert.drmConfig(castSourceConfigJson["drmConfig"]) - return SourceRemoteControlConfig(castSourceConfig: castSourceConfig) + return SourceRemoteControlConfig( + castSourceConfig: RCTConvert.sourceConfig( + json["castSourceConfig"], + drmConfig: RCTConvert.drmConfig(castSourceConfigJson["drmConfig"]).widevine + ) + ) } } From 2fe285fd3aaf95c2ed798af1c66814ba475e3be8 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 11:13:42 +0200 Subject: [PATCH 77/85] feat(casting): mention that sourceRemoteControlConfig is not supported on Android --- .../main/java/com/bitmovin/player/reactnative/SourceModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt index c95c3c56..d1d2d3e2 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt @@ -46,7 +46,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId ID to be associated with the `Source` instance. * @param drmNativeId ID of the DRM config to use. * @param config `SourceConfig` object received from JS. - * @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. + * @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. Not supported on Android. * @param analyticsSourceMetadata `SourceMetadata` object received from JS. */ @ReactMethod @@ -71,7 +71,7 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB * @param nativeId ID to be associated with the `Source` instance. * @param drmNativeId ID of the DRM config to use. * @param config `SourceConfig` object received from JS. - * @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. + * @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. Not supported on Android. */ @ReactMethod fun initWithConfig( From da165147a4cca8c5c1e49b9707212ca3e6389ca3 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 11:15:46 +0200 Subject: [PATCH 78/85] feat(casting): fix documentation --- ios/SourceModule.swift | 2 ++ src/source.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 35a67e24..154e2684 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -53,6 +53,7 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. - Parameter analyticsSourceMetadata: `SourceMetadata` object received from JS. + - Parameter sourceRemoteControlConfig: `SourceRemoteControlConfig` object received from JS. */ @objc(initWithAnalyticsConfig:drmNativeId:config:sourceRemoteControlConfig:analyticsSourceMetadata:) func initWithAnalyticsConfig( @@ -89,6 +90,7 @@ class SourceModule: NSObject, RCTBridgeModule { - Parameter nativeId: ID to be associated with the `Source` instance. - Parameter drmNativeId: ID of the DRM config object to use. - Parameter config: `SourceConfig` object received from JS. + - Parameter sourceRemoteControlConfig: `SourceRemoteControlConfig` object received from JS. */ @objc(initWithConfig:drmNativeId:config:sourceRemoteControlConfig:) func initWithConfig( diff --git a/src/source.ts b/src/source.ts index bfc46cc6..83536e45 100644 --- a/src/source.ts +++ b/src/source.ts @@ -137,7 +137,7 @@ export interface SourceConfig extends NativeInstanceConfig { } /** - * The remote playback config for a source. + * The remote control config for a source. * @platform iOS */ export interface SourceRemoteControlConfig { @@ -160,7 +160,7 @@ export class Source extends NativeInstance { */ drm?: Drm; /** - * The remote playback config for this source. + * The remote control config for this source. * This is only supported on iOS. * * @platform iOS From 65cee12006f9009a305c3b39ddefcbe36af8900c Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 11:33:13 +0200 Subject: [PATCH 79/85] feat(casting): fix casting sample screen --- example/src/screens/Casting.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/screens/Casting.tsx b/example/src/screens/Casting.tsx index ffe43f1d..7846181c 100644 --- a/example/src/screens/Casting.tsx +++ b/example/src/screens/Casting.tsx @@ -41,7 +41,7 @@ export default function Casting() { }); // Configure playing DASH source on Chromecast, even when casting from iOS. - source.remotePlayback = { + source.remoteControl = { castSourceConfig: { url: 'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd', type: SourceType.DASH, From 07943dfa90afe92dd1280a7ac412346b3c012909 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 12:29:25 +0200 Subject: [PATCH 80/85] feat(casting): fix tvOS build --- .../project.pbxproj | 6 ++++++ ios/BitmovinCastManagerModule.swift | 8 ++++++++ ios/Event+JSON.swift | 2 ++ ios/PlayerModule.swift | 6 ++++-- ios/RCTConvert+BitmovinPlayer.swift | 4 ++++ ios/RNPlayerView+PlayerListener.swift | 3 ++- ios/SourceModule.swift | 4 ++++ 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/example/ios/BitmovinPlayerReactNativeExample.xcodeproj/project.pbxproj b/example/ios/BitmovinPlayerReactNativeExample.xcodeproj/project.pbxproj index 4e8de29c..33f94a2d 100644 --- a/example/ios/BitmovinPlayerReactNativeExample.xcodeproj/project.pbxproj +++ b/example/ios/BitmovinPlayerReactNativeExample.xcodeproj/project.pbxproj @@ -423,10 +423,16 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitmovinPlayerReactNativeExample/Pods-BitmovinPlayerReactNativeExample-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core-iOS/AccessibilityResources.bundle", + "${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.0_static/GoogleCast.framework/GoogleCastCoreResources.bundle", + "${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.0_static/GoogleCast.framework/GoogleCastUIResources.bundle", + "${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.0_static/GoogleCast.framework/MaterialDialogs.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastCoreResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastUIResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialDialogs.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/ios/BitmovinCastManagerModule.swift b/ios/BitmovinCastManagerModule.swift index 07c0b5e0..f636c2f1 100644 --- a/ios/BitmovinCastManagerModule.swift +++ b/ios/BitmovinCastManagerModule.swift @@ -29,6 +29,7 @@ class BitmovinCastManagerModule: NSObject, RCTBridgeModule { resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock ) { +#if os(iOS) bridge.uiManager.addUIBlock { _, _ in if let config = config { guard let options = RCTConvert.castManagerOptions(config) else { @@ -42,6 +43,7 @@ class BitmovinCastManagerModule: NSObject, RCTBridgeModule { resolve(nil) } } +#endif } /** @@ -55,7 +57,11 @@ class BitmovinCastManagerModule: NSObject, RCTBridgeModule { rejecter reject: @escaping RCTPromiseRejectBlock ) { bridge.uiManager.addUIBlock { _, _ in +#if os(iOS) resolve(BitmovinCastManager.isInitialized()) +#else + resolve(false) +#endif } } @@ -68,8 +74,10 @@ class BitmovinCastManagerModule: NSObject, RCTBridgeModule { _ message: String, messageNamespace: String? ) { +#if os(iOS) bridge.uiManager.addUIBlock { _, _ in BitmovinCastManager.sharedInstance().sendMessage(message, withNamespace: messageNamespace) } +#endif } } diff --git a/ios/Event+JSON.swift b/ios/Event+JSON.swift index e53fee40..8dfc82b0 100644 --- a/ios/Event+JSON.swift +++ b/ios/Event+JSON.swift @@ -351,6 +351,7 @@ extension CastStartedEvent { } } +#if os(iOS) extension CastWaitingForDeviceEvent { func toJSON() -> [AnyHashable: Any] { [ @@ -360,3 +361,4 @@ extension CastWaitingForDeviceEvent { ] } } +#endif diff --git a/ios/PlayerModule.swift b/ios/PlayerModule.swift index 79e45b64..725e330e 100644 --- a/ios/PlayerModule.swift +++ b/ios/PlayerModule.swift @@ -45,8 +45,9 @@ class PlayerModule: NSObject, RCTBridgeModule { else { return } - +#if os(iOS) self?.setupRemoteControlConfig(playerConfig.remoteControlConfig) +#endif self?.players[nativeId] = PlayerFactory.create(playerConfig: playerConfig) } } @@ -67,8 +68,9 @@ class PlayerModule: NSObject, RCTBridgeModule { else { return } - +#if os(iOS) self?.setupRemoteControlConfig(playerConfig.remoteControlConfig) +#endif let defaultMetadata = RCTConvert.analyticsDefaultMetadataFromAnalyticsConfig(analyticsConfigJson) self?.players[nativeId] = PlayerFactory.create( playerConfig: playerConfig, diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index b42c0d6f..de636827 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -31,9 +31,11 @@ extension RCTConvert { if let adaptationConfig = RCTConvert.adaptationConfig(json["adaptationConfig"]) { playerConfig.adaptationConfig = adaptationConfig } +#if os(iOS) if let remoteControlConfig = RCTConvert.remoteControlConfig(json["remoteControlConfig"]) { playerConfig.remoteControlConfig = remoteControlConfig } +#endif return playerConfig } @@ -983,6 +985,7 @@ extension RCTConvert { return remoteControlConfig } +#if os(iOS) /** Utility method to instantiate a `BitmovinCastManagerOptions` from a JS object. - Parameter json: JS object @@ -1029,4 +1032,5 @@ extension RCTConvert { ) ) } +#endif } diff --git a/ios/RNPlayerView+PlayerListener.swift b/ios/RNPlayerView+PlayerListener.swift index 928774fa..0918a05b 100644 --- a/ios/RNPlayerView+PlayerListener.swift +++ b/ios/RNPlayerView+PlayerListener.swift @@ -169,6 +169,7 @@ extension RNPlayerView: PlayerListener { onVideoPlaybackQualityChanged?(event.toJSON()) } +#if os(iOS) func onCastAvailable(_ event: CastAvailableEvent, player: Player) { onCastAvailable?(event.toJSON()) } @@ -200,8 +201,8 @@ extension RNPlayerView: PlayerListener { func onCastTimeUpdated(_ event: CastTimeUpdatedEvent, player: Player) { onCastTimeUpdated?(event.toJSON()) } - func onCastWaitingForDevice(_ event: CastWaitingForDeviceEvent, player: Player) { onCastWaitingForDevice?(event.toJSON()) } +#endif } diff --git a/ios/SourceModule.swift b/ios/SourceModule.swift index 877e6251..d09d900d 100644 --- a/ios/SourceModule.swift +++ b/ios/SourceModule.swift @@ -79,9 +79,11 @@ class SourceModule: NSObject, RCTBridgeModule { return } self?.sources[nativeId] = SourceFactory.create(from: sourceConfig, sourceMetadata: sourceMetadata) +#if os(iOS) if let remoteConfig = RCTConvert.sourceRemoteControlConfig(sourceRemoteControlConfig){ self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig } +#endif } } @@ -114,9 +116,11 @@ class SourceModule: NSObject, RCTBridgeModule { return } self?.sources[nativeId] = SourceFactory.create(from: sourceConfig) +#if os(iOS) if let remoteConfig = RCTConvert.sourceRemoteControlConfig(sourceRemoteControlConfig) { self?.castSourceConfigs[nativeId] = remoteConfig.castSourceConfig } +#endif } } From faa89f941c7ef20e4c094d85995dd89e5f5065e9 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 13:16:44 +0200 Subject: [PATCH 81/85] fix: enable CI for all branches --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6737d22b..bfed1ce1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,10 @@ name: CI on: pull_request: - branches: [development] paths-ignore: - '*.md' push: - branches: [development] paths-ignore: - '*.md' From 23e76fd4a59cae133dd0960e8974ba187f44af84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20K=C3=A1konyi?= Date: Fri, 22 Sep 2023 13:57:11 +0200 Subject: [PATCH 82/85] fix: CustomHtmlUI hyperlink --- example/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/README.md b/example/README.md index 221b6a65..607cf0e2 100644 --- a/example/README.md +++ b/example/README.md @@ -7,7 +7,7 @@ inside the [`src/screens/`](https://github.com/bitmovin/bitmovin-player-react-na - [Basic DRM playback](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/BasicDrmPlayback.tsx) - [Custom subtitles](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/SubtitlePlayback.tsx) - [Picture in Picture](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/BasicPictureInPicture.tsx) -- [Custom HTML UI](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/CustomHtmlUI.tsx) (iOS and Android only) +- [Custom HTML UI](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/CustomHtmlUi.tsx) (iOS and Android only) - [Basic Fullscreen handling](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/BasicFullscreenHandling.tsx) (iOS and Android only) - [Landscape Fullscreen handling](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/LandscapeFullscreenHandling.tsx) (iOS and Android only) - [Basic Ads](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/BasicAds.tsx) From e1bcaf65f7fb1e9ff0e61544733b7dadb6831a06 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Fri, 22 Sep 2023 15:18:24 +0200 Subject: [PATCH 83/85] fix: only run CI workflow on push for development branch --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfed1ce1..fb95e76a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - '*.md' push: + branches: [development] paths-ignore: - '*.md' From 1e6aa33269bef6862cce5fb2bc1c44a9e4982a3d Mon Sep 17 00:00:00 2001 From: Lukas Knoch-Girstmair Date: Mon, 25 Sep 2023 14:39:41 +0200 Subject: [PATCH 84/85] docs: add changelog entry for Bitmovin Android Player version update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c000e116..ca50a962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `AnalyticsConfig` properties to match the bitmovin analytics v3 API - Use `jason` build of Bitmovin's native Android SDK +- Update Bitmovin's native Android SDK version to `3.44.0` ### Removed From f76f8757f75165dc7017ff6d6e6342c2b7b24626 Mon Sep 17 00:00:00 2001 From: Bitmovin Release Automation Date: Mon, 25 Sep 2023 13:02:11 +0000 Subject: [PATCH 85/85] chore: prepare release 0.12.0 --- CHANGELOG.md | 2 +- example/ios/Podfile.lock | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 805b3e97..21b2891c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.12.0] (2023-09-25) ### Added diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 78d7f03e..e65e671f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -345,7 +345,7 @@ PODS: - React-jsi (= 0.69.6-2) - React-logger (= 0.69.6-2) - React-perflogger (= 0.69.6-2) - - RNBitmovinPlayer (0.11.0): + - RNBitmovinPlayer (0.12.0): - BitmovinPlayer (= 3.44.2) - GoogleAds-IMA-iOS-SDK (= 3.18.4) - GoogleAds-IMA-tvOS-SDK (= 4.8.2) @@ -574,7 +574,7 @@ SPEC CHECKSUMS: React-RCTText: f72442f7436fd8624494963af4906000a5465ce6 React-runtimeexecutor: f1383f6460ea3d66ed122b4defb0b5ba664ee441 ReactCommon: 7857ab475239c5ba044b7ed946ba564f2e7f1626 - RNBitmovinPlayer: adbf3acdea0a8ce3ac7e9064fcb2a1cb54a41cd9 + RNBitmovinPlayer: 07918abac1e5de8b4ae2550f64b9ff541ce0d5a0 RNCPicker: 0250e95ad170569a96f5b0555cdd5e65b9084dca RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 diff --git a/package.json b/package.json index 12aa1c66..680bc96f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitmovin-player-react-native", - "version": "0.11.0", + "version": "0.12.0", "description": "Official React Native bindings for Bitmovin's mobile Player SDKs.", "main": "lib/index.js", "module": "lib/index.mjs",