fixed bugs

This commit is contained in:
2026-09-13 12:58:45 +08:00
parent f810cfedd4
commit 5328898971
7 changed files with 233 additions and 61 deletions

View File

@@ -31,8 +31,9 @@ final class PlayerController: ObservableObject {
private var swipeBackAnchorTime: TimeInterval = 0
private var speedBeforeLongPress: Float?
private var transientTask: Task<Void, Never>?
private var cacheSwapTask: Task<Void, Never>?
private var cacheFallbackTask: Task<Void, Never>?
private var activePlaybackURL: URL?
private var didAttemptFullFileFallback = false
private var sentenceEndTimeObserver: Any?
private var sentenceEndGuardToken = 0
@@ -91,8 +92,9 @@ final class PlayerController: ObservableObject {
@MainActor
func load(_ item: TrainingMediaItem) {
cancelSentenceBoundaryGuard()
cacheSwapTask?.cancel()
cacheSwapTask = nil
cacheFallbackTask?.cancel()
cacheFallbackTask = nil
didAttemptFullFileFallback = false
lastSentenceIndex = nil
swipeBackAnchorIndex = nil
pendingSeekPositionMs = nil
@@ -110,12 +112,8 @@ final class PlayerController: ObservableObject {
cacheState = .ready
preparePlaybackItem(with: cachedURL)
} else {
// 关键约束:云端视频没有完整本地文件前,不把 remote URL 交给 AVPlayer。
activePlaybackURL = nil
cacheState = .downloading
player.replaceCurrentItem(with: nil)
tick()
scheduleFullFileDownload(for: item)
cacheState = .notRequired
preparePlaybackItem(with: item.url)
}
}
@@ -480,39 +478,8 @@ final class PlayerController: ObservableObject {
pendingSeekPositionMs ?? currentPositionMs()
}
@MainActor
private func scheduleFullFileDownload(for item: TrainingMediaItem) {
let remoteURL = item.url
cacheSwapTask = Task { [weak self] in
let cachedURL: URL
do {
cachedURL = try await StreamingVideoCache.shared.prefetch(remoteURL)
} catch is CancellationError {
return
} catch {
self?.cacheState = .failed(message: error.localizedDescription)
self?.showTransientMessage("视频缓存失败:\(error.localizedDescription)")
return
}
self?.prepareDownloadedFileIfNeeded(remoteURL: remoteURL, cachedURL: cachedURL)
}
}
@MainActor
private func prepareDownloadedFileIfNeeded(remoteURL: URL, cachedURL: URL) {
guard !Task.isCancelled,
mediaItem?.url == remoteURL,
activePlaybackURL == nil else {
return
}
cacheState = .ready
preparePlaybackItem(with: cachedURL)
showTransientMessage("视频已完整缓存,可以播放")
}
private func preparePlaybackItem(with playbackURL: URL) {
let newItem = AVPlayerItem(url: playbackURL)
let newItem = makePlayerItem(url: playbackURL)
activePlaybackURL = playbackURL
attachItemObservers(to: newItem)
player.replaceCurrentItem(with: newItem)
@@ -524,6 +491,19 @@ final class PlayerController: ObservableObject {
tick()
}
private func makePlayerItem(url: URL) -> AVPlayerItem {
guard !url.isFileURL else {
return AVPlayerItem(url: url)
}
var options: [String: Any] = [:]
let ext = url.pathExtension.lowercased()
if ext.isEmpty || ["mp4", "m4v", "mov"].contains(ext) {
options["AVURLAssetOverrideMIMETypeKey"] = "video/mp4"
}
let asset = AVURLAsset(url: url, options: options.isEmpty ? nil : options)
return AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["playable", "tracks", "duration"])
}
/// AVPlayer 的 seek 是异步的;所有 seek 必须统一走这里,避免播放循环、
/// 用户手势和 seek 完成回调交错后把位置拉回旧句子。
private func performSeek(
@@ -844,12 +824,57 @@ final class PlayerController: ObservableObject {
if item.status == .failed {
let message = item.error?.localizedDescription ?? "未知错误"
self.showTransientMessage("播放失败:\(message)")
} else if item.status == .readyToPlay {
self.considerFullFileFallback(for: item)
}
self.tick()
}
}
}
private func considerFullFileFallback(for item: AVPlayerItem) {
guard !didAttemptFullFileFallback,
let media = mediaItem,
!media.isLocalFile,
!(activePlaybackURL?.isFileURL ?? false) else {
return
}
let hasVisibleVideo = item.presentationSize.height >= 2 && item.presentationSize.width >= 2
if hasVisibleVideo {
return
}
didAttemptFullFileFallback = true
cacheFallbackTask?.cancel()
cacheFallbackTask = Task { @MainActor [weak self] in
try? await Task.sleep(nanoseconds: 800_000_000)
guard let self, !Task.isCancelled else {
return
}
guard self.mediaItem?.id == media.id,
self.player.currentItem === item,
item.presentationSize.height < 2 else {
return
}
do {
let cachedURL = try await StreamingVideoCache.shared.prefetch(media.url)
guard self.mediaItem?.id == media.id else {
return
}
let shouldPlay = self.playbackIntent
let positionMs = self.pendingSeekPositionMs ?? self.currentPositionMs()
self.preparePlaybackItem(with: cachedURL)
self.cacheState = .ready
if positionMs > 0 {
self.performSeek(to: positionMs, resumePlayback: shouldPlay)
}
} catch is CancellationError {
return
} catch {
self.showTransientMessage("视频画面加载失败:\(error.localizedDescription)")
}
}
}
private func configureAudioSession() {
let session = AVAudioSession.sharedInstance()
try? session.setCategory(.playback, mode: .moviePlayback)