fix(playback): ghost ICY readers — stop() racing a blocked read fired onError, whose auto-restart resurrected unbounded zombie streams; every ghost flickered its own song over the now-playing field
- IcyStreamReader: requestedStop flag — intentional stops never report as errors - PlaybackManager.startIcyReader: always stop the previous reader first; all four listener callbacks ignore events from a replaced reader (ghost audio was also poisoning the rolling buffer)
This commit is contained in:
parent
a490b394d3
commit
6147e0fde2
2 changed files with 21 additions and 2 deletions
|
|
@ -465,11 +465,18 @@ class PlaybackManager(
|
|||
}
|
||||
|
||||
private fun startIcyReader(url: String) {
|
||||
// Never leave a previous reader's socket open: an unclosed stream
|
||||
// keeps its OkHttp connection (and its own thread) streaming forever,
|
||||
// and every ghost feeds the same now-playing field — that is how four
|
||||
// different songs flickered over one another.
|
||||
stopIcyReader()
|
||||
val reader = IcyStreamReader()
|
||||
icyReader = reader
|
||||
|
||||
reader.listener = object : IcyStreamReader.Listener {
|
||||
override fun onMetadata(metadata: StreamMetadata, audioBytesAtMetadata: Long) {
|
||||
// Drop callbacks from a reader we already replaced or stopped.
|
||||
if (icyReader !== reader) return
|
||||
Log.d(TAG, "ICY metadata: ${metadata.artist} - ${metadata.title} @ $audioBytesAtMetadata")
|
||||
// Record at the reader-reported ABSOLUTE audio position (the same
|
||||
// quantity as audioBuffer.totalBytes). The previous code read
|
||||
|
|
@ -486,6 +493,7 @@ class PlaybackManager(
|
|||
}
|
||||
|
||||
override fun onAudioBytes(data: ByteArray, offset: Int, length: Long) {
|
||||
if (icyReader !== reader) return
|
||||
audioBuffer?.write(data, offset, length.toInt())
|
||||
listener?.onBufferBytesWritten(audioBuffer?.totalBytes ?: 0)
|
||||
|
||||
|
|
@ -495,6 +503,7 @@ class PlaybackManager(
|
|||
}
|
||||
|
||||
override fun onError(error: String) {
|
||||
if (icyReader !== reader) return
|
||||
Log.w(TAG, "ICY reader error: $error")
|
||||
// ExoPlayer often rides out a transient drop on its own
|
||||
// buffer without erroring; if it does, the recorder would
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ class IcyStreamReader(
|
|||
var isRunning: Boolean = false
|
||||
private set
|
||||
|
||||
/** True once stop() was called by request. A close() racing a blocked
|
||||
* stream.read() throws IOException, NOT CancellationException — without
|
||||
* this flag the generic catch reports an intentional stop as an error,
|
||||
* and the manager's error-restart resurrects a ghost reader. */
|
||||
@Volatile private var requestedStop = false
|
||||
|
||||
private var metaInterval: Int = 0
|
||||
private var bytesInInterval: Int = 0
|
||||
private var pendingMetaBytes: Int = 0
|
||||
|
|
@ -52,6 +58,7 @@ class IcyStreamReader(
|
|||
fun start(streamUrl: String) {
|
||||
if (isRunning) stop()
|
||||
|
||||
requestedStop = false
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope?.launch {
|
||||
try {
|
||||
|
|
@ -59,8 +66,10 @@ class IcyStreamReader(
|
|||
} catch (e: CancellationException) {
|
||||
// Normal stop
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error reading stream", e)
|
||||
listener?.onError(e.message ?: "Connection error")
|
||||
if (!requestedStop) {
|
||||
Log.e(TAG, "Error reading stream", e)
|
||||
listener?.onError(e.message ?: "Connection error")
|
||||
}
|
||||
} finally {
|
||||
isRunning = false
|
||||
}
|
||||
|
|
@ -68,6 +77,7 @@ class IcyStreamReader(
|
|||
}
|
||||
|
||||
fun stop() {
|
||||
requestedStop = true
|
||||
scope?.cancel()
|
||||
scope = null
|
||||
closeConnection()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue