diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt index 36e6203..97f83a6 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/playback/PlayerFactory.kt @@ -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): String { +private fun buildClearKeyLicenseJson(keys: List): String { val arr = org.json.JSONArray() for (k in keys) { arr.put( @@ -299,8 +317,15 @@ private fun buildClearKeyLicenseJson(keys: List clean.substring(i * 2, i * 2 + 2).toInt(16).toByte() }