新增运营区域与飞手认证模块,并完善直播推流就绪流程

将运营区域与飞手认证从首页占位页迁移为完整模块,扩展 Live 播放与推流就绪流程,并新增飞手证书 OSS 上传。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 18:15:59 +08:00
parent fcb692b56a
commit a04168cf30
33 changed files with 3455 additions and 37 deletions

View 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
}
}