Initial release
This commit is contained in:
220
app/src/main/java/com/futbollibre/tv/player/ExoPlayerManager.kt
Normal file
220
app/src/main/java/com/futbollibre/tv/player/ExoPlayerManager.kt
Normal file
@@ -0,0 +1,220 @@
|
||||
package com.futbollibre.tv.player
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
||||
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
|
||||
import androidx.media3.exoplayer.drm.LocalMediaDrmCallback
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||
import com.futbollibre.tv.model.StreamType
|
||||
import com.futbollibre.tv.model.StreamUrl
|
||||
|
||||
/**
|
||||
* Manager class for ExoPlayer instance.
|
||||
*/
|
||||
class ExoPlayerManager private constructor() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ExoPlayerManager"
|
||||
private const val USER_AGENT =
|
||||
"Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
|
||||
private const val CONNECT_TIMEOUT_MS = 30_000
|
||||
private const val READ_TIMEOUT_MS = 30_000
|
||||
|
||||
@Volatile
|
||||
private var instance: ExoPlayerManager? = null
|
||||
|
||||
fun getInstance(): ExoPlayerManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: ExoPlayerManager().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createPlayer(
|
||||
context: Context,
|
||||
trackSelector: TrackSelector = DefaultTrackSelector(context)
|
||||
): ExoPlayer {
|
||||
Log.d(TAG, "Creating ExoPlayer instance")
|
||||
|
||||
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
||||
.setUserAgent(USER_AGENT)
|
||||
.setConnectTimeoutMs(CONNECT_TIMEOUT_MS)
|
||||
.setReadTimeoutMs(READ_TIMEOUT_MS)
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
|
||||
val dataSourceFactory = DefaultDataSource.Factory(context, httpDataSourceFactory)
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
|
||||
|
||||
return ExoPlayer.Builder(context)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build()
|
||||
.apply {
|
||||
playWhenReady = true
|
||||
volume = 1f
|
||||
Log.d(TAG, "ExoPlayer instance created successfully")
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareHlsStream(player: ExoPlayer, streamUrl: StreamUrl, context: Context) {
|
||||
prepareStream(player, streamUrl, context)
|
||||
}
|
||||
|
||||
fun prepareStream(player: ExoPlayer, streamUrl: StreamUrl, context: Context) {
|
||||
Log.d(TAG, "Preparing ${streamUrl.streamType} stream: ${streamUrl.url}")
|
||||
|
||||
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
||||
.setUserAgent(USER_AGENT)
|
||||
.setConnectTimeoutMs(CONNECT_TIMEOUT_MS)
|
||||
.setReadTimeoutMs(READ_TIMEOUT_MS)
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
|
||||
streamUrl.referer?.let { referer ->
|
||||
Log.d(TAG, "Setting Referer header: $referer")
|
||||
httpDataSourceFactory.setDefaultRequestProperties(
|
||||
mapOf(
|
||||
"Referer" to referer,
|
||||
"Origin" to extractOrigin(referer)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val dataSourceFactory = DefaultDataSource.Factory(context, httpDataSourceFactory)
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
|
||||
val mediaItemBuilder = MediaItem.Builder()
|
||||
.setUri(streamUrl.url)
|
||||
.setMimeType(
|
||||
when (streamUrl.streamType) {
|
||||
StreamType.HLS -> MimeTypes.APPLICATION_M3U8
|
||||
StreamType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
}
|
||||
)
|
||||
|
||||
if (streamUrl.streamType == StreamType.DASH && streamUrl.clearKeys.isNotEmpty()) {
|
||||
val drmSessionManager = buildClearKeyDrmSessionManager(streamUrl.clearKeys)
|
||||
mediaSourceFactory.setDrmSessionManagerProvider { drmSessionManager }
|
||||
mediaItemBuilder.setDrmConfiguration(
|
||||
MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID)
|
||||
.setPlayClearContentWithoutKey(true)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
val mediaSource = mediaSourceFactory.createMediaSource(mediaItemBuilder.build())
|
||||
player.setMediaSource(mediaSource)
|
||||
player.prepare()
|
||||
|
||||
Log.d(TAG, "Stream preparation started")
|
||||
}
|
||||
|
||||
fun prepareStream(player: ExoPlayer, url: String, referer: String?, context: Context) {
|
||||
val streamType = if (url.contains(".mpd", ignoreCase = true)) {
|
||||
StreamType.DASH
|
||||
} else {
|
||||
StreamType.HLS
|
||||
}
|
||||
prepareStream(player, StreamUrl(url = url, referer = referer, streamType = streamType), context)
|
||||
}
|
||||
|
||||
fun releasePlayer(player: ExoPlayer?) {
|
||||
player?.let {
|
||||
Log.d(TAG, "Releasing ExoPlayer instance")
|
||||
it.stop()
|
||||
it.release()
|
||||
Log.d(TAG, "ExoPlayer released successfully")
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildClearKeyDrmSessionManager(clearKeys: Map<String, String>): DefaultDrmSessionManager {
|
||||
val licenseResponse = buildClearKeyLicenseResponse(clearKeys)
|
||||
val drmCallback = LocalMediaDrmCallback(licenseResponse)
|
||||
|
||||
return DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(C.CLEARKEY_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.setPlayClearSamplesWithoutKeys(true)
|
||||
.build(drmCallback)
|
||||
}
|
||||
|
||||
private fun buildClearKeyLicenseResponse(clearKeys: Map<String, String>): ByteArray {
|
||||
val keysJson = clearKeys.entries.joinToString(",") { (kid, key) ->
|
||||
"""{"kty":"oct","kid":"${hexToBase64Url(kid)}","k":"${hexToBase64Url(key)}"}"""
|
||||
}
|
||||
return """{"keys":[$keysJson],"type":"temporary"}""".toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun hexToBase64Url(hex: String): String {
|
||||
val bytes = hex.chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
|
||||
return Base64.encodeToString(
|
||||
bytes,
|
||||
Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractOrigin(url: String): String {
|
||||
return try {
|
||||
val uri = android.net.Uri.parse(url)
|
||||
val scheme = uri.scheme ?: "https"
|
||||
val host = uri.host ?: ""
|
||||
val port = uri.port
|
||||
if (port > 0 && port != 80 && port != 443) {
|
||||
"$scheme://$host:$port"
|
||||
} else {
|
||||
"$scheme://$host"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error extracting origin from URL", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PlayerStateListener {
|
||||
fun onLoadingChanged(isLoading: Boolean)
|
||||
fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int)
|
||||
fun onPlayerError(error: PlaybackException)
|
||||
fun onTracksChanged(hasVideo: Boolean, hasAudio: Boolean)
|
||||
}
|
||||
|
||||
fun ExoPlayer.addStateListener(listener: PlayerStateListener) {
|
||||
addListener(object : Player.Listener {
|
||||
override fun onIsLoadingChanged(isLoading: Boolean) {
|
||||
listener.onLoadingChanged(isLoading)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState)
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
listener.onPlayerError(error)
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
var hasVideo = false
|
||||
var hasAudio = false
|
||||
for (group in tracks.groups) {
|
||||
for (i in 0 until group.length) {
|
||||
if (group.type == C.TRACK_TYPE_VIDEO) hasVideo = true
|
||||
if (group.type == C.TRACK_TYPE_AUDIO) hasAudio = true
|
||||
}
|
||||
}
|
||||
listener.onTracksChanged(hasVideo, hasAudio)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user