fixed a bug
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user