diff --git a/CHANGELOG.md b/CHANGELOG.md index ae271c3e..8f48f467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `LockScreenControlConfig` to configure the lock screen information for the application. When `isEnabled` is `true`, the current media information will be shown on the lock-screen and within the control center +- Android: `playerConfig.playbackConfig.isBackgroundPlaybackEnabled` to support background playback ### Changed diff --git a/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionConnectionManager.kt b/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionPlaybackManager.kt similarity index 57% rename from android/src/main/java/com/bitmovin/player/reactnative/MediaSessionConnectionManager.kt rename to android/src/main/java/com/bitmovin/player/reactnative/MediaSessionPlaybackManager.kt index f93dfeb1..40010966 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionConnectionManager.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/MediaSessionPlaybackManager.kt @@ -9,41 +9,30 @@ 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.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -class MediaSessionConnectionManager(val context: ReactApplicationContext) { - private var isServiceStarted = false +class MediaSessionPlaybackManager(val context: ReactApplicationContext) { private lateinit var playerId: NativeId + internal var serviceBinder: MediaSessionPlaybackService.ServiceBinder? = null - private val _serviceBinder = MutableStateFlow(null) - val serviceBinder = _serviceBinder.asStateFlow() - - inner class MediaSessionServiceConnection : ServiceConnection { + inner class MediaSessionPlaybackServiceConnection : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { - // We've bound to the Service, cast the IBinder and get the Player instance val binder = service as MediaSessionPlaybackService.ServiceBinder - _serviceBinder.value = binder + serviceBinder = binder binder.player = getPlayer() } override fun onServiceDisconnected(name: ComponentName) { - _serviceBinder.value?.player = null + serviceBinder?.player = null } } - fun setupMediaSession(playerId: NativeId) { - this@MediaSessionConnectionManager.playerId = playerId + fun setupMediaSessionPlayback(playerId: NativeId) { + this.playerId = playerId + val intent = Intent(context, MediaSessionPlaybackService::class.java) intent.action = Intent.ACTION_MEDIA_BUTTON - - val connection = MediaSessionServiceConnection() + val connection: ServiceConnection = MediaSessionPlaybackServiceConnection() context.bindService(intent, connection, Context.BIND_AUTO_CREATE) - - if (!isServiceStarted) { - context.startService(intent) - isServiceStarted = true - } } private fun getPlayer( 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 c32cc401..c2c06555 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt @@ -26,8 +26,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex */ private val players: Registry = mutableMapOf() - var mediaSessionConnectionManager: MediaSessionConnectionManager? = null - var isMediaSessionPlaybackEnabled: Boolean = false + var mediaSessionPlaybackManager: MediaSessionPlaybackManager? = null + var enableBackgroundPlayback: Boolean = false /** * JS exported module name. @@ -78,8 +78,10 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex val playerConfig = playerConfigJson?.toPlayerConfig() ?: PlayerConfig() val analyticsConfig = analyticsConfigJson?.toAnalyticsConfig() val defaultMetadata = analyticsConfigJson?.getMap("defaultMetadata")?.toAnalyticsDefaultMetadata() - isMediaSessionPlaybackEnabled = playerConfigJson?.getMap("lockScreenControlConfig") + val enableMediaSession = playerConfigJson?.getMap("lockScreenControlConfig") ?.toLockScreenControlConfig()?.isEnabled ?: false + enableBackgroundPlayback = playerConfigJson?.getMap("playbackConfig") + ?.getBoolean("isBackgroundPlaybackEnabled") ?: false val networkConfig = networkNativeId?.let { networkModule.getConfig(it) } if (networkConfig != null) { @@ -97,10 +99,10 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex ) } - if (isMediaSessionPlaybackEnabled) { - mediaSessionConnectionManager = MediaSessionConnectionManager(context) + if (enableMediaSession) { + mediaSessionPlaybackManager = MediaSessionPlaybackManager(context) promise.unit.resolveOnUiThread { - mediaSessionConnectionManager?.setupMediaSession(nativeId) + mediaSessionPlaybackManager?.setupMediaSessionPlayback(nativeId) } } } @@ -227,7 +229,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex promise.unit.resolveOnUiThreadWithPlayer(nativeId) { destroy() players.remove(nativeId) - mediaSessionConnectionManager = null + mediaSessionPlaybackManager = null } } 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 aea71de2..24ed8bf1 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt @@ -109,17 +109,19 @@ class RNPlayerView( */ private var playerEventRelay: EventRelay - private var mediaSessionServicePlayer: Player? - get() = context.playerModule?.mediaSessionConnectionManager?.serviceBinder?.value?.player + private var playerInMediaSessionService: Player? + get() = context.playerModule?.mediaSessionPlaybackManager?.serviceBinder?.player set(value) { - context.playerModule?.mediaSessionConnectionManager?.serviceBinder?.value?.player = value + value?.let { + context.playerModule?.mediaSessionPlaybackManager?.serviceBinder?.player = it + } } private val activityLifecycleObserver = object : DefaultLifecycleObserver { // Don't stop the player when going to background override fun onStart(owner: LifecycleOwner) { - if (mediaSessionServicePlayer != null) { - player = mediaSessionServicePlayer + if (playerInMediaSessionService != null) { + player = playerInMediaSessionService } playerView?.onStart() } @@ -133,8 +135,8 @@ class RNPlayerView( } override fun onStop(owner: LifecycleOwner) { - if (context.playerModule?.isMediaSessionPlaybackEnabled == false) { - mediaSessionServicePlayer = null + if (context.playerModule?.enableBackgroundPlayback == false) { + playerInMediaSessionService = null } else { player = null } 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 index 0091d873..b408e0d1 100644 --- a/android/src/main/java/com/bitmovin/player/reactnative/services/MediaSessionPlaybackService.kt +++ b/android/src/main/java/com/bitmovin/player/reactnative/services/MediaSessionPlaybackService.kt @@ -12,30 +12,23 @@ class MediaSessionPlaybackService : MediaSessionService() { var player: Player? get() = this@MediaSessionPlaybackService.player set(value) { - this@MediaSessionPlaybackService.player?.destroy() + disconnectSession() this@MediaSessionPlaybackService.player = value value?.let { - createMediaSession(it) + createSession(it) + connectSession() } } - - fun connectSession() = mediaSession?.let { addSession(it) } - fun disconnectSession() = mediaSession?.let { - removeSession(it) - it.release() - } } - private val binder = ServiceBinder() private var player: Player? = null + private val binder = ServiceBinder() private var mediaSession: MediaSession? = null - override fun onGetSession(): MediaSession? = mediaSession + override fun onGetSession(): MediaSession? = null override fun onDestroy() { - binder.disconnectSession() - - player?.destroy() + disconnectSession() player = null super.onDestroy() @@ -46,16 +39,17 @@ class MediaSessionPlaybackService : MediaSessionService() { return binder } - private fun createMediaSession(player: Player) { - binder.disconnectSession() - - val newMediaSession = MediaSession( + private fun createSession(player: Player) { + mediaSession = MediaSession( this, mainLooper, player, ) + } - mediaSession = newMediaSession - binder.connectSession() + private fun connectSession() = mediaSession?.let { addSession(it) } + private fun disconnectSession() = mediaSession?.let { + removeSession(it) + it.release() } } diff --git a/example/src/App.tsx b/example/src/App.tsx index a09e95e2..7eeda420 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -21,6 +21,7 @@ import SystemUI from './screens/SystemUi'; import OfflinePlayback from './screens/OfflinePlayback'; import Casting from './screens/Casting'; import LockScreenControls from './screens/LockScreenControls'; +import BackgroundPlayback from './screens/BackgroundPlayback'; export type RootStackParamsList = { ExamplesList: { @@ -60,6 +61,7 @@ export type RootStackParamsList = { Casting: undefined; SystemUI: undefined; LockScreenControls: undefined; + BackgroundPlayback: undefined; }; const RootStack = createNativeStackNavigator(); @@ -115,6 +117,10 @@ export default function App() { title: 'Lock-Screen Controls', routeName: 'LockScreenControls' as keyof RootStackParamsList, }, + { + title: 'Background Playback', + routeName: 'BackgroundPlayback' as keyof RootStackParamsList, + }, ], }; @@ -273,6 +279,11 @@ export default function App() { component={LockScreenControls} options={{ title: 'Lock-Screen Controls' }} /> + ); diff --git a/example/src/screens/BackgroundPlayback.tsx b/example/src/screens/BackgroundPlayback.tsx new file mode 100644 index 00000000..16f95725 --- /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, + }, + lockScreenControlConfig: { + 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/src/playbackConfig.ts b/src/playbackConfig.ts index 70a5748e..da60662d 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 LockScreenControlConfig.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; /**