135 lines
3.6 KiB
Swift
135 lines
3.6 KiB
Swift
import AVFoundation
|
|
import Foundation
|
|
|
|
@Observable
|
|
final class PlayerViewModel {
|
|
var player: AVPlayer?
|
|
var isPlaying = false
|
|
var currentTime: Double = 0
|
|
var duration: Double = 0
|
|
var isLoading = false
|
|
var error: String?
|
|
var playbackRate: Float = 1.0
|
|
|
|
private var timeObserver: Any?
|
|
private var contentId: String = ""
|
|
private var episodeId: Int = 0
|
|
|
|
var progress: Double {
|
|
guard duration > 0 else { return 0 }
|
|
return currentTime / duration
|
|
}
|
|
|
|
var currentTimeText: String {
|
|
formatTime(currentTime)
|
|
}
|
|
|
|
var durationText: String {
|
|
formatTime(duration)
|
|
}
|
|
|
|
func play(url: String, contentId: String, episodeId: Int = 0) {
|
|
self.contentId = contentId
|
|
self.episodeId = episodeId
|
|
|
|
guard let videoURL = URL(string: url) else {
|
|
error = "无效的视频地址"
|
|
return
|
|
}
|
|
|
|
let playerItem = AVPlayerItem(url: videoURL)
|
|
player = AVPlayer(playerItem: playerItem)
|
|
player?.rate = playbackRate
|
|
|
|
// 恢复上次进度
|
|
if let savedProgress = WatchProgressStore.shared.getProgress(for: contentId, episodeId: episodeId) {
|
|
let resumeTime = CMTime(seconds: savedProgress.currentTime, preferredTimescale: 600)
|
|
player?.seek(to: resumeTime)
|
|
}
|
|
|
|
setupTimeObserver()
|
|
player?.play()
|
|
isPlaying = true
|
|
}
|
|
|
|
func togglePlay() {
|
|
guard let player else { return }
|
|
if isPlaying {
|
|
player.pause()
|
|
} else {
|
|
player.play()
|
|
}
|
|
isPlaying.toggle()
|
|
}
|
|
|
|
func seek(to time: Double) {
|
|
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
|
|
player?.seek(to: cmTime)
|
|
}
|
|
|
|
func seekForward(_ seconds: Double = 10) {
|
|
seek(to: min(currentTime + seconds, duration))
|
|
}
|
|
|
|
func seekBackward(_ seconds: Double = 10) {
|
|
seek(to: max(currentTime - seconds, 0))
|
|
}
|
|
|
|
func setRate(_ rate: Float) {
|
|
playbackRate = rate
|
|
player?.rate = rate
|
|
}
|
|
|
|
func stop() {
|
|
saveProgress()
|
|
player?.pause()
|
|
if let observer = timeObserver {
|
|
player?.removeTimeObserver(observer)
|
|
}
|
|
timeObserver = nil
|
|
player = nil
|
|
isPlaying = false
|
|
}
|
|
|
|
private func setupTimeObserver() {
|
|
let interval = CMTime(seconds: 1, preferredTimescale: 600)
|
|
timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
|
guard let self else { return }
|
|
self.currentTime = time.seconds
|
|
if let item = self.player?.currentItem {
|
|
self.duration = item.duration.seconds.isNaN ? 0 : item.duration.seconds
|
|
}
|
|
// 每 10 秒保存一次进度
|
|
if Int(self.currentTime) % 10 == 0 {
|
|
self.saveProgress()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func saveProgress() {
|
|
guard duration > 0 else { return }
|
|
WatchProgressStore.shared.saveProgress(
|
|
contentId: contentId,
|
|
episodeId: episodeId,
|
|
currentTime: currentTime,
|
|
duration: duration
|
|
)
|
|
}
|
|
|
|
private func formatTime(_ seconds: Double) -> String {
|
|
guard !seconds.isNaN, seconds >= 0 else { return "00:00" }
|
|
let totalSeconds = Int(seconds)
|
|
let hours = totalSeconds / 3600
|
|
let minutes = (totalSeconds % 3600) / 60
|
|
let secs = totalSeconds % 60
|
|
if hours > 0 {
|
|
return String(format: "%d:%02d:%02d", hours, minutes, secs)
|
|
}
|
|
return String(format: "%02d:%02d", minutes, secs)
|
|
}
|
|
|
|
deinit {
|
|
stop()
|
|
}
|
|
}
|