bound range cache probe startup

This commit is contained in:
dirtydishes 2026-05-26 08:17:07 -04:00
parent 4d0e675aa3
commit 4b173e0b88
6 changed files with 254 additions and 5 deletions

View file

@ -49,3 +49,4 @@
{"id":"int-ff0aeb09","kind":"field_change","created_at":"2026-05-26T04:47:44.48931Z","actor":"dirtydishes","issue_id":"dreamio-2hw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed stale local range-cache prefetch state after cached seek reads and documented the validation."}} {"id":"int-ff0aeb09","kind":"field_change","created_at":"2026-05-26T04:47:44.48931Z","actor":"dirtydishes","issue_id":"dreamio-2hw","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed stale local range-cache prefetch state after cached seek reads and documented the validation."}}
{"id":"int-204223f5","kind":"field_change","created_at":"2026-05-26T04:56:13.920284Z","actor":"dirtydishes","issue_id":"dreamio-816","extra":{"field":"status","new_value":"closed","old_value":"in_progress","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."}} {"id":"int-204223f5","kind":"field_change","created_at":"2026-05-26T04:56:13.920284Z","actor":"dirtydishes","issue_id":"dreamio-816","extra":{"field":"status","new_value":"closed","old_value":"in_progress","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."}}
{"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."}}

View file

@ -1,3 +1,4 @@
{"_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-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}

View file

@ -265,14 +265,14 @@ final class HTTPRangeRemoteFetcher {
self.session = session self.session = session
} }
func probe() async -> HTTPRangeProbeResult { func probe(timeoutInterval: TimeInterval = 3) async -> HTTPRangeProbeResult {
guard ["http", "https"].contains(url.scheme?.lowercased() ?? "") else { guard ["http", "https"].contains(url.scheme?.lowercased() ?? "") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "non-http-url") return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "non-http-url")
} }
guard !url.path.lowercased().hasSuffix(".m3u8") else { guard !url.path.lowercased().hasSuffix(".m3u8") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "hls-playlist") return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "hls-playlist")
} }
if let head = try? await response(for: request(method: "HEAD")), if let head = try? await response(for: request(method: "HEAD", timeoutInterval: timeoutInterval)),
(200..<400).contains(head.statusCode) { (200..<400).contains(head.statusCode) {
let acceptsRanges = header("Accept-Ranges", in: head)?.lowercased().contains("bytes") == true let acceptsRanges = header("Accept-Ranges", in: head)?.lowercased().contains("bytes") == true
let length = header("Content-Length", in: head).flatMap(Int64.init) let length = header("Content-Length", in: head).flatMap(Int64.init)
@ -281,7 +281,7 @@ final class HTTPRangeRemoteFetcher {
} }
} }
var tinyRequest = request(method: "GET") var tinyRequest = request(method: "GET", timeoutInterval: timeoutInterval)
tinyRequest.setValue("bytes=0-0", forHTTPHeaderField: "Range") tinyRequest.setValue("bytes=0-0", forHTTPHeaderField: "Range")
do { do {
let (data, response) = try await session.data(for: tinyRequest) let (data, response) = try await session.data(for: tinyRequest)
@ -317,9 +317,12 @@ final class HTTPRangeRemoteFetcher {
return response as? HTTPURLResponse return response as? HTTPURLResponse
} }
private func request(method: String) -> URLRequest { private func request(method: String, timeoutInterval: TimeInterval? = nil) -> URLRequest {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = method request.httpMethod = method
if let timeoutInterval {
request.timeoutInterval = timeoutInterval
}
headers.forEach { key, value in headers.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key) request.setValue(value, forHTTPHeaderField: key)
} }

View file

@ -78,7 +78,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
return return
} }
let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers) let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers)
let probe = await fetcher.probe() let probe = await fetcher.probe(timeoutInterval: 1.5)
guard !Task.isCancelled else { guard !Task.isCancelled else {
return return
} }

View file

@ -44,6 +44,7 @@ struct StreamResolverTests {
await testRangeCacheForegroundMissReprioritizesPrefetch() await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeCacheHitFollowsActualPostSeekReadArea() await testRangeCacheHitFollowsActualPostSeekReadArea()
await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges() await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges()
await testRangeProbeAppliesRequestTimeout()
await testRangeProbeFallsBackWhenServerIgnoresRange() await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders() await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed") print("StreamResolverTests passed")
@ -572,6 +573,32 @@ struct StreamResolverTests {
MockURLProtocol.handler = nil MockURLProtocol.handler = nil
} }
private static func testRangeProbeAppliesRequestTimeout() async {
MockURLProtocol.handler = { request in
assertEqual(request.timeoutInterval, 1.5)
let response = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: [
"Accept-Ranges": "bytes",
"Content-Length": "20"
]
)!
return (Data(), response)
}
let fetcher = HTTPRangeRemoteFetcher(
url: URL(string: "https://cdn.example.test/show.mkv")!,
headers: [:],
session: mockSession()
)
let probe = await fetcher.probe(timeoutInterval: 1.5)
assertEqual(probe.isCacheable, true)
MockURLProtocol.handler = nil
}
private static func byteRange(fromHeader header: String, contentLength: Int64) -> HTTPByteRange { private static func byteRange(fromHeader header: String, contentLength: Int64) -> HTTPByteRange {
let value = header.replacingOccurrences(of: "bytes=", with: "") let value = header.replacingOccurrences(of: "bytes=", with: "")
let pieces = value.split(separator: "-", maxSplits: 1).map(String.init) let pieces = value.split(separator: "-", maxSplits: 1).map(String.init)

File diff suppressed because one or more lines are too long