Răsfoiți Sursa

player improvements

player improvements
Milan Jurkulak 3 ani în urmă
părinte
comite
15fc53de03
20 a modificat fișierele cu 288 adăugiri și 236 ștergeri
  1. 24 3
      app/build.gradle.kts
  2. 7 15
      app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt
  3. 0 24
      app/src/main/kotlin/org/mjdev/tvapp/repository/IMovieRepository.kt
  4. 0 56
      app/src/main/kotlin/org/mjdev/tvapp/repository/MovieRepository.kt
  5. 0 2
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt
  6. 0 5
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncService.kt
  7. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  8. 10 20
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt
  9. 43 7
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt
  10. 13 14
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt
  11. 4 3
      tvlib/build.gradle.kts
  12. 9 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt
  13. 2 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  14. 74 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/MediaItemExt.kt
  15. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/ExoPlayerImpl.kt
  16. 7 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/IMediaPlayer.kt
  17. 21 18
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt
  18. 42 62
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerState.kt
  19. 3 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt
  20. 27 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/PlayerControlView.kt

+ 24 - 3
app/build.gradle.kts

@@ -190,13 +190,14 @@ dependencies {
     // tv library by mjde milan jurkulak
     implementation(project(mapOf("path" to ":tvlib")))
     // base libs
-    implementation("androidx.core:core-ktx:1.12.0-beta01")
+    implementation("androidx.core:core-ktx:1.12.0-rc01")
+    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
     // compose base libs
     implementation(platform("androidx.compose:compose-bom:2023.06.01"))
     implementation("androidx.compose.ui:ui-tooling")
     implementation("androidx.activity:activity-compose:1.7.2")
     // more icons
-    implementation("androidx.compose.material:material-icons-extended:1.4.3")
+    implementation("androidx.compose.material:material-icons-extended:1.5.0")
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-alpha08")
     implementation("androidx.tv:tv-material:1.0.0-alpha08")
@@ -223,7 +224,7 @@ dependencies {
     // view model
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
     // navigation
-    implementation("androidx.navigation:navigation-compose:2.7.0-rc01")
+    implementation("androidx.navigation:navigation-compose:2.7.0")
     // retrofit
     implementation("com.squareup.retrofit2:retrofit:2.9.0")
     implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
@@ -247,6 +248,26 @@ dependencies {
     implementation("androidx.glance:glance-material3:1.0.0-rc01")
     // fix duplicates
     implementation("androidx.work:work-runtime-ktx:2.8.1")
+    // exoplayer
+    implementation("androidx.media3:media3-exoplayer:1.1.0")
+    implementation("androidx.media3:media3-exoplayer-dash:1.1.0")
+    implementation("androidx.media3:media3-exoplayer-hls:1.1.0")
+    implementation("androidx.media3:media3-exoplayer-rtsp:1.1.0")
+    implementation("androidx.media3:media3-exoplayer-ima:1.1.0")
+    implementation("androidx.media3:media3-datasource-cronet:1.1.0")
+    implementation("androidx.media3:media3-datasource-okhttp:1.1.0")
+    implementation("androidx.media3:media3-datasource-rtmp:1.1.0")
+    implementation("androidx.media3:media3-ui:1.1.0")
+    implementation("androidx.media3:media3-ui-leanback:1.1.0")
+    implementation("androidx.media3:media3-session:1.1.0")
+    implementation("androidx.media3:media3-extractor:1.1.0")
+    implementation("androidx.media3:media3-cast:1.1.0")
+    implementation("androidx.media3:media3-exoplayer-workmanager:1.1.0")
+    implementation("androidx.media3:media3-transformer:1.1.0")
+    implementation("androidx.media3:media3-database:1.1.0")
+    implementation("androidx.media3:media3-decoder:1.1.0")
+    implementation("androidx.media3:media3-datasource:1.1.0")
+    implementation("androidx.media3:media3-common:1.1.0")
     // libs mismatch
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0")?.apply {

+ 7 - 15
app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt

@@ -30,8 +30,6 @@ import org.mjdev.tvlib.network.CacheInterceptor
 import org.mjdev.tvlib.network.NetworkConnectivityService
 import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
 import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvapp.repository.IMovieRepository
-import org.mjdev.tvapp.repository.MovieRepository
 import retrofit2.Retrofit
 import retrofit2.converter.moshi.MoshiConverterFactory
 import javax.inject.Singleton
@@ -44,6 +42,13 @@ class ProvideModule {
     private val isDebug = BuildConfig.DEBUG
     private val baseUrl = BuildConfig.IPTV_API_URL
 
+    @Singleton
+    @Provides
+    fun provideDAO(
+        @ApplicationContext
+        context: Context
+    ): DAO = DAO(context)
+
     @Singleton
     @Provides
     fun providesAudioCursor(
@@ -72,12 +77,6 @@ class ProvideModule {
         context: Context
     ): NetworkConnectivityService = NetworkConnectivityServiceImpl(context)
 
-    @Singleton
-    @Provides
-    fun providesMovieRepository(
-        dao: DAO
-    ): IMovieRepository = MovieRepository(dao)
-
     @Singleton
     @Provides
     fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor().apply {
@@ -104,13 +103,6 @@ class ProvideModule {
     @Provides
     fun providesMoshi(): Moshi = Moshi.Builder().build()
 
-    @Singleton
-    @Provides
-    fun provideDAO(
-        @ApplicationContext
-        context: Context
-    ): DAO = DAO(context)
-
     @Singleton
     @Provides
     fun providesRetrofit(

+ 0 - 24
app/src/main/kotlin/org/mjdev/tvapp/repository/IMovieRepository.kt

@@ -1,24 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-@file:Suppress("unused")
-
-package org.mjdev.tvapp.repository
-
-import org.mjdev.tvapp.data.local.Message
-import org.mjdev.tvapp.data.local.Movie
-
-interface IMovieRepository {
-
-    suspend fun getMovies(page: Int = 0, count: Int = Int.MAX_VALUE): Result<List<Movie>>
-
-    suspend fun findMovieById(id: Long?): Result<Movie>
-
-    suspend fun getMessages(): Result<List<Message>>
-
-}

+ 0 - 56
app/src/main/kotlin/org/mjdev/tvapp/repository/MovieRepository.kt

@@ -1,56 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.repository
-
-import org.mjdev.tvapp.data.local.Message
-import org.mjdev.tvapp.data.local.Movie
-import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvapp.database.DAO.Companion.tx
-import org.mjdev.tvlib.extensions.GlobalExt.runSafe
-import javax.inject.Inject
-
-class MovieRepository @Inject constructor(
-    private var dao: DAO
-) : IMovieRepository {
-
-    override suspend fun getMovies(
-        page: Int,
-        count: Int
-    ): Result<List<Movie>> = runSafe {
-        val fromIdx = page * count
-        var toIdx = fromIdx + count
-        dao.movieDao.tx {
-            all.let { list ->
-                if (fromIdx >= list.size)
-                    emptyList()
-                else if (toIdx >= list.size) {
-                    toIdx = list.size - 1
-                    list.subList(fromIdx, toIdx)
-                } else {
-                    list.subList(fromIdx, toIdx)
-                }
-            }
-        } ?: emptyList()
-    }
-
-    override suspend fun findMovieById(
-        id: Long?
-    ): Result<Movie> = runSafe {
-        dao.movieDao.tx {
-            get(id!!)
-        } ?: throw (Exception("Movie not found."))
-    }
-
-    override suspend fun getMessages(): Result<List<Message>> = runSafe {
-        dao.messagesDao.tx {
-            all
-        } ?: emptyList()
-    }
-
-}

+ 0 - 2
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt

@@ -15,7 +15,6 @@ import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvapp.database.DAO
 import org.mjdev.tvapp.database.DAO.Companion.tx
 import org.mjdev.tvapp.repository.ApiService
-import org.mjdev.tvapp.repository.IMovieRepository
 import org.mjdev.tvlib.extensions.GlobalExt.safeGet
 import org.mjdev.tvlib.extensions.ListExt.contains
 import timber.log.Timber
@@ -23,7 +22,6 @@ import timber.log.Timber
 @Suppress("unused", "PrivatePropertyName")
 class SyncAdapter(
     context: Context,
-    val repository: IMovieRepository,
     val apiService: ApiService,
     val dao: DAO
 ) : AbstractThreadedSyncAdapter(context, true, false) {

+ 0 - 5
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncService.kt

@@ -14,16 +14,12 @@ import dagger.hilt.android.AndroidEntryPoint
 import org.mjdev.tvapp.BuildConfig
 import org.mjdev.tvapp.database.DAO
 import org.mjdev.tvapp.repository.ApiService
-import org.mjdev.tvapp.repository.IMovieRepository
 import timber.log.Timber
 import javax.inject.Inject
 
 @AndroidEntryPoint
 class SyncService : Service() {
 
-    @Inject
-    lateinit var repository: IMovieRepository
-
     @Inject
     lateinit var apiService: ApiService
 
@@ -35,7 +31,6 @@ class SyncService : Service() {
         synchronized(sSyncAdapterLock) {
             sSyncAdapter = sSyncAdapter ?: SyncAdapter(
                 context = applicationContext,
-                repository = repository,
                 apiService = apiService,
                 dao = dao
             )

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt

@@ -60,7 +60,7 @@ class DetailScreen : Screen() {
             is ItemAudio -> viewModel.localAudioCursor
             is ItemVideo -> viewModel.localVideoCursor
             is ItemPhoto -> viewModel.localPhotoCursor
-            is Movie -> viewModel.movieList()
+            is Movie -> viewModel.mediaItems
             else -> listOf(data)
         }
 

+ 10 - 20
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -21,16 +21,11 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
-import org.mjdev.tvapp.data.local.Movie
-import org.mjdev.tvapp.viewmodel.DetailViewModel
 import org.mjdev.tvlib.annotations.TvPreview
-import org.mjdev.tvlib.extensions.HiltExt.appViewModel
-import org.mjdev.tvlib.interfaces.ItemAudio
-import org.mjdev.tvlib.interfaces.ItemPhoto
-import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.screen.Screen
 import org.mjdev.tvlib.ui.components.media.MediaPlayerContainer
+import org.mjdev.tvlib.extensions.MediaItemExt.mediaItem
 
 class IPTVScreen : Screen() {
 
@@ -52,21 +47,16 @@ class IPTVScreen : Screen() {
     @Composable
     override fun ComposeScreen() {
 
-        val viewModel: DetailViewModel = appViewModel { context ->
-            DetailViewModel.mockDetailViewModel(context)
-        }
+//        val viewModel: DetailViewModel = appViewModel { context ->
+//            DetailViewModel.mockDetailViewModel(context)
+//        }
 
         val data: Any? = remember { args[data] }
 
-        val dataList = remember {
-            when (data) {
-                is ItemAudio -> viewModel.localAudioCursor
-                is ItemVideo -> viewModel.localVideoCursor
-                is ItemPhoto -> viewModel.localPhotoCursor
-                is Movie -> viewModel.movieList()
-                else -> listOf(data)
-            }
-        }
+        // todo anr
+//        val dataList :List<MediaItem> = remember {
+//            viewModel.mediaItemsFor(data)
+//        }
 
         Box(
             modifier = Modifier
@@ -75,8 +65,8 @@ class IPTVScreen : Screen() {
         ) {
             MediaPlayerContainer(
                 modifier = Modifier.fillMaxSize(),
-                items = dataList,
-                itemToPlay = dataList.indexOf(data)
+                items = listOf(data.mediaItem),
+                itemToPlay = 0
             )
         }
 

+ 43 - 7
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt

@@ -6,27 +6,33 @@
  * w: https://mjdev.org
  */
 
+@file:Suppress("unused")
+
 package org.mjdev.tvapp.viewmodel
 
 import android.content.Context
+import androidx.media3.common.MediaItem
 import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.runBlocking
+import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvlib.helpers.cursor.AudioCursor
 import org.mjdev.tvlib.helpers.cursor.PhotoCursor
 import org.mjdev.tvlib.helpers.cursor.VideoCursor
 import org.mjdev.tvlib.network.NetworkConnectivityService
 import org.mjdev.tvlib.viewmodel.BaseViewModel
 import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvapp.repository.IMovieRepository
-import org.mjdev.tvapp.repository.MovieRepository
+import org.mjdev.tvlib.extensions.MediaItemExt.mediaItem
+import org.mjdev.tvlib.interfaces.ItemAudio
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
 import javax.inject.Inject
 
+@Suppress("MemberVisibilityCanBePrivate")
 @HiltViewModel
 class DetailViewModel @Inject constructor() : BaseViewModel() {
 
     @Inject
-    lateinit var movieRepository: IMovieRepository
+    lateinit var dao: DAO
 
     @Inject
     lateinit var networkInfo: NetworkConnectivityService
@@ -40,8 +46,38 @@ class DetailViewModel @Inject constructor() : BaseViewModel() {
     @Inject
     lateinit var localPhotoCursor: PhotoCursor
 
-    fun movieList() = runBlocking {
-        movieRepository.getMovies().getOrDefault(emptyList())
+    val mediaItems: List<MediaItem> by lazy {
+        dao.movieDao.all.map { item ->
+            item.mediaItem
+        }
+    }
+
+    val localMediaItemsAudio by lazy {
+        localAudioCursor.map { item ->
+            item.mediaItem
+        }
+    }
+
+    val localMediaItemsVideo by lazy {
+        localVideoCursor.map { item ->
+            item.mediaItem
+        }
+    }
+
+    val localMediaItemsPhoto by lazy {
+        localPhotoCursor.map { item ->
+            item.mediaItem
+        }
+    }
+
+    fun mediaItemsFor(
+        data: Any?
+    ): List<MediaItem> = when (data) {
+        is ItemAudio -> localMediaItemsAudio
+        is ItemVideo -> localMediaItemsVideo
+        is ItemPhoto -> localMediaItemsPhoto
+        is Movie -> mediaItems
+        else -> listOf(data).map { item -> item.mediaItem }
     }
 
     companion object {
@@ -50,7 +86,7 @@ class DetailViewModel @Inject constructor() : BaseViewModel() {
         fun mockDetailViewModel(
             context: Context
         ): DetailViewModel = DetailViewModel().apply {
-            movieRepository = MovieRepository(DAO(context))
+            dao = DAO(context)
             networkInfo = NetworkConnectivityServiceImpl(context)
             localAudioCursor = AudioCursor(context)
             localVideoCursor = VideoCursor(context)

+ 13 - 14
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -15,7 +15,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.runBlocking
 import org.mjdev.tvapp.BuildConfig
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.activity.MainActivity
@@ -27,9 +26,8 @@ import org.mjdev.tvlib.network.NetworkConnectivityService
 import org.mjdev.tvlib.viewmodel.BaseViewModel
 import org.mjdev.tvapp.data.local.Message
 import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvapp.data.local.Movie_
 import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvapp.repository.IMovieRepository
-import org.mjdev.tvapp.repository.MovieRepository
 import org.mjdev.tvlib.helpers.apps.appsManager
 import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
 import javax.inject.Inject
@@ -41,7 +39,7 @@ class MainViewModel @Inject constructor(
 ) : BaseViewModel() {
 
     @Inject
-    lateinit var movieRepository: IMovieRepository
+    lateinit var dao: DAO
 
     @Inject
     lateinit var networkInfo: NetworkConnectivityService
@@ -67,12 +65,13 @@ class MainViewModel @Inject constructor(
     ).stateInViewModel()
 
     val messages: StateFlow<List<Message>> = channelFlow {
-        send(movieRepository.getMessages().getOrThrow())
+        send(dao.messagesDao.all)
     }.stateInViewModel()
 
-    val featuredMovieList: StateFlow<List<Any?>> = flow {
+    val featuredMovieList: StateFlow<List<Any?>> = channelFlow {
         mutableListOf<Any?>().apply {
-            addAll(movieRepository.getMovies().getOrThrow().takeLast(8))
+            // todo latest
+            addAll(dao.movieDao.query().build().find(0, 8))
             if (localAudioCursor.count > 0) {
                 add(localAudioCursor.getData(0))
             }
@@ -83,12 +82,12 @@ class MainViewModel @Inject constructor(
                 add(localPhotoCursor.getData(0))
             }
         }.also { result ->
-            emit(result)
+            send(result)
         }
     }.stateInViewModel()
 
     val movieList: StateFlow<Map<String, List<Movie>>> = flow {
-        movieRepository.getMovies().getOrDefault(emptyList()).map { movie ->
+        dao.movieDao.all.map { movie ->
             movie.apply {
                 category = category ?: noCategoryString
             }
@@ -102,7 +101,7 @@ class MainViewModel @Inject constructor(
     }.stateInViewModel()
 
     val countryList: StateFlow<List<String>> = flow {
-        movieRepository.getMovies().getOrThrow().filter { it.country != null }.map {
+        dao.movieDao.all.filter { it.country != null }.map {
             it.country.toString()
         }.distinct().sortedBy {
             it
@@ -111,9 +110,9 @@ class MainViewModel @Inject constructor(
         }
     }.stateInViewModel()
 
-    fun findMovie(id: Long?): Movie? = runBlocking {
-        movieRepository.findMovieById(id).getOrNull()
-    }
+    fun findMovie(id: Long?): Movie? = dao.movieDao.query()
+        .equal(Movie_.id, id ?: -1)
+        .build().findFirst()
 
     companion object {
 
@@ -121,7 +120,7 @@ class MainViewModel @Inject constructor(
         fun mockMainViewModel(
             context: Context
         ): MainViewModel = MainViewModel(context).apply {
-            movieRepository = MovieRepository(DAO(context))
+            dao = DAO(context)
             networkInfo = NetworkConnectivityServiceImpl(context)
             localAudioCursor = AudioCursor(context)
             localVideoCursor = VideoCursor(context)

+ 4 - 3
tvlib/build.gradle.kts

@@ -84,7 +84,8 @@ android {
 
 dependencies {
     // base libs
-    implementation("androidx.core:core-ktx:1.12.0-beta01")
+    implementation("androidx.core:core-ktx:1.12.0-rc01")
+    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
     // reflect / debug purposes only
     implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0")
     // window manager
@@ -94,14 +95,14 @@ dependencies {
     implementation("androidx.compose.ui:ui-tooling")
     implementation("androidx.activity:activity-compose:1.7.2")
     // more icons
-    implementation("androidx.compose.material:material-icons-extended:1.4.3")
+    implementation("androidx.compose.material:material-icons-extended:1.5.0")
     // tv compose
     implementation("androidx.tv:tv-foundation:1.0.0-alpha08")
     implementation("androidx.tv:tv-material:1.0.0-alpha08")
     // view model
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
     // navigation
-    implementation("androidx.navigation:navigation-compose:2.7.0-rc01")
+    implementation("androidx.navigation:navigation-compose:2.7.0")
     // okhttp
     implementation("com.squareup.okhttp3:okhttp:4.11.0")
     implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")

+ 9 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt

@@ -17,6 +17,8 @@ import android.os.Build
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.focus.FocusRequester
@@ -69,6 +71,13 @@ object ComposeExt {
     val DrawerState.isClosed: Boolean
         get() = currentValue == DrawerValue.Closed
 
+    @Composable
+    fun <T> rememberDerivedStateOf(
+        function: () -> T
+    ): State<T> = remember {
+        derivedStateOf(function)
+    }
+
     @Composable
     fun <T> textFrom(text: T?): String = when (text) {
         null -> ""

+ 2 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt

@@ -23,8 +23,8 @@ object GlobalExt {
 
     @Suppress("MemberVisibilityCanBePrivate")
     class CodeException(
-        val code:Int = -1,
-        message:String = String.format("$code : Unhandled Exception.")
+        val code: Int = -1,
+        message: String = String.format("$code : Unhandled Exception.")
     ) : Exception(message)
 
     fun MutableState<Boolean>.toggle() {

+ 74 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/MediaItemExt.kt

@@ -0,0 +1,74 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused", "MemberVisibilityCanBePrivate")
+
+package org.mjdev.tvlib.extensions
+
+import android.content.Context
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
+import org.mjdev.tvlib.extensions.StringExt.parseUri
+import org.mjdev.tvlib.helpers.media.MetadataRetriever
+import org.mjdev.tvlib.interfaces.ItemAudio
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemVideo
+import org.mjdev.tvlib.interfaces.ItemWithBackground
+import org.mjdev.tvlib.interfaces.ItemWithDate
+import org.mjdev.tvlib.interfaces.ItemWithImage
+import org.mjdev.tvlib.interfaces.ItemWithTitle
+import org.mjdev.tvlib.interfaces.ItemWithUri
+
+object MediaItemExt {
+
+    val Context.metadataRetriever
+        get() = MetadataRetriever(this)
+
+    val Any?.title: String
+        get() = (this as? ItemWithTitle<*>)?.title?.toString() ?: "-"
+
+    val Any?.date: String
+        get() = (this as? ItemWithDate)?.date ?: ""
+
+    fun Any?.details(context: Context): String =
+        context.metadataRetriever.getInfo(this)
+
+    val Any?.uri: String
+        get() = ((this as? ItemWithUri<*>)?.uri ?: this).toString()
+
+    val Any?.mediaType: Int
+        get() = when (this) {
+            is ItemAudio -> MediaMetadata.MEDIA_TYPE_MUSIC
+            is ItemVideo -> MediaMetadata.MEDIA_TYPE_MOVIE
+            is ItemPhoto -> MediaMetadata.MEDIA_TYPE_ALBUM
+            else -> MediaMetadata.MEDIA_TYPE_MIXED
+        }
+
+    val Any?.imageUrl: Any?
+        get() {
+            val photo = (this as? ItemPhoto)?.uri
+            val image = (this as? ItemWithImage<*>)?.image
+            val background = (this as? ItemWithBackground<*>)?.background
+            return photo ?: image ?: background
+        }
+
+    // todo more info
+    val Any?.metaData: MediaMetadata
+        get() = MediaMetadata.Builder()
+            .setDisplayTitle(title)
+            .setMediaType(mediaType)
+
+            .setArtworkUri(imageUrl.toString().parseUri())
+            .build()
+
+    val Any?.mediaItem: MediaItem
+        get() = MediaItem.Builder()
+            .setUri(uri)
+            .setMediaMetadata(metaData)
+            .build()
+}

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

@@ -260,7 +260,7 @@ class ExoPlayerImpl(
     @UnstableApi
     @Composable
     override fun GetPlayerView(
-
+        modifier: Modifier
     ) {
         val context = LocalContext.current
         val width = LocalConfiguration.current.screenWidthDp

+ 7 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/IMediaPlayer.kt

@@ -16,6 +16,7 @@ import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.view.TextureView
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.media3.common.AudioAttributes
 import androidx.media3.common.DeviceInfo
 import androidx.media3.common.MediaItem
@@ -38,7 +39,9 @@ interface IMediaPlayer : Player {
     val context: Context
 
     @Composable
-    fun GetPlayerView()
+    fun GetPlayerView(
+        modifier: Modifier
+    )
 
     fun setMediaUri(uri: Uri)
 
@@ -62,7 +65,9 @@ interface IMediaPlayer : Player {
             override val context: Context get() = throw (Exception("Unimplemented"))
 
             @Composable
-            override fun GetPlayerView() {
+            override fun GetPlayerView(
+                modifier: Modifier
+            ) {
             }
 
             override fun setMediaUri(uri: Uri) {}

+ 21 - 18
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt

@@ -19,8 +19,7 @@ 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 androidx.media3.common.MediaItem
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
@@ -35,45 +34,49 @@ fun MediaPlayerContainer(
     startSeek: Long = 0L,
     context: Context = LocalContext.current,
     isEdit: Boolean = isEditMode(),
-    items: List<Any?> = emptyList(),
-    itemToPlay: Int = 0
-) {
-    val mediaPlayer: IMediaPlayer = remember {
+    items: List<MediaItem> = emptyList(),
+    itemToPlay: Int = 0,
+    mediaPlayer: IMediaPlayer = remember {
         if (isEdit) {
             IMediaPlayer.EMPTY
         } else {
             MediaPlayerContainerDefaults.exoPlayer(context)
         }
-    }
-    val state = rememberMediaPlayerState(
+    },
+    state: MediaPlayerState = rememberMediaPlayerState(
         player = mediaPlayer,
         items = items,
         itemToPlay = itemToPlay,
         autoPlay = autoPlay,
         startSeek = startSeek,
     )
+) {
     Box(
         modifier = modifier
             .recomposeHighlighter()
             .fillMaxSize()
     ) {
-        mediaPlayer.GetPlayerView()
+        mediaPlayer.GetPlayerView(
+            modifier = modifier
+                .recomposeHighlighter()
+                .fillMaxSize()
+        )
         if (isEdit) {
-            AndroidView(
-                modifier = Modifier.fillMaxSize(),
-                factory = { context ->
-                    PlayerControlView(context)
-                }
+            PlayerControlView(
+                modifier = modifier
+                    .recomposeHighlighter()
+                    .fillMaxSize()
             )
         }
     }
     DisposableEffect(state) {
         if (state.hasMediaToPlay) {
-            mediaPlayer.setMediaItem(state.mediaItem)
+            mediaPlayer.setMediaItems(
+                state.mediaItems.value,
+                state.currentItemIndex.intValue,
+                state.currentPosition.longValue
+            )
             mediaPlayer.prepare()
-            if (state.seek.value > 0L) {
-                mediaPlayer.seekTo(state.seek.value)
-            }
             if (state.isAutoPlay) {
                 mediaPlayer.play()
             }

+ 42 - 62
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerState.kt

@@ -8,86 +8,50 @@
 
 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.derivedStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.media3.common.MediaItem
-import androidx.media3.common.MediaMetadata
-import androidx.media3.common.MediaMetadata.MEDIA_TYPE_ALBUM
-import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MOVIE
-import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MUSIC
-import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MIXED
-import org.mjdev.tvlib.helpers.media.MetadataRetriever
-import org.mjdev.tvlib.interfaces.ItemAudio
-import org.mjdev.tvlib.interfaces.ItemPhoto
-import org.mjdev.tvlib.interfaces.ItemVideo
-import org.mjdev.tvlib.interfaces.ItemWithBackground
-import org.mjdev.tvlib.interfaces.ItemWithImage
-import org.mjdev.tvlib.interfaces.ItemWithTitle
-import org.mjdev.tvlib.interfaces.ItemWithUri
-import org.mjdev.tvlib.extensions.StringExt.parseUri
-import org.mjdev.tvlib.interfaces.ItemWithDate
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.Player
+import org.mjdev.tvlib.extensions.MediaItemExt.mediaItem
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 class MediaPlayerState(
     val player: IMediaPlayer,
-    val items: List<Any?>,
-    initItem :Int = 0,
+    initItems: List<MediaItem>,
+    initItemIndex: 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)
-    val uri: String get() = ((src as? ItemWithUri<*>)?.uri ?: src).toString()
-
-    val mediaType: Int = when (src) {
-        is ItemAudio -> MEDIA_TYPE_MUSIC
-        is ItemVideo -> MEDIA_TYPE_MOVIE
-        is ItemPhoto -> MEDIA_TYPE_ALBUM
-        else -> MEDIA_TYPE_MIXED
-    }
+) : Player.Listener {
+
+    val mediaItems: MutableState<List<MediaItem>> = mutableStateOf(
+        initItems
+    )
+
+    val currentItemIndex = mutableIntStateOf(initItemIndex)
 
-    val metadataRetriever by lazy {
-        MetadataRetriever(player.context)
+    val currentItem = derivedStateOf {
+        mediaItems.value[currentItemIndex.intValue].mediaItem
     }
 
-    val mediaUri: MutableState<Any?> = mutableStateOf(uri)
+    val currentPosition = mutableLongStateOf(startSeek)
+
     val isPlaying: MutableState<Boolean> = mutableStateOf(autoPlay)
-    val seek: MutableState<Long> = mutableLongStateOf(startSeek)
 
-    val hasMediaToPlay = mediaUri.value != Uri.EMPTY
-    val isAutoPlay = isPlaying.value
+    val error: MutableState<Exception?> = mutableStateOf(null)
 
-    val imageUrl: Any?
-        get() {
-            val photo = (src as? ItemPhoto)?.uri
-            val image = (src as? ItemWithImage<*>)?.image
-            val background = (src as? ItemWithBackground<*>)?.background
-            return photo ?: image ?: background
-        }
+    val hasMediaToPlay = mediaItems.value.isNotEmpty()
+
+    val isAutoPlay = isPlaying.value
 
-    val metaData
-        get() = MediaMetadata.Builder()
-            .setDisplayTitle(title)
-            .setDescription(details)
-            .setMediaType(mediaType)
-            // todo more info
-            .setArtworkUri(imageUrl.toString().parseUri())
-            .build()
-
-    val mediaItem
-        get() = MediaItem.Builder()
-            .setUri(mediaUri.value.toString())
-            .setMediaMetadata(metaData)
-            .build()
+    init {
+        player.addListener(this)
+    }
 
     fun play() {
         isPlaying.value = true
@@ -106,19 +70,35 @@ class MediaPlayerState(
     }
 
     fun seekTo(ms: Long) {
-        seek.value = ms
+        currentPosition.longValue = ms
     }
 
     fun dispose() {
         player.release()
     }
 
+    override fun onIsPlayingChanged(isPlaying: Boolean) {
+        this.isPlaying.value = isPlaying
+    }
+
+    override fun onPlayerError(error: PlaybackException) {
+        this.error.value = error
+    }
+
+    override fun onPlayerErrorChanged(error: PlaybackException?) {
+        this.error.value = error
+    }
+
+    override fun onPlaybackStateChanged(playbackState: Int) {
+        currentPosition.longValue = player.currentPosition
+    }
+
     companion object {
 
         @Composable
         fun rememberMediaPlayerState(
             player: IMediaPlayer,
-            items: List<Any?>,
+            items: List<MediaItem>,
             itemToPlay: Int = 0,
             autoPlay: Boolean = true,
             startSeek: Long = 0,

+ 3 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt

@@ -49,7 +49,9 @@ class NativePlayerImpl(
 ) : IMediaPlayer {
 
     @Composable
-    override fun GetPlayerView() {
+    override fun GetPlayerView(
+        modifier: Modifier
+    ) {
         val width = LocalConfiguration.current.screenWidthDp
         val height = LocalConfiguration.current.screenHeightDp
         val isEdit = ComposeExt.isEditMode()

+ 27 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/PlayerControlView.kt

@@ -0,0 +1,27 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.media
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.media3.ui.PlayerControlView
+
+@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+@Composable
+fun  PlayerControlView(
+    modifier: Modifier = Modifier
+) {
+    AndroidView(
+        modifier = modifier,
+        factory = { context ->
+            PlayerControlView(context)
+        }
+    )
+}