Skip to content

Commit

Permalink
Merge pull request #554 from bitmovin/feature/base-enable-lock-screen…
Browse files Browse the repository at this point in the history
…-controls

Support Media Controls
  • Loading branch information
123mpozzi authored Nov 7, 2024
2 parents 1b6f198 + 891dbc2 commit 27a72c5
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 16 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## [Unreleased]

### 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
Expand Down
3 changes: 2 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -25,6 +26,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex
*/
private val players: Registry<Player> = mutableMapOf()

val mediaSessionPlaybackManager = MediaSessionPlaybackManager(context)

/**
* JS exported module name.
*/
Expand Down Expand Up @@ -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) {
Expand All @@ -90,6 +95,12 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex
defaultMetadata = defaultMetadata ?: DefaultMetadata(),
)
}

if (enableMediaSession) {
promise.unit.resolveOnUiThread {
mediaSessionPlaybackManager.setupMediaSessionPlayback(nativeId)
}
}
}

/**
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Player, Event> = EventRelay<Player, Event>(
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()
}

Expand All @@ -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 {
Expand All @@ -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<Player, Event>(
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
10 changes: 10 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!--END-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application
android:name=".MainApplication"
Expand Down Expand Up @@ -52,5 +53,14 @@
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.bitmovin.player.casting.BitmovinCastOptionsProvider" />

<service
android:name="com.bitmovin.player.reactnative.services.MediaSessionPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>

</application>
</manifest>
11 changes: 11 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,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: {
Expand Down Expand Up @@ -60,6 +61,7 @@ export type RootStackParamsList = {
};
Casting: undefined;
SystemUI: undefined;
BackgroundPlayback: undefined;
};

const RootStack = createNativeStackNavigator<RootStackParamsList>();
Expand Down Expand Up @@ -112,6 +114,10 @@ export default function App() {
title: 'Programmatic Track Selection',
routeName: 'ProgrammaticTrackSelection' as keyof RootStackParamsList,
},
{
title: 'Background Playback',
routeName: 'BackgroundPlayback' as keyof RootStackParamsList,
},
],
};

Expand Down Expand Up @@ -279,6 +285,11 @@ export default function App() {
options={{ title: 'Casting' }}
/>
)}
<RootStack.Screen
name="BackgroundPlayback"
component={BackgroundPlayback}
options={{ title: 'Background Playback' }}
/>
</RootStack.Navigator>
</NavigationContainer>
);
Expand Down
Loading

0 comments on commit 27a72c5

Please sign in to comment.