将运营区域与飞手认证从首页占位页迁移为完整模块,扩展 Live 播放与推流就绪流程,并新增飞手证书 OSS 上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.1 KiB
Swift
133 lines
3.1 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|