Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
3.6 KiB
Swift
144 lines
3.6 KiB
Swift
//
|
||
// LivePlaybackViewModel.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import AVFoundation
|
||
import Foundation
|
||
|
||
/// 直播播放地址解析器,避免把 RTMP 推流地址误当作播放地址。
|
||
enum LivePlaybackURLResolver {
|
||
/// playable链接相关逻辑。
|
||
static func playableURL(from live: LiveEntity) -> URL? {
|
||
playableURL(from: live.playbackURLCandidates)
|
||
}
|
||
|
||
/// playable链接相关逻辑。
|
||
static func playableURL(from candidates: [String]) -> URL? {
|
||
for candidate in candidates {
|
||
if let url = playableURL(from: candidate) {
|
||
return url
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// playable链接相关逻辑。
|
||
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
|
||
/// 直播播放器 ViewModel,管理系统播放器的 URL、播放和释放状态。
|
||
final class LivePlaybackViewModel {
|
||
var onChange: (() -> Void)?
|
||
var state: LivePlaybackState = .empty { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
private(set) var player: AVPlayer? { didSet { onChange?() } }
|
||
|
||
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))
|
||
}
|
||
|
||
/// play 业务逻辑。
|
||
func play() {
|
||
guard let url = playableURL else { return }
|
||
if player == nil {
|
||
player = AVPlayer(url: url)
|
||
}
|
||
player?.play()
|
||
state = .playing(url)
|
||
}
|
||
|
||
/// pause 业务逻辑。
|
||
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)
|
||
}
|
||
|
||
/// release 业务逻辑。
|
||
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)
|
||
}
|
||
|
||
/// releasePlayer相关逻辑。
|
||
private func releasePlayer() {
|
||
player?.pause()
|
||
player = nil
|
||
}
|
||
}
|