VLC Seeking With a Local Range Cache

Dreamio now probes file-like HTTP streams for byte-range support and, when safe, feeds MobileVLCKit through a loopback URL backed by a sparse progressive cache.

Summary

Implemented a Dreamio-owned progressive HTTP range cache for native VLC playback. Cacheable HTTP/HTTPS streams are served to VLC from 127.0.0.1, while HLS, live, non-HTTP, unknown-length, and non-range sources stay on direct MobileVLCKit playback.

Changes Made

Context

MobileVLCKit exposes coarse input caching knobs, but not a precise “keep nearby bytes around the playhead” buffer. This change puts Dreamio in charge of the byte window for regular file-like streams and leaves segment/playlist media on VLC’s normal path.

Important Implementation Details

Relevant Diff Snippets

Rendered with @pierre/diffs/ssr. These are selected snippets, not the full patch, to keep the turn note readable.

Dreamio/ProgressiveHTTPRangeCache.swift

Dreamio/ProgressiveHTTPRangeCache.swift
+25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct HTTPContentRange: Equatable {
let range: HTTPByteRange
let totalLength: Int64?
static func parse(_ value: String) -> HTTPContentRange? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.lowercased().hasPrefix("bytes ") else {
return nil
}
...
}
}
final class ProgressiveHTTPRangeCacheSession {
func data(for requestedRange: HTTPByteRange) async throws -> Data {
if let data = store.data(for: bounded) {
print("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")
return data
}
let data = try await fetcher.fetch(range: bounded)
store.insert(data: data, at: bounded.start)
prefetch(aroundByteOffset: bounded.end + 1)
return store.data(for: bounded) ?? data
}
}

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
-4+26
51 unmodified lines
48
49
50
34 unmodified lines
99
51 unmodified lines
let media = VLCMedia(url: request.playbackURL)
mediaPlayer.media = media
mediaPlayer.play()
34 unmodified lines
mediaPlayer.position = max(0, min(1, position))
51 unmodified lines
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
34 unmodified lines
124
125
126
127
51 unmodified lines
playbackStartupTask = Task { [weak self] in
let fetcher = HTTPRangeRemoteFetcher(url: request.playbackURL, headers: request.headers)
let probe = await fetcher.probe()
if probe.isCacheable, let contentLength = probe.contentLength, contentLength > 0 {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: fetcher,
contentLength: contentLength,
durationProvider: { [weak self] in self?.duration ?? 0 }
)
let localURL = try 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
}
await MainActor.run {
self?.startVLCMedia(url: request.playbackURL, request: request, playbackMode: "direct", cachingMilliseconds: 2500, includeRemoteHTTPOptions: true)
}
}
34 unmodified lines
let clamped = max(0, min(1, position))
rangeCacheSession?.prefetch(aroundByteOffset: rangeCacheSession?.byteOffset(for: clamped) ?? 0)
print("[DreamioVLC] seek targetPosition=\(clamped)")
mediaPlayer.position = clamped

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+15
23 unmodified lines
237 unmodified lines
23 unmodified lines
237 unmodified lines
23 unmodified lines
24
25
26
27
28
29
237 unmodified lines
273
274
275
276
277
278
279
280
281
23 unmodified lines
testContentRangeParsing()
testSparseRangeStoreMergesOverlaps()
testSparseRangeStoreHitPartialHitAndMiss()
testSparseRangeStoreEvictsOutsideWindow()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
237 unmodified lines
private static func testRangeFetcherPreservesHeaders() async {
MockURLProtocol.handler = { request in
assertEqual(request.value(forHTTPHeaderField: "User-Agent"), "DreamioTest/1")
assertEqual(request.value(forHTTPHeaderField: "Referer"), "https://web.stremio.com/")
assertEqual(request.value(forHTTPHeaderField: "Cookie"), "session=abc")
assertEqual(request.value(forHTTPHeaderField: "Range"), "bytes=5-7")
return (Data([5, 6, 7]), HTTPURLResponse(url: request.url!, statusCode: 206, httpVersion: nil, headerFields: nil)!)
}
}

Dreamio.xcodeproj/project.pbxproj

Dreamio.xcodeproj/project.pbxproj
+3
14 unmodified lines
71 unmodified lines
140 unmodified lines
14 unmodified lines
71 unmodified lines
140 unmodified lines
14 unmodified lines
15
71 unmodified lines
93
140 unmodified lines
240
14 unmodified lines
6F2A2B522C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2A2B532C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift */; };
71 unmodified lines
6F2A2B532C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift */,
140 unmodified lines
6F2A2B522C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift in Sources */,

Expected Impact for End-Users

Nearby seeks on cacheable MP4/MKV/AVI/WebM-style HTTP streams should recover faster because VLC reads from a local range-aware endpoint backed by targeted upstream fills. Unsupported sources should continue playing through the direct MobileVLCKit path.

Validation

Issues, Limitations, and Mitigations

Follow-up Work

New Changes as of 2026-05-25 18:32 EDT

Summary of changes

Fixed the loopback cache server startup so Dreamio waits for Network.framework to report the real assigned port before giving VLC the local playback URL.

Why this change was made

Device logs showed VLC opening http://127.0.0.1:0/stream/.... Port 0 is only the ephemeral-port request placeholder, not a usable listening port, so VLC immediately failed playback even though the upstream range cache had started fetching data.

Code diffs

ProgressiveHTTPRangeCacheServer.localURL(for:) is now async.
 let assignedPort = try await startIfNeeded()
 URL(string: "http://127.0.0.1:\(assignedPort)/stream/\(session.id)")

 listener.stateUpdateHandler waits for .ready
 port = UInt16(listener.port.rawValue)

 startup continuations resume only after the real port is available.

Related issues or PRs

Related to Beads issue dreamio-11s and branch lavender/vlc-local-range-cache.