Skip to content

Commit

Permalink
Add feature navigate between channels in Player
Browse files Browse the repository at this point in the history
  • Loading branch information
tungnk123 committed Nov 3, 2024
1 parent ca92e34 commit 69752cf
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 191 deletions.
174 changes: 67 additions & 107 deletions data/src/main/java/com/m3u/data/service/internal/PlayerManagerImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.m3u.data.service.internal

import android.content.Context
import android.graphics.Rect
import android.util.Log
import androidx.compose.runtime.snapshotFlow
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
Expand Down Expand Up @@ -97,6 +98,7 @@ class PlayerManagerImpl @Inject constructor(
downloadManager: DownloadManager,
delegate: Logger
) : PlayerManager, Player.Listener, MediaSession.Callback {

private val mainCoroutineScope = CoroutineScope(mainDispatcher)
private val ioCoroutineScope = CoroutineScope(ioDispatcher)

Expand All @@ -105,23 +107,21 @@ class PlayerManagerImpl @Inject constructor(

private val mediaCommand = MutableStateFlow<MediaCommand?>(null)

override val channel: StateFlow<Channel?> = mediaCommand
.onEach { logger.post { "receive media command: $it" } }
.flatMapLatest { command ->
when (command) {
is MediaCommand.Common -> channelRepository.observe(command.channelId)
is MediaCommand.XtreamEpisode -> channelRepository
.observe(command.channelId)
.map { it?.copyXtreamEpisode(command.episode) }
override val channel: StateFlow<Channel?> =
mediaCommand.onEach { logger.post { "receive media command: $it" } }
.flatMapLatest { command ->
when (command) {
is MediaCommand.Common -> channelRepository.observe(command.channelId)
is MediaCommand.XtreamEpisode -> channelRepository.observe(command.channelId)
.map { it?.copyXtreamEpisode(command.episode) }

else -> flowOf(null)
}
}
.stateIn(
scope = ioCoroutineScope,
initialValue = null,
started = SharingStarted.WhileSubscribed(5_000L)
)
else -> flowOf(null)
}
}.stateIn(
scope = ioCoroutineScope,
initialValue = null,
started = SharingStarted.WhileSubscribed(5_000L)
)

override val playlist: StateFlow<Playlist?> = mediaCommand.flatMapLatest { command ->
when (command) {
Expand All @@ -133,20 +133,18 @@ class PlayerManagerImpl @Inject constructor(
is MediaCommand.XtreamEpisode -> {
val channel = channelRepository.get(command.channelId)
channel?.let {
playlistRepository
.observe(it.playlistUrl)
playlistRepository.observe(it.playlistUrl)
.map { prev -> prev?.copyXtreamSeries(channel) }
} ?: flowOf(null)
}

null -> flowOf(null)
}
}
.stateIn(
scope = ioCoroutineScope,
initialValue = null,
started = SharingStarted.WhileSubscribed(5_000L)
)
}.stateIn(
scope = ioCoroutineScope,
initialValue = null,
started = SharingStarted.WhileSubscribed(5_000L)
)

override val playbackState = MutableStateFlow<@Player.State Int>(Player.STATE_IDLE)
override val playbackException = MutableStateFlow<PlaybackException?>(null)
Expand All @@ -163,8 +161,7 @@ class PlayerManagerImpl @Inject constructor(
mediaCommand.value = command
val channel = when (command) {
is MediaCommand.Common -> channelRepository.get(command.channelId)
is MediaCommand.XtreamEpisode -> channelRepository
.get(command.channelId)
is MediaCommand.XtreamEpisode -> channelRepository.get(command.channelId)
?.copyXtreamEpisode(command.episode)
}
if (channel != null) {
Expand Down Expand Up @@ -201,10 +198,8 @@ class PlayerManagerImpl @Inject constructor(
}
}

private val downloads: StateFlow<List<Download>> = downloadManager
.observeDownloads()
.flowOn(ioDispatcher)
.stateIn(
private val downloads: StateFlow<List<Download>> =
downloadManager.observeDownloads().flowOn(ioDispatcher).stateIn(
scope = ioCoroutineScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = emptyList()
Expand All @@ -220,11 +215,7 @@ class PlayerManagerImpl @Inject constructor(
val rtmp: Boolean = Url(url).protocol.name == "rtmp"
val tunneling: Boolean = preferences.tunneling
logger.post {
"play, mimetype: $mimeType," +
" url: $url," +
" user-agent: $userAgent," +
" rtmp: $rtmp, " +
"tunneling: $tunneling"
"play, mimetype: $mimeType, url: $url, user-agent: $userAgent, rtmp: $rtmp, tunneling: $tunneling"
}
val dataSourceFactory = if (rtmp) {
RtmpDataSource.Factory()
Expand All @@ -239,25 +230,24 @@ class PlayerManagerImpl @Inject constructor(
.setAllowChunklessPreparation(false)
.setExtractorFactory(HlsExtractorFactory.DEFAULT)

MimeTypes.APPLICATION_SS -> ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
MimeTypes.APPLICATION_RTSP -> RtspMediaSource.Factory()
.setDebugLoggingEnabled(true)
.setForceUseRtpTcp(true)
.setSocketFactory(SSLs.TLSTrustAll.socketFactory)
MimeTypes.APPLICATION_SS -> ProgressiveMediaSource.Factory(
dataSourceFactory, extractorsFactory
)

MimeTypes.APPLICATION_RTSP -> RtspMediaSource.Factory().setDebugLoggingEnabled(true)
.setForceUseRtpTcp(true).setSocketFactory(SSLs.TLSTrustAll.socketFactory)

else -> DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
}
logger.post { "media-source-factory: ${mediaSourceFactory::class.qualifiedName}" }
if (licenseType.isNotEmpty()) {
val drmCallback = when {
(licenseType in arrayOf(
Channel.LICENSE_TYPE_CLEAR_KEY,
Channel.LICENSE_TYPE_CLEAR_KEY_2
Channel.LICENSE_TYPE_CLEAR_KEY, Channel.LICENSE_TYPE_CLEAR_KEY_2
)) && !licenseKey.startsWith("http") -> LocalMediaDrmCallback(licenseKey.toByteArray())

else -> HttpMediaDrmCallback(
licenseKey,
dataSourceFactory
licenseKey, dataSourceFactory
)
}
val uuid = when (licenseType) {
Expand All @@ -271,11 +261,9 @@ class PlayerManagerImpl @Inject constructor(
.setUuidAndExoMediaDrmProvider(uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.setMultiSession(
licenseType !in arrayOf(
Channel.LICENSE_TYPE_CLEAR_KEY,
Channel.LICENSE_TYPE_CLEAR_KEY_2
Channel.LICENSE_TYPE_CLEAR_KEY, Channel.LICENSE_TYPE_CLEAR_KEY_2
)
)
.build(drmCallback)
).build(drmCallback)
mediaSourceFactory.setDrmSessionManagerProvider { drmSessionManager }
}
}
Expand Down Expand Up @@ -314,55 +302,42 @@ class PlayerManagerImpl @Inject constructor(

override fun clearCache() {
cache.keys.forEach {
cache.getCachedSpans(it)
.forEach { span ->
cache.removeSpan(span)
}
cache.getCachedSpans(it).forEach { span ->
cache.removeSpan(span)
}
}
}

override fun chooseTrack(group: TrackGroup, index: Int) {
val currentPlayer = player.value ?: return
val type = group.type
val override = TrackSelectionOverride(group, index)
currentPlayer.trackSelectionParameters = currentPlayer.trackSelectionParameters
.buildUpon()
.setOverrideForType(override)
.setTrackTypeDisabled(type, false)
.build()
currentPlayer.trackSelectionParameters =
currentPlayer.trackSelectionParameters.buildUpon().setOverrideForType(override)
.setTrackTypeDisabled(type, false).build()
}

override fun clearTrack(type: @C.TrackType Int) {
val currentPlayer = player.value ?: return
currentPlayer.trackSelectionParameters = currentPlayer
.trackSelectionParameters
.buildUpon()
.setTrackTypeDisabled(type, true)
.build()
currentPlayer.trackSelectionParameters =
currentPlayer.trackSelectionParameters.buildUpon().setTrackTypeDisabled(type, true)
.build()
}

override val cacheSpace: Flow<Long> = flow {
while (true) {
emit(cache.cacheSpace)
delay(1.seconds)
}
}
.flowOn(ioDispatcher)
}.flowOn(ioDispatcher)

private fun createPlayer(
mediaSourceFactory: MediaSource.Factory,
tunneling: Boolean
): ExoPlayer = ExoPlayer.Builder(context)
.setMediaSourceFactory(mediaSourceFactory)
.setRenderersFactory(renderersFactory)
.setTrackSelector(createTrackSelector(tunneling))
.setHandleAudioBecomingNoisy(true)
.build()
.apply {
val attributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
mediaSourceFactory: MediaSource.Factory, tunneling: Boolean
): ExoPlayer = ExoPlayer.Builder(context).setMediaSourceFactory(mediaSourceFactory)
.setRenderersFactory(renderersFactory).setTrackSelector(createTrackSelector(tunneling))
.setHandleAudioBecomingNoisy(true).build().apply {
val attributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build()
setAudioAttributes(attributes, true)
playWhenReady = true
addListener(this@PlayerManagerImpl)
Expand All @@ -375,35 +350,28 @@ class PlayerManagerImpl @Inject constructor(
private fun createTrackSelector(tunneling: Boolean): TrackSelector {
return DefaultTrackSelector(context).apply {
setParameters(
buildUponParameters()
.setForceHighestSupportedBitrate(true)
buildUponParameters().setForceHighestSupportedBitrate(true)
.setTunnelingEnabled(tunneling)
)
}
}

private fun createHttpDataSourceFactory(userAgent: String?): DataSource.Factory {
val upstream = OkHttpDataSource.Factory(okHttpClient)
.setUserAgent(userAgent)
val upstream = OkHttpDataSource.Factory(okHttpClient).setUserAgent(userAgent)
return if (preferences.cache) {
CacheDataSource.Factory()
.setUpstreamDataSourceFactory(upstream)
.setCache(cache)
CacheDataSource.Factory().setUpstreamDataSourceFactory(upstream).setCache(cache)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
} else upstream
}

private suspend fun observePreferencesChanging(
onChanged: suspend (timeout: Long, tunneling: Boolean, cache: Boolean) -> Unit
): Unit = coroutineScope {
combine(
snapshotFlow { preferences.connectTimeout },
combine(snapshotFlow { preferences.connectTimeout },
snapshotFlow { preferences.tunneling },
snapshotFlow { preferences.cache }
) { timeout, tunneling, cache ->
snapshotFlow { preferences.cache }) { timeout, tunneling, cache ->
onChanged(timeout, tunneling, cache)
}
.collect()
}.collect()
}

override fun onVideoSizeChanged(videoSize: VideoSize) {
Expand Down Expand Up @@ -448,15 +416,11 @@ class PlayerManagerImpl @Inject constructor(
}
}

PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED,
PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED,
PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED -> {
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED, PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED, PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED, PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED -> {
if (iterator.hasNext()) {
val next = iterator.next()
logger.post {
"[${PlaybackException.getErrorCodeName(exception.errorCode)}] " +
"Try another mimetype, from $iterator to $next"
"[${PlaybackException.getErrorCodeName(exception.errorCode)}] " + "Try another mimetype, from $iterator to $next"
}
iterator = next
when (next) {
Expand Down Expand Up @@ -514,14 +478,12 @@ class PlayerManagerImpl @Inject constructor(
*/
private fun String.readKodiUrlOptions(): Map<String, String?> {
val options = this.drop(this.indexOf("|") + 1).split("&")
return options
.filter { it.isNotBlank() }
.associate {
val pair = it.split("=")
val key = pair.getOrNull(0).orEmpty()
val value = pair.getOrNull(1)
key to value
}
return options.filter { it.isNotBlank() }.associate {
val pair = it.split("=")
val key = pair.getOrNull(0).orEmpty()
val value = pair.getOrNull(1)
key to value
}
}

/**
Expand Down Expand Up @@ -570,9 +532,7 @@ private sealed class MimetypeIterator {
operator fun next(): MimetypeIterator = when (this) {
is Unspecified -> Trying(ORDER_DEFAULT.first())
is Trying -> {
ORDER_DEFAULT
.getOrNull(ORDER_DEFAULT.indexOf(mimeType) + 1)
?.let { Trying(it) }
ORDER_DEFAULT.getOrNull(ORDER_DEFAULT.indexOf(mimeType) + 1)?.let { Trying(it) }
?: Unsupported
}

Expand Down
Loading

0 comments on commit 69752cf

Please sign in to comment.