make vlc range cache startup non-blocking

This commit is contained in:
dirtydishes 2026-05-26 20:43:25 -04:00
parent c4236afe7a
commit f141d26fb5
6 changed files with 424 additions and 4 deletions

View file

@ -58,6 +58,33 @@ struct HTTPRangeProbeResult {
let fallbackReason: String?
}
enum HTTPRangeCacheStartupDecision: Equatable {
case useLocalCache
case skip(reason: String)
}
enum HTTPRangeCacheStartupPolicy {
static let preparationTimeout: TimeInterval = 0.25
static let probeTimeout: TimeInterval = 0.2
static func immediateSkipReason(for url: URL) -> String? {
guard ["http", "https"].contains(url.scheme?.lowercased() ?? "") else {
return "non-http-url"
}
guard !url.path.lowercased().hasSuffix(".m3u8") else {
return "hls-playlist"
}
return nil
}
static func decision(for probe: HTTPRangeProbeResult) -> HTTPRangeCacheStartupDecision {
guard probe.isCacheable, probe.contentLength != nil else {
return .skip(reason: probe.fallbackReason ?? "range-probe-inconclusive")
}
return .useLocalCache
}
}
final class SparseHTTPByteRangeStore {
private struct Segment {
var range: HTTPByteRange

View file

@ -25,6 +25,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
#endif
private var rangeCacheSession: ProgressiveHTTPRangeCacheSession?
private var playbackStartupTask: Task<Void, Never>?
private var rangeCachePreparationTask: Task<Void, Never>?
private var playbackStartupID: UUID?
private var lastLoggedState: String?
private var lastBufferingLogTime: Date?
private var attachedSubtitleURLs = Set<URL>()
@ -56,6 +58,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
func play(request: NativePlaybackRequest) {
#if canImport(MobileVLCKit)
playbackStartupTask?.cancel()
rangeCachePreparationTask?.cancel()
attachedSubtitleURLs.removeAll()
pendingSubtitleCandidates.removeAll()
pendingSubtitleURLs.removeAll()
@ -70,8 +73,91 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
rangeCacheSession = nil
lastLoggedState = nil
lastBufferingLogTime = nil
startWithNonBlockingRangeCache(request: request)
#else
onFailure?(NativePlaybackError.backendUnavailable)
#endif
}
#if canImport(MobileVLCKit)
private func startWithNonBlockingRangeCache(request: NativePlaybackRequest) {
if let skipReason = HTTPRangeCacheStartupPolicy.immediateSkipReason(for: request.playbackURL) {
#if DEBUG
print("[DreamioVLC] cache fallback reason=startup-direct-preferred url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
print("[DreamioVLC] cache skipped reason=\(skipReason) url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
#endif
startDirectPlayback(request: request, fallbackReason: skipReason)
return
}
let startupID = UUID()
playbackStartupID = startupID
playbackStartupTask = Task { [weak self] in
guard let self else {
return
}
do {
let timeoutNanoseconds = UInt64(HTTPRangeCacheStartupPolicy.preparationTimeout * 1_000_000_000)
try await Task.sleep(nanoseconds: timeoutNanoseconds)
} catch {
return
}
await MainActor.run {
guard self.canStartPlayback(for: startupID) else {
return
}
#if DEBUG
print("[DreamioVLC] cache probe timed out timeoutMs=\(Int(HTTPRangeCacheStartupPolicy.preparationTimeout * 1000)) url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
#endif
self.rangeCachePreparationTask?.cancel()
self.startDirectPlayback(request: request, fallbackReason: "startup-direct-preferred")
}
}
rangeCachePreparationTask = Task { [weak self] in
guard let self else {
return
}
let result = await self.prepareRangeCache(for: request)
guard !Task.isCancelled else {
return
}
await MainActor.run {
guard self.canStartPlayback(for: startupID) else {
return
}
self.playbackStartupTask?.cancel()
switch result {
case .success(let prepared):
#if DEBUG
print("[DreamioVLC] cache used mode=local-cache url=\(URLRedactor.redactedURLString(prepared.localURL.absoluteString))")
#endif
self.rangeCacheSession = prepared.session
self.startVLCMedia(
url: prepared.localURL,
request: request,
playbackMode: "local-cache",
cachingMilliseconds: 1000,
includeRemoteHTTPOptions: false
)
case .failure(let failure):
self.startDirectPlayback(request: request, fallbackReason: failure.reason)
}
}
}
}
private func canStartPlayback(for startupID: UUID) -> Bool {
playbackStartupID == startupID && !hasStartedMedia
}
private struct RangeCacheStartupFailure: Error {
let reason: String
}
private func startDirectPlayback(request: NativePlaybackRequest, fallbackReason: String) {
#if DEBUG
print("[DreamioVLC] direct fallback started reason=\(fallbackReason) url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
#endif
startVLCMedia(
url: request.playbackURL,
@ -80,11 +166,47 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
cachingMilliseconds: 2500,
includeRemoteHTTPOptions: true
)
#else
onFailure?(NativePlaybackError.backendUnavailable)
#endif
}
private struct PreparedRangeCache {
let session: ProgressiveHTTPRangeCacheSession
let localURL: URL
}
private func prepareRangeCache(for request: NativePlaybackRequest) async -> Result<PreparedRangeCache, RangeCacheStartupFailure> {
let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers)
let probe = await fetcher.probe(timeoutInterval: HTTPRangeCacheStartupPolicy.probeTimeout)
switch HTTPRangeCacheStartupPolicy.decision(for: probe) {
case .skip(let reason):
#if DEBUG
print("[DreamioVLC] cache skipped reason=\(reason) url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
#endif
return .failure(RangeCacheStartupFailure(reason: reason))
case .useLocalCache:
break
}
guard let contentLength = probe.contentLength else {
return .failure(RangeCacheStartupFailure(reason: "range-probe-inconclusive"))
}
let session = ProgressiveHTTPRangeCacheSession(
fetcher: fetcher,
contentLength: contentLength,
durationProvider: { [weak self] in self?.duration ?? 0 }
)
do {
let localURL = try await ProgressiveHTTPRangeCacheServer.shared.localURL(for: session)
return .success(PreparedRangeCache(session: session, localURL: localURL))
} catch {
#if DEBUG
print("[DreamioVLC] local cache server failed error=\(error)")
#endif
return .failure(RangeCacheStartupFailure(reason: "local-cache-server-failed"))
}
}
#else
#endif
func play() {
#if canImport(MobileVLCKit)
mediaPlayer.play()
@ -193,6 +315,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
func stop() {
#if canImport(MobileVLCKit)
playbackStartupTask?.cancel()
rangeCachePreparationTask?.cancel()
playbackStartupID = nil
rangeCacheSession = nil
mediaPlayer.stop()
mediaPlayer.drawable = nil