Dreamio turn documentation ยท May 26, 2026

Fix VLC Range Cache for MKV Streams

Removed the blanket Matroska/WebM cache bypass so direct-file MKV streams can use Dreamio's local range cache when the origin server confirms byte-range support.

Summary

Dreamio was refusing to range-cache MKV streams before checking the server. That made Torrentio and Real-Debrid MKV playback open in direct mode, so seek prefetch could not run. The cache probe now lets normal HTTP range capability decide whether the local cache should be used.

Changes Made

Context

The diagnostic logs showed [DreamioVLC] cache fallback reason=tail-index-container, followed by opening mode=direct and direct-mode seek logs. That fallback came from an extension check, not a failed HTTP range probe. Because many debrid MKV streams do support byte ranges, the app was leaving useful buffering behavior on the table.

Important Implementation Details

Relevant Diff Snippets

Dreamio/ProgressiveHTTPRangeCache.swift

Dreamio/ProgressiveHTTPRangeCache.swift
-8
271 unmodified lines
272
273
274
275
276
277
278
279
280
281
52 unmodified lines
334
335
336
337
338
339
340
341
342
343
271 unmodified lines
guard !url.path.lowercased().hasSuffix(".m3u8") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "hls-playlist")
}
guard !Self.shouldBypassCache(for: url) else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "tail-index-container")
}
if let head = try? await response(for: request(method: "HEAD")),
(200..<400).contains(head.statusCode) {
let acceptsRanges = header("Accept-Ranges", in: head)?.lowercased().contains("bytes") == true
52 unmodified lines
response.value(forHTTPHeaderField: name)
}
private static func shouldBypassCache(for url: URL) -> Bool {
let extensionName = url.pathExtension.lowercased()
return ["mkv", "mk3d", "mka", "mks", "webm"].contains(extensionName)
}
}
enum HTTPRangeCacheError: Error {
271 unmodified lines
272
273
274
275
276
277
52 unmodified lines
330
331
332
333
334
335
271 unmodified lines
guard !url.path.lowercased().hasSuffix(".m3u8") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "hls-playlist")
}
if let head = try? await response(for: request(method: "HEAD")),
(200..<400).contains(head.statusCode) {
let acceptsRanges = header("Accept-Ranges", in: head)?.lowercased().contains("bytes") == true
52 unmodified lines
response.value(forHTTPHeaderField: name)
}
}
enum HTTPRangeCacheError: Error {

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
-8+13
42 unmodified lines
43
44
45
46
47
48
49
491 unmodified lines
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
3 unmodified lines
561
562
563
564
565
566
567
568
569
42 unmodified lines
testRangeCacheForegroundMissFetchesAlignedChunks()
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeCacheHitFollowsActualPostSeekReadArea()
await testRangeProbeBypassesTailIndexContainers()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed")
491 unmodified lines
try? await Task.sleep(nanoseconds: 50_000_000)
}
private static func testRangeProbeBypassesTailIndexContainers() async {
var requestCount = 0
MockURLProtocol.handler = { request in
requestCount += 1
let response = HTTPURLResponse(
url: request.url!,
statusCode: 206,
httpVersion: nil,
headerFields: ["Content-Range": "bytes 0-0/20"]
)!
return (Data([1]), response)
}
let fetcher = HTTPRangeRemoteFetcher(
3 unmodified lines
)
let probe = await fetcher.probe()
assertEqual(probe.isCacheable, false)
assertEqual(probe.fallbackReason, "tail-index-container")
assertEqual(requestCount, 0)
MockURLProtocol.handler = nil
}
42 unmodified lines
43
44
45
46
47
48
49
491 unmodified lines
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
3 unmodified lines
565
566
567
568
569
570
571
572
573
574
42 unmodified lines
testRangeCacheForegroundMissFetchesAlignedChunks()
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeCacheHitFollowsActualPostSeekReadArea()
await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed")
491 unmodified lines
try? await Task.sleep(nanoseconds: 50_000_000)
}
private static func testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges() async {
var requestCount = 0
MockURLProtocol.handler = { request in
requestCount += 1
assertEqual(request.httpMethod, "HEAD")
let response = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: [
"Accept-Ranges": "bytes",
"Content-Length": "20"
]
)!
return (Data(), response)
}
let fetcher = HTTPRangeRemoteFetcher(
3 unmodified lines
)
let probe = await fetcher.probe()
assertEqual(probe.isCacheable, true)
assertEqual(probe.contentLength, 20)
assertEqual(probe.fallbackReason, nil)
assertEqual(requestCount, 1)
MockURLProtocol.handler = nil
}

