init: init proj
This commit is contained in:
134
DDYSClient/ViewModels/PlayerViewModel.swift
Normal file
134
DDYSClient/ViewModels/PlayerViewModel.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user