From 93232a64faa1a467a29077297e645fd00b464e00 Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Sat, 12 Sep 2026 19:58:52 +0800 Subject: [PATCH] return android --- .../oraltrainer/sdk/OralTrainerController.kt | 449 +++++------------- .../oraltrainer/sample/MainActivity.kt | 60 +-- 2 files changed, 124 insertions(+), 385 deletions(-) diff --git a/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerController.kt b/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerController.kt index fdaa409..57d9fb5 100644 --- a/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerController.kt +++ b/android/oral-trainer-sdk/src/main/java/cn/learningpad/oraltrainer/sdk/OralTrainerController.kt @@ -11,6 +11,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.PlayerMessage import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import java.util.concurrent.CopyOnWriteArraySet import kotlin.math.max @@ -30,10 +31,8 @@ class OralTrainerController internal constructor( private var lastSentenceIndex: Int? = null private var stopAtSentenceEnd = false private var stopAtSentenceEndIndex: Int? = null - private var activeBaseItemIndex: Int = -1 - private var activeSentenceWindow: SentenceWindow? = null - private var sentenceWindows: List = emptyList() - private var sentencePlaylistBaseIndex: Int = -1 + private var boundaryMessage: PlayerMessage? = null + private var boundaryGuardToken = 0 var config: PlayerConfig = initialConfig private set @@ -48,20 +47,13 @@ class OralTrainerController internal constructor( if (released) { return } + enforceSentenceBoundary() notifySentenceIfChanged() notifySnapshot() mainHandler.postDelayed(this, 250L) } } - private data class SentenceWindow( - val baseItemIndex: Int, - val mediaId: String, - val sentenceIndex: Int, - val startMs: Long, - val endMs: Long, - ) - init { val mediaSourceFactory = DefaultMediaSourceFactory( StreamingCache.dataSourceFactory(appContext, sdkConfig) @@ -74,34 +66,24 @@ class OralTrainerController internal constructor( exoPlayer.playWhenReady = initialConfig.autoPlay exoPlayer.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { - if (playbackState == Player.STATE_ENDED) { - applySentenceEndForAnchor() - } - if (playbackState == Player.STATE_READY) { - refreshPlaybackWindow() - } - if (playbackState == Player.STATE_READY || - playbackState == Player.STATE_ENDED - ) { - refreshSentenceRepeatMode() - } notifySnapshot() } override fun onIsPlayingChanged(isPlaying: Boolean) { if (isPlaying) { - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() } else { - refreshPlaybackWindow() + cancelSentenceBoundaryGuard() } notifySnapshot() } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - activeSentenceWindow = currentSentenceWindow() - lastSentenceIndex = activeSentenceWindow?.sentenceIndex + lastSentenceIndex = null + cancelSentenceBoundaryGuard() listeners.forEach { it.onMediaChanged(currentTrainingItem()) } notifySentenceIfChanged(force = true) + refreshSentenceBoundaryGuard() notifySnapshot() } @@ -111,9 +93,7 @@ class OralTrainerController internal constructor( reason: Int, ) { notifySentenceIfChanged(force = true) - activeSentenceWindow = currentSentenceWindow() - lastSentenceIndex = activeSentenceWindow?.sentenceIndex - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() notifySnapshot() } @@ -160,11 +140,7 @@ class OralTrainerController internal constructor( lastSentenceIndex = null stopAtSentenceEnd = false stopAtSentenceEndIndex = null - activeBaseItemIndex = safeIndex - activeSentenceWindow = null - sentenceWindows = emptyList() - sentencePlaylistBaseIndex = -1 - player.setMediaItem(items[safeIndex].toMedia3Item(), max(0L, startPositionMs)) + player.setMediaItems(items.map { it.toMedia3Item() }, safeIndex, max(0L, startPositionMs)) player.prepare() player.playWhenReady = config.autoPlay listeners.forEach { it.onMediaChanged(currentTrainingItem()) } @@ -190,14 +166,10 @@ class OralTrainerController internal constructor( return true } mediaItems[index] = updatedItem - if (index == activeBaseItemIndex) { + if (index == player.currentMediaItemIndex) { listeners.forEach { it.onMediaChanged(updatedItem) } - if (config.sentenceMode && !config.continuousPlayback && sentences.isNotEmpty()) { - installSentencePlaylist(index, activeSentenceWindow?.sentenceIndex ?: 0) - } else { - refreshPlaybackWindow() - } notifySentenceIfChanged(force = true) + refreshSentenceBoundaryGuard() notifySnapshot() } return true @@ -205,12 +177,13 @@ class OralTrainerController internal constructor( fun play() { player.play() - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() } fun pause() { stopAtSentenceEnd = false stopAtSentenceEndIndex = null + cancelSentenceBoundaryGuard() player.pause() } @@ -225,31 +198,19 @@ class OralTrainerController internal constructor( fun stop() { stopAtSentenceEnd = false stopAtSentenceEndIndex = null + cancelSentenceBoundaryGuard() player.stop() } fun seekTo(positionMs: Long) { stopAtSentenceEnd = false stopAtSentenceEndIndex = null - val safePositionMs = max(0L, positionMs) - val anchor = sentenceAnchorAt(safePositionMs) - if (config.sentenceMode && !config.continuousPlayback && anchor != null) { - if (ensureSentenceWindow(anchor)) { - seekInSentenceWindow(anchor, safePositionMs) - } else { - player.seekTo(safePositionMs) - } - } else { - restoreFullWindow(safePositionMs) - player.seekTo(safePositionMs) - } - refreshPlaybackWindow() - notifySentenceIfChanged(force = true) - notifySnapshot() + player.seekTo(max(0L, positionMs)) + refreshSentenceBoundaryGuard() } fun seekRelative(deltaMs: Long) { - seekTo(playbackPositionMs() + deltaMs) + seekTo(player.currentPosition + deltaMs) } fun rewind(stepMs: Long = config.defaultSeekStepMs) { @@ -261,11 +222,11 @@ class OralTrainerController internal constructor( } fun previousItem() { - switchBaseItem(activeBaseItemIndex - 1) + player.seekToPreviousMediaItem() } fun nextItem() { - switchBaseItem(activeBaseItemIndex + 1) + player.seekToNextMediaItem() } fun setPlaybackSpeed(speed: Float) { @@ -279,50 +240,44 @@ class OralTrainerController internal constructor( fun setLoopMode(loopMode: LoopMode) { this.loopMode = loopMode - refreshSentenceRepeatMode() + player.repeatMode = when (loopMode) { + LoopMode.OFF -> Player.REPEAT_MODE_OFF + LoopMode.ONE -> Player.REPEAT_MODE_ONE + LoopMode.ALL -> Player.REPEAT_MODE_ALL + } } fun setSentenceMode(enabled: Boolean) { config = config.copy(sentenceMode = enabled) - refreshPlaybackWindow() } fun setContinuousPlayback(enabled: Boolean) { config = config.copy(continuousPlayback = enabled) - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() } fun playCurrentSentenceAndStop() { - val sentence = activeSentenceWindow?.let { window -> - currentTrainingItem()?.sentences?.getOrNull(window.sentenceIndex) - } ?: currentSentence() ?: return + val sentence = currentSentence() ?: return playSentenceAndStop(sentence.startMs, sentence.index) } /** Seek to a known sentence and stop automatically when that sentence ends. */ fun playSentenceAndStop(startMs: Long, sentenceIndex: Int) { - val sentence = currentTrainingItem()?.sentences?.getOrNull(sentenceIndex) stopAtSentenceEnd = true stopAtSentenceEndIndex = sentenceIndex - if (sentence != null) { - if (!ensureSentenceWindow(sentence)) { - player.seekTo(max(0L, startMs)) - } - } + player.seekTo(max(0L, startMs)) player.play() - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() } fun currentTrainingItem(): TrainingMediaItem? { - return mediaItems.getOrNull(activeBaseItemIndex) + val index = player.currentMediaItemIndex + return mediaItems.getOrNull(index) } fun currentSentence(): SentenceBoundary? { val item = currentTrainingItem() ?: return null - if (isSentencePlaylistActive()) { - return item.sentences.getOrNull(player.currentMediaItemIndex) - } - val position = playbackPositionMs() + val position = player.currentPosition return item.sentences.lastOrNull { sentence -> position >= sentence.startMs && position < sentence.endMs } @@ -333,12 +288,7 @@ class OralTrainerController internal constructor( if (item.sentences.isEmpty()) { return false } - if (isSentencePlaylistActive()) { - val index = (player.currentMediaItemIndex - 1).coerceAtLeast(0) - seekTo(item.sentences[index].startMs) - return true - } - val position = playbackPositionMs() + val position = player.currentPosition val target = item.sentences .asReversed() .firstOrNull { it.startMs < position - PREVIOUS_SENTENCE_TOLERANCE_MS } @@ -352,13 +302,7 @@ class OralTrainerController internal constructor( if (item.sentences.isEmpty()) { return false } - if (isSentencePlaylistActive()) { - val index = (player.currentMediaItemIndex + 1) - .takeIf { it < item.sentences.size } ?: return false - seekTo(item.sentences[index].startMs) - return true - } - val position = playbackPositionMs() + val position = player.currentPosition val target = item.sentences.firstOrNull { it.startMs > position + NEXT_SENTENCE_TOLERANCE_MS } ?: return false @@ -418,17 +362,12 @@ class OralTrainerController internal constructor( fun snapshot(): PlaybackSnapshot { val item = currentTrainingItem() - val window = activeSentenceWindow - val duration = if (window != null) { - window.endMs - } else { - player.duration.takeUnless { it == C.TIME_UNSET } ?: -1L - } + val duration = player.duration.takeUnless { it == C.TIME_UNSET } ?: -1L return PlaybackSnapshot( mediaId = item?.id, - positionMs = playbackPositionMs(), + positionMs = max(0L, player.currentPosition), durationMs = duration, - bufferedPositionMs = playbackBufferedPositionMs(), + bufferedPositionMs = max(0L, player.bufferedPosition), isPlaying = player.isPlaying, playbackState = player.playbackState.toOralTrainerState(), playbackSpeed = player.playbackParameters.speed, @@ -441,6 +380,7 @@ class OralTrainerController internal constructor( return } released = true + cancelSentenceBoundaryGuard() mainHandler.removeCallbacks(ticker) listeners.clear() player.release() @@ -464,42 +404,101 @@ class OralTrainerController internal constructor( lastSentenceIndex = index listeners.forEach { it.onSentenceChanged(sentence) } if (changed && !config.continuousPlayback && !stopAtSentenceEnd) { - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() } } - private fun refreshPlaybackWindow() { - val item = currentTrainingItem() + private fun enforceSentenceBoundary() { + if (!config.sentenceMode || !player.isPlaying) { + return + } + val item = currentTrainingItem() ?: return + if (item.sentences.isEmpty()) { + return + } + val position = player.currentPosition + if (stopAtSentenceEnd) { + val index = stopAtSentenceEndIndex ?: return + val sentence = item.sentences.getOrNull(index) ?: return + if (position >= sentence.endMs) { + applySentenceEndAction(sentence) + } + return + } + if (config.continuousPlayback) { + return + } + val anchorIndex = lastSentenceIndex ?: currentSentence()?.index ?: return + val anchor = item.sentences.getOrNull(anchorIndex) ?: return + if (position >= anchor.endMs) { + applySentenceEndAction(anchor) + } + } + + private fun refreshSentenceBoundaryGuard() { + cancelSentenceBoundaryGuard() if (released || !config.sentenceMode) { - restoreFullWindow(playbackPositionMs()) + return + } + if (!player.isPlaying && !player.playWhenReady) { + return + } + val item = currentTrainingItem() ?: return + if (item.sentences.isEmpty()) { return } - if (item == null || item.sentences.isEmpty()) { - restoreFullWindow(playbackPositionMs()) + 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 } - if (config.continuousPlayback && !stopAtSentenceEnd) { - restoreFullWindow(playbackPositionMs()) - 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() + } - val sentence = sentenceForPlaybackWindow(item) ?: return - if (!player.isPlaying && !player.playWhenReady && activeSentenceWindow == null) { - return + private fun cancelSentenceBoundaryGuard() { + boundaryGuardToken++ + boundaryMessage?.let { message -> + runCatching { message.cancel() } } - - ensureSentenceWindow(sentence) - refreshSentenceRepeatMode() + boundaryMessage = null } private fun applySentenceEndAction(sentence: SentenceBoundary) { if (stopAtSentenceEnd && stopAtSentenceEndIndex == sentence.index) { stopAtSentenceEnd = false stopAtSentenceEndIndex = null + cancelSentenceBoundaryGuard() player.pause() - player.seekTo(0L) + player.seekTo(sentence.startMs) lastSentenceIndex = sentence.index notifySentenceIfChanged(force = true) notifySnapshot() @@ -508,232 +507,20 @@ class OralTrainerController internal constructor( if (config.continuousPlayback) { return } - player.pause() - // 当前 MediaItem 已 clipped 到 [startMs, endMs),句首就是窗口内的 0ms。 - player.seekTo(0L) - if (player.playbackState == Player.STATE_ENDED) { - player.prepare() - } - player.play() + // Loop the current sentence without leaking into the next one. + player.seekTo(sentence.startMs) lastSentenceIndex = sentence.index notifySentenceIfChanged(force = true) - refreshPlaybackWindow() + refreshSentenceBoundaryGuard() notifySnapshot() } - private fun applySentenceEndForAnchor() { - if (!config.sentenceMode || config.continuousPlayback) { - return - } - val item = currentTrainingItem() - val index = currentSentenceIndex() ?: lastSentenceIndex ?: return - val sentence = item?.sentences?.getOrNull(index) ?: return - applySentenceEndAction(sentence) - } - - private fun sentenceAnchorAt(positionMs: Long): SentenceBoundary? { - val item = currentTrainingItem() ?: return null - return item.sentences.lastOrNull { positionMs >= it.startMs && positionMs < it.endMs } - ?: item.sentences.lastOrNull { it.startMs <= positionMs } - } - - private fun sentenceForPlaybackWindow(item: TrainingMediaItem?): SentenceBoundary? { - if (item == null || item.sentences.isEmpty()) { - return null - } - return when { - !config.sentenceMode -> null - stopAtSentenceEnd -> item.sentences.getOrNull(stopAtSentenceEndIndex ?: return null) - config.continuousPlayback -> null - else -> { - val index = currentSentenceIndex() - ?: lastSentenceIndex - ?: currentSentence()?.index - ?: sentenceAnchorAt(playbackPositionMs())?.index - item.sentences.getOrNull(index ?: return null) - } - } - } - - private fun playbackPositionMs(): Long { - val window = activeSentenceWindow ?: return max(0L, player.currentPosition) - val lengthMs = (window.endMs - window.startMs).coerceAtLeast(0L) - val clippedPositionMs = player.currentPosition.coerceIn(0L, lengthMs) - return window.startMs + clippedPositionMs - } - - private fun playbackBufferedPositionMs(): Long { - val window = activeSentenceWindow ?: return max(0L, player.bufferedPosition) - val lengthMs = (window.endMs - window.startMs).coerceAtLeast(0L) - val clippedBufferedMs = player.bufferedPosition.coerceIn(0L, lengthMs) - return window.startMs + clippedBufferedMs - } - - private fun ensureSentenceWindow(target: SentenceBoundary): Boolean { - if (released) { - return false - } - if (sentencePlaylistBaseIndex != activeBaseItemIndex && - player.playbackState in listOf(Player.STATE_READY, Player.STATE_ENDED) - ) { - installSentencePlaylist(activeBaseItemIndex, target.index) - return true - } - if (!isSentencePlaylistActive()) { - return false - } - - val desired = sentenceWindows.getOrNull(target.index) ?: return false - if (player.currentMediaItemIndex != target.index) { - player.seekTo(target.index, 0L) - } - activeSentenceWindow = desired - return true - } - - private fun installSentencePlaylist(baseIndex: Int, startIndex: Int) { - if (released) return - val baseItem = mediaItems.getOrNull(baseIndex) ?: return - val sentences = baseItem.sentences - if (sentences.isEmpty()) return - - sentenceWindows = sentences.mapIndexed { index, sentence -> - SentenceWindow( - baseItemIndex = baseIndex, - mediaId = baseItem.id, - sentenceIndex = index, - startMs = sentence.startMs, - endMs = sentence.endMs, - ) - } - sentencePlaylistBaseIndex = baseIndex - activeBaseItemIndex = baseIndex - val safeStartIndex = startIndex.coerceIn(0, sentenceWindows.lastIndex) - activeSentenceWindow = sentenceWindows[safeStartIndex] - lastSentenceIndex = safeStartIndex - - val playWhenReady = player.playWhenReady - player.setMediaItems( - sentenceWindows.map { window -> - baseItem.toMedia3Item(window.startMs, window.endMs) - }, - safeStartIndex, - 0L, - ) - player.prepare() - player.playWhenReady = playWhenReady - refreshSentenceRepeatMode() - } - - private fun currentSentenceWindow(): SentenceWindow? { - if (!isSentencePlaylistActive()) return null - return sentenceWindows.getOrNull(player.currentMediaItemIndex) - } - - private fun currentSentenceIndex(): Int? { - if (!isSentencePlaylistActive()) return null - return player.currentMediaItemIndex.takeIf { it in sentenceWindows.indices } - } - - private fun isSentencePlaylistActive(): Boolean { - return sentencePlaylistBaseIndex == activeBaseItemIndex && - sentenceWindows.isNotEmpty() && - player.currentMediaItemIndex in sentenceWindows.indices - } - - private fun refreshSentenceRepeatMode() { - if (!isSentencePlaylistActive()) { - player.repeatMode = when (loopMode) { - LoopMode.OFF -> Player.REPEAT_MODE_OFF - LoopMode.ONE -> Player.REPEAT_MODE_ONE - LoopMode.ALL -> Player.REPEAT_MODE_ALL - } - return - } - player.repeatMode = when { - stopAtSentenceEnd || !config.continuousPlayback -> Player.REPEAT_MODE_ONE - loopMode == LoopMode.OFF -> Player.REPEAT_MODE_OFF - else -> Player.REPEAT_MODE_ALL - } - } - - private fun seekInSentenceWindow(target: SentenceBoundary, originalPositionMs: Long) { - val relativePositionMs = (originalPositionMs - target.startMs).coerceIn( - 0L, - (target.endMs - target.startMs).coerceAtLeast(0L), - ) - player.seekTo(relativePositionMs) - } - - private fun restoreFullWindow(positionMs: Long) { - val baseIndex = activeBaseItemIndex - val baseItem = mediaItems.getOrNull(baseIndex) ?: return - if (sentencePlaylistBaseIndex == -1 && - activeSentenceWindow == null && - baseItem.id == player.currentMediaItem?.mediaId - ) { - return - } - - val playWhenReady = player.playWhenReady - activeSentenceWindow = null - sentenceWindows = emptyList() - sentencePlaylistBaseIndex = -1 - player.setMediaItem(baseItem.toMedia3Item(), max(0L, positionMs)) - player.prepare() - player.playWhenReady = playWhenReady - player.repeatMode = when (loopMode) { - LoopMode.OFF -> Player.REPEAT_MODE_OFF - LoopMode.ONE -> Player.REPEAT_MODE_ONE - LoopMode.ALL -> Player.REPEAT_MODE_ALL - } - } - - private fun switchBaseItem(targetIndex: Int) { - if (targetIndex !in mediaItems.indices) { - return - } - activeBaseItemIndex = targetIndex - activeSentenceWindow = null - sentenceWindows = emptyList() - sentencePlaylistBaseIndex = -1 - lastSentenceIndex = null - stopAtSentenceEnd = false - stopAtSentenceEndIndex = null - if (config.sentenceMode && mediaItems[targetIndex].sentences.isNotEmpty()) { - installSentencePlaylist(targetIndex, 0) - } else { - player.setMediaItem(mediaItems[targetIndex].toMedia3Item(), 0L) - player.prepare() - player.repeatMode = when (loopMode) { - LoopMode.OFF -> Player.REPEAT_MODE_OFF - LoopMode.ONE -> Player.REPEAT_MODE_ONE - LoopMode.ALL -> Player.REPEAT_MODE_ALL - } - } - listeners.forEach { it.onMediaChanged(currentTrainingItem()) } - notifySentenceIfChanged(force = true) - notifySnapshot() - } - - private fun TrainingMediaItem.toMedia3Item( - clippingStartPositionMs: Long = 0L, - clippingEndPositionMs: Long? = null, - ): MediaItem { + private fun TrainingMediaItem.toMedia3Item(): MediaItem { val builder = MediaItem.Builder() .setMediaId(id) .setUri(uri) mimeType?.let(builder::setMimeType) customCacheKey?.let(builder::setCustomCacheKey) - if (clippingEndPositionMs != null) { - builder.setClippingConfiguration( - MediaItem.ClippingConfiguration.Builder() - .setStartPositionMs(max(0L, clippingStartPositionMs)) - .setEndPositionMs(max(0L, clippingEndPositionMs)) - .setStartsAtKeyFrame(false) - .build() - ) - } subtitleUri?.let { uri -> builder.setSubtitleConfigurations( listOf( diff --git a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt index eff30ad..12642f5 100644 --- a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt +++ b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt @@ -21,7 +21,6 @@ import cn.learningpad.oraltrainer.sdk.SentenceBoundaryApiCallback import cn.learningpad.oraltrainer.sdk.SentenceBoundaryApiResult import cn.learningpad.oraltrainer.sdk.TrainingMediaItem import cn.learningpad.oraltrainer.sdk.TrainingVideoSummary -import kotlin.math.roundToInt class MainActivity : Activity() { private lateinit var sdk: OralTrainerSdk @@ -363,43 +362,12 @@ class MainActivity : Activity() { if (!progressBarDragging) { bindings.timeText.text = "${PlaybackFormat.time(snapshot.positionMs)} / ${PlaybackFormat.time(snapshot.durationMs)}" - bindings.progressBar.progress = if (controller.config.continuousPlayback) { - PlaybackFormat.progress(snapshot) - } else { - sentenceProgress(snapshot) - } + bindings.progressBar.progress = PlaybackFormat.progress(snapshot) } bindings.speedText.text = PlaybackFormat.speed(snapshot.playbackSpeed) bindings.statusText.text = PlaybackFormat.status(snapshot) } - private fun sentenceCount(): Int { - return controller.currentTrainingItem()?.sentences?.size?.takeIf { it > 0 } - ?: screenState.currentSentenceCount - } - - private fun sentenceProgress(snapshot: PlaybackSnapshot): Int { - val count = sentenceCount() - val index = snapshot.sentenceIndex ?: return 0 - if (count <= 1) return 0 - return (index * PROGRESS_MAX.toFloat() / (count - 1)).roundToInt() - .coerceIn(0, PROGRESS_MAX) - } - - private fun sentenceForProgress(progress: Int): Triple { - val sentences = controller.currentTrainingItem()?.sentences.orEmpty() - if (sentences.isEmpty()) { - return Triple(0, null, 0) - } - val index = if (sentences.size == 1) { - 0 - } else { - (progress.toFloat() / PROGRESS_MAX * (sentences.size - 1)).roundToInt() - .coerceIn(0, sentences.lastIndex) - } - return Triple(index, sentences.getOrNull(index), sentences.size) - } - private fun applySentence(sentence: SentenceBoundary?) { if (!bindings.hasPlayerChrome) return if (sentence == null) { @@ -431,21 +399,9 @@ class MainActivity : Activity() { override fun onSeekProgressPreview(progress: Int) { if (!bindings.hasPlayerChrome) return - if (controller.config.continuousPlayback) { - val duration = controller.snapshot().durationMs - bindings.timeText.text = - "${PlaybackFormat.time(PlaybackFormat.seekPosition(progress, duration))} / " + - PlaybackFormat.time(duration) - return - } - val (index, sentence, count) = sentenceForProgress(progress) + val duration = controller.snapshot().durationMs bindings.timeText.text = - if (sentence == null) { - "暂无句子边界" - } else { - "第 ${index + 1}/$count 句 " + - "${PlaybackFormat.time(sentence.startMs)}-${PlaybackFormat.time(sentence.endMs)}" - } + "${PlaybackFormat.time(PlaybackFormat.seekPosition(progress, duration))} / ${PlaybackFormat.time(duration)}" } override fun onSeekStart() { @@ -454,13 +410,9 @@ class MainActivity : Activity() { override fun onSeekStop(progress: Int) { progressBarDragging = false - if (controller.config.continuousPlayback) { - val duration = controller.snapshot().durationMs - controller.seekTo(PlaybackFormat.seekPosition(progress, duration)) - return - } - val (_, sentence, _) = sentenceForProgress(progress) - sentence?.let { controller.seekTo(it.startMs) } + controller.seekTo( + PlaybackFormat.seekPosition(progress, controller.snapshot().durationMs), + ) } override fun onContinuousPlaybackChanged(enabled: Boolean) {