fixed some bugs
This commit is contained in:
@@ -10,7 +10,10 @@ import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.PlayerMessage
|
||||
import androidx.media3.exoplayer.SeekParameters
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import kotlin.math.max
|
||||
@@ -34,6 +37,11 @@ class OralTrainerController internal constructor(
|
||||
private var clippedSentence: SentenceBoundary? = null
|
||||
private var updatingPlaybackWindow = false
|
||||
private var unclippedDurationMs = -1L
|
||||
private var sentenceEndMessage: PlayerMessage? = null
|
||||
private var playFromStartAfterReady = false
|
||||
private var clipStartNeedsWarmup = false
|
||||
private var clipStartWarmupVolume: Float? = null
|
||||
private val finishClipStartWarmupRunnable = Runnable { finishClipStartWarmup() }
|
||||
|
||||
var config: PlayerConfig = initialConfig
|
||||
private set
|
||||
@@ -48,6 +56,7 @@ class OralTrainerController internal constructor(
|
||||
if (released) {
|
||||
return
|
||||
}
|
||||
enforceStopAtSentenceEnd()
|
||||
notifySentenceIfChanged()
|
||||
notifySnapshot()
|
||||
mainHandler.postDelayed(this, IDLE_POLL_MS)
|
||||
@@ -64,15 +73,38 @@ class OralTrainerController internal constructor(
|
||||
.also { exoPlayer ->
|
||||
exoPlayer.repeatMode = Player.REPEAT_MODE_ALL
|
||||
exoPlayer.playWhenReady = initialConfig.autoPlay
|
||||
exoPlayer.setSeekParameters(SeekParameters.EXACT)
|
||||
exoPlayer.setForegroundMode(true)
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setAudioOffloadPreferences(
|
||||
AudioOffloadPreferences.Builder()
|
||||
.setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_DISABLED)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
exoPlayer.addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
onClippedSentenceEnded()
|
||||
if (clipStartWarmupVolume != null) {
|
||||
finishClipStartWarmup()
|
||||
} else {
|
||||
onClippedSentenceEnded()
|
||||
}
|
||||
}
|
||||
if (playbackState == Player.STATE_READY && unclippedDurationMs <= 0L) {
|
||||
val duration = player.duration
|
||||
if (duration != C.TIME_UNSET && clippedSentence == null) {
|
||||
unclippedDurationMs = duration
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
if (playFromStartAfterReady) {
|
||||
playFromStartAfterReady = false
|
||||
playClippedSentenceFromStart()
|
||||
}
|
||||
if (stopAtSentenceEnd) {
|
||||
scheduleSentenceEndGuard()
|
||||
}
|
||||
if (unclippedDurationMs <= 0L) {
|
||||
val duration = player.duration
|
||||
if (duration != C.TIME_UNSET && clippedSentence == null) {
|
||||
unclippedDurationMs = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
notifySnapshot()
|
||||
@@ -148,6 +180,8 @@ class OralTrainerController internal constructor(
|
||||
lastSentenceIndex = null
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
cancelClipStartWarmup(restoreVolume = true)
|
||||
cancelSentenceEndGuard()
|
||||
baseItemIndex = safeIndex
|
||||
clippedSentence = null
|
||||
unclippedDurationMs = -1L
|
||||
@@ -193,12 +227,18 @@ class OralTrainerController internal constructor(
|
||||
|
||||
fun play() {
|
||||
syncPlaybackWindow()
|
||||
if (clippedSentence != null && !player.isPlaying && shouldWarmClipStart()) {
|
||||
playClippedSentenceFromStart()
|
||||
return
|
||||
}
|
||||
player.play()
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
cancelClipStartWarmup(restoreVolume = true)
|
||||
cancelSentenceEndGuard()
|
||||
player.pause()
|
||||
applySentenceRepeatMode()
|
||||
}
|
||||
@@ -214,12 +254,16 @@ class OralTrainerController internal constructor(
|
||||
fun stop() {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
cancelClipStartWarmup(restoreVolume = true)
|
||||
cancelSentenceEndGuard()
|
||||
player.stop()
|
||||
}
|
||||
|
||||
fun seekTo(positionMs: Long) {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
cancelClipStartWarmup(restoreVolume = true)
|
||||
cancelSentenceEndGuard()
|
||||
val safePositionMs = max(0L, positionMs)
|
||||
lockSentenceAt(safePositionMs)
|
||||
syncPlaybackWindow(safePositionMs)
|
||||
@@ -283,17 +327,25 @@ class OralTrainerController internal constructor(
|
||||
|
||||
/** Seek to a known sentence and stop automatically when that sentence ends. */
|
||||
fun playSentenceAndStop(startMs: Long, sentenceIndex: Int) {
|
||||
val item = currentTrainingItem()
|
||||
val sentence = item?.sentences?.getOrNull(sentenceIndex)
|
||||
cancelClipStartWarmup(restoreVolume = true)
|
||||
stopAtSentenceEnd = true
|
||||
stopAtSentenceEndIndex = sentenceIndex
|
||||
lastSentenceIndex = sentenceIndex
|
||||
val item = currentTrainingItem()
|
||||
val sentence = item?.sentences?.getOrNull(sentenceIndex)
|
||||
player.playWhenReady = false
|
||||
if (item != null && sentence != null) {
|
||||
installSentenceClip(item, sentence, 0L)
|
||||
if (!isCurrentClip(sentence)) {
|
||||
installSentenceClip(item, sentence, 0L)
|
||||
player.playWhenReady = false
|
||||
}
|
||||
playClippedSentenceFromStart()
|
||||
} else {
|
||||
cancelSentenceEndGuard()
|
||||
player.seekTo(max(0L, startMs))
|
||||
player.playWhenReady = true
|
||||
player.play()
|
||||
}
|
||||
player.play()
|
||||
notifySentenceIfChanged(force = true)
|
||||
notifySnapshot()
|
||||
}
|
||||
@@ -429,6 +481,8 @@ class OralTrainerController internal constructor(
|
||||
return
|
||||
}
|
||||
released = true
|
||||
cancelClipStartWarmup(restoreVolume = false)
|
||||
cancelSentenceEndGuard()
|
||||
mainHandler.removeCallbacks(ticker)
|
||||
listeners.clear()
|
||||
player.release()
|
||||
@@ -498,17 +552,28 @@ class OralTrainerController internal constructor(
|
||||
return
|
||||
}
|
||||
rememberUnclippedDuration()
|
||||
cancelSentenceEndGuard()
|
||||
val lengthMs = (sentence.endMs - sentence.startMs).coerceAtLeast(0L)
|
||||
val startPositionMs = relativePositionMs.coerceIn(0L, lengthMs)
|
||||
val playWhenReady = player.playWhenReady
|
||||
val warmFromStart = startPositionMs <= CLIP_START_WARMUP_POSITION_MS
|
||||
clipStartNeedsWarmup = warmFromStart
|
||||
updatingPlaybackWindow = true
|
||||
clippedSentence = sentence
|
||||
lastSentenceIndex = sentence.index
|
||||
player.setMediaItem(item.toMedia3Item(sentence.startMs, sentence.endMs), startPositionMs)
|
||||
player.prepare()
|
||||
player.playWhenReady = playWhenReady
|
||||
player.playWhenReady = false
|
||||
applySentenceRepeatMode()
|
||||
updatingPlaybackWindow = false
|
||||
if (playWhenReady) {
|
||||
if (warmFromStart) {
|
||||
playClippedSentenceFromStart()
|
||||
} else {
|
||||
player.playWhenReady = true
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreUnclipped(positionMs: Long) {
|
||||
@@ -522,6 +587,7 @@ class OralTrainerController internal constructor(
|
||||
val playWhenReady = player.playWhenReady
|
||||
updatingPlaybackWindow = true
|
||||
clippedSentence = null
|
||||
clipStartNeedsWarmup = false
|
||||
player.setMediaItems(
|
||||
mediaItems.map { it.toMedia3Item() },
|
||||
baseItemIndex.coerceIn(0, mediaItems.lastIndex),
|
||||
@@ -545,16 +611,16 @@ class OralTrainerController internal constructor(
|
||||
|
||||
private fun onClippedSentenceEnded() {
|
||||
val sentence = clippedSentence ?: return
|
||||
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
|
||||
handleStopAtSentenceEnd(sentence)
|
||||
return
|
||||
}
|
||||
clipStartNeedsWarmup = false
|
||||
player.seekTo(0L)
|
||||
if (player.playbackState == Player.STATE_ENDED) {
|
||||
player.prepare()
|
||||
}
|
||||
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
player.pause()
|
||||
applySentenceRepeatMode()
|
||||
} else if (!config.continuousPlayback) {
|
||||
if (!config.continuousPlayback) {
|
||||
player.play()
|
||||
}
|
||||
lastSentenceIndex = sentence.index
|
||||
@@ -562,6 +628,63 @@ class OralTrainerController internal constructor(
|
||||
notifySnapshot()
|
||||
}
|
||||
|
||||
private fun enforceStopAtSentenceEnd() {
|
||||
if (!stopAtSentenceEnd) {
|
||||
return
|
||||
}
|
||||
val index = stopAtSentenceEndIndex ?: return
|
||||
val sentence = currentTrainingItem()?.sentences?.getOrNull(index) ?: return
|
||||
if (playbackPositionMs() >= sentence.endMs) {
|
||||
handleStopAtSentenceEnd(sentence)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStopAtSentenceEnd(sentence: SentenceBoundary) {
|
||||
if (!stopAtSentenceEnd || stopAtSentenceEndIndex != sentence.index) {
|
||||
return
|
||||
}
|
||||
cancelSentenceEndGuard()
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
player.pause()
|
||||
if (clippedSentence?.index == sentence.index) {
|
||||
player.seekTo(0L)
|
||||
if (player.playbackState == Player.STATE_ENDED) {
|
||||
player.prepare()
|
||||
}
|
||||
} else {
|
||||
seekInCurrentWindow(sentence.startMs)
|
||||
}
|
||||
applySentenceRepeatMode()
|
||||
lastSentenceIndex = sentence.index
|
||||
notifySentenceIfChanged(force = true)
|
||||
notifySnapshot()
|
||||
}
|
||||
|
||||
private fun scheduleSentenceEndGuard() {
|
||||
val index = stopAtSentenceEndIndex ?: return
|
||||
val sentence = currentTrainingItem()?.sentences?.getOrNull(index) ?: return
|
||||
cancelSentenceEndGuard()
|
||||
val endInWindowMs = if (clippedSentence?.index == sentence.index) {
|
||||
(sentence.endMs - sentence.startMs - 1L).coerceAtLeast(0L)
|
||||
} else {
|
||||
sentence.endMs
|
||||
}
|
||||
sentenceEndMessage = player.createMessage { _, _ ->
|
||||
if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) {
|
||||
handleStopAtSentenceEnd(sentence)
|
||||
}
|
||||
}.setLooper(mainHandler.looper)
|
||||
.setPosition(endInWindowMs)
|
||||
.setDeleteAfterDelivery(true)
|
||||
.send()
|
||||
}
|
||||
|
||||
private fun cancelSentenceEndGuard() {
|
||||
sentenceEndMessage?.cancel()
|
||||
sentenceEndMessage = null
|
||||
}
|
||||
|
||||
private fun switchBaseItem(targetIndex: Int) {
|
||||
if (targetIndex !in mediaItems.indices) {
|
||||
return
|
||||
@@ -570,6 +693,8 @@ class OralTrainerController internal constructor(
|
||||
lastSentenceIndex = null
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = null
|
||||
cancelClipStartWarmup(restoreVolume = true)
|
||||
cancelSentenceEndGuard()
|
||||
clippedSentence = null
|
||||
unclippedDurationMs = -1L
|
||||
val item = mediaItems[targetIndex]
|
||||
@@ -694,9 +819,93 @@ class OralTrainerController internal constructor(
|
||||
return CancellableAssessment {}
|
||||
}
|
||||
|
||||
private fun isCurrentClip(sentence: SentenceBoundary): Boolean {
|
||||
val current = clippedSentence ?: return false
|
||||
return current.index == sentence.index &&
|
||||
current.startMs == sentence.startMs &&
|
||||
current.endMs == sentence.endMs
|
||||
}
|
||||
|
||||
private fun shouldWarmClipStart(): Boolean {
|
||||
if (clippedSentence == null) {
|
||||
return false
|
||||
}
|
||||
return clipStartNeedsWarmup || player.currentPosition <= CLIP_START_WARMUP_POSITION_MS
|
||||
}
|
||||
|
||||
private fun playClippedSentenceFromStart() {
|
||||
if (released) {
|
||||
return
|
||||
}
|
||||
if (player.playbackState == Player.STATE_IDLE ||
|
||||
player.playbackState == Player.STATE_ENDED ||
|
||||
player.playbackState == Player.STATE_BUFFERING
|
||||
) {
|
||||
playFromStartAfterReady = true
|
||||
player.playWhenReady = false
|
||||
if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) {
|
||||
player.prepare()
|
||||
}
|
||||
return
|
||||
}
|
||||
startClipStartWarmup()
|
||||
}
|
||||
|
||||
private fun startClipStartWarmup() {
|
||||
if (released) {
|
||||
return
|
||||
}
|
||||
mainHandler.removeCallbacks(finishClipStartWarmupRunnable)
|
||||
if (clipStartWarmupVolume == null) {
|
||||
clipStartWarmupVolume = player.volume
|
||||
}
|
||||
player.volume = 0f
|
||||
player.seekTo(0L)
|
||||
if (player.playbackState == Player.STATE_ENDED) {
|
||||
playFromStartAfterReady = true
|
||||
player.prepare()
|
||||
return
|
||||
}
|
||||
player.playWhenReady = true
|
||||
player.play()
|
||||
mainHandler.postDelayed(finishClipStartWarmupRunnable, CLIP_START_WARMUP_MS)
|
||||
}
|
||||
|
||||
private fun finishClipStartWarmup() {
|
||||
if (released) {
|
||||
return
|
||||
}
|
||||
val restoreVolume = clipStartWarmupVolume ?: return
|
||||
clipStartWarmupVolume = null
|
||||
clipStartNeedsWarmup = false
|
||||
mainHandler.removeCallbacks(finishClipStartWarmupRunnable)
|
||||
player.seekTo(0L)
|
||||
if (player.playbackState == Player.STATE_ENDED) {
|
||||
player.prepare()
|
||||
}
|
||||
player.volume = restoreVolume
|
||||
player.playWhenReady = true
|
||||
player.play()
|
||||
if (stopAtSentenceEnd) {
|
||||
scheduleSentenceEndGuard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelClipStartWarmup(restoreVolume: Boolean) {
|
||||
playFromStartAfterReady = false
|
||||
mainHandler.removeCallbacks(finishClipStartWarmupRunnable)
|
||||
val volume = clipStartWarmupVolume ?: return
|
||||
clipStartWarmupVolume = null
|
||||
if (restoreVolume) {
|
||||
player.volume = volume
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PREVIOUS_SENTENCE_TOLERANCE_MS = 250L
|
||||
const val NEXT_SENTENCE_TOLERANCE_MS = 150L
|
||||
const val IDLE_POLL_MS = 250L
|
||||
const val CLIP_START_WARMUP_MS = 180L
|
||||
const val CLIP_START_WARMUP_POSITION_MS = 120L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class OralTrainerPlayerView @JvmOverloads constructor(
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
val playerView: PlayerView = PlayerView(context).apply {
|
||||
useController = false
|
||||
setKeepContentOnPlayerReset(true)
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user