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.Renderer
|
||||||
import androidx.media3.exoplayer.audio.AudioRendererEventListener
|
import androidx.media3.exoplayer.audio.AudioRendererEventListener
|
||||||
import androidx.media3.exoplayer.audio.AudioSink
|
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.mediacodec.MediaCodecSelector
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
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 androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import nl.vonas.mediacluster.tv.data.ClearKeyEntry
|
||||||
|
import nl.vonas.mediacluster.tv.data.DrmInfo
|
||||||
/**
|
/**
|
||||||
* Hardware-video + configureerbare audio.
|
* Hardware-video + configureerbare audio.
|
||||||
* - PCM (standaard): FFmpeg prefer → decode naar PCM (veilig op TV zonder AVR-decoder).
|
* - PCM (standaard): FFmpeg prefer → decode naar PCM (veilig op TV zonder AVR-decoder).
|
||||||
|
|
@ -185,7 +190,7 @@ private val liveOkHttp: OkHttpClient by lazy {
|
||||||
.build()
|
.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)
|
@OptIn(UnstableApi::class)
|
||||||
fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
||||||
val appContext = context.applicationContext
|
val appContext = context.applicationContext
|
||||||
|
|
@ -216,9 +221,14 @@ fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
||||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory)
|
||||||
|
.setDrmSessionManagerProvider { mediaItem ->
|
||||||
|
clearKeyDrmSessionManagerFromTag(mediaItem) ?: DrmSessionManager.DRM_UNSUPPORTED
|
||||||
|
}
|
||||||
|
|
||||||
return ExoPlayer.Builder(appContext, renderersFactory)
|
return ExoPlayer.Builder(appContext, renderersFactory)
|
||||||
.setTrackSelector(trackSelector)
|
.setTrackSelector(trackSelector)
|
||||||
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
.setLoadControl(loadControl)
|
.setLoadControl(loadControl)
|
||||||
.setAudioAttributes(audioAttributes, /* handleAudioFocus = */ true)
|
.setAudioAttributes(audioAttributes, /* handleAudioFocus = */ true)
|
||||||
.setHandleAudioBecomingNoisy(true)
|
.setHandleAudioBecomingNoisy(true)
|
||||||
|
|
@ -227,11 +237,26 @@ fun buildLiveTvPlayer(context: Context): ExoPlayer {
|
||||||
.also { it.volume = 1f }
|
.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). */
|
/** TS / HLS / DASH(+ClearKey). */
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
fun liveMediaItem(
|
fun liveMediaItem(
|
||||||
url: String,
|
url: String,
|
||||||
format: String,
|
format: String,
|
||||||
drm: nl.vonas.mediacluster.tv.data.DrmInfo? = null,
|
drm: DrmInfo? = null,
|
||||||
): MediaItem {
|
): MediaItem {
|
||||||
val isDash = format.equals("dash", ignoreCase = true) ||
|
val isDash = format.equals("dash", ignoreCase = true) ||
|
||||||
url.contains(".mpd", 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()
|
val clearKeys = drm?.takeIf { it.type.equals("clearkey", ignoreCase = true) }?.keys.orEmpty()
|
||||||
if (clearKeys.isNotEmpty()) {
|
if (clearKeys.isNotEmpty()) {
|
||||||
val licenseJson = buildClearKeyLicenseJson(clearKeys)
|
val licenseJson = buildClearKeyLicenseJson(clearKeys)
|
||||||
val licenseUri =
|
builder
|
||||||
"data:application/json;base64," +
|
.setTag(ClearKeyLicenseTag(licenseJson))
|
||||||
android.util.Base64.encodeToString(
|
.setDrmConfiguration(
|
||||||
licenseJson.toByteArray(Charsets.UTF_8),
|
MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID).build(),
|
||||||
android.util.Base64.NO_WRAP,
|
|
||||||
)
|
|
||||||
builder.setDrmConfiguration(
|
|
||||||
MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID)
|
|
||||||
.setLicenseUri(licenseUri)
|
|
||||||
.setForceDefaultLicenseUri(true)
|
|
||||||
.build(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return builder.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()
|
val arr = org.json.JSONArray()
|
||||||
for (k in keys) {
|
for (k in keys) {
|
||||||
arr.put(
|
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. */
|
/** ClearKey JSON uses URL-safe base64 without padding. */
|
||||||
private fun hexToBase64Url(hex: String): String {
|
private fun hexToBase64Url(hex: String): String {
|
||||||
val clean = hex.trim().removePrefix("0x").replace(Regex("[\\s:\\-]"), "").lowercase()
|
val clean = hex.trim()
|
||||||
require(clean.length % 2 == 0) { "ongeldige hex key" }
|
.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 ->
|
val bytes = ByteArray(clean.length / 2) { i ->
|
||||||
clean.substring(i * 2, i * 2 + 2).toInt(16).toByte()
|
clean.substring(i * 2, i * 2 + 2).toInt(16).toByte()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue