Skip to content

Commit

Permalink
Merge pull request #543 from bitmovin/feature/enhance-lock-screen-con…
Browse files Browse the repository at this point in the history
…trols

Enhance lock screen controls
  • Loading branch information
123mpozzi authored Oct 25, 2024
2 parents c78df51 + 5c3bb13 commit 1233369
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MediaSessionPlaybackService.ServiceBinder?>(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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex
*/
private val players: Registry<Player> = mutableMapOf()

var mediaSessionConnectionManager: MediaSessionConnectionManager? = null
var isMediaSessionPlaybackEnabled: Boolean = false
var mediaSessionPlaybackManager: MediaSessionPlaybackManager? = null
var enableBackgroundPlayback: Boolean = false

/**
* JS exported module name.
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -227,7 +229,7 @@ class PlayerModule(context: ReactApplicationContext) : BitmovinBaseModule(contex
promise.unit.resolveOnUiThreadWithPlayer(nativeId) {
destroy()
players.remove(nativeId)
mediaSessionConnectionManager = null
mediaSessionPlaybackManager = null
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,19 @@ class RNPlayerView(
*/
private var playerEventRelay: EventRelay<Player, Event>

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()
}
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
}
}
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 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: {
Expand Down Expand Up @@ -60,6 +61,7 @@ export type RootStackParamsList = {
Casting: undefined;
SystemUI: undefined;
LockScreenControls: undefined;
BackgroundPlayback: undefined;
};

const RootStack = createNativeStackNavigator<RootStackParamsList>();
Expand Down Expand Up @@ -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,
},
],
};

Expand Down Expand Up @@ -273,6 +279,11 @@ export default function App() {
component={LockScreenControls}
options={{ title: 'Lock-Screen Controls' }}
/>
<RootStack.Screen
name="BackgroundPlayback"
component={BackgroundPlayback}
options={{ title: 'Background Playback' }}
/>
</RootStack.Navigator>
</NavigationContainer>
);
Expand Down
90 changes: 90 additions & 0 deletions example/src/screens/BackgroundPlayback.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={styles.container}>
<PlayerView
player={player}
style={styles.player}
onPlay={onEvent}
onPlaying={onEvent}
onPaused={onEvent}
onReady={onReady}
onSourceLoaded={onEvent}
onSeek={onEvent}
onSeeked={onEvent}
onStallStarted={onEvent}
onStallEnded={onEvent}
onVideoPlaybackQualityChanged={onEvent}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'black',
},
player: {
flex: 1,
},
});
15 changes: 9 additions & 6 deletions src/playbackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down

0 comments on commit 1233369

Please sign in to comment.