mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
fix(windows): make VLC playback work through the stream proxy
The proxy copied every upstream header, duplicating Content-Length, which VLC rejects. Forward only the range/validator headers. Bump vlcj-bundler to 0.2.1, which bundles the Matroska demuxer needed for WebM. Make StreamingUrlRepository's platform injectable and add VLC log switch via DebugConfig. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ea01dec6ea
commit
5b36edd259
@ -39,4 +39,6 @@ object DebugConfig {
|
|||||||
* Keep it `false` when committing.
|
* Keep it `false` when committing.
|
||||||
*/
|
*/
|
||||||
const val FORCE_SUPPORT_REMINDER: Boolean = false
|
const val FORCE_SUPPORT_REMINDER: Boolean = false
|
||||||
|
|
||||||
|
const val FORCE_VLC_LOGS: Boolean = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -425,7 +425,7 @@ internal class StreamProxy(
|
|||||||
if (requestMethod == HttpMethod.Head) {
|
if (requestMethod == HttpMethod.Head) {
|
||||||
call.respond(object : OutgoingContent.NoContent() {
|
call.respond(object : OutgoingContent.NoContent() {
|
||||||
override val status: HttpStatusCode = upstream.status
|
override val status: HttpStatusCode = upstream.status
|
||||||
override val headers: Headers = downstreamHeaders
|
override val headers: Headers = downstreamHeaders.passthroughForPlayer()
|
||||||
override val contentLength: Long? = downstreamContentLength
|
override val contentLength: Long? = downstreamContentLength
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -446,7 +446,7 @@ internal class StreamProxy(
|
|||||||
|
|
||||||
call.respond(object : OutgoingContent.WriteChannelContent() {
|
call.respond(object : OutgoingContent.WriteChannelContent() {
|
||||||
override val status: HttpStatusCode = upstream.status
|
override val status: HttpStatusCode = upstream.status
|
||||||
override val headers: Headers = downstreamHeaders
|
override val headers: Headers = downstreamHeaders.passthroughForPlayer()
|
||||||
override val contentLength: Long? = downstreamContentLength
|
override val contentLength: Long? = downstreamContentLength
|
||||||
override val contentType: ContentType? =
|
override val contentType: ContentType? =
|
||||||
downstreamHeaders[HttpHeaders.ContentType]?.let(ContentType.Companion::parse)
|
downstreamHeaders[HttpHeaders.ContentType]?.let(ContentType.Companion::parse)
|
||||||
@ -594,4 +594,28 @@ internal class StreamProxy(
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PLAYER_PASSTHROUGH_HEADERS = listOf(
|
||||||
|
HttpHeaders.ContentRange,
|
||||||
|
HttpHeaders.AcceptRanges,
|
||||||
|
HttpHeaders.ETag,
|
||||||
|
HttpHeaders.LastModified,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The upstream headers a player actually needs. Copying them all makes Ktor emit duplicate
|
||||||
|
* Content-Length/Content-Type (also set through the OutgoingContent) and forwards hop-by-hop
|
||||||
|
* headers like `Connection: keep-alive`, which VLC rejects by closing the socket right after
|
||||||
|
* the response headers. Content-Range and Accept-Ranges must stay: without them the player
|
||||||
|
* treats a 206 as invalid and the stream as unseekable, and e.g. WebM can't be demuxed.
|
||||||
|
*/
|
||||||
|
internal fun Headers.passthroughForPlayer(): Headers {
|
||||||
|
val upstream = this
|
||||||
|
return HeadersBuilder().apply {
|
||||||
|
PLAYER_PASSTHROUGH_HEADERS.forEach { name ->
|
||||||
|
upstream.getAll(name)?.forEach { value -> append(name, value) }
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,7 +80,8 @@ 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) : KoinComponent {
|
private val streamUrlCacheTtlMs: Long = 30.seconds.inWholeMilliseconds,
|
||||||
|
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()
|
||||||
@ -145,7 +146,7 @@ class StreamingUrlRepository(
|
|||||||
} else {
|
} else {
|
||||||
stream.streams
|
stream.streams
|
||||||
}
|
}
|
||||||
return selectPreferredAudioStream(candidates, preferredFormat, preferredQuality)
|
return selectPreferredAudioStream(candidates, preferredFormat, preferredQuality, platform)
|
||||||
.also { logSelection(trackId, origin, candidates, preferredFormat, preferredQuality, it) }
|
.also { logSelection(trackId, origin, candidates, preferredFormat, preferredQuality, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +260,7 @@ class StreamingUrlRepository(
|
|||||||
"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(currentPlatform)) 0 else 1}, " +
|
"$marker${stream.describeForLog()} key=(${if (stream.isPlayableOn(platform)) 0 else 1}, " +
|
||||||
"${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" }}, " +
|
||||||
@ -268,8 +269,8 @@ class StreamingUrlRepository(
|
|||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
selected == null -> logger.w { "[Resolve $trackId] no stream selected" }
|
selected == null -> logger.w { "[Resolve $trackId] no stream selected" }
|
||||||
!selected.isPlayableOn(currentPlatform) -> logger.w {
|
!selected.isPlayableOn(platform) -> logger.w {
|
||||||
"[Resolve $trackId] NO PLAYABLE STREAM on $currentPlatform; falling back to " +
|
"[Resolve $trackId] NO PLAYABLE STREAM on $platform; falling back to " +
|
||||||
"${selected.describeForLog()}, which the player will likely reject"
|
"${selected.describeForLog()}, which the player will likely reject"
|
||||||
}
|
}
|
||||||
formatMismatchCount(selected, preferredFormat) > 0 -> logger.w {
|
formatMismatchCount(selected, preferredFormat) > 0 -> logger.w {
|
||||||
@ -424,12 +425,16 @@ internal fun selectPreferredAudioStream(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AVPlayer has no WebM/Ogg/Matroska demuxer, so e.g. YouTube's opus/webm streams fail to load.
|
private val MATROSKA_CONTAINERS = setOf("webm", "mkv", "matroska")
|
||||||
private val IOS_UNSUPPORTED_CONTAINERS = setOf("webm", "ogg", "oga", "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. VLC (desktop) and ExoPlayer (Android)
|
* Whether [platform]'s player can play this stream. ExoPlayer (Android) and VLC elsewhere handle
|
||||||
* handle everything plugins return; AVPlayer (iOS) can't demux WebM/Ogg and can't play DASH.
|
* everything plugins return; AVPlayer (iOS) can't demux WebM/Ogg and can't play DASH.
|
||||||
*/
|
*/
|
||||||
internal fun AudioStream.isPlayableOn(platform: PlatformType): Boolean =
|
internal fun AudioStream.isPlayableOn(platform: PlatformType): Boolean =
|
||||||
isContainerPlayableOn(container, platform) &&
|
isContainerPlayableOn(container, platform) &&
|
||||||
@ -437,7 +442,7 @@ internal fun AudioStream.isPlayableOn(platform: PlatformType): Boolean =
|
|||||||
|
|
||||||
/** Whether [platform]'s player can open [container] at all; see [AudioStream.isPlayableOn]. */
|
/** Whether [platform]'s player can open [container] at all; see [AudioStream.isPlayableOn]. */
|
||||||
internal fun isContainerPlayableOn(container: String, platform: PlatformType): Boolean =
|
internal fun isContainerPlayableOn(container: String, platform: PlatformType): Boolean =
|
||||||
platform != PlatformType.IOS || container.lowercase() !in IOS_UNSUPPORTED_CONTAINERS
|
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) +
|
||||||
|
|||||||
@ -108,14 +108,14 @@ data class JamBroker(
|
|||||||
val connectionTimeoutSeconds: Int = 10)
|
val connectionTimeoutSeconds: Int = 10)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* opus/webm everywhere except iOS, whose AVPlayer can't open WebM/Ogg, so it streams AAC.
|
* opus/webm wherever the platform's player can open WebM; iOS (AVPlayer) can't, so it streams AAC.
|
||||||
* TODO: revisit once iOS can play Opus (e.g. remuxing to CAF or a non-AVPlayer backend).
|
* TODO: revisit once iOS can play Opus (e.g. remuxing to CAF or a non-AVPlayer backend).
|
||||||
*/
|
*/
|
||||||
internal fun defaultStreamingMusicFormat(platform: PlatformType = currentPlatform): AudioFormat {
|
internal fun defaultStreamingMusicFormat(platform: PlatformType = currentPlatform): AudioFormat {
|
||||||
val ios = platform == PlatformType.IOS
|
val aac = !isContainerPlayableOn("webm", platform)
|
||||||
return AudioFormat(
|
return AudioFormat(
|
||||||
codec = if (ios) "aac" else "opus",
|
codec = if (aac) "aac" else "opus",
|
||||||
container = if (ios) "mp4" else "webm",
|
container = if (aac) "mp4" else "webm",
|
||||||
qualities = listOf(
|
qualities = listOf(
|
||||||
AudioQuality.Lossy(bitrate = 44_000),
|
AudioQuality.Lossy(bitrate = 44_000),
|
||||||
AudioQuality.Lossy(bitrate = 96_000),
|
AudioQuality.Lossy(bitrate = 96_000),
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.spotube.spotube.core.server
|
||||||
|
|
||||||
|
import io.ktor.http.HeadersBuilder
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class StreamProxyHeadersTest {
|
||||||
|
|
||||||
|
// Lowercase names and values as googlevideo sends them
|
||||||
|
private val upstream = HeadersBuilder().apply {
|
||||||
|
append("accept-ranges", "bytes")
|
||||||
|
append("alt-svc", "h3=\":443\"")
|
||||||
|
append("cache-control", "private, max-age=21298")
|
||||||
|
append("connection", "keep-alive")
|
||||||
|
append("content-length", "3523553")
|
||||||
|
append("content-range", "bytes 0-3523552/3523553")
|
||||||
|
append("content-type", "audio/webm")
|
||||||
|
append("last-modified", "Tue, 18 Feb 2025 22:42:11 GMT")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `keeps the headers a player needs to seek`() {
|
||||||
|
val forwarded = upstream.passthroughForPlayer()
|
||||||
|
assertEquals("bytes 0-3523552/3523553", forwarded["Content-Range"])
|
||||||
|
assertEquals("bytes", forwarded["Accept-Ranges"])
|
||||||
|
assertEquals("Tue, 18 Feb 2025 22:42:11 GMT", forwarded["Last-Modified"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `drops headers that are duplicated or hop-by-hop`() {
|
||||||
|
val forwarded = upstream.passthroughForPlayer()
|
||||||
|
// Length and type are set on the OutgoingContent; forwarding them too emits them twice
|
||||||
|
assertNull(forwarded["Content-Length"])
|
||||||
|
assertNull(forwarded["Content-Type"])
|
||||||
|
assertNull(forwarded["Connection"])
|
||||||
|
assertNull(forwarded["Alt-Svc"])
|
||||||
|
assertNull(forwarded["Cache-Control"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ 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
|
||||||
@ -99,7 +100,9 @@ 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)
|
||||||
@ -236,6 +239,11 @@ class StreamingUrlRepositoryBehaviorTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The default format depends on the host platform (aac/mp4 on Windows and iOS)
|
||||||
|
h.settings.update {
|
||||||
|
copy(streamingMusicFormat = streamingMusicFormat.copy(codec = "opus", container = "webm"))
|
||||||
|
}
|
||||||
|
|
||||||
val first = h.repository.resolveStreamInfo(t)
|
val first = h.repository.resolveStreamInfo(t)
|
||||||
assertEquals("https://example.com/opus.webm", first?.url)
|
assertEquals("https://example.com/opus.webm", first?.url)
|
||||||
|
|
||||||
|
|||||||
@ -35,8 +35,10 @@ class StreamingFormatDefaultsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `other platforms keep opus in webm`() {
|
fun `other platforms keep opus in webm`() {
|
||||||
val format = defaultStreamingMusicFormat(PlatformType.Android)
|
listOf(PlatformType.Android, PlatformType.Linux, PlatformType.MacOS, PlatformType.Windows).forEach { platform ->
|
||||||
assertEquals("opus" to "webm", format.codec to format.container)
|
val format = defaultStreamingMusicFormat(platform)
|
||||||
|
assertEquals("opus" to "webm", format.codec to format.container, platform.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.spotube.spotube.core.DebugConfig
|
||||||
import uk.co.caprica.vlcj.factory.MediaPlayerFactory
|
import uk.co.caprica.vlcj.factory.MediaPlayerFactory
|
||||||
import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
|
import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
|
||||||
import uk.co.caprica.vlcj.medialist.MediaList
|
import uk.co.caprica.vlcj.medialist.MediaList
|
||||||
@ -87,11 +88,13 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
|||||||
private val _error = MutableSharedFlow<Throwable>(extraBufferCapacity = 1)
|
private val _error = MutableSharedFlow<Throwable>(extraBufferCapacity = 1)
|
||||||
|
|
||||||
actual override val playerStateFlow: StateFlow<PlayerState> = _playerState.asStateFlow()
|
actual override val playerStateFlow: StateFlow<PlayerState> = _playerState.asStateFlow()
|
||||||
actual override val currentMediaItemFlow: StateFlow<MediaItem?> = _currentMediaItem.asStateFlow()
|
actual override val currentMediaItemFlow: StateFlow<MediaItem?> =
|
||||||
|
_currentMediaItem.asStateFlow()
|
||||||
actual override val playlistFlow: StateFlow<List<MediaItem>> = _playlist.asStateFlow()
|
actual override val playlistFlow: StateFlow<List<MediaItem>> = _playlist.asStateFlow()
|
||||||
actual override val durationFlow: StateFlow<Duration> = _duration.asStateFlow()
|
actual override val durationFlow: StateFlow<Duration> = _duration.asStateFlow()
|
||||||
actual override val positionFlow: StateFlow<Duration> = _position.asStateFlow()
|
actual override val positionFlow: StateFlow<Duration> = _position.asStateFlow()
|
||||||
actual override val bufferingPositionFlow: StateFlow<Duration> = _bufferingPosition.asStateFlow()
|
actual override val bufferingPositionFlow: StateFlow<Duration> =
|
||||||
|
_bufferingPosition.asStateFlow()
|
||||||
actual override val loopStateFlow: StateFlow<LoopState> = _loopState.asStateFlow()
|
actual override val loopStateFlow: StateFlow<LoopState> = _loopState.asStateFlow()
|
||||||
actual override val shuffleModeFlow: StateFlow<Boolean> = _shuffleMode.asStateFlow()
|
actual override val shuffleModeFlow: StateFlow<Boolean> = _shuffleMode.asStateFlow()
|
||||||
actual override val playbackSpeedFlow: StateFlow<Float> = _playbackSpeed.asStateFlow()
|
actual override val playbackSpeedFlow: StateFlow<Float> = _playbackSpeed.asStateFlow()
|
||||||
@ -101,6 +104,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
VLCBundleLoaderGenerated.getVerifiedPath()
|
VLCBundleLoaderGenerated.getVerifiedPath()
|
||||||
|
val vlcLogArgs = if (DebugConfig.FORCE_VLC_LOGS) arrayOf("--verbose=2") else emptyArray()
|
||||||
mediaPlayerFactory = MediaPlayerFactory(
|
mediaPlayerFactory = MediaPlayerFactory(
|
||||||
null as NativeDiscovery?,
|
null as NativeDiscovery?,
|
||||||
"--no-video",
|
"--no-video",
|
||||||
@ -108,8 +112,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
|||||||
"--aout=any",
|
"--aout=any",
|
||||||
"--no-osd",
|
"--no-osd",
|
||||||
"--no-snapshot-preview",
|
"--no-snapshot-preview",
|
||||||
// "--verbose=2"
|
*vlcLogArgs
|
||||||
// "--quiet"
|
|
||||||
)
|
)
|
||||||
audioListPlayerComponent = object : AudioListPlayerComponent(mediaPlayerFactory) {
|
audioListPlayerComponent = object : AudioListPlayerComponent(mediaPlayerFactory) {
|
||||||
override fun opening(mediaPlayer: MediaPlayer) {
|
override fun opening(mediaPlayer: MediaPlayer) {
|
||||||
@ -122,7 +125,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
|||||||
) {
|
) {
|
||||||
val durationMs = mediaPlayer.status().length()
|
val durationMs = mediaPlayer.status().length()
|
||||||
_bufferingPosition.tryEmit(
|
_bufferingPosition.tryEmit(
|
||||||
if (durationMs > 0) (durationMs * (newCache / 100f)).toLong().milliseconds else Duration.ZERO)
|
if (durationMs > 0) (durationMs * (newCache / 100f)).toLong().milliseconds else Duration.ZERO
|
||||||
|
)
|
||||||
_playerState.tryEmit(if (newCache < 100f) PlayerState.BUFFERING else PlayerState.READY)
|
_playerState.tryEmit(if (newCache < 100f) PlayerState.BUFFERING else PlayerState.READY)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +188,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
|||||||
|
|
||||||
override fun nextItem(
|
override fun nextItem(
|
||||||
mediaListPlayer: uk.co.caprica.vlcj.player.list.MediaListPlayer,
|
mediaListPlayer: uk.co.caprica.vlcj.player.list.MediaListPlayer,
|
||||||
item: uk.co.caprica.vlcj.media.MediaRef) {
|
item: uk.co.caprica.vlcj.media.MediaRef
|
||||||
|
) {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
val targetIndex = pendingNextIndex ?: run {
|
val targetIndex = pendingNextIndex ?: run {
|
||||||
val currentMrl = runCatching {
|
val currentMrl = runCatching {
|
||||||
@ -336,7 +341,11 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual override suspend fun load(playlist: List<MediaItem>, autoPlay: Boolean, startPosition: Int) {
|
actual override suspend fun load(
|
||||||
|
playlist: List<MediaItem>,
|
||||||
|
autoPlay: Boolean,
|
||||||
|
startPosition: Int
|
||||||
|
) {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,7 @@ ksp = "2.3.12"
|
|||||||
spotube-gradle = "0.1.2"
|
spotube-gradle = "0.1.2"
|
||||||
cryptography = "0.6.0"
|
cryptography = "0.6.0"
|
||||||
reorderable = "3.1.0"
|
reorderable = "3.1.0"
|
||||||
vlcjBundler = "0.2.0"
|
vlcjBundler = "0.2.1"
|
||||||
vanniktechMavenPublish = "0.37.0"
|
vanniktechMavenPublish = "0.37.0"
|
||||||
uniffi = "0.3.7"
|
uniffi = "0.3.7"
|
||||||
kotlinx-atomicfu = "0.33.0"
|
kotlinx-atomicfu = "0.33.0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user