/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer.models import android.content.Context import com.facebook.react.bridge.ReadableMap import com.doublesymmetry.trackplayer.TrackPlayerModule.Companion.PLAYER_PREFS_NAME import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @Serializable enum class RemoteControlHandling { NATIVE, JS, HYBRID, } @Serializable data class PlayerConfig( /** * Audio content type: "music" or "speech". */ val contentType: String = "music", /** * Toggle whether the player should pause automatically when audio is rerouted from a headset to device speakers. */ val handleAudioBecomingNoisy: Boolean = true, /** * When true (default), stream-derived metadata (ICY/ID3) is written back into * the queued MediaItem so getActiveMediaItem() and the system Now Playing * info (notification, Bluetooth, Android Auto) follow the live title — even * when the JS bundle isn't running. When false, only the MetadataReceived * event fires; apps mutate the queue themselves via updateMetadata(). */ val autoUpdateMetadataFromStream: Boolean = true, /** * How this player's audio coexists with other apps' audio. * "exclusive" (default) interrupts others; "mix" plays layered without * requesting audio focus. Maps to ExoPlayer setAudioAttributes handleAudioFocus. */ val audioMixing: String = "exclusive", /** * The wake mode to use for the player. */ val wakeMode: WakeMode = WakeMode.NONE, /** * Toggle whether skipping silences is enabled. */ val skipSilenceEnabled: Boolean = false, val taskRemovedBehavior: TaskRemovedBehavior = TaskRemovedBehavior.CONTINUE, /** * The available commands for the player. Defaults to PlayPause only. * Updated at runtime via setCommands(). */ val availableCommands: List = listOf(PlayerCommand.PLAY_PAUSE), /** * Who handles remote control button presses. * @default NATIVE */ val remoteControlHandling: RemoteControlHandling = RemoteControlHandling.NATIVE, /** * Per-command handling overrides (only used when remoteControlHandling is HYBRID). */ val perCommandHandling: Map = mapOf(), /** * Skip-forward interval in seconds (default 15). */ val forwardInterval: Long = 15, /** * Skip-backward interval in seconds (default 15). */ val backwardInterval: Long = 15, /** * Android notification channel ID. */ val notificationChannelId: String? = null, /** * Android notification channel name. */ val notificationChannelName: String? = null, /** * Android notification small icon resource name. */ val notificationSmallIcon: String? = null, /** * Max disk cache size in bytes. Null means caching is disabled. */ val cacheMaxSizeBytes: Long? = null, /** * Auto-preload window — preload next N tracks. 0 means disabled. */ val preloadWindow: Int = 0, /** * Cast receiver app ID. Null means Cast is disabled. * Use CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID for the default receiver. */ val castReceiverAppId: String? = null, val progressSyncIntervalSeconds: Double = 0.0, val progressSyncHttpUrl: String? = null, val progressSyncHttpHeaders: Map? = null, ): Loadable { override fun store(context: Context) { val sharedPreferences = context.getSharedPreferences(PLAYER_PREFS_NAME, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() val json = Json.encodeToString(this) editor.putString(PLAYER_CONFIG_KEY, json) editor.apply() } override fun load(context: Context): PlayerConfig { val sharedPreferences = context.getSharedPreferences(PLAYER_PREFS_NAME, Context.MODE_PRIVATE) val json = sharedPreferences.getString(PLAYER_CONFIG_KEY, null) return if (json != null) { Json.decodeFromString(json) } else { PlayerConfig() } } fun withCommands(commands: ReadableMap): PlayerConfig { val handlingStr = commands.getString("handling") val handling = if (handlingStr != null) { when (handlingStr) { "native" -> RemoteControlHandling.NATIVE "js" -> RemoteControlHandling.JS "hybrid" -> RemoteControlHandling.HYBRID else -> remoteControlHandling } } else { remoteControlHandling } val newPerCommandHandling = if (handling == RemoteControlHandling.HYBRID && commands.hasKey("perCommandHandling")) { val map = mutableMapOf() val perCommand = commands.getMap("perCommandHandling") perCommand?.let { pc -> val iterator = pc.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() val value = pc.getString(key) map[key] = when (value) { "native" -> RemoteControlHandling.NATIVE "js" -> RemoteControlHandling.JS else -> RemoteControlHandling.NATIVE } } } map } else { perCommandHandling } return copy( availableCommands = if (commands.hasKey("capabilities")) { commands.getArray("capabilities")?.toArrayList()?.mapNotNull { when (it) { "seek" -> PlayerCommand.SEEK "playPause" -> PlayerCommand.PLAY_PAUSE "next" -> PlayerCommand.NEXT "previous" -> PlayerCommand.PREVIOUS "stop" -> PlayerCommand.STOP "skipForward" -> PlayerCommand.SKIP_FORWARD "skipBackward" -> PlayerCommand.SKIP_BACKWARD else -> null } } ?: availableCommands } else { availableCommands }, remoteControlHandling = handling, perCommandHandling = newPerCommandHandling, forwardInterval = if (commands.hasKey("forwardInterval")) commands.getDouble("forwardInterval").toLong() else forwardInterval, backwardInterval = if (commands.hasKey("backwardInterval")) commands.getDouble("backwardInterval").toLong() else backwardInterval, ) } companion object { private const val PLAYER_CONFIG_KEY = "player_config" fun fromReadableMap(map: ReadableMap): PlayerConfig { val android = map.getMap("android") val notification = android?.getMap("notification") val cacheMap = map.getMap("cache") return PlayerConfig( contentType = map.getString("contentType") ?: "music", handleAudioBecomingNoisy = if (map.hasKey("handleAudioBecomingNoisy")) map.getBoolean("handleAudioBecomingNoisy") else true, autoUpdateMetadataFromStream = if (map.hasKey("autoUpdateMetadataFromStream")) map.getBoolean("autoUpdateMetadataFromStream") else true, audioMixing = map.getString("audioMixing") ?: "exclusive", wakeMode = when (android?.getString("wakeMode")) { "none" -> WakeMode.NONE "local" -> WakeMode.LOCAL "network" -> WakeMode.NETWORK else -> WakeMode.NONE }, skipSilenceEnabled = if (android?.hasKey("skipSilenceEnabled") == true) android.getBoolean("skipSilenceEnabled") else false, taskRemovedBehavior = when (android?.getString("taskRemovedBehavior")) { "stop" -> TaskRemovedBehavior.STOP else -> TaskRemovedBehavior.CONTINUE }, notificationChannelId = notification?.getString("channelId"), notificationChannelName = notification?.getString("channelName"), notificationSmallIcon = notification?.getString("smallIcon"), cacheMaxSizeBytes = cacheMap?.let { if (it.hasKey("maxSizeBytes")) it.getDouble("maxSizeBytes").toLong() else 500L * 1024 * 1024 }, preloadWindow = cacheMap?.let { val preloadMap = if (it.hasKey("preloading")) it.getMap("preloading") else null preloadMap?.let { pm -> if (pm.hasKey("window")) pm.getInt("window") else 0 } ?: 0 } ?: 0, castReceiverAppId = if (android?.hasKey("cast") == true) android.getString("cast") else null, progressSyncIntervalSeconds = map.getMap("progressSync")?.let { if (it.hasKey("intervalSeconds")) it.getDouble("intervalSeconds") else 0.0 } ?: 0.0, progressSyncHttpUrl = map.getMap("progressSync")?.getMap("http")?.getString("url"), progressSyncHttpHeaders = map.getMap("progressSync")?.getMap("http")?.getMap("headers")?.let { headersMap -> val result = mutableMapOf() val iterator = headersMap.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() headersMap.getString(key)?.let { result[key] = it } } result }, ) } } }