Files
ddys-client/DDYSClient/ViewModels/PlayerViewModel.swift
2026-02-26 22:15:35 +08:00

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()
}
}