Expected Impact for End-Users

Compatible MKV direct-file streams should start through Dreamio's local range cache instead of direct VLC mode. Backward and forward skips can now prime nearby bytes, which should reduce stalls after seeking on supported servers.

Validation

Issues, Limitations, and Mitigations

This does not guarantee every MKV will use the cache. Servers that lack byte-range support, omit content length, reject range requests, or fail the local cache server setup still fall back to direct VLC playback. That is intentional so playback continues instead of failing hard.

Follow-up Work

New Changes as of May 26, 2026 at 8:16 AM

Summary of changes

Added a short timeout to the range-cache probe path so a slow HEAD or tiny range request cannot prevent native playback from starting.

Why this change was made

Device logs showed the MKV stream reached [DreamioVLC] cache-probe but never logged either opening mode=local-cache or opening mode=direct before the native-player startup timeout. The cache probe was waiting too long before any VLC media was opened.

Code diffs

Dreamio/ProgressiveHTTPRangeCache.swift

Dreamio/ProgressiveHTTPRangeCache.swift
-4+7
264 unmodified lines
265
266
267
268
269
270
271
272
273
274
275
276
277
278
2 unmodified lines
281
282
283
284
285
286
287
29 unmodified lines
317
318
319
320
321
322
323
324
325
264 unmodified lines
self.session = session
}
func probe() async -> HTTPRangeProbeResult {
guard ["http", "https"].contains(url.scheme?.lowercased() ?? "") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "non-http-url")
}
guard !url.path.lowercased().hasSuffix(".m3u8") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "hls-playlist")
}
if let head = try? await response(for: request(method: "HEAD")),
(200..<400).contains(head.statusCode) {
let acceptsRanges = header("Accept-Ranges", in: head)?.lowercased().contains("bytes") == true
let length = header("Content-Length", in: head).flatMap(Int64.init)
2 unmodified lines
}
}
var tinyRequest = request(method: "GET")
tinyRequest.setValue("bytes=0-0", forHTTPHeaderField: "Range")
do {
let (data, response) = try await session.data(for: tinyRequest)
29 unmodified lines
return response as? HTTPURLResponse
}
private func request(method: String) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method
headers.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}
264 unmodified lines
265
266
267
268
269
270
271
272
273
274
275
276
277
278
2 unmodified lines
281
282
283
284
285
286
287
29 unmodified lines
317
318
319
320
321
322
323
324
325
326
327
328
264 unmodified lines
self.session = session
}
func probe(timeoutInterval: TimeInterval = 3) async -> HTTPRangeProbeResult {
guard ["http", "https"].contains(url.scheme?.lowercased() ?? "") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "non-http-url")
}
guard !url.path.lowercased().hasSuffix(".m3u8") else {
return HTTPRangeProbeResult(isCacheable: false, contentLength: nil, fallbackReason: "hls-playlist")
}
if let head = try? await response(for: request(method: "HEAD", timeoutInterval: timeoutInterval)),
(200..<400).contains(head.statusCode) {
let acceptsRanges = header("Accept-Ranges", in: head)?.lowercased().contains("bytes") == true
let length = header("Content-Length", in: head).flatMap(Int64.init)
2 unmodified lines
}
}
var tinyRequest = request(method: "GET", timeoutInterval: timeoutInterval)
tinyRequest.setValue("bytes=0-0", forHTTPHeaderField: "Range")
do {
let (data, response) = try await session.data(for: tinyRequest)
29 unmodified lines
return response as? HTTPURLResponse
}
private func request(method: String, timeoutInterval: TimeInterval? = nil) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method
if let timeoutInterval {
request.timeoutInterval = timeoutInterval
}
headers.forEach { key, value in
request.setValue(value, forHTTPHeaderField: key)
}

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
-1+1
77 unmodified lines
78
79
80
81
82
83
84
77 unmodified lines
return
}
let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers)
let probe = await fetcher.probe()
guard !Task.isCancelled else {
return
}
77 unmodified lines
78
79
80
81
82
83
84
77 unmodified lines
return
}
let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers)
let probe = await fetcher.probe(timeoutInterval: 1.5)
guard !Task.isCancelled else {
return
}

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+27
43 unmodified lines
44
45
46
47
48
49
522 unmodified lines
572
573
574
575
576
577
43 unmodified lines
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeCacheHitFollowsActualPostSeekReadArea()
await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed")
522 unmodified lines
MockURLProtocol.handler = nil
}
private static func byteRange(fromHeader header: String, contentLength: Int64) -> HTTPByteRange {
let value = header.replacingOccurrences(of: "bytes=", with: "")
let pieces = value.split(separator: "-", maxSplits: 1).map(String.init)
43 unmodified lines
44
45
46
47
48
49
50
522 unmodified lines
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
43 unmodified lines
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeCacheHitFollowsActualPostSeekReadArea()
await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges()
await testRangeProbeAppliesRequestTimeout()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed")
522 unmodified lines
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 {
let value = header.replacingOccurrences(of: "bytes=", with: "")
let pieces = value.split(separator: "-", maxSplits: 1).map(String.init)

Related issues or PRs

Beads issue: dreamio-btc. This follows the earlier dreamio-3sw MKV cache enablement work.

Validation

New Changes as of May 26, 2026 at 9:01 AM

Summary of changes

Changed VLC startup to open direct playback immediately instead of waiting for the range-cache probe.

Why this change was made

Device logs showed the probe request timing out and the native player still failing to start before the startup watchdog. The reliable behavior is to start VLC first, then revisit cache probing as a non-blocking optimization later.

Code diffs

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
-53+9
70 unmodified lines
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
70 unmodified lines
lastLoggedState = nil
lastBufferingLogTime = nil
#if DEBUG
print("[DreamioVLC] cache-probe url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
#endif
playbackStartupTask = Task { [weak self] in
guard let self else {
return
}
let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers)
let probe = await fetcher.probe(timeoutInterval: 1.5)
guard !Task.isCancelled else {
return
}
if probe.isCacheable, let contentLength = probe.contentLength, contentLength > 0 {
do {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: fetcher,
contentLength: contentLength,
durationProvider: { [weak self] in self?.duration ?? 0 }
)
let localURL = try await ProgressiveHTTPRangeCacheServer.shared.localURL(for: session)
await MainActor.run {
self.rangeCacheSession = session
session.prefetch(aroundByteOffset: 0)
self.startVLCMedia(
url: localURL,
request: request,
playbackMode: "local-cache",
cachingMilliseconds: 500,
includeRemoteHTTPOptions: false
)
}
return
} catch {
#if DEBUG
print("[DreamioVLC] cache fallback reason=local-server-error-\(error)")
#endif
}
} else {
#if DEBUG
print("[DreamioVLC] cache fallback reason=\(probe.fallbackReason ?? "unknown")")
#endif
}
await MainActor.run {
self.startVLCMedia(
url: request.playbackURL,
request: request,
playbackMode: "direct",
cachingMilliseconds: 2500,
includeRemoteHTTPOptions: true
)
}
}
#else
onFailure?(NativePlaybackError.backendUnavailable)
#endif
70 unmodified lines
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
70 unmodified lines
lastLoggedState = nil
lastBufferingLogTime = nil
#if DEBUG
print("[DreamioVLC] cache fallback reason=startup-direct-preferred url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
#endif
startVLCMedia(
url: request.playbackURL,
request: request,
playbackMode: "direct",
cachingMilliseconds: 2500,
includeRemoteHTTPOptions: true
)
#else
onFailure?(NativePlaybackError.backendUnavailable)
#endif

Related issues or PRs

Beads issue: dreamio-dd7. This supersedes the blocking startup probe behavior from the earlier MKV range-cache experiment.

Validation