fix a bug about ios

This commit is contained in:
2026-08-31 13:37:38 +08:00
parent 637101bcdb
commit 7e4c0f5f90
4 changed files with 113 additions and 34 deletions

View File

@@ -18,6 +18,8 @@ final class PlayerController: ObservableObject {
private var mediaItem: TrainingMediaItem?
private var lastSentenceIndex: Int?
private var pendingSeekPositionMs: Int64?
private var seekGeneration = 0
private var stopAtSentenceEnd = false
private var stopAtSentenceEndIndex: Int?
private var speedBeforeLongPress: Float?
@@ -67,6 +69,7 @@ final class PlayerController: ObservableObject {
func load(_ item: TrainingMediaItem) {
lastSentenceIndex = nil
pendingSeekPositionMs = nil
stopAtSentenceEnd = false
stopAtSentenceEndIndex = nil
mediaItem = item
@@ -109,18 +112,17 @@ final class PlayerController: ObservableObject {
stopAtSentenceEnd = false
stopAtSentenceEndIndex = nil
player.pause()
player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
seek(to: .zero)
}
func seek(to positionMs: Int64) {
stopAtSentenceEnd = false
stopAtSentenceEndIndex = nil
let time = CMTime(seconds: Double(max(0, positionMs)) / 1000, preferredTimescale: 600)
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
performSeek(to: positionMs)
}
func seekRelative(_ deltaMs: Int64) {
seek(to: snapshot.positionMs + deltaMs)
seek(to: referencePositionMs() + deltaMs)
}
func rewind() {
@@ -138,7 +140,7 @@ final class PlayerController: ObservableObject {
guard let item = mediaItem, !item.sentences.isEmpty else {
return false
}
let position = snapshot.positionMs
let position = referencePositionMs()
guard let target = item.sentences.reversed().first(where: { $0.startMs < position - 250 })
?? item.sentences.first else {
return false
@@ -152,7 +154,7 @@ final class PlayerController: ObservableObject {
guard let item = mediaItem, !item.sentences.isEmpty else {
return false
}
let position = snapshot.positionMs
let position = referencePositionMs()
guard let target = item.sentences.first(where: { $0.startMs > position + 150 }) else {
return false
}
@@ -177,8 +179,7 @@ final class PlayerController: ObservableObject {
guard let sentence = currentSentence else {
return
}
let time = CMTime(seconds: Double(sentence.startMs) / 1000, preferredTimescale: 600)
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
performSeek(to: sentence.startMs)
stopAtSentenceEnd = true
stopAtSentenceEndIndex = sentence.index
play()
@@ -277,9 +278,14 @@ final class PlayerController: ObservableObject {
snapshot = PlaybackSnapshot(mediaId: mediaItem?.id, state: .idle, speed: currentSpeed())
return
}
let position = currentPositionMs()
enforceSentenceBoundary(positionMs: position)
let finalPosition = currentPositionMs()
let position = referencePositionMs()
if pendingSeekPositionMs == nil, lastSentenceIndex == nil {
lastSentenceIndex = sentence(at: position)?.index
}
if pendingSeekPositionMs == nil {
enforceSentenceBoundary(positionMs: position)
}
let finalPosition = referencePositionMs()
let sentence = sentence(at: finalPosition)
if sentence?.index != lastSentenceIndex {
lastSentenceIndex = sentence?.index
@@ -307,6 +313,34 @@ final class PlayerController: ObservableObject {
return max(0, Int64(seconds * 1000))
}
/// 连续手势/快进快退时使用 pending seek 的目标位置,避免仍基于旧的播放位置计算。
private func referencePositionMs() -> Int64 {
pendingSeekPositionMs ?? currentPositionMs()
}
/// AVPlayer 的 seek 是异步的;所有 seek 必须统一走这里,避免播放循环、
/// 用户手势和 seek 完成回调交错后把位置拉回旧句子。
private func performSeek(to positionMs: Int64) {
let targetPositionMs = max(0, positionMs)
seekGeneration += 1
let generation = seekGeneration
pendingSeekPositionMs = targetPositionMs
// 立即切换句子锚点。否则 periodic observer 仍可能以旧句子为锚点,
// 在 seek 完成前后把新位置拉回旧句开头。
lastSentenceIndex = sentence(at: targetPositionMs)?.index
let time = CMTime(seconds: Double(targetPositionMs) / 1000, preferredTimescale: 600)
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
DispatchQueue.main.async {
guard let self, self.seekGeneration == generation else {
return
}
self.pendingSeekPositionMs = nil
self.tick()
}
}
tick()
}
private func durationMs(of item: AVPlayerItem) -> Int64 {
let seconds = item.duration.seconds
guard seconds.isFinite, seconds > 0 else {
@@ -393,8 +427,7 @@ final class PlayerController: ObservableObject {
}
private func seekRaw(_ positionMs: Int64) {
let time = CMTime(seconds: Double(max(0, positionMs)) / 1000, preferredTimescale: 600)
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
performSeek(to: positionMs)
}
private func handleEnded() {
@@ -402,7 +435,7 @@ final class PlayerController: ObservableObject {
tick()
return
}
player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
performSeek(to: .zero)
player.play()
tick()
}