-
Notifications
You must be signed in to change notification settings - Fork 14
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
Android: support Media Controls #539
Changes from 52 commits
eb019c3
1579ed5
dc1f4bf
f08ca29
07a70c6
257ca00
ae88186
140317a
904d8c0
34f63e1
7fde45f
5c04e05
ec76873
9a39c49
2d14fb7
40be6a7
e239fce
a3698fd
44008f9
64a572c
96eafe9
fca2dda
08d0dd5
c78df51
d534946
30e5466
a0c9e86
f2659cc
8a80b44
6181e74
89f856f
62c7c42
b7e672f
496edd9
5c3bb13
1233369
503a4fc
5a4ff3c
abaedf2
7356122
95b632e
fe1e651
72f68f1
b97c8c1
2ee1f11
d244a2c
36e88e6
8d2fdb3
2f09d63
e22981e
a702767
cf7c34f
f0b06d7
ad495fb
5549678
adb0ea1
6d62572
b12d6b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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<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() | ||
} | ||
|
||
|
@@ -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 | ||
playerView?.player?.let { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I'm not a big fan of using It's also generally nice to avoid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address in b12d6b0 |
||
if (!enableBackgroundPlayback) { | ||
return | ||
} | ||
if (context.playerModule?.mediaSessionPlaybackManager?.player != it) { | ||
return | ||
} | ||
|
||
playerInMediaSessionService = it | ||
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<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. | ||
|
@@ -190,6 +215,7 @@ class RNPlayerView( | |
// is responsible for destroying the player in the end. | ||
playerView.player = null | ||
playerView.onDestroy() | ||
(playerView.parent as? ViewGroup)?.removeView(playerView) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was just for debugging, please remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address in 5549678 |
||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
|
@@ -296,6 +297,7 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple | |
block() | ||
} catch (e: Exception) { | ||
Log.e(MODULE_NAME, "Error while executing command", e) | ||
// TODO: re throw | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove and create ticket for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address in 5549678 |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -883,6 +883,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 } | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This config is a cross-platform wrapper, it does not exist in either native SDK. |
||
/** | ||
* Converts a [CastPayload] object into its JS representation. | ||
*/ | ||
|
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() | ||
} | ||
hawk23 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
private var player: Player? = null | ||
private val binder = ServiceBinder() | ||
private var mediaSession: MediaSession? = null | ||
|
||
override fun onGetSession(): MediaSession? = null | ||
zigavehovec marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Maybe a bit more specific?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep it as it is since the widget also pops up in the lock-screen in Android as well