Add OperatingArea and PilotCertification modules with live push readiness.
Migrate operating area and pilot certification from home placeholders, extend Live with playback and push readiness flows, and add pilot certificate OSS upload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
132
suixinkan/Features/Live/ViewModels/LivePlaybackViewModel.swift
Normal file
132
suixinkan/Features/Live/ViewModels/LivePlaybackViewModel.swift
Normal file
@ -0,0 +1,132 @@
|
||||
//
|
||||
// LivePlaybackViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/25.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// 直播播放地址解析器,避免把 RTMP 推流地址误当作播放地址。
|
||||
enum LivePlaybackURLResolver {
|
||||
static func playableURL(from live: LiveEntity) -> URL? {
|
||||
playableURL(from: live.playbackURLCandidates)
|
||||
}
|
||||
|
||||
static func playableURL(from candidates: [String]) -> URL? {
|
||||
for candidate in candidates {
|
||||
if let url = playableURL(from: candidate) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func playableURL(from rawValue: String) -> URL? {
|
||||
let value = rawValue.liveTrimmed
|
||||
guard !value.isEmpty, let url = URL(string: value) else { return nil }
|
||||
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else { return nil }
|
||||
if url.pathExtension.lowercased() == "flv" {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放器状态。
|
||||
enum LivePlaybackState: Equatable {
|
||||
case empty
|
||||
case ready(URL)
|
||||
case playing(URL)
|
||||
case failed(String)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播播放器 ViewModel,管理系统播放器的 URL、播放和释放状态。
|
||||
final class LivePlaybackViewModel {
|
||||
var state: LivePlaybackState = .empty
|
||||
var errorMessage: String?
|
||||
|
||||
@ObservationIgnored private(set) var player: AVPlayer?
|
||||
|
||||
var playableURL: URL? {
|
||||
switch state {
|
||||
case .ready(let url), .playing(let url):
|
||||
url
|
||||
case .empty, .failed:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
var isPlaying: Bool {
|
||||
if case .playing = state { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
init(live: LiveEntity? = nil, urlString: String? = nil) {
|
||||
if let live {
|
||||
load(live: live)
|
||||
} else if let urlString {
|
||||
load(urlString: urlString)
|
||||
}
|
||||
}
|
||||
|
||||
func load(live: LiveEntity) {
|
||||
load(url: LivePlaybackURLResolver.playableURL(from: live))
|
||||
}
|
||||
|
||||
func load(urlString: String) {
|
||||
load(url: LivePlaybackURLResolver.playableURL(from: urlString))
|
||||
}
|
||||
|
||||
func play() {
|
||||
guard let url = playableURL else { return }
|
||||
if player == nil {
|
||||
player = AVPlayer(url: url)
|
||||
}
|
||||
player?.play()
|
||||
state = .playing(url)
|
||||
}
|
||||
|
||||
func pause() {
|
||||
guard let url = playableURL else { return }
|
||||
player?.pause()
|
||||
state = .ready(url)
|
||||
}
|
||||
|
||||
func reload() {
|
||||
guard let url = playableURL else { return }
|
||||
releasePlayer()
|
||||
player = AVPlayer(url: url)
|
||||
state = .ready(url)
|
||||
}
|
||||
|
||||
func release() {
|
||||
releasePlayer()
|
||||
if let url = playableURL {
|
||||
state = .ready(url)
|
||||
} else {
|
||||
state = .empty
|
||||
}
|
||||
}
|
||||
|
||||
private func load(url: URL?) {
|
||||
releasePlayer()
|
||||
guard let url else {
|
||||
errorMessage = "暂无可播放地址"
|
||||
state = .empty
|
||||
return
|
||||
}
|
||||
errorMessage = nil
|
||||
player = AVPlayer(url: url)
|
||||
state = .ready(url)
|
||||
}
|
||||
|
||||
private func releasePlayer() {
|
||||
player?.pause()
|
||||
player = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user