Jelajahi Sumber

media session

media session
Milan Jurkulak 3 tahun lalu
induk
melakukan
056b823199

+ 0 - 1
app/src/main/AndroidManifest.xml

@@ -29,7 +29,6 @@
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
     <uses-permission
         android:name="android.permission.AUTHENTICATE_ACCOUNTS"
         android:maxSdkVersion="22" />

+ 2 - 9
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Tv
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -69,12 +68,6 @@ class IPTVScreen : Screen() {
             }
         }
 
-        val currentIndex = remember {
-            mutableIntStateOf(dataList.indexOf(data))
-        }
-
-        val currentData = dataList[currentIndex.intValue]
-
         Box(
             modifier = Modifier
                 .fillMaxSize()
@@ -82,8 +75,8 @@ class IPTVScreen : Screen() {
         ) {
             MediaPlayerContainer(
                 modifier = Modifier.fillMaxSize(),
-                src = currentData,
-                // items = dataList // todo
+                items = dataList,
+                itemToPlay = dataList.indexOf(data)
             )
         }
 

+ 3 - 2
tvlib/src/main/AndroidManifest.xml

@@ -31,12 +31,13 @@
 
         <service
             android:name=".service.PlaybackService"
-            android:exported="true"
             android:foregroundServiceType="mediaPlayback"
+            android:exported="true"
             tools:ignore="ExportedService">
 
             <intent-filter>
-                <action android:name="androidx.media3.session.MediaSessionService" />
+                <action android:name="androidx.media3.session.MediaSessionService"/>
+                <action android:name="android.media.browse.MediaBrowserService"/>
             </intent-filter>
 
         </service>

+ 23 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/LifecycleExt.kt

@@ -8,9 +8,9 @@
 
 package org.mjdev.tvlib.extensions
 
+import android.annotation.SuppressLint
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
@@ -18,6 +18,7 @@ import androidx.lifecycle.LifecycleOwner
 
 object LifecycleExt {
 
+    @SuppressLint("ComposableNaming")
     @Composable
     fun rememberLifeCycleEventObserver(
         key1: Any? = Unit,
@@ -34,4 +35,25 @@ object LifecycleExt {
         }
     }
 
+    @SuppressLint("ComposableNaming")
+    @Composable
+    fun rememberPlayPauseLifeCycleObserver(
+        key1: Any? = Unit,
+        onPause: () -> Unit = {},
+        onResume: () -> Unit = {}
+    ) = rememberLifeCycleEventObserver(key1) { _, event ->
+        when (event) {
+            Lifecycle.Event.ON_PAUSE -> {
+                onPause()
+            }
+
+            Lifecycle.Event.ON_RESUME -> {
+                onResume()
+            }
+
+            else -> {
+            }
+        }
+    }
+
 }

+ 155 - 106
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt

@@ -8,10 +8,12 @@
 
 package org.mjdev.tvlib.ui.components.media
 
+import android.app.Activity
 import android.app.NotificationChannel
 import android.app.NotificationManager
 import android.app.PendingIntent
 import android.content.Context
+import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.net.Uri
@@ -26,7 +28,6 @@ import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.key.onPreviewKeyEvent
 import androidx.compose.ui.platform.LocalConfiguration
@@ -34,8 +35,6 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.core.content.getSystemService
-import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
-import androidx.lifecycle.Lifecycle.Event.ON_RESUME
 import androidx.media3.common.AudioAttributes
 import androidx.media3.common.C
 import androidx.media3.common.DeviceInfo
@@ -54,32 +53,78 @@ import androidx.media3.common.text.CueGroup
 import androidx.media3.common.util.Size
 import androidx.media3.common.util.UnstableApi
 import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaSession
 import androidx.media3.ui.PlayerControlView
 import androidx.media3.ui.PlayerNotificationManager
 import androidx.media3.ui.PlayerView
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
-import org.mjdev.tvlib.extensions.LifecycleExt.rememberLifeCycleEventObserver
+import org.mjdev.tvlib.extensions.LifecycleExt.rememberPlayPauseLifeCycleObserver
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ViewExt.controller
 import org.mjdev.tvlib.extensions.ViewExt.findView
 import org.mjdev.tvlib.ui.components.audiopreview.AudioPreview
+import timber.log.Timber
+import java.util.UUID
 
-@Suppress("DEPRECATION")
+// todo recomposing 2 times
+@Suppress("DEPRECATION", "unused")
 @UnstableApi
 class ExoPlayerImpl(
     override val context: Context,
-) : IMediaPlayer, PlayerNotificationManager.MediaDescriptionAdapter {
-
-//    private val sessionToken by lazy {
-//        MediaSessionCompat.Token.fromToken(
-//            SessionToken(
-//                context, ComponentName(
-//                    context,
-//                    PlaybackService::class.java
-//                )
-//            )
-//        )
-//    }
+) : IMediaPlayer, PlayerNotificationManager.MediaDescriptionAdapter, MediaSession.Callback {
+
+    private val requestCode :Int = 999
+
+    // todo : check activity
+    private val pendingIntent: PendingIntent by lazy {
+        Intent(context, (context as Activity).javaClass).let { intent ->
+            PendingIntent.getActivity(
+                context,
+                requestCode,
+                intent,
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+                null // todo
+            )
+        }
+
+    }
+
+    private val mediaSessionID by lazy {
+        UUID.randomUUID().toString()
+    }
+
+//    val sessionToken = SessionToken(
+//        context,
+//        ComponentName(context, PlaybackService::class.java)
+//    )
+
+    private val exoPlayer: ExoPlayer by lazy {
+//        controllerFuture.addListener({
+//            playerView.player = controllerFuture.get().apply {
+//                setAudioAttributes(
+//                    AudioAttributes.Builder()
+//                        .setUsage(C.USAGE_MEDIA)
+//                        .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
+//                        .build(), true
+//                ).build()
+//            }
+//        }, MoreExecutors.directExecutor())
+        ExoPlayer.Builder(context)
+            .setAudioAttributes(
+                AudioAttributes.Builder()
+                    .setUsage(C.USAGE_MEDIA)
+                    .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
+                    .build(), true
+            ).build()
+    }
+
+    private val mediaSessionConnector by lazy {
+        MediaSession.Builder(context, exoPlayer)
+            .setId(mediaSessionID)
+            .setCallback(this@ExoPlayerImpl)
+            .setSessionActivity(pendingIntent)
+            .build()
+    }
 
     private val playerNotificationManager by lazy {
         val notifyChannelName = this::class.java.`package`?.name ?: "MediaPlayer"
@@ -103,23 +148,79 @@ class ExoPlayerImpl(
             .setMediaDescriptionAdapter(this@ExoPlayerImpl)
             .build()
             .apply {
-//                setMediaSessionToken(sessionToken)
+                setMediaSessionToken(mediaSessionConnector.sessionCompatToken)
             }
     }
 
-    // todo from session
-    private val exoPlayer: ExoPlayer by lazy {
-        ExoPlayer.Builder(context)
-            .setAudioAttributes(
-                AudioAttributes.Builder()
-                    .setUsage(C.USAGE_MEDIA)
-                    .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
-                    .build(), true
-            ).build()
+    private val audioPlayerView by lazy {
+        AudioPreview(context)
+    }
+
+    private val playerView by lazy {
+        playerNotificationManager.setPlayer(exoPlayer)
+        exoPlayer.addListener(object : Player.Listener {
+            override fun onIsPlayingChanged(isPlaying: Boolean) {
+                if (isPlaying) {
+                    audioPlayerView.resume()
+                } else {
+                    audioPlayerView.pause()
+                }
+            }
+
+            override fun onPlaybackStateChanged(playbackState: Int) {
+                when (playbackState) {
+                    STATE_READY -> {
+                        audioPlayerView.setData(currentMediaItem)
+                    }
+
+                    STATE_ENDED -> {
+                        audioPlayerView.stop()
+                    }
+
+                    else -> {
+                        // no op
+                    }
+                }
+            }
+        })
+        object : PlayerView(context) {
+            init {
+                setBackgroundColor(0)
+                setShutterBackgroundColor(0)
+                drawingCacheBackgroundColor = 0
+                controllerAutoShow = true
+                controllerHideOnTouch = true
+                useController = true
+                useArtwork = false
+                artworkDisplayMode = ARTWORK_DISPLAY_MODE_OFF
+                player = exoPlayer
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    outlineAmbientShadowColor = 0
+                }
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    outlineSpotShadowColor = 0
+                }
+                setShowBuffering(SHOW_BUFFERING_ALWAYS)
+                setShowMultiWindowTimeBar(true)
+                addView(audioPlayerView)
+                findView<PlayerControlView>()?.apply {
+                    removeView(this)
+                    addView(this)
+                }
+            }
+        }
     }
 
+//    private val controllerFuture: ListenableFuture<MediaController> by lazy {
+//        MediaController.Builder(context, sessionToken)
+//            .buildAsync()
+//    }
+
+//    private val controller: MediaController?
+//        get() = if (controllerFuture.isDone) controllerFuture.get() else null
+
     override fun getCurrentContentTitle(player: Player): String =
-        currentMediaItem?.mediaMetadata?.title?.toString() ?: ""
+        currentMediaItem?.mediaMetadata?.displayTitle?.toString() ?: ""
 
     override fun getCurrentContentText(player: Player): String =
         currentMediaItem?.mediaMetadata?.description?.toString() ?: ""
@@ -137,65 +238,38 @@ class ExoPlayerImpl(
         return null
     }
 
+    init {
+        mediaSessionConnector.apply {
+            Timber.d("Media session started : [id=${this.id}]")
+        }
+    }
+
+    private fun onPause() {
+        audioPlayerView.pause()
+        playerView.player = null
+        playerView.visibility = View.INVISIBLE
+    }
+
+    private fun onResume() {
+        audioPlayerView.resume()
+        playerView.visibility = View.VISIBLE
+        playerView.player = exoPlayer
+    }
+
     @Suppress("UNUSED_VARIABLE")
     @UnstableApi
     @Composable
-    override fun GetPlayerView() {
+    override fun GetPlayerView(
+
+    ) {
         val context = LocalContext.current
         val width = LocalConfiguration.current.screenWidthDp
         val height = LocalConfiguration.current.screenHeightDp
         val isEdit = isEditMode()
-        val audioPlayerView = remember(exoPlayer) {
-            AudioPreview(context)
-        }
-        val playerView = remember(exoPlayer) {
-            object : PlayerView(context) {
-                init {
-                    setBackgroundColor(0)
-                    setShutterBackgroundColor(0)
-                    drawingCacheBackgroundColor = 0
-                    controllerAutoShow = true
-                    controllerHideOnTouch = true
-                    useController = true
-                    useArtwork = false
-                    artworkDisplayMode = ARTWORK_DISPLAY_MODE_OFF
-                    player = exoPlayer
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-                        outlineAmbientShadowColor = 0
-                    }
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-                        outlineSpotShadowColor = 0
-                    }
-                    setShowBuffering(SHOW_BUFFERING_ALWAYS)
-                    setShowMultiWindowTimeBar(true)
-                    addView(audioPlayerView)
-                    findView<PlayerControlView>()?.apply {
-                        removeView(this)
-                        addView(this)
-                    }
-                }
-            }
-        }
-        val lifecycleObserver = rememberLifeCycleEventObserver(
-            exoPlayer
-        ) { _, event ->
-            when (event) {
-                ON_PAUSE -> {
-                    audioPlayerView.pause()
-                    playerView.player = null
-                    playerView.visibility = View.INVISIBLE
-                }
-
-                ON_RESUME -> {
-                    audioPlayerView.resume()
-                    playerView.visibility = View.VISIBLE
-                    playerView.player = exoPlayer
-                }
-
-                else -> {
-                }
-            }
-        }
+        val lifecycleObserver = rememberPlayPauseLifeCycleObserver(
+            ::onPause,
+            ::onResume
+        )
         Box(
             modifier = Modifier
                 .recomposeHighlighter()
@@ -220,34 +294,8 @@ class ExoPlayerImpl(
                         playerView
                     }
                 )
-                exoPlayer.addListener(object : Player.Listener {
-                    override fun onIsPlayingChanged(isPlaying: Boolean) {
-                        if (isPlaying) {
-                            audioPlayerView.resume()
-                        } else {
-                            audioPlayerView.pause()
-                        }
-                    }
-
-                    override fun onPlaybackStateChanged(playbackState: Int) {
-                        when (playbackState) {
-                            STATE_READY -> {
-                                audioPlayerView.setData(currentMediaItem)
-                            }
-
-                            STATE_ENDED -> {
-                                audioPlayerView.stop()
-                            }
-
-                            else -> {
-                                // no op
-                            }
-                        }
-                    }
-                })
             }
         }
