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:
Jos Vooges | STH 2026-09-18 19:37:27 +02:00
parent 0700c79878
commit 2a734cbcd1

View file

@ -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()
}