fixed some bugs
This commit is contained in:
@@ -22,9 +22,16 @@ final class PlayerController: ObservableObject {
|
||||
private var seekGeneration = 0
|
||||
private var stopAtSentenceEnd = false
|
||||
private var stopAtSentenceEndIndex: Int?
|
||||
private var swipeBackAnchorIndex: Int?
|
||||
private var swipeBackAnchorTime: TimeInterval = 0
|
||||
private var speedBeforeLongPress: Float?
|
||||
private var transientTask: Task<Void, Never>?
|
||||
|
||||
private static let swipeBackPhaseTimeout: TimeInterval = 1.2
|
||||
/// AVPlayer 在个别句子边界会把 seek 结果上报得比目标早 1ms 左右。
|
||||
/// 如果恰好落在句边界前,tick 会把句子重新判定回上一句;稍微进入目标句可消除该问题。
|
||||
private static let sentenceSeekInsetMs: Int64 = 20
|
||||
|
||||
private var timeObserver: Any?
|
||||
private var endObserver: NSObjectProtocol?
|
||||
private var itemStatusObserver: NSKeyValueObservation?
|
||||
@@ -69,6 +76,7 @@ final class PlayerController: ObservableObject {
|
||||
|
||||
func load(_ item: TrainingMediaItem) {
|
||||
lastSentenceIndex = nil
|
||||
swipeBackAnchorIndex = nil
|
||||
pendingSeekPositionMs = nil
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = nil
|
||||
@@ -140,12 +148,9 @@ final class PlayerController: ObservableObject {
|
||||
guard let item = mediaItem, !item.sentences.isEmpty else {
|
||||
return false
|
||||
}
|
||||
let position = referencePositionMs()
|
||||
guard let target = item.sentences.reversed().first(where: { $0.startMs < position - 250 })
|
||||
?? item.sentences.first else {
|
||||
return false
|
||||
}
|
||||
seek(to: target.startMs)
|
||||
guard let currentIndex = sentenceAnchorIndex() else { return false }
|
||||
let target = item.sentences[max(0, currentIndex - 1)]
|
||||
seekToSentenceBeginning(target)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -154,14 +159,47 @@ final class PlayerController: ObservableObject {
|
||||
guard let item = mediaItem, !item.sentences.isEmpty else {
|
||||
return false
|
||||
}
|
||||
let position = referencePositionMs()
|
||||
guard let target = item.sentences.first(where: { $0.startMs > position + 150 }) else {
|
||||
guard let currentIndex = sentenceAnchorIndex() else { return false }
|
||||
let targetIndex = currentIndex + 1
|
||||
guard item.sentences.indices.contains(targetIndex) else {
|
||||
return false
|
||||
}
|
||||
seek(to: target.startMs)
|
||||
let target = item.sentences[targetIndex]
|
||||
seekToSentenceBeginning(target)
|
||||
return true
|
||||
}
|
||||
|
||||
/// 按索引跳转,而不是每次用异步的 AVPlayer 时间重新搜索。
|
||||
/// 这样一次手势只前进/后退一句,也不会因 seek 后时间上报偏差导致重复或无效。
|
||||
@discardableResult
|
||||
func seekToSentence(at index: Int) -> Bool {
|
||||
guard let item = mediaItem, item.sentences.indices.contains(index) else {
|
||||
return false
|
||||
}
|
||||
seekToSentenceBeginning(item.sentences[index])
|
||||
return true
|
||||
}
|
||||
|
||||
private func sentenceAnchorIndex() -> Int? {
|
||||
guard let item = mediaItem, !item.sentences.isEmpty else { return nil }
|
||||
if let index = currentSentence?.index, item.sentences.indices.contains(index) {
|
||||
return index
|
||||
}
|
||||
if let index = lastSentenceIndex, item.sentences.indices.contains(index) {
|
||||
return index
|
||||
}
|
||||
|
||||
let position = referencePositionMs()
|
||||
if let sentence = sentence(at: position) {
|
||||
return sentence.index
|
||||
}
|
||||
// 句间隙中时,取前一句作为锚点;在第一句之前时取下一句。
|
||||
if let previous = item.sentences.last(where: { $0.startMs <= position }) {
|
||||
return previous.index
|
||||
}
|
||||
return item.sentences.first?.index
|
||||
}
|
||||
|
||||
func previousSentenceOrRewind() {
|
||||
if !config.sentenceMode || !seekToPreviousSentence() {
|
||||
rewind()
|
||||
@@ -174,12 +212,55 @@ final class PlayerController: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放中向左划:第一次回到当前句开头重播;短时间内再划一次才到上一句。
|
||||
/// 暂停时保持原来的“上一句”语义,测试页“上一句”按钮也不受影响。
|
||||
@discardableResult
|
||||
func replayOrPreviousSentence() -> SwipeBackResult {
|
||||
guard config.sentenceMode,
|
||||
let item = mediaItem,
|
||||
!item.sentences.isEmpty,
|
||||
let anchorIndex = sentenceAnchorIndex() else {
|
||||
rewind()
|
||||
return .rewound
|
||||
}
|
||||
|
||||
let isActivelyPlaying = player.timeControlStatus == .playing
|
||||
|| player.timeControlStatus == .waitingToPlayAtSpecifiedRate
|
||||
guard isActivelyPlaying else {
|
||||
swipeBackAnchorIndex = nil
|
||||
return seekToPreviousSentence() ? .movedPrevious : .stayedAtFirst
|
||||
}
|
||||
|
||||
let now = Date().timeIntervalSinceReferenceDate
|
||||
if swipeBackAnchorIndex == anchorIndex,
|
||||
now - swipeBackAnchorTime <= Self.swipeBackPhaseTimeout {
|
||||
swipeBackAnchorIndex = nil
|
||||
guard anchorIndex > 0 else {
|
||||
seekToSentenceBeginning(item.sentences[anchorIndex])
|
||||
return .stayedAtFirst
|
||||
}
|
||||
seekToSentenceBeginning(item.sentences[anchorIndex - 1])
|
||||
return .movedPrevious
|
||||
}
|
||||
|
||||
swipeBackAnchorIndex = anchorIndex
|
||||
swipeBackAnchorTime = now
|
||||
let current = item.sentences[anchorIndex]
|
||||
if stopAtSentenceEnd {
|
||||
playCurrentSentenceAndStop()
|
||||
} else {
|
||||
seekToSentenceBeginning(current)
|
||||
play()
|
||||
}
|
||||
return .restartedCurrent
|
||||
}
|
||||
|
||||
/// 只播放当前句子,播完自动暂停并回到句首(评测“播放原句”)。
|
||||
func playCurrentSentenceAndStop() {
|
||||
guard let sentence = currentSentence else {
|
||||
return
|
||||
}
|
||||
performSeek(to: sentence.startMs)
|
||||
performSeek(to: sentenceSeekPosition(sentence))
|
||||
stopAtSentenceEnd = true
|
||||
stopAtSentenceEndIndex = sentence.index
|
||||
play()
|
||||
@@ -249,8 +330,16 @@ final class PlayerController: ObservableObject {
|
||||
togglePlayPause()
|
||||
showTransientMessage("播放状态已切换")
|
||||
case .swipeLeft:
|
||||
previousSentenceOrRewind()
|
||||
showTransientMessage("已跳到上一句")
|
||||
switch replayOrPreviousSentence() {
|
||||
case .restartedCurrent:
|
||||
showTransientMessage("已回到本句开头,再左划返回上一句")
|
||||
case .movedPrevious:
|
||||
showTransientMessage("已跳到上一句")
|
||||
case .stayedAtFirst:
|
||||
showTransientMessage("已是第一句")
|
||||
case .rewound:
|
||||
showTransientMessage("已快退")
|
||||
}
|
||||
case .swipeRight:
|
||||
nextSentenceOrForward()
|
||||
showTransientMessage("已跳到下一句")
|
||||
@@ -290,6 +379,9 @@ final class PlayerController: ObservableObject {
|
||||
if sentence?.index != lastSentenceIndex {
|
||||
lastSentenceIndex = sentence?.index
|
||||
}
|
||||
if sentence?.index != swipeBackAnchorIndex {
|
||||
swipeBackAnchorIndex = nil
|
||||
}
|
||||
let duration = durationMs(of: item)
|
||||
let state = playbackState(of: item, positionMs: finalPosition, durationMs: duration)
|
||||
snapshot = PlaybackSnapshot(
|
||||
@@ -422,7 +514,7 @@ final class PlayerController: ObservableObject {
|
||||
return
|
||||
}
|
||||
if positionMs >= anchor.endMs {
|
||||
seekRaw(anchor.startMs)
|
||||
seekRaw(sentenceSeekPosition(anchor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +522,16 @@ final class PlayerController: ObservableObject {
|
||||
performSeek(to: positionMs)
|
||||
}
|
||||
|
||||
private func sentenceSeekPosition(_ sentence: SentenceBoundary) -> Int64 {
|
||||
let available = max(0, sentence.endMs - sentence.startMs - 1)
|
||||
let inset = min(Self.sentenceSeekInsetMs, available)
|
||||
return sentence.startMs + inset
|
||||
}
|
||||
|
||||
private func seekToSentenceBeginning(_ sentence: SentenceBoundary) {
|
||||
seek(to: sentenceSeekPosition(sentence))
|
||||
}
|
||||
|
||||
private func handleEnded() {
|
||||
guard loopMode != .off else {
|
||||
tick()
|
||||
|
||||
Reference in New Issue
Block a user