-        playerNotificationManager.setPlayer(exoPlayer)
     }
 
     override fun setMediaUri(uri: Uri) {
@@ -280,6 +328,7 @@ class ExoPlayerImpl(
 
     override fun dispose() {
         stop()
+        mediaSessionConnector.release()
         exoPlayer.release()
     }
 

+ 16 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt

@@ -11,11 +11,14 @@
 package org.mjdev.tvlib.ui.components.media
 
 import android.annotation.SuppressLint
+import android.content.Context
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.media3.ui.PlayerControlView
 import org.mjdev.tvlib.annotations.TvPreview
@@ -28,15 +31,24 @@ import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMe
 @Composable
 fun MediaPlayerContainer(
     modifier: Modifier = Modifier,
-    src: Any? = null,
     autoPlay: Boolean = true,
     startSeek: Long = 0L,
-    mediaPlayer: IMediaPlayer = MediaPlayerContainerDefaults.exoPlayer,
+    context: Context = LocalContext.current,
+    isEdit: Boolean = isEditMode(),
+    items: List<Any?> = emptyList(),
+    itemToPlay: Int = 0
 ) {
-    val isEdit = isEditMode()
+    val mediaPlayer: IMediaPlayer = remember {
+        if (isEdit) {
+            IMediaPlayer.EMPTY
+        } else {
+            MediaPlayerContainerDefaults.exoPlayer(context)
+        }
+    }
     val state = rememberMediaPlayerState(
         player = mediaPlayer,
-        src = src,
+        items = items,
+        itemToPlay = itemToPlay,
         autoPlay = autoPlay,
         startSeek = startSeek,
     )

+ 7 - 34
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainerDefaults.kt

@@ -8,44 +8,17 @@
 
 package org.mjdev.tvlib.ui.components.media
 
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
+import android.content.Context
 import androidx.media3.common.util.UnstableApi
-import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 
-@UnstableApi @Suppress("unused", "MemberVisibilityCanBePrivate")
+@UnstableApi
+@Suppress("unused", "MemberVisibilityCanBePrivate")
 object MediaPlayerContainerDefaults {
 
-    val isEdit: Boolean
-        @Composable
-        get() = isEditMode()
+    fun exoPlayer(context: Context): IMediaPlayer =
+        ExoPlayerImpl(context)
 
-    val engines: Array<IMediaPlayer?>
-        @Composable
-        get() = arrayOf(
-            exoPlayer,
-            internalPlayer
-        )
-
-    val  exoPlayer: IMediaPlayer
-        @Composable
-        get() = run {
-            val context = LocalContext.current
-            if (isEdit) IMediaPlayer.EMPTY
-            else remember {
-                ExoPlayerImpl(context)
-            }
-        }
-
-    val internalPlayer: IMediaPlayer
-        @Composable
-        get() = run {
-            val context = LocalContext.current
-            if (isEdit) IMediaPlayer.EMPTY
-            else remember {
-                NativePlayerImpl(context)
-            }
-        }
+    fun internalPlayer(context: Context): IMediaPlayer =
+        NativePlayerImpl(context)
 
 }

+ 9 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerState.kt

@@ -11,6 +11,7 @@ package org.mjdev.tvlib.ui.components.media
 import android.net.Uri
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -34,11 +35,14 @@ import org.mjdev.tvlib.interfaces.ItemWithDate
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 class MediaPlayerState(
     val player: IMediaPlayer,
-    val src: Any? = null,
+    val items: List<Any?>,
+    initItem :Int = 0,
     autoPlay: Boolean = true,
     startSeek: Long = 0,
 ) {
+    val currentItem = mutableIntStateOf(initItem)
 
+    val src = items[initItem]
     val title: String get() = (src as? ItemWithTitle<*>)?.title?.toString() ?: "-"
     val date: String get() = (src as? ItemWithDate)?.date ?: "-"
     val details: String get() = metadataRetriever.getInfo(src)
@@ -114,13 +118,15 @@ class MediaPlayerState(
         @Composable
         fun rememberMediaPlayerState(
             player: IMediaPlayer,
-            src: Any? = null,
+            items: List<Any?>,
+            itemToPlay: Int = 0,
             autoPlay: Boolean = true,
             startSeek: Long = 0,
         ) = remember {
             MediaPlayerState(
                 player,
-                src,
+                items,
+                itemToPlay,
                 autoPlay,
                 startSeek,
             )