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
- Added
ProgressiveHTTPRangeCache.swiftwith range parsing, sparse cached byte storage, remote range fetching, prefetch window logic, and a small loopback HTTP server. - Updated
VLCNativePlaybackBackendto probe before playback, choose local-cache vs direct mode, apply separate VLC caching options, and reprioritize prefetching on seek/jump. - Preserved upstream request headers in remote range fetches, including user agent, referrer, cookies, and custom auth headers.
- Added diagnostics for cache mode, probe fallback reasons, seek byte estimates, cache hits/misses, fetched ranges, and throttled repeated buffering logs.
- Added range/cache unit coverage and URLProtocol-backed fetcher tests.
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
- The cache uses sparse byte ranges and merges overlapping or adjacent segments rather than downloading whole files.
- Probe logic uses
HEADfirst, then a tinyRange: bytes=0-0request if needed. - The local server responds to VLC range requests with
206 Partial Contentand fills misses from the upstream URL. - Prefetch targets roughly 30 seconds behind and 60 seconds ahead when duration is known; otherwise it uses fixed byte heuristics.
- Direct fallback uses a larger
network-cachingvalue andhttp-reconnect; loopback playback uses lower caching because Dreamio is buffering locally.
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
12345678910111213141516171819202122232425struct 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
51 unmodified lines48495034 unmodified lines9951 unmodified lineslet media = VLCMedia(url: request.playbackURL)mediaPlayer.media = mediamediaPlayer.play()34 unmodified linesmediaPlayer.position = max(0, min(1, position))51 unmodified lines5253545556575859606162636465666768697071727334 unmodified lines12412512612751 unmodified linesplaybackStartupTask = Task { [weak self] inlet 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 = sessionsession.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 lineslet 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
23 unmodified lines237 unmodified lines23 unmodified lines237 unmodified lines23 unmodified lines242526272829237 unmodified lines27327427527627727827928028123 unmodified linestestContentRangeParsing()testSparseRangeStoreMergesOverlaps()testSparseRangeStoreHitPartialHitAndMiss()testSparseRangeStoreEvictsOutsideWindow()await testRangeProbeFallsBackWhenServerIgnoresRange()await testRangeFetcherPreservesHeaders()237 unmodified linesprivate static func testRangeFetcherPreservesHeaders() async {MockURLProtocol.handler = { request inassertEqual(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
14 unmodified lines71 unmodified lines140 unmodified lines14 unmodified lines71 unmodified lines140 unmodified lines14 unmodified lines1571 unmodified lines93140 unmodified lines24014 unmodified lines6F2A2B522C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2A2B532C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift */; };71 unmodified lines6F2A2B532C00100100DREAMIO /* ProgressiveHTTPRangeCache.swift */,140 unmodified lines6F2A2B522C00100100DREAMIO /* 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
xcrun swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Dreamio/ProgressiveHTTPRangeCache.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTestspassed.pod installrestored missing CocoaPods support files for the worktree.xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' buildpassed.
Issues, Limitations, and Mitigations
- Manual device validation against the original problematic stream is still needed because local playback quality depends on real server range behavior.
- The first version intentionally does not cache HLS/live streams; those remain on direct VLC playback because their model is playlist/segment based.
- The loopback server is intentionally small and per-process. If future playback needs multiple concurrent videos, session lifecycle cleanup should be tightened further.
Follow-up Work
- Run the manual 15-second seek validation on device with the stream that produced the buffering logs.
- Add an end-to-end local server integration test that opens the loopback URL and verifies repeated range reuse through the full server path.
- Consider exposing cache counters in a debug overlay if native playback diagnostics continue to be a focus.