diff --git a/.github/workflows/ci-ios-tvos.yml b/.github/workflows/ci-ios-tvos.yml index ad9ba65e..8016f762 100644 --- a/.github/workflows/ci-ios-tvos.yml +++ b/.github/workflows/ci-ios-tvos.yml @@ -78,9 +78,7 @@ jobs: restore-keys: pods- - name: Install pods - run: | - pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || pod repo update bitmovin - yarn pods + run: yarn pods env: CP_HOME_DIR: ${{ github.workspace }}/.cocoapods-cache @@ -131,9 +129,7 @@ jobs: restore-keys: pods- - name: Install pods - run: | - pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || pod repo update bitmovin - yarn example pods + run: yarn example pods env: CP_HOME_DIR: ${{ github.workspace }}/.cocoapods-cache diff --git a/.github/workflows/create-sdk-update-pr.yml b/.github/workflows/create-sdk-update-pr.yml index 0aac4863..bfd3e9d8 100644 --- a/.github/workflows/create-sdk-update-pr.yml +++ b/.github/workflows/create-sdk-update-pr.yml @@ -66,7 +66,6 @@ jobs: if: ${{ inputs.sdk_name == 'ios' }} run: | sed -i '' 's/s.dependency "BitmovinPlayer", ".*/s.dependency "BitmovinPlayer", "${{ inputs.version_number }}"/g' RNBitmovinPlayer.podspec - pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git yarn bootstrap - name: Save Pods cache diff --git a/.github/workflows/start-release-train.yml b/.github/workflows/start-release-train.yml index d4d66d13..45ba362b 100644 --- a/.github/workflows/start-release-train.yml +++ b/.github/workflows/start-release-train.yml @@ -77,13 +77,6 @@ jobs: run: | yarn version --new-version ${{ inputs.version_number }} --no-git-tag-version - - name: Set up CocoaPods - run: | - pod repo add bitmovin https://github.com/bitmovin/cocoapod-specs.git || pod repo update bitmovin - working-directory: example/ios - env: - CP_HOME_DIR: ${{ github.workspace }}/.cocoapods-cache - - name: Install pods to update Podfile.lock run: | yarn bootstrap diff --git a/CHANGELOG.md b/CHANGELOG.md index 416d06e9..dfa57153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.31.0] + +### Added + +- `MediaControlConfig` to configure the media control information for the application. When `isEnabled` is `true`, the current media information will be shown on the lock-screen, in notifications, and within the control center +- Android: `playerConfig.playbackConfig.isBackgroundPlaybackEnabled` to support background playback + +### Changed + +- Update Bitmovin's native Android SDK version to `3.91.0` + +### Deprecated + +- `TweaksConfig.updatesNowPlayingInfoCenter` in favor of `MediaControlConfig.isEnabled` + ## [0.30.0] - 2024-10-31 ### Changed diff --git a/android/build.gradle b/android/build.gradle index 46980e61..37d586f7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -105,5 +105,6 @@ dependencies { // Bitmovin implementation 'com.google.ads.interactivemedia.v3:interactivemedia:3.33.0' implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1' - implementation 'com.bitmovin.player:player:3.90.0+jason' + implementation 'com.bitmovin.player:player:3.91.0+jason' + implementation 'com.bitmovin.player:player-media-session:3.91.0+jason' } diff --git a/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionPlaybackManager.kt b/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionPlaybackManager.kt new file mode 100644 index 00000000..15de7745 --- /dev/null +++ b/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionPlaybackManager.kt @@ -0,0 +1,50 @@ +package com.bitmovin.player.reactnative + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import com.bitmovin.player.api.Player +import com.bitmovin.player.reactnative.extensions.playerModule +import com.bitmovin.player.reactnative.services.MediaSessionPlaybackService +import com.facebook.react.bridge.* + +class MediaSessionPlaybackManager(val context: ReactApplicationContext) { + private var serviceBinder: MediaSessionPlaybackService.ServiceBinder? = null + private lateinit var playerId: NativeId + val player: Player? + get() = serviceBinder?.player + + inner class MediaSessionPlaybackServiceConnection : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as MediaSessionPlaybackService.ServiceBinder + serviceBinder = binder + binder.player = getPlayer() + } + + override fun onServiceDisconnected(name: ComponentName) { + destroy(playerId) + } + } + + fun setupMediaSessionPlayback(playerId: NativeId) { + this.playerId = playerId + + val intent = Intent(context, MediaSessionPlaybackService::class.java) + intent.action = Intent.ACTION_MEDIA_BUTTON + val connection: ServiceConnection = MediaSessionPlaybackServiceConnection() + context.bindService(intent, connection, Context.BIND_AUTO_CREATE) + } + + fun destroy(nativeId: NativeId) { + if (nativeId != playerId) { return } + serviceBinder?.player = null + serviceBinder = null + } + + private fun getPlayer( + nativeId: NativeId = playerId, + playerModule: PlayerModule? = context.playerModule, + ): Player = playerModule?.getPlayerOrNull(nativeId) ?: throw IllegalArgumentException("Invalid PlayerId $nativeId") +} 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 7922fb4c..b4a45185 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -10,6 +10,7 @@ import com.bitmovin.player.reactnative.converter.toAdItem import com.bitmovin.player.reactnative.converter.toAnalyticsConfig import com.bitmovin.player.reactnative.converter.toAnalyticsDefaultMetadata import com.bitmovin.player.reactnative.converter.toJson +import com.bitmovin.player.reactnative.converter.toMediaControlConfig import com.bitmovin.player.reactnative.converter.toPlayerConfig import com.bitmovin.player.reactnative.extensions.mapToReactArray import com.facebook.react.bridge.* @@ -25,6 +26,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ private val players: Registry = mutableMapOf() + val mediaSessionPlaybackManager = MediaSessionPlaybackManager(context) + /** * JS exported module name. */ @@ -74,6 +77,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex val playerConfig = playerConfigJson?.toPlayerConfig() ?: PlayerConfig() val analyticsConfig = analyticsConfigJson?.toAnalyticsConfig() val defaultMetadata = analyticsConfigJson?.getMap("defaultMetadata")?.toAnalyticsDefaultMetadata() + val enableMediaSession = playerConfigJson?.getMap("mediaControlConfig") + ?.toMediaControlConfig()?.isEnabled ?: true val networkConfig = networkNativeId?.let { networkModule.getConfig(it) } if (networkConfig != null) { @@ -90,6 +95,12 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex defaultMetadata = defaultMetadata ?: DefaultMetadata(), ) } + + if (enableMediaSession) { + promise.unit.resolveOnUiThread { + mediaSessionPlaybackManager.setupMediaSessionPlayback(nativeId) + } + } } /** @@ -211,6 +222,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ @ReactMethod fun destroy(nativeId: NativeId, promise: Promise) { + mediaSessionPlaybackManager.destroy(nativeId) promise.unit.resolveOnUiThreadWithPlayer(nativeId) { destroy() players.remove(nativeId) 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 647cf14c..db019681 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt @@ -16,6 +16,7 @@ import com.bitmovin.player.api.event.SourceEvent import com.bitmovin.player.api.ui.PlayerViewConfig import com.bitmovin.player.api.ui.StyleConfig import com.bitmovin.player.reactnative.converter.toJson +import com.bitmovin.player.reactnative.extensions.playerModule import com.facebook.react.ReactActivity import com.facebook.react.bridge.* import com.facebook.react.uimanager.events.RCTEventEmitter @@ -102,8 +103,23 @@ class RNPlayerView( private val activityLifecycle = (context.currentActivity as? ReactActivity)?.lifecycle ?: error("Trying to create an instance of ${this::class.simpleName} while not attached to a ReactActivity") + /** + * Relays the provided set of events, emitted by the player, together with the associated name + * to the `eventOutput` callback. + */ + private var playerEventRelay: EventRelay = EventRelay( + EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING, + ::emitEventFromPlayer, + ) + + internal var enableBackgroundPlayback: Boolean = false + var playerInMediaSessionService: Player? = null + private val activityLifecycleObserver = object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { + if (playerInMediaSessionService != null) { + playerView?.player = playerInMediaSessionService + } playerView?.onStart() } @@ -116,10 +132,28 @@ class RNPlayerView( } override fun onStop(owner: LifecycleOwner) { + removePlayerForBackgroundPlayback() playerView?.onStop() } override fun onDestroy(owner: LifecycleOwner) = dispose() + + // When background playback is enabled, + // remove player from view so it does not get paused when entering background + private fun removePlayerForBackgroundPlayback() { + playerInMediaSessionService = null + val player = playerView?.player ?: return + + if (!enableBackgroundPlayback) { + return + } + if (context.playerModule?.mediaSessionPlaybackManager?.player != player) { + return + } + + playerInMediaSessionService = player + playerView?.player = null + } } init { @@ -133,15 +167,6 @@ class RNPlayerView( activityLifecycle.addObserver(activityLifecycleObserver) } - /** - * Relays the provided set of events, emitted by the player, together with the associated name - * to the `eventOutput` callback. - */ - private val playerEventRelay = EventRelay( - EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING, - ::emitEventFromPlayer, - ) - /** * Relays the provided set of events, emitted by the player view, together with the associated name * to the `eventOutput` callback. 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 1cb0ffe2..842c945f 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt @@ -248,6 +248,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple val playbackConfig = playerConfig?.getMap("playbackConfig") val isPictureInPictureEnabled = view.config?.pictureInPictureConfig?.isEnabled == true || playbackConfig?.getBooleanOrNull("isPictureInPictureEnabled") == true + view.enableBackgroundPlayback = playbackConfig?.getBoolean("isBackgroundPlaybackEnabled") ?: false val rnStyleConfigWrapper = playerConfig?.toRNStyleConfigWrapperFromPlayerConfig() val configuredPlayerViewConfig = view.config?.playerViewConfig ?: PlayerViewConfig() 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 cd6bec77..78438e5c 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 @@ -898,6 +898,14 @@ fun String.toMediaType(): MediaType? = when (this) { else -> null } +data class MediaControlConfig( + var isEnabled: Boolean = true, +) + +fun ReadableMap.toMediaControlConfig(): MediaControlConfig = MediaControlConfig().apply { + withBoolean("isEnabled") { isEnabled = it } +} + /** * Converts a [CastPayload] object into its JS representation. */ diff --git a/android/src/main/java/com/bitmovin/player/reactnative/services/MediaSessionPlaybackService.kt b/android/src/main/java/com/bitmovin/player/reactnative/services/MediaSessionPlaybackService.kt new file mode 100644 index 00000000..ff7afa28 --- /dev/null +++ b/android/src/main/java/com/bitmovin/player/reactnative/services/MediaSessionPlaybackService.kt @@ -0,0 +1,59 @@ +package com.bitmovin.player.reactnative.services + +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import com.bitmovin.player.api.Player +import com.bitmovin.player.api.media.session.MediaSession +import com.bitmovin.player.api.media.session.MediaSessionService + +class MediaSessionPlaybackService : MediaSessionService() { + inner class ServiceBinder : Binder() { + var player: Player? + get() = this@MediaSessionPlaybackService.player + set(value) { + if (player == value) { + return + } + + disconnectSession() + this@MediaSessionPlaybackService.player = value + value?.let { + createSession(it) + connectSession() + } + } + } + + private var player: Player? = null + private val binder = ServiceBinder() + private var mediaSession: MediaSession? = null + + override fun onGetSession(): MediaSession? = null + + override fun onDestroy() { + disconnectSession() + player = null + + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder { + super.onBind(intent) + return binder + } + + private fun createSession(player: Player) { + mediaSession = MediaSession( + this, + mainLooper, + player, + ) + } + + private fun connectSession() = mediaSession?.let { addSession(it) } + private fun disconnectSession() = mediaSession?.let { + removeSession(it) + it.release() + } +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 7559418f..edd84fd4 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + + + + + + + diff --git a/example/ios/Podfile b/example/ios/Podfile index 8839b4f7..1f1edce6 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -2,7 +2,6 @@ require_relative '../node_modules/react-native/scripts/react_native_pods' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' source 'https://cdn.cocoapods.org' -source 'https://github.com/bitmovin/cocoapod-specs.git' source 'https://github.com/react-native-tvos/react-native-tvos-podspecs.git' prepare_react_native_project! diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 07ca8539..bd10c783 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1048,7 +1048,7 @@ PODS: - React-jsi (= 0.73.4-0) - React-logger (= 0.73.4-0) - React-perflogger (= 0.73.4-0) - - RNBitmovinPlayer (0.30.0): + - RNBitmovinPlayer (0.31.0): - BitmovinPlayer (= 3.77.0) - GoogleAds-IMA-iOS-SDK (= 3.23.0) - GoogleAds-IMA-tvOS-SDK (= 4.13.0) @@ -1122,13 +1122,12 @@ DEPENDENCIES: - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: - https://github.com/bitmovin/cocoapod-specs.git: - - BitmovinPlayerCore https://github.com/react-native-tvos/react-native-tvos-podspecs.git: - libevent trunk: - BitmovinAnalyticsCollector - BitmovinPlayer + - BitmovinPlayerCore - google-cast-sdk - GoogleAds-IMA-iOS-SDK - GoogleAds-IMA-tvOS-SDK @@ -1247,7 +1246,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BitmovinAnalyticsCollector: d08e0b13bcc32973370e0d71f2faa739561bac0a BitmovinPlayer: 65866e13f7a8246ccbc7378607d6ca789f0f459e - BitmovinPlayerCore: 7151c7836f0e0c906a17c897576d3bd892b51b4a + BitmovinPlayerCore: 158b4a5b81b12b819f0f51b7b89711ca47e11583 boost: 88202336c3ba1e7a264a83c0c888784b0f360c28 DoubleConversion: 74cb0ce4de271b23e772567504735c87134edf0a FBLazyVector: 33a271a7e8de0bd321e47356d8bc3b2d5fb9ddba @@ -1302,12 +1301,12 @@ SPEC CHECKSUMS: React-runtimescheduler: 20b2202e3396589a71069d12ae9f328949c7c7b8 React-utils: 0307d396f233e47a167b5aaf045b0e4e1dc19d74 ReactCommon: 17891ca337bfa5a7263649b09f27a8c664537bf2 - RNBitmovinPlayer: 5df76edfd71a5887aa20210108bdf9ac7227048b + RNBitmovinPlayer: a706e0f5a1a595271feda6d1fc9a9a7ff6d94104 RNCPicker: b18aaf30df596e9b1738e7c1f9ee55402a229dca RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: ab50eb8f7fcf1b36aad1801b5687b66b2c0aa000 -PODFILE CHECKSUM: e89ebfe6215567e7e15880586d123e5ea122588c +PODFILE CHECKSUM: 11ac6cb62c1978622f6d687b574d9de3441a2680 COCOAPODS: 1.15.2 diff --git a/example/src/App.tsx b/example/src/App.tsx index 54799276..2e10c807 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -8,6 +8,7 @@ import ExamplesList from './screens/ExamplesList'; import BasicAds from './screens/BasicAds'; import BasicAnalytics from './screens/BasicAnalytics'; import BasicPlayback from './screens/BasicPlayback'; +import BasicTvPlayback from './screens/BasicTvPlayback'; import BasicDrmPlayback from './screens/BasicDrmPlayback'; import SubtitlePlayback from './screens/SubtitlePlayback'; import ProgrammaticTrackSelection from './screens/ProgrammaticTrackSelection'; @@ -20,6 +21,7 @@ import LandscapeFullscreenHandling from './screens/LandscapeFullscreenHandling'; import SystemUI from './screens/SystemUi'; import OfflinePlayback from './screens/OfflinePlayback'; import Casting from './screens/Casting'; +import BackgroundPlayback from './screens/BackgroundPlayback'; export type RootStackParamsList = { ExamplesList: { @@ -31,6 +33,7 @@ export type RootStackParamsList = { BasicAds: undefined; BasicAnalytics: undefined; BasicPlayback: undefined; + BasicTvPlayback: undefined; BasicDrmPlayback: undefined; BasicPictureInPicture: { navigation: NativeStackNavigationProp; @@ -58,11 +61,13 @@ export type RootStackParamsList = { }; Casting: undefined; SystemUI: undefined; + BackgroundPlayback: undefined; }; const RootStack = createNativeStackNavigator(); const isTVOS = Platform.OS === 'ios' && Platform.isTV; +const isAndroidTV = Platform.OS === 'android' && Platform.isTV; export default function App() { useEffect(() => { @@ -109,9 +114,20 @@ export default function App() { title: 'Programmatic Track Selection', routeName: 'ProgrammaticTrackSelection' as keyof RootStackParamsList, }, + { + title: 'Background Playback', + routeName: 'BackgroundPlayback' as keyof RootStackParamsList, + }, ], }; + if (isAndroidTV) { + stackParams.data.unshift({ + title: 'Basic TV playback', + routeName: 'BasicTvPlayback', + }); + } + if (!isTVOS) { stackParams.data.push({ title: 'Custom HTML UI', @@ -186,6 +202,13 @@ export default function App() { component={BasicPlayback} options={{ title: 'Basic playback' }} /> + {isAndroidTV && ( + + )} )} + ); diff --git a/example/src/screens/BackgroundPlayback.tsx b/example/src/screens/BackgroundPlayback.tsx new file mode 100644 index 00000000..ba5a30ba --- /dev/null +++ b/example/src/screens/BackgroundPlayback.tsx @@ -0,0 +1,90 @@ +import React, { useCallback } from 'react'; +import { View, Platform, StyleSheet } from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; +import { + Event, + usePlayer, + PlayerView, + SourceType, +} from 'bitmovin-player-react-native'; +import { useTVGestures } from '../hooks'; + +function prettyPrint(header: string, obj: any) { + console.log(header, JSON.stringify(obj, null, 2)); +} + +export default function BackgroundPlayback() { + useTVGestures(); + + const player = usePlayer({ + playbackConfig: { + isBackgroundPlaybackEnabled: true, + }, + mediaControlConfig: { + isEnabled: true, + }, + remoteControlConfig: { + isCastEnabled: false, + }, + }); + + useFocusEffect( + useCallback(() => { + player.load({ + 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 }, + }); + 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, + }, +}); diff --git a/example/src/screens/BasicPlayback.tsx b/example/src/screens/BasicPlayback.tsx index 8f47cedf..027bad63 100644 --- a/example/src/screens/BasicPlayback.tsx +++ b/example/src/screens/BasicPlayback.tsx @@ -6,9 +6,6 @@ import { usePlayer, PlayerView, SourceType, - TvUi, - SmallScreenUi, - PlayerViewConfig, } from 'bitmovin-player-react-native'; import { useTVGestures } from '../hooks'; @@ -25,12 +22,6 @@ export default function BasicPlayback() { }, }); - const config: PlayerViewConfig = { - uiConfig: { - variant: Platform.isTV ? new TvUi() : new SmallScreenUi(), - }, - }; - useFocusEffect( useCallback(() => { player.load({ @@ -65,7 +56,6 @@ export default function BasicPlayback() { { + player.load({ + 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 }, + }); + 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, + }, +}); diff --git a/integration_test/ios/Podfile b/integration_test/ios/Podfile index 2c17c613..097a6334 100644 --- a/integration_test/ios/Podfile +++ b/integration_test/ios/Podfile @@ -2,7 +2,6 @@ require_relative '../node_modules/react-native/scripts/react_native_pods' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' source 'https://cdn.cocoapods.org' -source 'https://github.com/bitmovin/cocoapod-specs.git' source 'https://github.com/react-native-tvos/react-native-tvos-podspecs.git' prepare_react_native_project! diff --git a/integration_test/ios/Podfile.lock b/integration_test/ios/Podfile.lock index 3cc951b5..a1de6a4f 100644 --- a/integration_test/ios/Podfile.lock +++ b/integration_test/ios/Podfile.lock @@ -1040,7 +1040,7 @@ PODS: - React-jsi (= 0.73.4-0) - React-logger (= 0.73.4-0) - React-perflogger (= 0.73.4-0) - - RNBitmovinPlayer (0.30.0): + - RNBitmovinPlayer (0.31.0): - BitmovinPlayer (= 3.77.0) - GoogleAds-IMA-iOS-SDK (= 3.23.0) - GoogleAds-IMA-tvOS-SDK (= 4.13.0) @@ -1103,13 +1103,12 @@ DEPENDENCIES: - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: - https://github.com/bitmovin/cocoapod-specs.git: - - BitmovinPlayerCore https://github.com/react-native-tvos/react-native-tvos-podspecs.git: - libevent trunk: - BitmovinAnalyticsCollector - BitmovinPlayer + - BitmovinPlayerCore - GoogleAds-IMA-iOS-SDK - SocketRocket @@ -1217,7 +1216,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BitmovinAnalyticsCollector: d08e0b13bcc32973370e0d71f2faa739561bac0a BitmovinPlayer: 65866e13f7a8246ccbc7378607d6ca789f0f459e - BitmovinPlayerCore: 7151c7836f0e0c906a17c897576d3bd892b51b4a + BitmovinPlayerCore: 158b4a5b81b12b819f0f51b7b89711ca47e11583 boost: 88202336c3ba1e7a264a83c0c888784b0f360c28 DoubleConversion: 74cb0ce4de271b23e772567504735c87134edf0a FBLazyVector: 33a271a7e8de0bd321e47356d8bc3b2d5fb9ddba @@ -1267,10 +1266,10 @@ SPEC CHECKSUMS: React-runtimescheduler: 20b2202e3396589a71069d12ae9f328949c7c7b8 React-utils: 0307d396f233e47a167b5aaf045b0e4e1dc19d74 ReactCommon: 17891ca337bfa5a7263649b09f27a8c664537bf2 - RNBitmovinPlayer: 5df76edfd71a5887aa20210108bdf9ac7227048b + RNBitmovinPlayer: a706e0f5a1a595271feda6d1fc9a9a7ff6d94104 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: ab50eb8f7fcf1b36aad1801b5687b66b2c0aa000 -PODFILE CHECKSUM: 0bfe194f5e28f1cf54d3d732eda8c78fadbeeedd +PODFILE CHECKSUM: d1cd0316ec7219d421f4dfb46ced3af29fd4e932 COCOAPODS: 1.15.2 diff --git a/ios/RCTConvert+BitmovinPlayer.swift b/ios/RCTConvert+BitmovinPlayer.swift index ef80c629..4cef95dd 100644 --- a/ios/RCTConvert+BitmovinPlayer.swift +++ b/ios/RCTConvert+BitmovinPlayer.swift @@ -42,6 +42,9 @@ extension RCTConvert { if let networkConfig = RCTConvert.networkConfig(json["networkConfig"]) { playerConfig.networkConfig = networkConfig } + if let nowPlayingConfig = RCTConvert.mediaControlConfig(json["mediaControlConfig"]) { + playerConfig.nowPlayingConfig = nowPlayingConfig + } #if os(iOS) if let remoteControlConfig = RCTConvert.remoteControlConfig(json["remoteControlConfig"]) { playerConfig.remoteControlConfig = remoteControlConfig @@ -1332,6 +1335,18 @@ extension RCTConvert { "body": toJson(data: httpResponse.body) ] } + + static func mediaControlConfig(_ json: Any?) -> NowPlayingConfig? { + let nowPlayingConfig = NowPlayingConfig() + guard let json = json as? [String: Any?] else { + nowPlayingConfig.isNowPlayingInfoEnabled = true + return nowPlayingConfig + } + if let isEnabled = json["isEnabled"] as? Bool { + nowPlayingConfig.isNowPlayingInfoEnabled = isEnabled + } + return nowPlayingConfig + } } /** * React native specific PlayerViewConfig. diff --git a/package.json b/package.json index ae3f9c51..db7727e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitmovin-player-react-native", - "version": "0.30.0", + "version": "0.31.0", "description": "Official React Native bindings for Bitmovin's mobile Player SDKs.", "main": "lib/index.js", "module": "lib/index.mjs", diff --git a/src/index.ts b/src/index.ts index b54b2472..20d34fc7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,3 +24,4 @@ export * from './playerConfig'; export * from './liveConfig'; export * from './bufferApi'; export * from './network'; +export * from './mediaControlConfig'; diff --git a/src/mediaControlConfig.ts b/src/mediaControlConfig.ts new file mode 100644 index 00000000..145a0c38 --- /dev/null +++ b/src/mediaControlConfig.ts @@ -0,0 +1,59 @@ +/** + * Configures the media control information for the application. This information will be displayed + * wherever current media information typically appears, such as the lock screen, in notifications, and + * and inside the control center. + */ +export interface MediaControlConfig { + /** + * Enable the default behavior of displaying media information + * on the lock screen, in notifications, and within the control center. + * + * Default is `true`. + * + * For a detailed list of the supported features in the **default behavior**, + * check the **Default Supported Features** section. + * + * @note Enabling this flag will automatically treat {@link TweaksConfig.updatesNowPlayingInfoCenter} as `false`. + * + * ## Limitations + * --- + * - Android: If an app creates multiple player instances, the player shown in media controls is the latest one created having media controls enabled. + * - At the moment, the current media information is disabled during casting. + * + * ## Known Issues + * --- + * **iOS**: + * - There is unexpected behavior when using the IMA SDK. The Google IMA SDK adds its own commands + * for play/pause as soon as the ad starts loading (not when it starts playing). Within this window + * (approximately around 10 seconds), it is possible that both the ad and the main content are playing + * at the same time when a user interacts with the media control feature. + * + * ## Default Supported Features + * --- + * Here is the list of features supported by the default behavior. + * + * ### Populated Metadata + * - media type (to visualize the correct kind of data — _e.g. a waveform for audio files_) + * - title + * - artwork + * - elapsed time + * - duration + * + * **Android-only** + * - source description + * + * **iOS-only** + * - live or VOD status + * - playback rate + * - default playback rate + * + * ### Registered Commands + * - toggle play/pause + * - change playback position + * + * **iOS-only** + * - skip forward + * - skip backward + */ + isEnabled?: boolean; +} diff --git a/src/playbackConfig.ts b/src/playbackConfig.ts index 70a5748e..acf019ee 100644 --- a/src/playbackConfig.ts +++ b/src/playbackConfig.ts @@ -47,19 +47,22 @@ export interface PlaybackConfig { * When set to `true`, also make sure to properly configure your app to allow * background playback. * - * On tvOS, background playback is only supported for audio-only content. - * * Default is `false`. * + * @note + * On Android, {@link MediaControlConfig.isEnabled} has to be `true` for + * background playback to work. + * @note + * On tvOS, background playback is only supported for audio-only content. + * * @example * ``` * const player = new Player({ - * { + * playbackConfig: { * isBackgroundPlaybackEnabled: true, - * } - * }) + * }, + * }); * ``` - * @platform iOS, tvOS */ isBackgroundPlaybackEnabled?: boolean; /** diff --git a/src/playerConfig.ts b/src/playerConfig.ts index cffe2308..e6a68690 100644 --- a/src/playerConfig.ts +++ b/src/playerConfig.ts @@ -9,6 +9,7 @@ import { NativeInstanceConfig } from './nativeInstance'; import { PlaybackConfig } from './playbackConfig'; import { LiveConfig } from './liveConfig'; import { NetworkConfig } from './network/networkConfig'; +import { MediaControlConfig } from './mediaControlConfig'; /** * Object used to configure a new `Player` instance. @@ -73,4 +74,10 @@ export interface PlayerConfig extends NativeInstanceConfig { * Configures network request manipulation functionality. A default {@link NetworkConfig} is set initially. */ networkConfig?: NetworkConfig; + /** + * Configures the media control information for the application. This information will be displayed + * wherever current media information typically appears, such as the lock screen, in notifications, + * and inside the control center. + */ + mediaControlConfig?: MediaControlConfig; } diff --git a/src/tweaksConfig.ts b/src/tweaksConfig.ts index 45ac1cfb..2269baf7 100644 --- a/src/tweaksConfig.ts +++ b/src/tweaksConfig.ts @@ -166,6 +166,7 @@ export interface TweaksConfig { * * Default is `true`. * + * @deprecated To enable the Now Playing information use {@link MediaControlConfig.isEnabled} * @platform iOS */ updatesNowPlayingInfoCenter?: boolean;