tv-anarchy/Sources/TVAnarchyiOS/VLCPlayerModel.swift
Natalie f0669f1ca8 feat(ios): TVAnarchyiOS app target + UI tests
(cherry picked from commit 7f8f4b0dd92358ba687f8230a922d8f316cb06e9)
2026-06-09 05:50:26 -07:00

68 lines
2.2 KiB
Swift

// Thin SwiftUI-facing wrapper over VLCMediaPlayer. VLCKit (not AVPlayer) because
// the library is torrent rips mostly mkv / x265 with embedded subs and
// multiple audio tracks which AVPlayer cannot open. VLCKit plays the raw file
// the bridge range-serves, so there is zero transcoding anywhere.
//
// State is polled on a 0.5s main-thread timer rather than via VLCMediaPlayerDelegate
// to keep everything @MainActor-clean (the delegate fires on VLCKit's own queue).
import Foundation
import MobileVLCKit
@MainActor
final class VLCPlayerModel: ObservableObject {
let player = VLCMediaPlayer()
@Published var isPlaying = false
@Published var position: Double = 0 // 0...1 along the media
@Published var elapsed = "00:00"
@Published var remaining = "00:00"
@Published var buffering = true
private var timer: Timer?
private var scrubbing = false
func start(url: URL, networkCachingMs: Int) {
let media = VLCMedia(url: url)
media.addOption("--network-caching=\(networkCachingMs)")
player.media = media
player.play()
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
Task { @MainActor in self?.tick() }
}
}
private func tick() {
isPlaying = player.isPlaying
let state = player.state
buffering = (state == .buffering || state == .opening)
if !scrubbing {
position = Double(player.position)
}
elapsed = player.time.stringValue
// remainingTime is negative ("-12:34"); show it as-is, it reads naturally.
remaining = player.remainingTime?.stringValue ?? ""
}
func togglePlay() {
if player.isPlaying { player.pause() } else { player.play() }
}
func skip(seconds: Int32) {
if seconds >= 0 { player.jumpForward(seconds) } else { player.jumpBackward(-seconds) }
}
func beginScrub() { scrubbing = true }
func commitScrub(to fraction: Double) {
player.position = Float(fraction)
scrubbing = false
}
func teardown() {
timer?.invalidate()
timer = nil
player.stop()
}
}