mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
make vlc range cache startup non-blocking
This commit is contained in:
parent
c4236afe7a
commit
f141d26fb5
6 changed files with 424 additions and 4 deletions
|
|
@ -51,3 +51,4 @@
|
||||||
{"id":"int-b6f641ed","kind":"field_change","created_at":"2026-05-26T12:10:16.392655Z","actor":"dirtydishes","issue_id":"dreamio-3sw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Removed the Matroska/WebM extension-level range-cache bypass and added a regression test proving MKV URLs use the cache when the origin advertises byte-range support."}}
|
{"id":"int-b6f641ed","kind":"field_change","created_at":"2026-05-26T12:10:16.392655Z","actor":"dirtydishes","issue_id":"dreamio-3sw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Removed the Matroska/WebM extension-level range-cache bypass and added a regression test proving MKV URLs use the cache when the origin advertises byte-range support."}}
|
||||||
{"id":"int-2b073805","kind":"field_change","created_at":"2026-05-26T12:16:53.567972Z","actor":"dirtydishes","issue_id":"dreamio-btc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added a short timeout to range-cache probe requests so slow MKV HEAD/range probes fall back to direct VLC startup instead of tripping the native-player startup timeout."}}
|
{"id":"int-2b073805","kind":"field_change","created_at":"2026-05-26T12:16:53.567972Z","actor":"dirtydishes","issue_id":"dreamio-btc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added a short timeout to range-cache probe requests so slow MKV HEAD/range probes fall back to direct VLC startup instead of tripping the native-player startup timeout."}}
|
||||||
{"id":"int-1ed0a18a","kind":"field_change","created_at":"2026-05-26T13:01:27.690486Z","actor":"dirtydishes","issue_id":"dreamio-dd7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Changed VLC startup to open direct playback immediately instead of waiting for slow range-cache probes, restoring reliable native-player startup for MKV streams."}}
|
{"id":"int-1ed0a18a","kind":"field_change","created_at":"2026-05-26T13:01:27.690486Z","actor":"dirtydishes","issue_id":"dreamio-dd7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Changed VLC startup to open direct playback immediately instead of waiting for slow range-cache probes, restoring reliable native-player startup for MKV streams."}}
|
||||||
|
{"id":"int-ffb67dfa","kind":"field_change","created_at":"2026-05-27T00:43:02.592758Z","actor":"dirtydishes","issue_id":"dreamio-5cz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented bounded non-blocking range-cache startup for VLC, with direct fallback on timeout, skipped probes, or local server failures; added focused startup policy tests and updated the turn document."}}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
{"_type":"issue","id":"dreamio-btc","title":"Bound VLC range cache probe startup latency","description":"After enabling MKV range cache probing, some Torrentio/Real-Debrid MKV streams log cache-probe but never reach opening mode before the native-player startup timeout. Add a bounded probe/local-cache startup path that falls back to direct playback when the range probe is slow or inconclusive.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:14:02Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:16:53Z","started_at":"2026-05-26T12:14:11Z","closed_at":"2026-05-26T12:16:53Z","close_reason":"Added a short timeout to range-cache probe requests so slow MKV HEAD/range probes fall back to direct VLC startup instead of tripping the native-player startup timeout.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-btc","title":"Bound VLC range cache probe startup latency","description":"After enabling MKV range cache probing, some Torrentio/Real-Debrid MKV streams log cache-probe but never reach opening mode before the native-player startup timeout. Add a bounded probe/local-cache startup path that falls back to direct playback when the range probe is slow or inconclusive.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:14:02Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:16:53Z","started_at":"2026-05-26T12:14:11Z","closed_at":"2026-05-26T12:16:53Z","close_reason":"Added a short timeout to range-cache probe requests so slow MKV HEAD/range probes fall back to direct VLC startup instead of tripping the native-player startup timeout.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-mun","title":"fix vlc cache loopback port startup","description":"Device logs showed local-cache playback opening http://127.0.0.1:0, because the NWListener ephemeral port was read before the listener reached ready. Wait for the real assigned port before returning the local cache URL.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T22:32:41Z","created_by":"dirtydishes","updated_at":"2026-05-25T22:33:15Z","started_at":"2026-05-25T22:33:14Z","closed_at":"2026-05-25T22:33:15Z","close_reason":"Wait for NWListener ready state before returning the local cache URL; verified tests and simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-mun","title":"fix vlc cache loopback port startup","description":"Device logs showed local-cache playback opening http://127.0.0.1:0, because the NWListener ephemeral port was read before the listener reached ready. Wait for the real assigned port before returning the local cache URL.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T22:32:41Z","created_by":"dirtydishes","updated_at":"2026-05-25T22:33:15Z","started_at":"2026-05-25T22:33:14Z","closed_at":"2026-05-25T22:33:15Z","close_reason":"Wait for NWListener ready state before returning the local cache URL; verified tests and simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-8cz","title":"fix stremio external subtitle loading regression","description":"After adding late subtitle forwarding for native playback, Stremio external subtitle loading is failing. Investigate the injected bridge and native subtitle forwarding path, then adjust behavior so Stremio can still load external subtitles while native playback receives late candidates.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T11:05:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T11:07:35Z","started_at":"2026-05-25T11:05:55Z","closed_at":"2026-05-25T11:07:35Z","close_reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-8cz","title":"fix stremio external subtitle loading regression","description":"After adding late subtitle forwarding for native playback, Stremio external subtitle loading is failing. Investigate the injected bridge and native subtitle forwarding path, then adjust behavior so Stremio can still load external subtitles while native playback receives late candidates.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T11:05:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T11:07:35Z","started_at":"2026-05-25T11:05:55Z","closed_at":"2026-05-25T11:07:35Z","close_reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"dreamio-5cz","title":"Make VLC range cache non-blocking at startup","description":"Native playback startup currently bypasses Dreamio's local range cache after cache probing caused VLC startup timeouts. Reintroduce cache startup only when preparation is fast and safe, otherwise fall back to direct playback immediately, with focused tests and clear logs.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T00:36:56Z","created_by":"dirtydishes","updated_at":"2026-05-27T00:43:03Z","started_at":"2026-05-27T00:37:03Z","closed_at":"2026-05-27T00:43:03Z","close_reason":"Implemented bounded non-blocking range-cache startup for VLC, with direct fallback on timeout, skipped probes, or local server failures; added focused startup policy tests and updated the turn document.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-3sw","title":"Fix VLC range cache fallback for tail-index MKV streams","description":"Video range caching currently refuses streams classified as tail-index containers, causing VLC playback to use direct mode and lose seek prefetch behavior. Investigate the probe logic and enable safe local range caching for these streams without breaking playback startup.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:05:20Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:10:16Z","started_at":"2026-05-26T12:05:38Z","closed_at":"2026-05-26T12:10:16Z","close_reason":"Removed the Matroska/WebM extension-level range-cache bypass and added a regression test proving MKV URLs use the cache when the origin advertises byte-range support.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-3sw","title":"Fix VLC range cache fallback for tail-index MKV streams","description":"Video range caching currently refuses streams classified as tail-index containers, causing VLC playback to use direct mode and lose seek prefetch behavior. Investigate the probe logic and enable safe local range caching for these streams without breaking playback startup.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:05:20Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:10:16Z","started_at":"2026-05-26T12:05:38Z","closed_at":"2026-05-26T12:10:16Z","close_reason":"Removed the Matroska/WebM extension-level range-cache bypass and added a regression test proving MKV URLs use the cache when the origin advertises byte-range support.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-816","title":"Fix local range cache playback buffering","description":"Normal VLC playback can stay in buffering after the local progressive HTTP range cache is enabled. Logs show VLC repeatedly probes header/tail MKV ranges through the loopback server while the cache foreground fetch path serializes 1 MB remote requests. Investigate and adjust the cache path so normal direct-file playback can start reliably.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:54:13Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:56:14Z","started_at":"2026-05-26T04:54:17Z","closed_at":"2026-05-26T04:56:14Z","close_reason":"Bypassed the local range cache for Matroska-family tail-index containers and added a regression test confirming MKV probes fall back to direct VLC playback without issuing cache probe requests.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-816","title":"Fix local range cache playback buffering","description":"Normal VLC playback can stay in buffering after the local progressive HTTP range cache is enabled. Logs show VLC repeatedly probes header/tail MKV ranges through the loopback server while the cache foreground fetch path serializes 1 MB remote requests. Investigate and adjust the cache path so normal direct-file playback can start reliably.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:54:13Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:56:14Z","started_at":"2026-05-26T04:54:17Z","closed_at":"2026-05-26T04:56:14Z","close_reason":"Bypassed the local range cache for Matroska-family tail-index containers and added a regression test confirming MKV probes fall back to direct VLC playback without issuing cache probe requests.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-2hw","title":"Fix range cache prefetch cursor after cached seek reads","description":"Skipping after the local range cache has warmed can leave prefetch following an older foreground cursor instead of the post-seek cached read position. Update the cache so cached foreground reads can reset the follow cursor and add regression coverage.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:45:44Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:47:44Z","started_at":"2026-05-26T04:46:36Z","closed_at":"2026-05-26T04:47:44Z","close_reason":"Fixed stale local range-cache prefetch state after cached seek reads and documented the validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-2hw","title":"Fix range cache prefetch cursor after cached seek reads","description":"Skipping after the local range cache has warmed can leave prefetch following an older foreground cursor instead of the post-seek cached read position. Update the cache so cached foreground reads can reset the follow cursor and add regression coverage.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:45:44Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:47:44Z","started_at":"2026-05-26T04:46:36Z","closed_at":"2026-05-26T04:47:44Z","close_reason":"Fixed stale local range-cache prefetch state after cached seek reads and documented the validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,33 @@ struct HTTPRangeProbeResult {
|
||||||
let fallbackReason: String?
|
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 {
|
final class SparseHTTPByteRangeStore {
|
||||||
private struct Segment {
|
private struct Segment {
|
||||||
var range: HTTPByteRange
|
var range: HTTPByteRange
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
#endif
|
#endif
|
||||||
private var rangeCacheSession: ProgressiveHTTPRangeCacheSession?
|
private var rangeCacheSession: ProgressiveHTTPRangeCacheSession?
|
||||||
private var playbackStartupTask: Task<Void, Never>?
|
private var playbackStartupTask: Task<Void, Never>?
|
||||||
|
private var rangeCachePreparationTask: Task<Void, Never>?
|
||||||
|
private var playbackStartupID: UUID?
|
||||||
private var lastLoggedState: String?
|
private var lastLoggedState: String?
|
||||||
private var lastBufferingLogTime: Date?
|
private var lastBufferingLogTime: Date?
|
||||||
private var attachedSubtitleURLs = Set<URL>()
|
private var attachedSubtitleURLs = Set<URL>()
|
||||||
|
|
@ -56,6 +58,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
func play(request: NativePlaybackRequest) {
|
func play(request: NativePlaybackRequest) {
|
||||||
#if canImport(MobileVLCKit)
|
#if canImport(MobileVLCKit)
|
||||||
playbackStartupTask?.cancel()
|
playbackStartupTask?.cancel()
|
||||||
|
rangeCachePreparationTask?.cancel()
|
||||||
attachedSubtitleURLs.removeAll()
|
attachedSubtitleURLs.removeAll()
|
||||||
pendingSubtitleCandidates.removeAll()
|
pendingSubtitleCandidates.removeAll()
|
||||||
pendingSubtitleURLs.removeAll()
|
pendingSubtitleURLs.removeAll()
|
||||||
|
|
@ -70,8 +73,91 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
rangeCacheSession = nil
|
rangeCacheSession = nil
|
||||||
lastLoggedState = nil
|
lastLoggedState = nil
|
||||||
lastBufferingLogTime = 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
|
#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
|
#endif
|
||||||
startVLCMedia(
|
startVLCMedia(
|
||||||
url: request.playbackURL,
|
url: request.playbackURL,
|
||||||
|
|
@ -80,11 +166,47 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
cachingMilliseconds: 2500,
|
cachingMilliseconds: 2500,
|
||||||
includeRemoteHTTPOptions: true
|
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() {
|
func play() {
|
||||||
#if canImport(MobileVLCKit)
|
#if canImport(MobileVLCKit)
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
|
|
@ -193,6 +315,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
func stop() {
|
func stop() {
|
||||||
#if canImport(MobileVLCKit)
|
#if canImport(MobileVLCKit)
|
||||||
playbackStartupTask?.cancel()
|
playbackStartupTask?.cancel()
|
||||||
|
rangeCachePreparationTask?.cancel()
|
||||||
|
playbackStartupID = nil
|
||||||
rangeCacheSession = nil
|
rangeCacheSession = nil
|
||||||
mediaPlayer.stop()
|
mediaPlayer.stop()
|
||||||
mediaPlayer.drawable = nil
|
mediaPlayer.drawable = nil
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ struct StreamResolverTests {
|
||||||
testRangeCacheForegroundMissFetchesAlignedChunks()
|
testRangeCacheForegroundMissFetchesAlignedChunks()
|
||||||
await testRangeCacheForegroundMissReprioritizesPrefetch()
|
await testRangeCacheForegroundMissReprioritizesPrefetch()
|
||||||
await testRangeCacheHitFollowsActualPostSeekReadArea()
|
await testRangeCacheHitFollowsActualPostSeekReadArea()
|
||||||
|
testRangeCacheStartupPolicySkipsHLSAndNonHTTPImmediately()
|
||||||
|
testRangeCacheStartupPolicyUsesCacheOnlyForConclusiveProbe()
|
||||||
|
testRangeCacheStartupPolicySkipsInconclusiveProbe()
|
||||||
await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges()
|
await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges()
|
||||||
await testRangeProbeAppliesRequestTimeout()
|
await testRangeProbeAppliesRequestTimeout()
|
||||||
await testRangeProbeFallsBackWhenServerIgnoresRange()
|
await testRangeProbeFallsBackWhenServerIgnoresRange()
|
||||||
|
|
@ -542,6 +545,41 @@ struct StreamResolverTests {
|
||||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func testRangeCacheStartupPolicySkipsHLSAndNonHTTPImmediately() {
|
||||||
|
assertEqual(
|
||||||
|
HTTPRangeCacheStartupPolicy.immediateSkipReason(for: URL(string: "https://cdn.example.test/live.m3u8")!),
|
||||||
|
"hls-playlist"
|
||||||
|
)
|
||||||
|
assertEqual(
|
||||||
|
HTTPRangeCacheStartupPolicy.immediateSkipReason(for: URL(string: "file:///tmp/movie.mkv")!),
|
||||||
|
"non-http-url"
|
||||||
|
)
|
||||||
|
assertEqual(
|
||||||
|
HTTPRangeCacheStartupPolicy.immediateSkipReason(for: URL(string: "https://cdn.example.test/movie.mkv")!),
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func testRangeCacheStartupPolicyUsesCacheOnlyForConclusiveProbe() {
|
||||||
|
let decision = HTTPRangeCacheStartupPolicy.decision(
|
||||||
|
for: HTTPRangeProbeResult(isCacheable: true, contentLength: 20, fallbackReason: nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEqual(decision, .useLocalCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func testRangeCacheStartupPolicySkipsInconclusiveProbe() {
|
||||||
|
let rejectedDecision = HTTPRangeCacheStartupPolicy.decision(
|
||||||
|
for: HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "range-probe-status-200")
|
||||||
|
)
|
||||||
|
let missingLengthDecision = HTTPRangeCacheStartupPolicy.decision(
|
||||||
|
for: HTTPRangeProbeResult(isCacheable: true, contentLength: nil, fallbackReason: nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEqual(rejectedDecision, .skip(reason: "range-probe-status-200"))
|
||||||
|
assertEqual(missingLengthDecision, .skip(reason: "range-probe-inconclusive"))
|
||||||
|
}
|
||||||
|
|
||||||
private static func testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges() async {
|
private static func testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges() async {
|
||||||
var requestCount = 0
|
var requestCount = 0
|
||||||
MockURLProtocol.handler = { request in
|
MockURLProtocol.handler = { request in
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue