fixed some bugs
This commit is contained in:
@@ -20,12 +20,18 @@ final class PlayerController: ObservableObject {
|
||||
private var lastSentenceIndex: Int?
|
||||
private var pendingSeekPositionMs: Int64?
|
||||
private var seekGeneration = 0
|
||||
/// ExoPlayer 的 playWhenReady 等价物。AVPlayer 的 actionAtItemEnd 可能在
|
||||
/// 句尾和切句 seek 的极短竞态里先自动暂停;这里不能依赖 player.rate 判断
|
||||
/// 用户是否希望继续播放。
|
||||
private var playbackIntent = false
|
||||
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 var cacheSwapTask: Task<Void, Never>?
|
||||
private var activePlaybackURL: URL?
|
||||
private var sentenceEndTimeObserver: Any?
|
||||
private var sentenceEndGuardToken = 0
|
||||
|
||||
@@ -79,6 +85,7 @@ final class PlayerController: ObservableObject {
|
||||
|
||||
// MARK: - 装载
|
||||
|
||||
@MainActor
|
||||
func load(_ item: TrainingMediaItem) {
|
||||
cancelSentenceBoundaryGuard()
|
||||
lastSentenceIndex = nil
|
||||
@@ -89,18 +96,51 @@ final class PlayerController: ObservableObject {
|
||||
mediaItem = item
|
||||
currentItem = item
|
||||
boundaryState = .idle
|
||||
let playerItem = AVPlayerItem(url: item.url)
|
||||
let playbackURL: URL
|
||||
if item.isLocalFile {
|
||||
playbackURL = item.url
|
||||
} else if let cachedURL = StreamingVideoCache.shared.cachedFileIfAvailable(for: item.url) {
|
||||
playbackURL = cachedURL
|
||||
} else {
|
||||
playbackURL = item.url
|
||||
}
|
||||
|
||||
activePlaybackURL = playbackURL
|
||||
let playerItem = AVPlayerItem(url: playbackURL)
|
||||
attachItemObservers(to: playerItem)
|
||||
player.replaceCurrentItem(with: playerItem)
|
||||
if config.autoPlay {
|
||||
player.play()
|
||||
}
|
||||
playbackIntent = config.autoPlay
|
||||
scheduleCachePrefetchIfNeeded(for: item, activeURL: playbackURL)
|
||||
tick()
|
||||
}
|
||||
|
||||
/// 句边界请求完成后只更新训练数据,不重新创建 AVPlayerItem。
|
||||
/// 否则会把 AVFoundation 已经缓存的媒体数据丢掉,回跳上一句时可能重新联网。
|
||||
@MainActor
|
||||
func updateBoundaries(_ sentences: [SentenceBoundary]) {
|
||||
guard let oldItem = mediaItem else {
|
||||
return
|
||||
}
|
||||
let newItem = TrainingMediaItem(
|
||||
id: oldItem.id,
|
||||
title: oldItem.title,
|
||||
url: oldItem.url,
|
||||
sentences: sentences,
|
||||
isLocalFile: oldItem.isLocalFile
|
||||
)
|
||||
mediaItem = newItem
|
||||
currentItem = newItem
|
||||
lastSentenceIndex = nil
|
||||
tick()
|
||||
}
|
||||
|
||||
// MARK: - 播放控制
|
||||
|
||||
func play() {
|
||||
playbackIntent = true
|
||||
player.play()
|
||||
// play() 可能在 AVPlayer 还没有把 timeControlStatus 切到 playing 前被调用;
|
||||
// 立即强制注册一次,避免最初的几十毫秒没有被句尾守卫覆盖。
|
||||
@@ -110,6 +150,7 @@ final class PlayerController: ObservableObject {
|
||||
func pause() {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = nil
|
||||
playbackIntent = false
|
||||
cancelSentenceBoundaryGuard()
|
||||
player.pause()
|
||||
}
|
||||
@@ -129,6 +170,7 @@ final class PlayerController: ObservableObject {
|
||||
func stop() {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = nil
|
||||
playbackIntent = false
|
||||
cancelSentenceBoundaryGuard()
|
||||
player.pause()
|
||||
seek(to: .zero)
|
||||
@@ -423,10 +465,52 @@ final class PlayerController: ObservableObject {
|
||||
pendingSeekPositionMs ?? currentPositionMs()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func scheduleCachePrefetchIfNeeded(for item: TrainingMediaItem, activeURL: URL) {
|
||||
cacheSwapTask?.cancel()
|
||||
cacheSwapTask = nil
|
||||
guard !item.isLocalFile,
|
||||
activeURL != item.url else {
|
||||
return
|
||||
}
|
||||
|
||||
let remoteURL = item.url
|
||||
cacheSwapTask = Task { [weak self] in
|
||||
guard let cachedURL = await StreamingVideoCache.shared.prefetch(remoteURL) else {
|
||||
return
|
||||
}
|
||||
self?.swapToCachedFileIfNeeded(remoteURL: remoteURL, cachedURL: cachedURL)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func swapToCachedFileIfNeeded(remoteURL: URL, cachedURL: URL) {
|
||||
guard !Task.isCancelled,
|
||||
mediaItem?.url == remoteURL,
|
||||
activePlaybackURL != cachedURL else {
|
||||
return
|
||||
}
|
||||
|
||||
let targetPositionMs = pendingSeekPositionMs ?? currentPositionMs()
|
||||
let newItem = AVPlayerItem(url: cachedURL)
|
||||
activePlaybackURL = cachedURL
|
||||
|
||||
// 递增 seek generation,让旧远程 Item 上未完成的 seek 回调失效。
|
||||
seekGeneration += 1
|
||||
attachItemObservers(to: newItem)
|
||||
player.replaceCurrentItem(with: newItem)
|
||||
performSeek(to: targetPositionMs)
|
||||
}
|
||||
|
||||
/// AVPlayer 的 seek 是异步的;所有 seek 必须统一走这里,避免播放循环、
|
||||
/// 用户手势和 seek 完成回调交错后把位置拉回旧句子。
|
||||
private func performSeek(to positionMs: Int64, completion: (() -> Void)? = nil) {
|
||||
private func performSeek(
|
||||
to positionMs: Int64,
|
||||
resumePlayback: Bool? = nil,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
let targetPositionMs = max(0, positionMs)
|
||||
let shouldResumePlayback = resumePlayback ?? playbackIntent
|
||||
seekGeneration += 1
|
||||
let generation = seekGeneration
|
||||
pendingSeekPositionMs = targetPositionMs
|
||||
@@ -441,6 +525,9 @@ final class PlayerController: ObservableObject {
|
||||
}
|
||||
self.pendingSeekPositionMs = nil
|
||||
self.tick()
|
||||
if shouldResumePlayback {
|
||||
self.player.play()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
@@ -628,6 +715,7 @@ final class PlayerController: ObservableObject {
|
||||
if stopAtSentenceEnd, stopAtSentenceEndIndex == sentence.index {
|
||||
stopAtSentenceEnd = false
|
||||
stopAtSentenceEndIndex = nil
|
||||
playbackIntent = false
|
||||
player.pause()
|
||||
seekRaw(sentenceSeekPosition(sentence))
|
||||
tick()
|
||||
@@ -687,8 +775,9 @@ final class PlayerController: ObservableObject {
|
||||
tick()
|
||||
return
|
||||
}
|
||||
// 播放中自然结束时 playbackIntent 仍为 true;如果用户本来已暂停,
|
||||
// 则不应因为到达 media end 而突然自动循环。
|
||||
performSeek(to: .zero)
|
||||
player.play()
|
||||
tick()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user