mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
feat(ios): drop AVPlayer-era streaming format restrictions
VLC on iOS plays every container, codec and protocol plugins return, so iOS no longer needs special casing: - Stream selection no longer ranks WebM/MKV/Ogg or DASH streams as unplayable on iOS; the platform playability mechanism is removed. - Opus in WebM is the default streaming format on every platform. - Saved Opus/WebM preferences are no longer migrated to AAC on iOS. - The streaming format picker offers all formats on iOS.
This commit is contained in:
parent
0a30aa7137
commit
538ba636d4
@ -23,11 +23,9 @@ import org.spotube.plugin_interfaces.plugin_apis.audio.AudioQuality
|
|||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import org.spotube.spotube.PlatformType
|
|
||||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||||
import org.spotube.spotube.core.di.injectLogger
|
import org.spotube.spotube.core.di.injectLogger
|
||||||
import org.spotube.spotube.getPlatform
|
|
||||||
import org.spotube.spotube.modules.plugin.AudioPluginSource
|
import org.spotube.spotube.modules.plugin.AudioPluginSource
|
||||||
import org.spotube.spotube.modules.settings.UserSettingsSource
|
import org.spotube.spotube.modules.settings.UserSettingsSource
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
@ -80,8 +78,7 @@ class StreamingUrlRepository(
|
|||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
private val matchedTracksRepository: TrackSourceRepository,
|
private val matchedTracksRepository: TrackSourceRepository,
|
||||||
private val settingsRepository: UserSettingsSource,
|
private val settingsRepository: UserSettingsSource,
|
||||||
private val streamUrlCacheTtlMs: Long = 30.seconds.inWholeMilliseconds,
|
private val streamUrlCacheTtlMs: Long = 30.seconds.inWholeMilliseconds) : KoinComponent {
|
||||||
private val platform: PlatformType = currentPlatform) : KoinComponent {
|
|
||||||
|
|
||||||
private val logger by injectLogger<StreamingUrlRepository>()
|
private val logger by injectLogger<StreamingUrlRepository>()
|
||||||
private val streamUrlCacheMutex = Mutex()
|
private val streamUrlCacheMutex = Mutex()
|
||||||
@ -146,7 +143,7 @@ class StreamingUrlRepository(
|
|||||||
} else {
|
} else {
|
||||||
stream.streams
|
stream.streams
|
||||||
}
|
}
|
||||||
return selectPreferredAudioStream(candidates, preferredFormat, preferredQuality, platform)
|
return selectPreferredAudioStream(candidates, preferredFormat, preferredQuality)
|
||||||
.also { logSelection(trackId, origin, candidates, preferredFormat, preferredQuality, it) }
|
.also { logSelection(trackId, origin, candidates, preferredFormat, preferredQuality, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,12 +253,11 @@ class StreamingUrlRepository(
|
|||||||
logger.i {
|
logger.i {
|
||||||
"[Resolve $trackId] selecting from ${streams.size} candidates ($origin), " +
|
"[Resolve $trackId] selecting from ${streams.size} candidates ($origin), " +
|
||||||
"want ${preferredFormat.codec}/${preferredFormat.container} $preferredQuality " +
|
"want ${preferredFormat.codec}/${preferredFormat.container} $preferredQuality " +
|
||||||
"(rank key = unplayable, formatMismatch, qualityTypeMismatch, qualityDistance, channelDistance; " +
|
"(rank key = formatMismatch, qualityTypeMismatch, qualityDistance, channelDistance; " +
|
||||||
"lowest wins):\n" +
|
"lowest wins):\n" +
|
||||||
streams.joinToString("\n") { stream ->
|
streams.joinToString("\n") { stream ->
|
||||||
val marker = if (stream === selected) " -> " else " "
|
val marker = if (stream === selected) " -> " else " "
|
||||||
"$marker${stream.describeForLog()} key=(${if (stream.isPlayableOn(platform)) 0 else 1}, " +
|
"$marker${stream.describeForLog()} key=(${formatMismatchCount(stream, preferredFormat)}, " +
|
||||||
"${formatMismatchCount(stream, preferredFormat)}, " +
|
|
||||||
"${if (stream.matchesQualityType(preferredQuality)) 0 else 1}, " +
|
"${if (stream.matchesQualityType(preferredQuality)) 0 else 1}, " +
|
||||||
"${stream.qualityDistance(preferredQuality).let { if (it == Long.MAX_VALUE) "inf" else "$it" }}, " +
|
"${stream.qualityDistance(preferredQuality).let { if (it == Long.MAX_VALUE) "inf" else "$it" }}, " +
|
||||||
"${stream.channelDistance(preferredQuality)}) ${urlSummary(stream.url)}"
|
"${stream.channelDistance(preferredQuality)}) ${urlSummary(stream.url)}"
|
||||||
@ -269,10 +265,6 @@ class StreamingUrlRepository(
|
|||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
selected == null -> logger.w { "[Resolve $trackId] no stream selected" }
|
selected == null -> logger.w { "[Resolve $trackId] no stream selected" }
|
||||||
!selected.isPlayableOn(platform) -> logger.w {
|
|
||||||
"[Resolve $trackId] NO PLAYABLE STREAM on $platform; falling back to " +
|
|
||||||
"${selected.describeForLog()}, which the player will likely reject"
|
|
||||||
}
|
|
||||||
formatMismatchCount(selected, preferredFormat) > 0 -> logger.w {
|
formatMismatchCount(selected, preferredFormat) > 0 -> logger.w {
|
||||||
"[Resolve $trackId] FORMAT FALLBACK: wanted ${preferredFormat.codec}/${preferredFormat.container}, " +
|
"[Resolve $trackId] FORMAT FALLBACK: wanted ${preferredFormat.codec}/${preferredFormat.container}, " +
|
||||||
"got ${selected.codec}/${selected.container} (${selected.protocol})"
|
"got ${selected.codec}/${selected.container} (${selected.protocol})"
|
||||||
@ -400,21 +392,16 @@ internal fun normalizeManifestUrl(url: String, protocol: StreamProtocol): String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val currentPlatform: PlatformType by lazy { getPlatform().type }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects the available stream closest to the user's preferred format and quality.
|
* Selects the available stream closest to the user's preferred format and quality. Every
|
||||||
* Streams the [platform]'s player can decode always win over ones it can't, whatever the
|
* platform's player (ExoPlayer on Android, VLC elsewhere including iOS) can play any container,
|
||||||
* preferences say: e.g. opus/webm is unplayable on iOS.
|
* codec and protocol plugins return, so nothing is ruled out up front.
|
||||||
*/
|
*/
|
||||||
internal fun selectPreferredAudioStream(
|
internal fun selectPreferredAudioStream(
|
||||||
streams: List<AudioStream>,
|
streams: List<AudioStream>,
|
||||||
preferredFormat: AudioFormat,
|
preferredFormat: AudioFormat,
|
||||||
preferredQuality: AudioQuality,
|
preferredQuality: AudioQuality): AudioStream? = streams.minWithOrNull(
|
||||||
platform: PlatformType = currentPlatform): AudioStream? = streams.minWithOrNull(
|
|
||||||
compareBy<AudioStream> { stream ->
|
compareBy<AudioStream> { stream ->
|
||||||
if (stream.isPlayableOn(platform)) 0 else 1
|
|
||||||
}.thenBy { stream ->
|
|
||||||
formatMismatchCount(stream, preferredFormat)
|
formatMismatchCount(stream, preferredFormat)
|
||||||
}.thenBy { stream ->
|
}.thenBy { stream ->
|
||||||
if (stream.matchesQualityType(preferredQuality)) 0 else 1
|
if (stream.matchesQualityType(preferredQuality)) 0 else 1
|
||||||
@ -425,25 +412,6 @@ internal fun selectPreferredAudioStream(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private val MATROSKA_CONTAINERS = setOf("webm", "mkv", "matroska")
|
|
||||||
|
|
||||||
// Containers a platform's player can't open, so e.g. YouTube's opus/webm streams fail to load there.
|
|
||||||
private val UNSUPPORTED_CONTAINERS: Map<PlatformType, Set<String>> = mapOf(
|
|
||||||
// AVPlayer has no WebM/Ogg/Matroska demuxer.
|
|
||||||
PlatformType.IOS to MATROSKA_CONTAINERS + setOf("ogg", "oga"))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether [platform]'s player can play this stream. ExoPlayer (Android) and VLC elsewhere handle
|
|
||||||
* everything plugins return; AVPlayer (iOS) can't demux WebM/Ogg and can't play DASH.
|
|
||||||
*/
|
|
||||||
internal fun AudioStream.isPlayableOn(platform: PlatformType): Boolean =
|
|
||||||
isContainerPlayableOn(container, platform) &&
|
|
||||||
!(platform == PlatformType.IOS && protocol == StreamProtocol.DASH)
|
|
||||||
|
|
||||||
/** Whether [platform]'s player can open [container] at all; see [AudioStream.isPlayableOn]. */
|
|
||||||
internal fun isContainerPlayableOn(container: String, platform: PlatformType): Boolean =
|
|
||||||
container.lowercase() !in UNSUPPORTED_CONTAINERS[platform].orEmpty()
|
|
||||||
|
|
||||||
private fun formatMismatchCount(stream: AudioStream, preferredFormat: AudioFormat): Int =
|
private fun formatMismatchCount(stream: AudioStream, preferredFormat: AudioFormat): Int =
|
||||||
(if (stream.codec.equals(preferredFormat.codec, ignoreCase = true)) 0 else 1) +
|
(if (stream.codec.equals(preferredFormat.codec, ignoreCase = true)) 0 else 1) +
|
||||||
(if (stream.container.equals(preferredFormat.container, ignoreCase = true)) 0 else 1)
|
(if (stream.container.equals(preferredFormat.container, ignoreCase = true)) 0 else 1)
|
||||||
|
|||||||
@ -17,9 +17,6 @@
|
|||||||
|
|
||||||
package org.spotube.spotube.modules.settings
|
package org.spotube.spotube.modules.settings
|
||||||
|
|
||||||
import org.spotube.spotube.core.server.isContainerPlayableOn
|
|
||||||
import org.spotube.spotube.core.server.currentPlatform
|
|
||||||
import org.spotube.spotube.PlatformType
|
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioFormat
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioFormat
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioQuality
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioQuality
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
@ -101,23 +98,12 @@ const val DEFAULT_JAM_SERVER_URL = "https://spotube-jam.workers.dev"
|
|||||||
internal fun isJamServerUrl(url: String): Boolean =
|
internal fun isJamServerUrl(url: String): Boolean =
|
||||||
Regex("^(https?|wss?)://[^\\s/?#]+(/\\S*)?$", RegexOption.IGNORE_CASE).matches(url.trim())
|
Regex("^(https?|wss?)://[^\\s/?#]+(/\\S*)?$", RegexOption.IGNORE_CASE).matches(url.trim())
|
||||||
|
|
||||||
/**
|
/** opus/webm on every platform: all of their players (ExoPlayer, VLC) can play it. */
|
||||||
* opus/webm wherever the platform's player can open WebM; iOS (AVPlayer) can't, so it streams AAC.
|
internal fun defaultStreamingMusicFormat(): AudioFormat = AudioFormat(
|
||||||
* TODO: revisit once iOS can play Opus (e.g. remuxing to CAF or a non-AVPlayer backend).
|
codec = "opus",
|
||||||
*/
|
container = "webm",
|
||||||
internal fun defaultStreamingMusicFormat(platform: PlatformType = currentPlatform): AudioFormat {
|
qualities = listOf(
|
||||||
val aac = !isContainerPlayableOn("webm", platform)
|
AudioQuality.Lossy(bitrate = 44_000),
|
||||||
return AudioFormat(
|
AudioQuality.Lossy(bitrate = 96_000),
|
||||||
codec = if (aac) "aac" else "opus",
|
AudioQuality.Lossy(bitrate = 128_000),
|
||||||
container = if (aac) "mp4" else "webm",
|
AudioQuality.Lossy(bitrate = 256_000)))
|
||||||
qualities = listOf(
|
|
||||||
AudioQuality.Lossy(bitrate = 44_000),
|
|
||||||
AudioQuality.Lossy(bitrate = 96_000),
|
|
||||||
AudioQuality.Lossy(bitrate = 128_000),
|
|
||||||
AudioQuality.Lossy(bitrate = 256_000)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Moves settings saved with a streaming format [platform] can't play (iOS' old opus/webm default) to the default. */
|
|
||||||
internal fun UserSettings.withPlayableStreamingFormat(platform: PlatformType = currentPlatform): UserSettings =
|
|
||||||
if (isContainerPlayableOn(streamingMusicFormat.container, platform)) this
|
|
||||||
else copy(streamingMusicFormat = defaultStreamingMusicFormat(platform))
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ class SettingsRepository(private val database: Database) : UserSettingsStore {
|
|||||||
override val userSettings: StateFlow<UserSettings> = database.settingsDataStore.data.map { prefs ->
|
override val userSettings: StateFlow<UserSettings> = database.settingsDataStore.data.map { prefs ->
|
||||||
val json = prefs[SETTINGS_KEY]
|
val json = prefs[SETTINGS_KEY]
|
||||||
if (json != null) {
|
if (json != null) {
|
||||||
settingsJson.decodeFromString<UserSettings>(json as String).withPlayableStreamingFormat()
|
settingsJson.decodeFromString<UserSettings>(json as String)
|
||||||
} else {
|
} else {
|
||||||
UserSettings() // Default value
|
UserSettings() // Default value
|
||||||
}
|
}
|
||||||
@ -78,6 +78,6 @@ class SettingsRepository(private val database: Database) : UserSettingsStore {
|
|||||||
suspend fun getCurrentSettings(): UserSettings {
|
suspend fun getCurrentSettings(): UserSettings {
|
||||||
val preferences = database.settingsDataStore.data.first()
|
val preferences = database.settingsDataStore.data.first()
|
||||||
val storedJson = preferences[SETTINGS_KEY] ?: return UserSettings()
|
val storedJson = preferences[SETTINGS_KEY] ?: return UserSettings()
|
||||||
return settingsJson.decodeFromString<UserSettings>(storedJson).withPlayableStreamingFormat()
|
return settingsJson.decodeFromString<UserSettings>(storedJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,6 @@
|
|||||||
package org.spotube.spotube.modules.settings.sections
|
package org.spotube.spotube.modules.settings.sections
|
||||||
|
|
||||||
|
|
||||||
import org.spotube.spotube.core.server.isContainerPlayableOn
|
|
||||||
import org.spotube.spotube.core.server.currentPlatform
|
|
||||||
import org.spotube.millennium.composables.MillenniumIcon
|
import org.spotube.millennium.composables.MillenniumIcon
|
||||||
import org.spotube.millennium.composables.MillenniumSeparator
|
import org.spotube.millennium.composables.MillenniumSeparator
|
||||||
import org.spotube.millennium.theme.MillenniumTheme
|
import org.spotube.millennium.theme.MillenniumTheme
|
||||||
@ -58,7 +56,6 @@ private val standardLossyQualities = listOf(
|
|||||||
AudioQuality.Lossy(bitrate = 128_000),
|
AudioQuality.Lossy(bitrate = 128_000),
|
||||||
AudioQuality.Lossy(bitrate = 256_000))
|
AudioQuality.Lossy(bitrate = 256_000))
|
||||||
|
|
||||||
// Formats the platform's player can't open aren't offered (iOS: no WebM/Ogg).
|
|
||||||
internal val streamingFormatPresets = listOf(
|
internal val streamingFormatPresets = listOf(
|
||||||
AudioFormat(
|
AudioFormat(
|
||||||
codec = "opus",
|
codec = "opus",
|
||||||
@ -72,7 +69,7 @@ internal val streamingFormatPresets = listOf(
|
|||||||
codec = "vorbis",
|
codec = "vorbis",
|
||||||
container = "ogg",
|
container = "ogg",
|
||||||
qualities = standardLossyQualities)
|
qualities = standardLossyQualities)
|
||||||
).filter { isContainerPlayableOn(it.container, currentPlatform) }
|
)
|
||||||
|
|
||||||
internal val downloadFormatPresets = listOf(
|
internal val downloadFormatPresets = listOf(
|
||||||
AudioFormat(
|
AudioFormat(
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import org.spotube.plugin_interfaces.plugin_apis.audio.AudioSource
|
|||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import org.spotube.spotube.PlatformType
|
|
||||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||||
import org.spotube.spotube.modules.settings.UserSettings
|
import org.spotube.spotube.modules.settings.UserSettings
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
@ -100,9 +99,7 @@ class StreamingUrlRepositoryBehaviorTest {
|
|||||||
audioPlayerQueue = queue,
|
audioPlayerQueue = queue,
|
||||||
matchedTracksRepository = matchedTracksRepository,
|
matchedTracksRepository = matchedTracksRepository,
|
||||||
settingsRepository = settings,
|
settingsRepository = settings,
|
||||||
streamUrlCacheTtlMs = cacheTtlMs,
|
streamUrlCacheTtlMs = cacheTtlMs)
|
||||||
// Not the host's platform: Windows and iOS don't prefer opus/webm
|
|
||||||
platform = PlatformType.Linux)
|
|
||||||
|
|
||||||
fun selectPlugin(pluginId: String, audioAPI: FakeAudioAPI = FakeAudioAPI()): FakeAudioPluginService {
|
fun selectPlugin(pluginId: String, audioAPI: FakeAudioAPI = FakeAudioAPI()): FakeAudioPluginService {
|
||||||
val service = FakeAudioPluginService(pluginId, audioAPI)
|
val service = FakeAudioPluginService(pluginId, audioAPI)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.spotube.spotube.core.server
|
package org.spotube.spotube.core.server
|
||||||
|
|
||||||
import org.spotube.spotube.PlatformType
|
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioFormat
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioFormat
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioQuality
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioQuality
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
||||||
@ -14,7 +13,6 @@ import kotlin.test.assertSame
|
|||||||
* Tests for the stream preference selection used by [StreamingUrlRepository].
|
* Tests for the stream preference selection used by [StreamingUrlRepository].
|
||||||
*
|
*
|
||||||
* Ordering contract (best first):
|
* Ordering contract (best first):
|
||||||
* 0. Playable by the platform's player (iOS: no WebM/Ogg containers, no DASH).
|
|
||||||
* 1. Fewest codec/container mismatches against the preferred format.
|
* 1. Fewest codec/container mismatches against the preferred format.
|
||||||
* 2. Matching quality family (lossy vs lossless).
|
* 2. Matching quality family (lossy vs lossless).
|
||||||
* 3. Closest bitrate / sample rate.
|
* 3. Closest bitrate / sample rate.
|
||||||
@ -56,8 +54,7 @@ class StreamingUrlRepositoryTest {
|
|||||||
private fun select(
|
private fun select(
|
||||||
streams: List<AudioStream>,
|
streams: List<AudioStream>,
|
||||||
format: AudioFormat = opusWebm(),
|
format: AudioFormat = opusWebm(),
|
||||||
quality: AudioQuality = AudioQuality.Lossy(256_000),
|
quality: AudioQuality = AudioQuality.Lossy(256_000)) = selectPreferredAudioStream(streams, format, quality)
|
||||||
platform: PlatformType = PlatformType.Linux) = selectPreferredAudioStream(streams, format, quality, platform)
|
|
||||||
|
|
||||||
// ---------- empty / single ----------
|
// ---------- empty / single ----------
|
||||||
|
|
||||||
@ -407,33 +404,20 @@ class StreamingUrlRepositoryTest {
|
|||||||
assertEquals("https://www.youtube.com", normalizeManifestUrl("", StreamProtocol.HLS))
|
assertEquals("https://www.youtube.com", normalizeManifestUrl("", StreamProtocol.HLS))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- device playability ----------
|
// ---------- no platform restrictions ----------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on iOS a playable m4a stream beats the preferred but unplayable opus webm`() {
|
fun `the preferred opus webm stream wins over aac`() {
|
||||||
val opus = lossy("opus-160", 160_000)
|
val opus = lossy("opus-160", 160_000)
|
||||||
val aac = lossy("aac-128", 128_000, codec = "mp4a.40.2", container = "m4a")
|
val aac = lossy("aac-128", 128_000, codec = "mp4a.40.2", container = "m4a")
|
||||||
assertSame(aac, select(listOf(opus, aac), platform = PlatformType.IOS))
|
assertSame(opus, select(listOf(aac, opus)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on iOS dash manifests are skipped in favour of progressive streams`() {
|
fun `a dash manifest closest to the preferences is not skipped`() {
|
||||||
val dash = lossy("dash", 256_000, codec = "mp4a.40.2", container = "m4a", protocol = StreamProtocol.DASH)
|
val dash = lossy("dash", 256_000, protocol = StreamProtocol.DASH)
|
||||||
val progressive = lossy("aac-128", 128_000, codec = "mp4a.40.2", container = "m4a")
|
val progressive = lossy("aac-128", 128_000, codec = "mp4a.40.2", container = "m4a")
|
||||||
assertSame(progressive, select(listOf(dash, progressive), platform = PlatformType.IOS))
|
assertSame(dash, select(listOf(progressive, dash)))
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `on iOS an unplayable stream is still returned when nothing else exists`() {
|
|
||||||
val opus = lossy("opus-160", 160_000)
|
|
||||||
assertSame(opus, select(listOf(opus), platform = PlatformType.IOS))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `off iOS the preferred opus webm stream still wins`() {
|
|
||||||
val opus = lossy("opus-160", 160_000)
|
|
||||||
val aac = lossy("aac-128", 128_000, codec = "mp4a.40.2", container = "m4a")
|
|
||||||
assertSame(opus, select(listOf(opus, aac), platform = PlatformType.Android))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,39 +17,19 @@
|
|||||||
|
|
||||||
package org.spotube.spotube.modules.settings
|
package org.spotube.spotube.modules.settings
|
||||||
|
|
||||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioFormat
|
|
||||||
import org.spotube.spotube.PlatformType
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertSame
|
|
||||||
|
|
||||||
class StreamingFormatDefaultsTest {
|
class StreamingFormatDefaultsTest {
|
||||||
|
|
||||||
private val opusWebm = AudioFormat(codec = "opus", container = "webm", qualities = emptyList())
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ios defaults to aac in mp4`() {
|
fun `streaming defaults to opus in webm`() {
|
||||||
val format = defaultStreamingMusicFormat(PlatformType.IOS)
|
val format = defaultStreamingMusicFormat()
|
||||||
assertEquals("aac" to "mp4", format.codec to format.container)
|
assertEquals("opus" to "webm", format.codec to format.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `other platforms keep opus in webm`() {
|
fun `user settings default to the streaming default`() {
|
||||||
listOf(PlatformType.Android, PlatformType.Linux, PlatformType.MacOS, PlatformType.Windows).forEach { platform ->
|
assertEquals(defaultStreamingMusicFormat(), UserSettings().streamingMusicFormat)
|
||||||
val format = defaultStreamingMusicFormat(platform)
|
|
||||||
assertEquals("opus" to "webm", format.codec to format.container, platform.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saved opus webm is moved to aac on ios`() {
|
|
||||||
val migrated = UserSettings(streamingMusicFormat = opusWebm).withPlayableStreamingFormat(PlatformType.IOS)
|
|
||||||
assertEquals("mp4", migrated.streamingMusicFormat.container)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saved opus webm is kept off ios`() {
|
|
||||||
val settings = UserSettings(streamingMusicFormat = opusWebm)
|
|
||||||
assertSame(settings, settings.withPlayableStreamingFormat(PlatformType.Linux))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user