Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Media Controls #554

Merged
merged 76 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
4f89231
Introduce `lockScreenControlConfig`
123mpozzi Oct 7, 2024
8e10b3d
Add `lockScreenControlConfig.isEnabled` converters
123mpozzi Oct 7, 2024
cbbaaee
Add `LockScreenControlConfig` docs
123mpozzi Oct 7, 2024
6274ad9
Add `isEnabled` docs
123mpozzi Oct 7, 2024
9bfa60f
Add `lockScreenControlConfig` docs
123mpozzi Oct 7, 2024
31a0d93
Deprecate `updatesNowPlayingInfoCenter`
123mpozzi Oct 7, 2024
c8989bf
Change confusing wording
123mpozzi Oct 7, 2024
00451e7
Use ts docs notation for class link
123mpozzi Oct 7, 2024
6907b4d
Generalize wording to be less iOS-specific
123mpozzi Oct 7, 2024
e6f1377
Remove unnecessary TODO
123mpozzi Oct 7, 2024
f0de732
Simplify wording
123mpozzi Oct 7, 2024
76bc8fc
Add entry about `LockScreenControlConfig`
123mpozzi Oct 7, 2024
8ebef4c
Add entry about deprecating `udpatesNowPlayingInfoCenter`
123mpozzi Oct 7, 2024
a0db607
Remove ending dot
123mpozzi Oct 7, 2024
eb019c3
Implement lock-screen controls
123mpozzi Oct 14, 2024
1579ed5
Move the player instance creation to the service
123mpozzi Oct 16, 2024
dc1f4bf
Bind the player correctly to the service on player creation
123mpozzi Oct 16, 2024
f08ca29
Remove media session module from typescript
123mpozzi Oct 16, 2024
07a70c6
Fix media session player not getting replaced properly
123mpozzi Oct 16, 2024
257ca00
Introduce the lock-screen controls sample
123mpozzi Oct 17, 2024
ae88186
Revert changes to basic playback
123mpozzi Oct 17, 2024
140317a
Fix auto-pausing when minimizing the app
123mpozzi Oct 17, 2024
904d8c0
Fix every sample having background playback
123mpozzi Oct 18, 2024
34f63e1
Fix every sample having background playback after service is on
123mpozzi Oct 18, 2024
7fde45f
Cleanup code
123mpozzi Oct 18, 2024
5c04e05
Cleanup unnecessary code and comments
123mpozzi Oct 18, 2024
ec76873
Remove unnecessary decorator
123mpozzi Oct 18, 2024
9a39c49
Make the connection manager a normal class instead of a module
123mpozzi Oct 18, 2024
2d14fb7
Add newline at end of the file
123mpozzi Oct 18, 2024
40be6a7
Update docs with android details
123mpozzi Oct 18, 2024
e239fce
Simplify `LockScreenControlConfig` serialization
123mpozzi Oct 23, 2024
a3698fd
Update docs for bg-playback support on Android
123mpozzi Oct 23, 2024
44008f9
Add bg playback sample
123mpozzi Oct 23, 2024
64a572c
temp: tentative changes
123mpozzi Oct 23, 2024
96eafe9
Unify most of the logic for bg and mediasession
123mpozzi Oct 23, 2024
fca2dda
Rename bg manager more generic and run ktlint
123mpozzi Oct 23, 2024
08d0dd5
Remove unnecessary `onCreate`
123mpozzi Oct 24, 2024
c78df51
Format
123mpozzi Oct 24, 2024
d534946
Merge branch 'feature/android-enable-lock-screen-controls' into featu…
123mpozzi Oct 24, 2024
30e5466
pair-programming : just use one service, destroy media session
123mpozzi Oct 24, 2024
a0c9e86
Remove simple bg service
123mpozzi Oct 24, 2024
f2659cc
pair-programming session to unify services
123mpozzi Oct 24, 2024
8a80b44
Rename service
123mpozzi Oct 25, 2024
6181e74
Make media session independent of background playback
123mpozzi Oct 25, 2024
89f856f
Simplify since media session can be on without bg playback
123mpozzi Oct 25, 2024
62c7c42
Enable lock-screen to provide a notification
123mpozzi Oct 25, 2024
b7e672f
Use simpler names for variables
123mpozzi Oct 25, 2024
496edd9
Add note about Android needing a notification for bg playback
123mpozzi Oct 25, 2024
5c3bb13
Rename BackgroundPlaybackService-related classes and vars
123mpozzi Oct 25, 2024
1233369
Merge pull request #543 from bitmovin/feature/enhance-lock-screen-con…
123mpozzi Oct 25, 2024
503a4fc
Move `playerEventRelay` initialization outside the `init` block
123mpozzi Nov 4, 2024
5a4ff3c
Remove confusing comment
123mpozzi Nov 4, 2024
abaedf2
Allow proper setting to `null` as well, and remove useless code
123mpozzi Nov 4, 2024
7356122
Fix player recovering from service
123mpozzi Nov 5, 2024
95b632e
Refactor: move bg playback flag to view and cleanup
123mpozzi Nov 5, 2024
fe1e651
Rename lockScreenControlConfig to mediaControlConfig
123mpozzi Nov 5, 2024
72f68f1
Enable media controls by default
123mpozzi Nov 5, 2024
b97c8c1
Rename sample as well
123mpozzi Nov 6, 2024
2ee1f11
Fix wrong default value
123mpozzi Nov 6, 2024
d244a2c
Remove default value in case of nil
123mpozzi Nov 6, 2024
36e88e6
Fix state management
123mpozzi Nov 6, 2024
8d2fdb3
Export bg playback handling in a func for `onStop`
123mpozzi Nov 6, 2024
2f09d63
Fix default value
123mpozzi Nov 6, 2024
e22981e
Move statement inside block
123mpozzi Nov 6, 2024
a702767
Remove media control sample since it is enabled by default
123mpozzi Nov 6, 2024
cf7c34f
Move initialization of media session manager outside init block
123mpozzi Nov 6, 2024
f0b06d7
Optimize var
123mpozzi Nov 6, 2024
ad495fb
Document android limitation
123mpozzi Nov 6, 2024
5549678
Remove unnecessary code
123mpozzi Nov 6, 2024
adb0ea1
Remove correct line
123mpozzi Nov 6, 2024
6d62572
Update to stable version having Media Session API
123mpozzi Nov 6, 2024
b12d6b0
Do not use `let` for nullability check to save indentation space
123mpozzi Nov 7, 2024
1c7cfe2
Merge pull request #532 from bitmovin/feature/enable-lock-screen-cont…
123mpozzi Nov 7, 2024
134e11e
Merge pull request #539 from bitmovin/feature/android-enable-lock-scr…
123mpozzi Nov 7, 2024
97f7f89
Merge branch 'development' into feature/base-enable-lock-screen-controls
123mpozzi Nov 7, 2024
891dbc2
Fix changelog
123mpozzi Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() = [email protected]
set(value) {
if (player == value) {
return
}

disconnectSession()
[email protected] = 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
Loading