/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer.models import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Bundle import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper data class TrackPlayerMediaItem( val mediaId: String? = null, val url: String, val title: String? = null, val artist: String? = null, val albumTitle: String? = null, val artworkUrl: String? = null, val duration: Double? = null, val isLive: Boolean? = null, val mimeType: String? = null, val headers: Map? = null, val extras: Bundle? = null ) { /** * Build a Media3 [MediaItem] from this descriptor. * * Pass a [context] to resolve `asset://` URLs into Android resource * URIs (`android.resource:////`). Without a context * we still build a valid [MediaItem], but `asset://` URLs are left * untouched and will fail in ExoPlayer. */ fun asMediaItem(context: Context? = null): MediaItem { val resolvedUrl = context?.let { resolveAssetUrl(it, url) } ?: url val resolvedArtwork = artworkUrl?.let { aw -> context?.let { resolveAssetUrl(it, aw) } ?: aw } val id = mediaId ?: resolvedUrl val metadataExtras = Bundle() duration?.let { metadataExtras.putDouble("duration", it) } isLive?.let { metadataExtras.putBoolean("isLive", it) } extras?.let { MediaItemExtras.attachToMetadataExtras(metadataExtras, it) } val metadata = MediaMetadata.Builder() .setTitle(title) .setArtist(artist) .setAlbumTitle(albumTitle) .setArtworkUri(resolvedArtwork?.let { Uri.parse(it) }) .setExtras(metadataExtras) .build() val requestExtras = Bundle() headers?.let { h -> val headerBundle = Bundle() h.forEach { (k, v) -> headerBundle.putString(k, v) } requestExtras.putBundle("headers", headerBundle) } // Only set mediaUri for remote URLs — CastPlayer uses this to tell the // Chromecast device what to stream. Local file://, android.resource://, // content://, and asset:// URIs are all inaccessible from a Chromecast. val uri = Uri.parse(resolvedUrl) val requestMetadataBuilder = MediaItem.RequestMetadata.Builder().setExtras(requestExtras) if (uri.scheme == "http" || uri.scheme == "https") { requestMetadataBuilder.setMediaUri(uri) } val builder = MediaItem.Builder() .setUri(resolvedUrl) .setMediaId(id) .setMediaMetadata(metadata) .setRequestMetadata(requestMetadataBuilder.build()) .apply { mimeType?.let { setMimeType(it) } } if (isLive == true) { builder.setLiveConfiguration( MediaItem.LiveConfiguration.Builder().build() ) } val mediaItem = builder.build() headers?.let { MediaHeaders.set(resolvedUrl, it) } return mediaItem } fun toWritableMap(): WritableMap { val map = Arguments.createMap() map.putString("mediaId", mediaId ?: url) map.putString("url", url) title?.let { map.putString("title", it) } artist?.let { map.putString("artist", it) } albumTitle?.let { map.putString("albumTitle", it) } artworkUrl?.let { map.putString("artworkUrl", it) } duration?.let { map.putDouble("duration", it) } isLive?.let { map.putBoolean("isLive", it) } extras?.let { map.putMap("extras", Arguments.fromBundle(it)) } return map } companion object { /** * Resolve an `asset://` URL into an Android resource URI by * looking up `name` in the app's drawable and raw resources. Returns the * input unchanged for non-`asset://` URLs, or when no matching resource * exists (ExoPlayer will then surface the failure). */ fun resolveAssetUrl(context: Context, url: String): String { if (!url.startsWith("asset://")) return url val name = url.removePrefix("asset://") if (name.isEmpty()) return url val baseName = name.substringBeforeLast('.').lowercase().replace("-", "_") val res = context.resources val pkg = context.packageName // Try drawable first (images), then raw (audio/video/other). var id = res.getIdentifier(baseName, "drawable", pkg) if (id == 0) id = res.getIdentifier(baseName, "raw", pkg) if (id == 0) { // Fallback: let ResourceDrawableIdHelper try its own heuristics. id = ResourceDrawableIdHelper.getResourceDrawableId(context, name) } if (id == 0) return url return Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(res.getResourcePackageName(id)) .appendPath(res.getResourceTypeName(id)) .appendPath(res.getResourceEntryName(id)) .build() .toString() } private fun parseUrl(map: ReadableMap): Pair?> { if (!map.hasKey("url")) return "" to null return when (map.getType("url").name) { "Map" -> { val urlMap = map.getMap("url")!! val uri = urlMap.getString("uri") ?: urlMap.getString("url") ?: "" val headers = mutableMapOf() urlMap.getMap("headers")?.let { h -> val iter = h.keySetIterator() while (iter.hasNextKey()) { val key = iter.nextKey() h.getString(key)?.let { headers[key] = it } } } uri to headers.ifEmpty { null } } else -> (map.getString("url") ?: "") to null } } fun fromReadableMap(map: ReadableMap): TrackPlayerMediaItem { val (url, headers) = parseUrl(map) val extras = MediaItemExtras.parseBundle(map) return TrackPlayerMediaItem( mediaId = if (map.hasKey("mediaId")) map.getString("mediaId") else null, url = url, title = if (map.hasKey("title")) map.getString("title") else null, artist = if (map.hasKey("artist")) map.getString("artist") else null, albumTitle = if (map.hasKey("albumTitle")) map.getString("albumTitle") else null, artworkUrl = if (map.hasKey("artworkUrl")) map.getString("artworkUrl") else null, duration = if (map.hasKey("duration")) map.getDouble("duration") else null, isLive = if (map.hasKey("isLive")) map.getBoolean("isLive") else null, mimeType = if (map.hasKey("mimeType")) map.getString("mimeType") else null, headers = headers, extras = extras, ) } fun fromMediaItem(mediaItem: MediaItem): TrackPlayerMediaItem { val metadata = mediaItem.mediaMetadata val metadataExtras = metadata.extras return TrackPlayerMediaItem( mediaId = mediaItem.mediaId, url = mediaItem.localConfiguration?.uri?.toString() ?: mediaItem.mediaId, title = metadata.title?.toString(), artist = metadata.artist?.toString(), albumTitle = metadata.albumTitle?.toString(), artworkUrl = metadata.artworkUri?.toString(), duration = metadataExtras?.getDouble("duration"), isLive = if (metadataExtras?.containsKey("isLive") == true) metadataExtras.getBoolean("isLive") else null, extras = metadataExtras?.getBundle(MediaItemExtras.METADATA_KEY), ) } } }