From 7e801f558540ad1bd9a6494923a4429dbfbbaed4 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Mon, 18 Sep 2023 16:56:22 +0200 Subject: [PATCH 01/61] 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 02/61] 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 089a40d971c23cb89387f8f55702caa2416823ad Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 09:19:25 +0200 Subject: [PATCH 03/61] 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 04/61] 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 05/61] 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 06/61] 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 07/61] 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 08/61] 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 09/61] 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 10/61] 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 11/61] 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 12/61] 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 13/61] 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 14/61] 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 15/61] 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 16/61] 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 17/61] 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 18/61] 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 19/61] 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 20/61] 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 4315ea966d6bd8d933e5276812837ec2b8440b30 Mon Sep 17 00:00:00 2001 From: Roland Kakonyi Date: Tue, 19 Sep 2023 15:59:11 +0200 Subject: [PATCH 21/61] 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 22/61] 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 23/61] 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 24/61] 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 25/61] 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 26/61] 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 27/61] 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 28/61] 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 29/61] 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 30/61] 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 31/61] 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 32/61] 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 33/61] 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 34/61] 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 35/61] 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 36/61] 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 37/61] 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 38/61] 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 39/61] 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 40/61] 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 41/61] 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 42/61] 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 43/61] 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 44/61] 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 45/61] 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 46/61] 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 47/61] 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 48/61] 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 49/61] 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 50/61] 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 51/61] 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 52/61] 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 53/61] 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 54/61] 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 55/61] 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 56/61] 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 57/61] 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 58/61] 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 59/61] 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 60/61] 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 61/61] 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 } }