Fix ClearKey live playback by using LocalMediaDrmCallback.
OkHttp cannot fetch data: license URIs, which caused SOURCE_ERROR on DRM channels.
This commit is contained in:
parent
0700c79878
commit
2a734cbcd1
1 changed files with 44 additions and 19 deletions
|
|
@ -16,6 +16,10 @@ import androidx.media3.exoplayer.ExoPlayer
|
|||
import androidx.media3.exoplayer.Renderer
|
||||
import androidx.media3.exoplayer.audio.AudioRendererEventListener
|
||||
import androidx.media3.exoplayer.audio.AudioSink
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManager
|
||||
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
|
||||
import androidx.media3.exoplayer.drm.LocalMediaDrmCallback
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
|
|
@ -26,7 +30,8 @@ import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE
|
|||
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import nl.vonas.mediacluster.tv.data.ClearKeyEntry
|
||||
import nl.vonas.mediacluster.tv.data.DrmInfo
|
||||
/**
|
||||
* Hardware-video + configureerbare audio.
|
||||
* - PCM (standaard): FFmpeg prefer → decode naar PCM (veilig op TV zonder AVR-decoder).
|
||||
|
|
@ -185,7 +190,7 @@ private val liveOkHttp: OkHttpClient by lazy {
|
|||
.build()
|
||||
}
|
||||
|
||||
/** Live IPTV: TS-first, kleine buffer, lage latency. */
|
||||
/** Live IPTV: TS-first, kleine buffer, lage latency. ClearKey via LocalMediaDrmCallback (geen data:-URI). */
|
||||
@OptIn(UnstableApi::class)
|
||||
fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
||||
val appContext = context.applicationContext
|
||||
|
|
@ -216,9 +221,14 @@ fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
|||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setDrmSessionManagerProvider { mediaItem ->
|
||||
clearKeyDrmSessionManagerFromTag(mediaItem) ?: DrmSessionManager.DRM_UNSUPPORTED
|
||||
}
|
||||
|
||||
return ExoPlayer.Builder(appContext, renderersFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.setLoadControl(loadControl)
|
||||
.setAudioAttributes(audioAttributes, /* handleAudioFocus = */ true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
|
|
@ -227,11 +237,26 @@ fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
|||
.also { it.volume = 1f }
|
||||
}
|
||||
|
||||
/** ClearKey-keys in MediaItem.tag — OkHttp kan geen data:-license-URI laden. */
|
||||
private data class ClearKeyLicenseTag(val json: String)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun clearKeyDrmSessionManagerFromTag(mediaItem: MediaItem): DrmSessionManager? {
|
||||
val tag = mediaItem.localConfiguration?.tag as? ClearKeyLicenseTag ?: return null
|
||||
val callback = LocalMediaDrmCallback(tag.json.toByteArray(Charsets.UTF_8))
|
||||
return DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(C.CLEARKEY_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.setMultiSession(false)
|
||||
.setPlayClearSamplesWithoutKeys(true)
|
||||
.build(callback)
|
||||
}
|
||||
|
||||
/** TS / HLS / DASH(+ClearKey). */
|
||||
@OptIn(UnstableApi::class)
|
||||
fun liveMediaItem(
|
||||
url: String,
|
||||
format: String,
|
||||
drm: nl.vonas.mediacluster.tv.data.DrmInfo? = null,
|
||||
drm: DrmInfo? = null,
|
||||
): MediaItem {
|
||||
val isDash = format.equals("dash", ignoreCase = true) ||
|
||||
url.contains(".mpd", ignoreCase = true)
|
||||
|
|
@ -265,23 +290,16 @@ fun liveMediaItem(
|
|||
val clearKeys = drm?.takeIf { it.type.equals("clearkey", ignoreCase = true) }?.keys.orEmpty()
|
||||
if (clearKeys.isNotEmpty()) {
|
||||
val licenseJson = buildClearKeyLicenseJson(clearKeys)
|
||||
val licenseUri =
|
||||
"data:application/json;base64," +
|
||||
android.util.Base64.encodeToString(
|
||||
licenseJson.toByteArray(Charsets.UTF_8),
|
||||
android.util.Base64.NO_WRAP,
|
||||
)
|
||||
builder.setDrmConfiguration(
|
||||
MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID)
|
||||
.setLicenseUri(licenseUri)
|
||||
.setForceDefaultLicenseUri(true)
|
||||
.build(),
|
||||
)
|
||||
builder
|
||||
.setTag(ClearKeyLicenseTag(licenseJson))
|
||||
.setDrmConfiguration(
|
||||
MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID).build(),
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildClearKeyLicenseJson(keys: List<nl.vonas.mediacluster.tv.data.ClearKeyEntry>): String {
|
||||
private fun buildClearKeyLicenseJson(keys: List<ClearKeyEntry>): String {
|
||||
val arr = org.json.JSONArray()
|
||||
for (k in keys) {
|
||||
arr.put(
|
||||
|
|
@ -299,8 +317,15 @@ private fun buildClearKeyLicenseJson(keys: List<nl.vonas.mediacluster.tv.data.Cl
|
|||
|
||||
/** ClearKey JSON uses URL-safe base64 without padding. */
|
||||
private fun hexToBase64Url(hex: String): String {
|
||||
val clean = hex.trim().removePrefix("0x").replace(Regex("[\\s:\\-]"), "").lowercase()
|
||||
require(clean.length % 2 == 0) { "ongeldige hex key" }
|
||||
val clean = hex.trim()
|
||||
.removePrefix("0x")
|
||||
.removePrefix("0X")
|
||||
.replace("-", "")
|
||||
.replace(Regex("[\\s:]"), "")
|
||||
.lowercase()
|
||||
require(clean.length == 32 && clean.all { it in "0123456789abcdef" }) {
|
||||
"ongeldige ClearKey hex (verwacht 32 tekens): ${hex.take(16)}…"
|
||||
}
|
||||
val bytes = ByteArray(clean.length / 2) { i ->
|
||||
clean.substring(i * 2, i * 2 + 2).toInt(16).toByte()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue