mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-05 05:36:17 +00:00
Implement media sessions notification
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-feature
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".OmniPlaybackService"
|
||||
android:name="dev.zoriya.omni.OmniPlayerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.margelo.nitro.NitroModules
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class OmniPlaybackService : MediaSessionService() {
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
serviceRef.set(this)
|
||||
setMediaNotificationProvider(
|
||||
DefaultMediaNotificationProvider.Builder(this)
|
||||
.setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
.build()
|
||||
)
|
||||
attachedPlayer.get()?.let { ensureSession(it) }
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
attachedPlayer.get()?.let { ensureSession(it) }
|
||||
return mediaSession
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (!isPlaybackOngoing) {
|
||||
pauseAllPlayersAndStopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
serviceRef.compareAndSet(this, null)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun ensureSession(player: Player) {
|
||||
if (mediaSession?.player === player) return
|
||||
mediaSession?.release()
|
||||
mediaSession = MediaSession.Builder(this, player)
|
||||
.setId(SESSION_ID)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun clearSessionIf(player: Player) {
|
||||
if (mediaSession?.player !== player) return
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SESSION_ID = "omni-session"
|
||||
private const val NOTIFICATION_CHANNEL_ID = "omni_playback"
|
||||
private val attachedPlayer = AtomicReference<Player?>(null)
|
||||
private val serviceRef = AtomicReference<OmniPlaybackService?>(null)
|
||||
|
||||
fun attachPlayer(player: Player) {
|
||||
attachedPlayer.set(player)
|
||||
serviceRef.get()?.ensureSession(player)
|
||||
}
|
||||
|
||||
fun detachPlayer(player: Player) {
|
||||
if (!attachedPlayer.compareAndSet(player, null)) return
|
||||
serviceRef.get()?.clearSessionIf(player)
|
||||
}
|
||||
|
||||
fun ensureStarted(player: Player) {
|
||||
val context = NitroModules.Companion.applicationContext ?: return
|
||||
attachPlayer(player)
|
||||
ContextCompat.startForegroundService(
|
||||
context,
|
||||
Intent(context, OmniPlaybackService::class.java)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.SurfaceHolder
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import com.margelo.nitro.NitroModules
|
||||
import com.margelo.nitro.omni.HybridOmniPlayerSpec
|
||||
import com.margelo.nitro.omni.PlayerStatus
|
||||
@@ -12,7 +16,12 @@ import com.margelo.nitro.omni.Rendition
|
||||
import com.margelo.nitro.omni.Source
|
||||
import com.margelo.nitro.omni.Track
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem.RequestMetadata
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
@@ -20,15 +29,26 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
val player = MpvPlayer(ctx)
|
||||
override val eventMap = EventMap(player)
|
||||
|
||||
init {
|
||||
OmniPlaybackService.attachPlayer(player)
|
||||
}
|
||||
override var showNotification: Boolean? = false
|
||||
set(value) {
|
||||
Log.e("omni", "Toggle show notif, old: ${field}, new: ${value}")
|
||||
if (value == true) {
|
||||
if (notificationPlayer != null) {
|
||||
throw Error("Two players can't display notifications at the same time.")
|
||||
}
|
||||
notificationPlayer = player
|
||||
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
||||
} else if (field == true) {
|
||||
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
showNotification = false
|
||||
super.dispose()
|
||||
|
||||
eventMap.dispose()
|
||||
OmniPlaybackService.detachPlayer(player)
|
||||
player.release()
|
||||
}
|
||||
|
||||
@@ -38,7 +58,50 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
?: throw IllegalStateException("source should be initialized before get")
|
||||
set(value) {
|
||||
currentSource = value
|
||||
player.setMediaItem(buildMediaItem(value))
|
||||
val src = source.src.firstOrNull() ?: return player.setMediaItem(MediaItem.EMPTY)
|
||||
// val headers = Bundle().apply {
|
||||
// putStringArrayList(
|
||||
// MpvPlayer.REQUEST_HEADER_NAMES_KEY,
|
||||
// ArrayList(src.headers.keys)
|
||||
// )
|
||||
// putStringArrayList(
|
||||
// MpvPlayer.REQUEST_HEADER_VALUES_KEY,
|
||||
// ArrayList(src.headers.values)
|
||||
// )
|
||||
// source.startTime?.let {
|
||||
// putLong(MpvPlayer.REQUEST_START_MS_KEY, (it.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
// }
|
||||
// }
|
||||
player.setMediaItem(
|
||||
MediaItem.Builder()
|
||||
.setUri(src.uri)
|
||||
.setMimeType(src.mimeType)
|
||||
.setMediaId(src.uri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(value.metadata?.title)
|
||||
.setAlbumTitle(value.metadata?.album)
|
||||
.setArtist(value.metadata?.artist)
|
||||
.apply {
|
||||
value.metadata?.imageLink?.let { setArtworkUri(it.toUri()) }
|
||||
}
|
||||
.build())
|
||||
.setSubtitleConfigurations(value.subtitles.map { subtitle ->
|
||||
SubtitleConfiguration.Builder(subtitle.link.toUri())
|
||||
.setId(subtitle.id)
|
||||
.setLanguage(subtitle.language)
|
||||
.setLabel(subtitle.label)
|
||||
.setMimeType(subtitle.mimeType)
|
||||
.build()
|
||||
})
|
||||
.setRequestMetadata(
|
||||
RequestMetadata.Builder()
|
||||
.setMediaUri(src.uri.toUri())
|
||||
// .setExtras(headers)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
fun setSurface(holder: SurfaceHolder?) {
|
||||
@@ -53,10 +116,9 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
override val hasNext get() = player.hasNextMediaItem()
|
||||
override val status: PlayerStatus
|
||||
get() = when (player.playbackState) {
|
||||
androidx.media3.common.Player.STATE_IDLE,
|
||||
androidx.media3.common.Player.STATE_ENDED -> PlayerStatus.IDLE
|
||||
|
||||
androidx.media3.common.Player.STATE_BUFFERING -> PlayerStatus.LOADING
|
||||
Player.STATE_IDLE,
|
||||
Player.STATE_ENDED -> PlayerStatus.IDLE
|
||||
Player.STATE_BUFFERING -> PlayerStatus.LOADING
|
||||
else -> PlayerStatus.READYTOPLAY
|
||||
}
|
||||
|
||||
@@ -104,7 +166,6 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
override val rendition: Array<Rendition> get() = emptyArray()
|
||||
|
||||
override fun play() {
|
||||
OmniPlaybackService.ensureStarted(player)
|
||||
player.play()
|
||||
}
|
||||
|
||||
@@ -147,57 +208,6 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
override fun selectRendition(rendition: Rendition?) {
|
||||
}
|
||||
|
||||
private fun buildMediaItem(source: Source): MediaItem {
|
||||
val src = source.src.firstOrNull() ?: return MediaItem.EMPTY
|
||||
|
||||
val mediaMetadata = MediaMetadata.Builder()
|
||||
.setTitle(source.metadata?.title)
|
||||
.setAlbumTitle(source.metadata?.album)
|
||||
.setArtist(source.metadata?.artist)
|
||||
.apply {
|
||||
source.metadata?.imageLink?.let { setArtworkUri(it.toUri()) }
|
||||
}
|
||||
.build()
|
||||
|
||||
val subtitleConfigurations = source.subtitles.map { subtitle ->
|
||||
MediaItem.SubtitleConfiguration.Builder(subtitle.link.toUri())
|
||||
.setId(subtitle.id)
|
||||
.setLanguage(subtitle.language)
|
||||
.setLabel(subtitle.label)
|
||||
.setMimeType(subtitle.mimeType)
|
||||
.build()
|
||||
}
|
||||
|
||||
// val headers = Bundle().apply {
|
||||
// putStringArrayList(
|
||||
// MpvPlayer.REQUEST_HEADER_NAMES_KEY,
|
||||
// ArrayList(src.headers.keys)
|
||||
// )
|
||||
// putStringArrayList(
|
||||
// MpvPlayer.REQUEST_HEADER_VALUES_KEY,
|
||||
// ArrayList(src.headers.values)
|
||||
// )
|
||||
// source.startTime?.let {
|
||||
// putLong(MpvPlayer.REQUEST_START_MS_KEY, (it.coerceAtLeast(0.0) * 1000.0).toLong())
|
||||
// }
|
||||
// }
|
||||
|
||||
val requestMetadata = MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(src.uri.toUri())
|
||||
// .setExtras(headers)
|
||||
.build()
|
||||
|
||||
val itemBuilder = MediaItem.Builder()
|
||||
.setUri(src.uri)
|
||||
.setMimeType(src.mimeType)
|
||||
.setMediaId(src.uri)
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
.setSubtitleConfigurations(subtitleConfigurations)
|
||||
.setRequestMetadata(requestMetadata)
|
||||
|
||||
return itemBuilder.build()
|
||||
}
|
||||
|
||||
private fun tracksByType(trackType: Int): Array<Track> {
|
||||
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
||||
if (groups.isEmpty()) return emptyArray()
|
||||
@@ -237,4 +247,43 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var notificationPlayer: Player? = null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class OmniPlayerService : MediaSessionService() {
|
||||
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
|
||||
private val player = OmniPlayer.notificationPlayer ?: throw Error("No player available")
|
||||
var mediaSession: MediaSession = MediaSession.Builder(ctx, player).build()
|
||||
|
||||
init {
|
||||
Log.e("omni", "service inited")
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
Log.e("omni", "service created")
|
||||
super.onCreate()
|
||||
setMediaNotificationProvider(
|
||||
DefaultMediaNotificationProvider.Builder(ctx).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (!isPlaybackOngoing) {
|
||||
pauseAllPlayersAndStopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun getApplicationContext(): Context? {
|
||||
return NitroModules.applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.util.Log
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.margelo.nitro.omni.HybridOmniPlayerFactorySpec
|
||||
import com.margelo.nitro.omni.HybridOmniPlayerSpec
|
||||
|
||||
@@ -3,7 +3,6 @@ package dev.zoriya.omni
|
||||
import android.util.Log
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.margelo.nitro.omni.HybridOmniPlayerSpec
|
||||
@@ -25,7 +24,6 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
|
||||
|
||||
override lateinit var player: HybridOmniPlayerSpec
|
||||
override var autoplay: Boolean? = true
|
||||
override var showNotification: Boolean? = true
|
||||
override var autoPip: Boolean? = true
|
||||
|
||||
override fun afterUpdate() {
|
||||
@@ -62,7 +60,6 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
|
||||
}
|
||||
|
||||
override fun onDropView() {
|
||||
Log.e("omniView", "omni-view dropped")
|
||||
if (!::player.isInitialized) return
|
||||
|
||||
val omniPlayer = player as? OmniPlayer ?: return
|
||||
@@ -72,7 +69,6 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
surfaceReady = true
|
||||
Log.e("omniView", "surface created")
|
||||
boundPlayer?.setSurface(holder)
|
||||
}
|
||||
|
||||
@@ -81,12 +77,9 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) {
|
||||
// Surface size is resolved from SurfaceHolder in MpvPlayer.
|
||||
}
|
||||
) { }
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Log.e("omniView", "surface destroyed")
|
||||
surfaceReady = false
|
||||
boundPlayer?.setSurface(null)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user