mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-17 19:35:58 +08:00
feat(tv): preview channel.
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
package com.m3u.data.codec
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import com.google.auto.service.AutoService
|
||||
@ -12,4 +14,6 @@ class LiteCodecs: Codecs {
|
||||
setEnableDecoderFallback(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getThumbnail(context: Context, uri: Uri): Bitmap? = null
|
||||
}
|
@ -18,7 +18,8 @@ dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.androidx.media3.exoplayer)
|
||||
implementation(libs.nextlib.media3.ext)
|
||||
implementation(libs.nextlib.media3ext)
|
||||
implementation(libs.nextlib.mediainfo)
|
||||
|
||||
implementation(libs.auto.service.annotations)
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
package com.m3u.data.codec
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import com.google.auto.service.AutoService
|
||||
import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory
|
||||
import io.github.anilbeesetti.nextlib.mediainfo.MediaInfoBuilder
|
||||
|
||||
@AutoService(Codecs::class)
|
||||
class RichCodec: Codecs {
|
||||
@ -14,4 +17,13 @@ class RichCodec: Codecs {
|
||||
setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getThumbnail(context: Context, uri: Uri): Bitmap? {
|
||||
val mediaInfo = MediaInfoBuilder()
|
||||
.from(context, uri)
|
||||
.build()
|
||||
val frame = mediaInfo?.getFrame()
|
||||
mediaInfo?.release()
|
||||
return frame
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package com.m3u.data.codec
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
import java.util.ServiceLoader
|
||||
|
||||
interface Codecs {
|
||||
fun createRenderersFactory(context: Context): RenderersFactory
|
||||
fun getThumbnail(context: Context, uri: Uri): Bitmap?
|
||||
|
||||
companion object {
|
||||
fun load(): Codecs {
|
||||
|
@ -49,6 +49,8 @@ interface PlayerManager {
|
||||
|
||||
val cwPositionObserver: SharedFlow<Long>
|
||||
suspend fun onRewind(channelUrl: String)
|
||||
suspend fun reloadThumbnail(channelUrl: String): Uri?
|
||||
suspend fun syncThumbnail(channelUrl: String): Uri?
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
@ -1,5 +1,8 @@
|
||||
package com.m3u.data.service.internal
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.disklrucache.DiskLruCache
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -9,29 +12,46 @@ import java.security.MessageDigest
|
||||
internal class ChannelPreferenceProvider(
|
||||
directory: File,
|
||||
appVersion: Int,
|
||||
private val ioDispatcher: CoroutineDispatcher
|
||||
ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
private val cache = DiskLruCache.open(directory, appVersion, 2, 4 * 1024 * 1024) // 4mb
|
||||
private val limitedParallelism = ioDispatcher.limitedParallelism(1, "channel-preference")
|
||||
private val cache = DiskLruCache.open(directory, appVersion, 3, 4 * 1024 * 1024) // 4mb
|
||||
|
||||
suspend operator fun get(channelUrl: String): ChannelPreference? = withContext(ioDispatcher) {
|
||||
suspend operator fun get(
|
||||
channelUrl: String
|
||||
): ChannelPreference? = withContext(limitedParallelism) {
|
||||
runCatching {
|
||||
val key = encodeKey(channelUrl)
|
||||
val snapshot = cache.get(key) ?: return@withContext null
|
||||
ChannelPreference(
|
||||
cwPosition = snapshot.getString(0).toLong(),
|
||||
mineType = snapshot.getString(1)
|
||||
)
|
||||
snapshot.use {
|
||||
ChannelPreference(
|
||||
cwPosition = it.getString(0)?.toLong() ?: -1L,
|
||||
mineType = it.getString(1)?.takeIf { it.isNotEmpty() },
|
||||
thumbnail = it.getString(2)?.toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Log.e("TAG", "get: ", it)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
suspend operator fun set(channelUrl: String, value: ChannelPreference) = withContext(ioDispatcher) {
|
||||
|
||||
suspend operator fun set(
|
||||
channelUrl: String,
|
||||
value: ChannelPreference
|
||||
) = withContext(limitedParallelism) {
|
||||
runCatching {
|
||||
val key = encodeKey(channelUrl)
|
||||
val editor = cache.edit(key) ?: return@withContext
|
||||
editor.set(0, value.cwPosition.toString())
|
||||
editor.set(1, value.mineType)
|
||||
editor.set(1, value.mineType.orEmpty())
|
||||
editor.set(2, value.thumbnail?.toString().orEmpty())
|
||||
editor.commit()
|
||||
}
|
||||
.onFailure {
|
||||
Log.e("TAG", "set: ", it)
|
||||
}
|
||||
}
|
||||
|
||||
// [a-z0-9_-]{1,64}
|
||||
@ -49,4 +69,5 @@ internal class ChannelPreferenceProvider(
|
||||
internal data class ChannelPreference(
|
||||
val cwPosition: Long = -1L,
|
||||
val mineType: String? = null,
|
||||
val thumbnail: Uri? = null
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.m3u.data.service.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
@ -99,6 +101,9 @@ import kotlinx.coroutines.flow.updateAndGet
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ -425,6 +430,38 @@ class PlayerManagerImpl @Inject constructor(
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
|
||||
override suspend fun reloadThumbnail(channelUrl: String): Uri? {
|
||||
val channelPreference = getChannelPreference(channelUrl)
|
||||
return channelPreference?.thumbnail
|
||||
}
|
||||
|
||||
private val thumbnailDir by lazy {
|
||||
context.cacheDir.resolve("thumbnails").apply {
|
||||
if (!exists()) {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun syncThumbnail(channelUrl: String): Uri? = withContext(ioDispatcher) {
|
||||
val thumbnail = codecs.getThumbnail(context, channelUrl.toUri())?: return@withContext null
|
||||
val filename = UUID.randomUUID().toString() + ".jpeg"
|
||||
val file = File(thumbnailDir, filename)
|
||||
while (!file.createNewFile()) {
|
||||
ensureActive()
|
||||
file.delete()
|
||||
}
|
||||
FileOutputStream(file).use {
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 50, it)
|
||||
}
|
||||
val uri = file.toUri()
|
||||
addChannelPreference(
|
||||
channelUrl,
|
||||
getChannelPreference(channelUrl)?.copy(
|
||||
thumbnail = uri
|
||||
) ?: ChannelPreference(thumbnail = uri)
|
||||
)
|
||||
uri
|
||||
}
|
||||
private fun createPlayer(
|
||||
mediaSourceFactory: MediaSource.Factory,
|
||||
tunneling: Boolean
|
||||
@ -444,8 +481,9 @@ class PlayerManagerImpl @Inject constructor(
|
||||
addListener(this@PlayerManagerImpl)
|
||||
}
|
||||
|
||||
private val codecs: Codecs by lazy { Codecs.load() }
|
||||
private val renderersFactory: RenderersFactory by lazy {
|
||||
Codecs.load().createRenderersFactory(context)
|
||||
codecs.createRenderersFactory(context)
|
||||
}
|
||||
|
||||
private fun createTrackSelector(tunneling: Boolean): TrackSelector {
|
||||
@ -514,7 +552,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
logger.post { "onPlayerErrorChanged, parsing error! invalidate remembered mimeType!" }
|
||||
val channelPreference = getChannelPreference(chain.url)
|
||||
if (channelPreference != null) {
|
||||
updateChannelPreference(
|
||||
addChannelPreference(
|
||||
chain.url,
|
||||
channelPreference.copy(mineType = null)
|
||||
)
|
||||
@ -697,7 +735,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
|
||||
is MimetypeChain.Trying -> {
|
||||
val channelPreference = getChannelPreference(chain.url)
|
||||
updateChannelPreference(
|
||||
addChannelPreference(
|
||||
chain.url,
|
||||
channelPreference?.copy(mineType = chain.mimetype)
|
||||
?: ChannelPreference(mineType = chain.mimetype)
|
||||
@ -737,7 +775,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
logger.post { "storeContinueWatching, received new position: $cwPosition" }
|
||||
if (cwPosition == -1L) return@collect
|
||||
val channelPreference = getChannelPreference(channelUrl)
|
||||
updateChannelPreference(
|
||||
addChannelPreference(
|
||||
channelUrl,
|
||||
channelPreference?.copy(cwPosition = cwPosition)
|
||||
?: ChannelPreference(cwPosition = cwPosition)
|
||||
@ -766,7 +804,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
val player = this@PlayerManagerImpl.player.value
|
||||
withContext(mainDispatcher) {
|
||||
if (player != null && continueWatchingCondition.isResettingSupported(player, ignorePositionCondition)) {
|
||||
updateChannelPreference(
|
||||
addChannelPreference(
|
||||
channelUrl,
|
||||
channelPreference?.copy(cwPosition = -1L) ?: ChannelPreference(cwPosition = -1L)
|
||||
)
|
||||
@ -815,7 +853,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
return channelPreferenceProvider[channelUrl]
|
||||
}
|
||||
|
||||
private suspend fun updateChannelPreference(
|
||||
private suspend fun addChannelPreference(
|
||||
channelUrl: String,
|
||||
channelPreference: ChannelPreference
|
||||
) {
|
||||
|
Reference in New Issue
Block a user