fixed a bug

This commit is contained in:
2026-09-02 19:45:21 +08:00
parent cb87cf8cb1
commit a38679d4a0

View File

@@ -11,6 +11,7 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.PlayerMessage
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.max import kotlin.math.max
@@ -30,6 +31,8 @@ class OralTrainerController internal constructor(
private var lastSentenceIndex: Int? = null private var lastSentenceIndex: Int? = null
private var stopAtSentenceEnd = false private var stopAtSentenceEnd = false
private var stopAtSentenceEndIndex: Int? = null private var stopAtSentenceEndIndex: Int? = null
private var boundaryMessage: PlayerMessage? = null
private var boundaryGuardToken = 0
var config: PlayerConfig = initialConfig var config: PlayerConfig = initialConfig
private set private set
@@ -67,13 +70,20 @@ class OralTrainerController internal constructor(
} }
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
refreshSentenceBoundaryGuard()
} else {
cancelSentenceBoundaryGuard()
}
notifySnapshot() notifySnapshot()
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
lastSentenceIndex = null lastSentenceIndex = null
cancelSentenceBoundaryGuard()
listeners.forEach { it.onMediaChanged(currentTrainingItem()) } listeners.forEach { it.onMediaChanged(currentTrainingItem()) }
notifySentenceIfChanged(force = true) notifySentenceIfChanged(force = true)
refreshSentenceBoundaryGuard()
notifySnapshot() notifySnapshot()
} }
@@ -83,6 +93,7 @@ class OralTrainerController internal constructor(
reason: Int, reason: Int,
) { ) {
notifySentenceIfChanged(force = true) notifySentenceIfChanged(force = true)
refreshSentenceBoundaryGuard()
notifySnapshot() notifySnapshot()
} }
@@ -158,6 +169,7 @@ class OralTrainerController internal constructor(
if (index == player.currentMediaItemIndex) { if (index == player.currentMediaItemIndex) {
listeners.forEach { it.onMediaChanged(updatedItem) } listeners.forEach { it.onMediaChanged(updatedItem) }
notifySentenceIfChanged(force = true) notifySentenceIfChanged(force = true)
refreshSentenceBoundaryGuard()
notifySnapshot() notifySnapshot()
} }
return true return true
@@ -165,11 +177,13 @@ class OralTrainerController internal constructor(
fun play() { fun play() {
player.play() player.play()
refreshSentenceBoundaryGuard()
} }
fun pause() { fun pause() {
stopAtSentenceEnd = false stopAtSentenceEnd = false
stopAtSentenceEndIndex = null stopAtSentenceEndIndex = null
cancelSentenceBoundaryGuard()
player.pause() player.pause()
} }
@@ -184,6 +198,7 @@ class OralTrainerController internal constructor(
fun stop() { fun stop() {
stopAtSentenceEnd = false stopAtSentenceEnd = false
stopAtSentenceEndIndex = null stopAtSentenceEndIndex = null
cancelSentenceBoundaryGuard()
player.stop() player.stop()
} }
@@ -191,6 +206,7 @@ class OralTrainerController internal constructor(
stopAtSentenceEnd = false stopAtSentenceEnd = false
stopAtSentenceEndIndex = null stopAtSentenceEndIndex = null
player.seekTo(max(0L, positionMs)) player.seekTo(max(0L, positionMs))
refreshSentenceBoundaryGuard()
} }
fun seekRelative(deltaMs: Long) { fun seekRelative(deltaMs: Long) {
@@ -237,6 +253,7 @@ class OralTrainerController internal constructor(
fun setContinuousPlayback(enabled: Boolean) { fun setContinuousPlayback(enabled: Boolean) {
config = config.copy(continuousPlayback = enabled) config = config.copy(continuousPlayback = enabled)
refreshSentenceBoundaryGuard()
} }
fun playCurrentSentenceAndStop() { fun playCurrentSentenceAndStop() {
@@ -245,6 +262,7 @@ class OralTrainerController internal constructor(
stopAtSentenceEnd = true stopAtSentenceEnd = true
stopAtSentenceEndIndex = sentence.index stopAtSentenceEndIndex = sentence.index
player.play() player.play()
refreshSentenceBoundaryGuard()
} }
fun currentTrainingItem(): TrainingMediaItem? { fun currentTrainingItem(): TrainingMediaItem? {
@@ -357,6 +375,7 @@ class OralTrainerController internal constructor(
return return
} }
released = true released = true
cancelSentenceBoundaryGuard()
mainHandler.removeCallbacks(ticker) mainHandler.removeCallbacks(ticker)
listeners.clear() listeners.clear()
player.release() player.release()
@@ -376,8 +395,12 @@ class OralTrainerController internal constructor(
if (!force && index == lastSentenceIndex) { if (!force && index == lastSentenceIndex) {
return return
} }
val changed = index != lastSentenceIndex
lastSentenceIndex = index lastSentenceIndex = index
listeners.forEach { it.onSentenceChanged(sentence) } listeners.forEach { it.onSentenceChanged(sentence) }
if (changed && !config.continuousPlayback && !stopAtSentenceEnd) {
refreshSentenceBoundaryGuard()
}
} }
private fun enforceSentenceBoundary() { private fun enforceSentenceBoundary() {
@@ -393,23 +416,100 @@ class OralTrainerController internal constructor(
val index = stopAtSentenceEndIndex ?: return val index = stopAtSentenceEndIndex ?: return
val sentence = item.sentences.getOrNull(index) ?: return val sentence = item.sentences.getOrNull(index) ?: return
if (position >= sentence.endMs) { if (position >= sentence.endMs) {
stopAtSentenceEnd = false applySentenceEndAction(sentence)
stopAtSentenceEndIndex = null
player.pause()
player.seekTo(sentence.startMs)
} }
return return
} }
if (config.continuousPlayback) { if (config.continuousPlayback) {
return return
} }
val anchorIndex = lastSentenceIndex ?: return val anchorIndex = lastSentenceIndex ?: currentSentence()?.index ?: return
val anchor = item.sentences.getOrNull(anchorIndex) ?: return val anchor = item.sentences.getOrNull(anchorIndex) ?: return
if (position >= anchor.endMs) { if (position >= anchor.endMs) {
player.seekTo(anchor.startMs) applySentenceEndAction(anchor)
} }
} }
private fun refreshSentenceBoundaryGuard() {
cancelSentenceBoundaryGuard()
if (released || !config.sentenceMode) {
return
}
if (!player.isPlaying && !player.playWhenReady) {
return
}
val item = currentTrainingItem() ?: return
if (item.sentences.isEmpty()) {
return
}
val sentence = when {
stopAtSentenceEnd -> item.sentences.getOrNull(stopAtSentenceEndIndex ?: return)
config.continuousPlayback -> null
else -> {
val index = lastSentenceIndex ?: currentSentence()?.index
item.sentences.getOrNull(index ?: return)
}
} ?: return
val mediaItemIndex = player.currentMediaItemIndex
if (player.currentPosition >= sentence.endMs) {
applySentenceEndAction(sentence)
return
}
val token = ++boundaryGuardToken
boundaryMessage = player.createMessage { _, _ ->
if (token != boundaryGuardToken || released) {
return@createMessage
}
val active = currentTrainingItem() ?: return@createMessage
val latest = when {
stopAtSentenceEnd -> active.sentences.getOrNull(stopAtSentenceEndIndex ?: return@createMessage)
else -> active.sentences.getOrNull(sentence.index)
} ?: return@createMessage
if (player.currentMediaItemIndex != mediaItemIndex) {
return@createMessage
}
applySentenceEndAction(latest)
}
.setLooper(mainHandler.looper)
.setPosition(mediaItemIndex, sentence.endMs)
.setDeleteAfterDelivery(true)
.send()
}
private fun cancelSentenceBoundaryGuard() {
boundaryGuardToken++
boundaryMessage?.let { message ->
runCatching { message.cancel() }
}
boundaryMessage = null
}
private fun applySentenceEndAction(sentence: SentenceBoundary) {
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
stopAtSentenceEnd = false
stopAtSentenceEndIndex = null
cancelSentenceBoundaryGuard()
player.pause()
player.seekTo(sentence.startMs)
lastSentenceIndex = sentence.index
notifySentenceIfChanged(force = true)
notifySnapshot()
return
}
if (config.continuousPlayback) {
return
}
// Loop the current sentence without leaking into the next one.
player.seekTo(sentence.startMs)
lastSentenceIndex = sentence.index
notifySentenceIfChanged(force = true)
refreshSentenceBoundaryGuard()
notifySnapshot()
}
private fun TrainingMediaItem.toMedia3Item(): MediaItem { private fun TrainingMediaItem.toMedia3Item(): MediaItem {
val builder = MediaItem.Builder() val builder = MediaItem.Builder()
.setMediaId(id) .setMediaId(id)