/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer.models import android.net.Uri import android.os.Bundle import androidx.media3.cast.DefaultMediaItemConverter import androidx.media3.cast.MediaItemConverter import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaQueueItem import org.json.JSONArray import org.json.JSONException import org.json.JSONObject /** * Cast [MediaItemConverter] that preserves RNTP fields dropped by [DefaultMediaItemConverter] * (app [MediaItemExtras.METADATA_KEY] payload, artist/album/artwork hints, duration/isLive). */ @UnstableApi internal class TrackPlayerCastMediaItemConverter : MediaItemConverter { private val delegate = DefaultMediaItemConverter() override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem { val queueItem = delegate.toMediaQueueItem(mediaItem) val mediaInfo = queueItem.media ?: return queueItem val customData = copyCustomData(mediaInfo.customData) if (!encodeRntpPayload(mediaItem, customData)) { return queueItem } val contentId = checkNotNull(mediaInfo.contentId) { "Cast MediaInfo must have a contentId" } val builder = MediaInfo.Builder(contentId) .setStreamType(mediaInfo.streamType) .setMetadata(mediaInfo.metadata) .setCustomData(customData) mediaInfo.contentType?.let { builder.setContentType(it) } mediaInfo.contentUrl?.let { builder.setContentUrl(it) } val updatedInfo = builder.build() return MediaQueueItem.Builder(updatedInfo).build() } override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem { val item = delegate.toMediaItem(mediaQueueItem) val rntp = mediaQueueItem.media?.customData?.optJSONObject(KEY_RNTP) ?: return item return decodeRntpPayload(item, rntp) } private fun encodeRntpPayload(mediaItem: MediaItem, customData: JSONObject): Boolean { val meta = mediaItem.mediaMetadata val payload = JSONObject() try { meta.artist?.toString()?.let { payload.put(KEY_ARTIST, it) } meta.albumTitle?.toString()?.let { payload.put(KEY_ALBUM_TITLE, it) } meta.artworkUri?.toString()?.let { payload.put(KEY_ARTWORK_URL, it) } meta.extras?.let { extras -> if (extras.containsKey(KEY_DURATION)) { payload.put(KEY_DURATION, extras.getDouble(KEY_DURATION)) } if (extras.containsKey(KEY_IS_LIVE)) { payload.put(KEY_IS_LIVE, extras.getBoolean(KEY_IS_LIVE)) } extras.getBundle(MediaItemExtras.METADATA_KEY)?.let { appExtras -> payload.put(KEY_EXTRAS, bundleToJson(appExtras)) } } if (payload.length() == 0) { return false } customData.put(KEY_RNTP, payload) return true } catch (e: JSONException) { throw RuntimeException(e) } } private fun decodeRntpPayload(item: MediaItem, rntp: JSONObject): MediaItem { val existing = item.mediaMetadata val metaBuilder = existing.buildUpon() if (existing.artist.isNullOrEmpty() && rntp.has(KEY_ARTIST)) { metaBuilder.setArtist(rntp.getString(KEY_ARTIST)) } if (existing.albumTitle.isNullOrEmpty() && rntp.has(KEY_ALBUM_TITLE)) { metaBuilder.setAlbumTitle(rntp.getString(KEY_ALBUM_TITLE)) } if (existing.artworkUri == null && rntp.has(KEY_ARTWORK_URL)) { metaBuilder.setArtworkUri(Uri.parse(rntp.getString(KEY_ARTWORK_URL))) } val metadataExtras = (item.mediaMetadata.extras?.let { Bundle(it) }) ?: Bundle() if (rntp.has(KEY_DURATION) && !metadataExtras.containsKey(KEY_DURATION)) { metadataExtras.putDouble(KEY_DURATION, rntp.getDouble(KEY_DURATION)) } if (rntp.has(KEY_IS_LIVE) && !metadataExtras.containsKey(KEY_IS_LIVE)) { metadataExtras.putBoolean(KEY_IS_LIVE, rntp.getBoolean(KEY_IS_LIVE)) } if (rntp.has(KEY_EXTRAS) && !metadataExtras.containsKey(MediaItemExtras.METADATA_KEY)) { MediaItemExtras.attachToMetadataExtras( metadataExtras, jsonToBundle(rntp.getJSONObject(KEY_EXTRAS)), ) } metaBuilder.setExtras(metadataExtras) return item.buildUpon().setMediaMetadata(metaBuilder.build()).build() } private fun copyCustomData(customData: JSONObject?): JSONObject = if (customData == null) JSONObject() else JSONObject(customData.toString()) private companion object { const val KEY_RNTP = "rntp" const val KEY_ARTIST = "artist" const val KEY_ALBUM_TITLE = "albumTitle" const val KEY_ARTWORK_URL = "artworkUrl" const val KEY_DURATION = "duration" const val KEY_IS_LIVE = "isLive" const val KEY_EXTRAS = "extras" fun bundleToJson(bundle: Bundle): JSONObject { val json = JSONObject() for (key in bundle.keySet()) { @Suppress("DEPRECATION") when (val value = bundle.get(key)) { null -> Unit is Boolean -> json.put(key, value) is Int -> json.put(key, value) is Long -> json.put(key, value) is Double -> json.put(key, value) is Float -> json.put(key, value.toDouble()) is String -> json.put(key, value) is Bundle -> json.put(key, bundleToJson(value)) is ArrayList<*> -> json.put(key, JSONArray(value.map { listValueToJson(it) })) else -> json.put(key, value.toString()) } } return json } private fun listValueToJson(value: Any?): Any = when (value) { null -> JSONObject.NULL is Boolean -> value is Int -> value is Long -> value is Double -> value is Float -> value.toDouble() is String -> value is Bundle -> bundleToJson(value) is ArrayList<*> -> JSONArray(value.map { listValueToJson(it) }) else -> value.toString() } fun jsonToBundle(json: JSONObject): Bundle { val bundle = Bundle() val keys = json.keys() while (keys.hasNext()) { val key = keys.next() when (val value = json.get(key)) { JSONObject.NULL -> Unit is Boolean -> bundle.putBoolean(key, value) is Int -> bundle.putInt(key, value) is Long -> bundle.putLong(key, value) is Double -> bundle.putDouble(key, value) is String -> bundle.putString(key, value) is JSONObject -> bundle.putBundle(key, jsonToBundle(value)) is JSONArray -> { val list = ArrayList() for (i in 0 until value.length()) { list.add(jsonArrayValueToKotlin(value.get(i))) } @Suppress("DEPRECATION") bundle.putSerializable(key, list) } } } return bundle } private fun jsonArrayValueToKotlin(value: Any?): Any? = when (value) { JSONObject.NULL, null -> null is Boolean -> value is Int -> value is Long -> value is Double -> value is String -> value is JSONObject -> jsonToBundle(value) is JSONArray -> { val list = ArrayList() for (i in 0 until value.length()) { list.add(jsonArrayValueToKotlin(value.get(i))) } list } else -> value.toString() } } }