2026-05-26

VLC Seek Range Cache Priming

Dreamio now primes local VLC range-cache seeks from stable 1 MB boundaries behind the estimated target, retains useful nearby bytes under a bounded budget, and logs foreground misses with more precision.

Summary

Reduced post-seek buffering risk in native VLC playback by making seek and jump prefetch conservative behind the target instead of only racing ahead from the estimated byte offset.

Changes Made

Context

A 15-second skip estimated byte 213615760, while VLC’s first real read started at 212942432. The old prefetch overlapped the request but missed its front edge, then eviction could discard ranges that were still useful during the next VLC probes.

Important Implementation Details

Relevant Diff Snippets

Rendered with @pierre/diffs/ssr.

Dreamio/ProgressiveHTTPRangeCache.swift

Dreamio/ProgressiveHTTPRangeCache.swift
-15+174
72 unmodified lines
73
74
75
76
77
78
38 unmodified lines
117
118
119
120
121
122
12 unmodified lines
135
136
137
138
139
140
116 unmodified lines
257
258
259
260
261
262
263
264
265
266
267
268
269
270
2 unmodified lines
273
274
275
276
277
278
1 unmodified line
280
281
282
283
284
285
286
287
288
289
290
291
292
293
5 unmodified lines
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
1 unmodified line
330
331
332
333
334
335
37 unmodified lines
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
3 unmodified lines
391
392
393
394
395
396
397
3 unmodified lines
401
402
403
404
405
406
72 unmodified lines
}
}
func insert(data: Data, at start: Int64) {
guard !data.isEmpty else {
return
38 unmodified lines
data(for: range) != nil
}
func evict(keeping window: HTTPByteRange) {
lock.withLock {
segments = segments.compactMap { segment in
12 unmodified lines
}
}
private func mergeSegments() {
guard !segments.isEmpty else {
return
116 unmodified lines
let durationProvider: () -> TimeInterval
private let prefetchChunkSize: Int64 = 1_048_576
private let responseChunkSize: Int64 = 1_048_576
private var prefetchTask: Task<Void, Never>?
private var activePrefetchWindow: HTTPByteRange?
private var activePrefetchPreferredOffset: Int64?
init(fetcher: HTTPRangeRemoteFetcher, contentLength: Int64, durationProvider: @escaping () -> TimeInterval) {
self.fetcher = fetcher
self.contentLength = contentLength
self.durationProvider = durationProvider
}
deinit {
2 unmodified lines
func data(for requestedRange: HTTPByteRange) async throws -> Data {
let bounded = clamp(requestedRange)
if let data = store.data(for: bounded) {
#if DEBUG
print("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")
1 unmodified line
return data
}
#if DEBUG
print("[DreamioRangeCache] cache=miss range=\(bounded.start)-\(bounded.end)")
#endif
cancelPrefetchIfNeeded(forForegroundRange: bounded)
let data = try await fetcher.fetch(range: bounded)
store.insert(data: data, at: bounded.start)
prefetch(aroundByteOffset: bounded.end + 1, forceRestart: true)
return store.data(for: bounded) ?? data
}
func responseRange(for requestedRange: HTTPByteRange) -> HTTPByteRange {
5 unmodified lines
}
func prefetch(aroundByteOffset offset: Int64) {
prefetch(aroundByteOffset: offset, forceRestart: false)
}
func prefetch(aroundByteOffset offset: Int64, forceRestart: Bool) {
if !forceRestart, activePrefetchWindow?.contains(offset) == true, prefetchTask?.isCancelled == false {
return
}
prefetchTask?.cancel()
let window = targetWindow(aroundByteOffset: offset)
activePrefetchWindow = window
activePrefetchPreferredOffset = offset
store.evict(keeping: window)
guard !store.hasData(for: window) else {
activePrefetchWindow = nil
activePrefetchPreferredOffset = nil
return
}
prefetchTask = Task { [weak self] in
guard let self else {
return
}
for chunk in self.prefetchChunks(in: window, preferredOffset: offset) {
guard !Task.isCancelled else {
return
}
1 unmodified line
do {
let data = try await fetcher.fetch(range: chunk)
store.insert(data: data, at: chunk.start)
#if DEBUG
print("[DreamioRangeCache] fetched range=\(chunk.start)-\(chunk.end) bytes=\(data.count)")
#endif
37 unmodified lines
}
private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {
let bytesPerSecond = estimatedBytesPerSecond()
let behind = max(prefetchChunkSize, bytesPerSecond * 30)
let ahead = max(prefetchChunkSize * 2, bytesPerSecond * 60)
return clamp(HTTPByteRange(start: offset - behind, end: offset + ahead))
}
func prefetchChunks(in window: HTTPByteRange, preferredOffset offset: Int64) -> [HTTPByteRange] {
let boundedOffset = max(window.start, min(window.end, offset))
let preferredStart = window.start + ((boundedOffset - window.start) / prefetchChunkSize) * prefetchChunkSize
var chunks: [HTTPByteRange] = []
var cursor = preferredStart
3 unmodified lines
cursor = chunk.end + 1
}
cursor = window.start
while cursor < preferredStart {
let chunk = HTTPByteRange(start: cursor, end: min(preferredStart - 1, cursor + prefetchChunkSize - 1))
chunks.append(chunk)
3 unmodified lines
return chunks
}
private func estimatedBytesPerSecond() -> Int64 {
let duration = durationProvider()
guard duration > 1 else {
72 unmodified lines
73
74
75
76
77
78
79
80
81
82
83
84
38 unmodified lines
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
12 unmodified lines
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
116 unmodified lines
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
2 unmodified lines
371
372
373
374
375
376
377
1 unmodified line
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
5 unmodified lines
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
1 unmodified line
457
458
459
460
461
462
463
37 unmodified lines
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
3 unmodified lines
529
530
531
532
533
534
535
3 unmodified lines
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
72 unmodified lines
}
}
var cachedByteCount: Int64 {
lock.withLock {
segments.reduce(0) { $0 + Int64($1.data.count) }
}
}
func insert(data: Data, at start: Int64) {
guard !data.isEmpty else {
return
38 unmodified lines
data(for: range) != nil
}
func missingRanges(in range: HTTPByteRange) -> [HTTPByteRange] {
lock.withLock {
var cursor = range.start
var missing: [HTTPByteRange] = []
for segment in segments where segment.range.end >= cursor {
guard segment.range.start <= range.end else {
break
}
if segment.range.start > cursor {
missing.append(HTTPByteRange(start: cursor, end: min(range.end, segment.range.start - 1)))
}
if segment.range.end >= cursor {
cursor = max(cursor, segment.range.end + 1)
}
if cursor > range.end {
break
}
}
if cursor <= range.end {
missing.append(HTTPByteRange(start: cursor, end: range.end))
}
return missing
}
}
func evict(keeping window: HTTPByteRange) {
lock.withLock {
segments = segments.compactMap { segment in
12 unmodified lines
}
}
func evict(toByteBudget byteBudget: Int64, preserving protectedRanges: [HTTPByteRange]) -> [HTTPByteRange] {
lock.withLock {
guard byteBudget > 0 else {
let evicted = segments.map(\.range)
segments.removeAll()
return evicted
}
var totalBytes = segments.reduce(0) { $0 + Int64($1.data.count) }
guard totalBytes > byteBudget else {
return []
}
var evicted: [HTTPByteRange] = []
while totalBytes > byteBudget,
let index = evictionCandidateIndex(protectedRanges: protectedRanges) {
let removed = segments.remove(at: index)
totalBytes -= Int64(removed.data.count)
evicted.append(removed.range)
}
return evicted
}
}
private func evictionCandidateIndex(protectedRanges: [HTTPByteRange]) -> Int? {
var bestIndex: Int?
var bestDistance: Int64 = .min
for (index, segment) in segments.enumerated() {
if protectedRanges.contains(where: { $0.overlapsOrTouches(segment.range) }) {
continue
}
let distance = protectedRanges
.map { rangeDistance(from: segment.range, to: $0) }
.min() ?? segment.range.start
if distance > bestDistance {
bestDistance = distance
bestIndex = index
}
}
return bestIndex
}
private func rangeDistance(from range: HTTPByteRange, to protectedRange: HTTPByteRange) -> Int64 {
if range.overlapsOrTouches(protectedRange) {
return 0
}
if range.end < protectedRange.start {
return protectedRange.start - range.end
}
return range.start - protectedRange.end
}
private func mergeSegments() {
guard !segments.isEmpty else {
return
116 unmodified lines
let durationProvider: () -> TimeInterval
private let prefetchChunkSize: Int64 = 1_048_576
private let responseChunkSize: Int64 = 1_048_576
private let seekPrimeBehindBytes: Int64 = 4 * 1_048_576
private let cacheByteBudget: Int64
private var prefetchTask: Task<Void, Never>?
private var activePrefetchWindow: HTTPByteRange?
private var activePrefetchPreferredOffset: Int64?
private var recentSeekRange: HTTPByteRange?
private var recentForegroundRange: HTTPByteRange?
init(
fetcher: HTTPRangeRemoteFetcher,
contentLength: Int64,
durationProvider: @escaping () -> TimeInterval,
cacheByteBudget: Int64 = 64 * 1_048_576
) {
self.fetcher = fetcher
self.contentLength = contentLength
self.durationProvider = durationProvider
self.cacheByteBudget = cacheByteBudget
}
deinit {
2 unmodified lines
func data(for requestedRange: HTTPByteRange) async throws -> Data {
let bounded = clamp(requestedRange)
recentForegroundRange = bounded
if let data = store.data(for: bounded) {
#if DEBUG
print("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")
1 unmodified line
return data
}
let missingRanges = store.missingRanges(in: bounded)
#if DEBUG
let missKind = missingRanges.count == 1 && missingRanges[0] == bounded ? "uncached" : "partial-miss"
print("[DreamioRangeCache] cache=\(missKind) range=\(bounded.start)-\(bounded.end) missing=\(missingRanges.map { "\($0.start)-\($0.end)" }.joined(separator: ","))")
#endif
cancelPrefetchIfNeeded(forForegroundRange: bounded)
for missingRange in missingRanges {
let data = try await fetcher.fetch(range: missingRange)
store.insert(data: data, at: missingRange.start)
}
prefetch(aroundByteOffset: bounded.end + 1, forceRestart: true)
return store.data(for: bounded) ?? Data()
}
func responseRange(for requestedRange: HTTPByteRange) -> HTTPByteRange {
5 unmodified lines
}
func prefetch(aroundByteOffset offset: Int64) {
prefetch(aroundByteOffset: offset, forceRestart: false, startsAtWindowStart: false)
}
func prefetchForSeek(aroundByteOffset offset: Int64) {
let window = seekPrimeWindow(aroundByteOffset: offset)
recentSeekRange = window
prefetch(
aroundByteOffset: offset,
forceRestart: true,
explicitWindow: window,
startsAtWindowStart: true
)
}
func seekPrimeWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {
targetWindow(aroundByteOffset: offset, minimumBehind: seekPrimeBehindBytes)
}
func prefetch(aroundByteOffset offset: Int64, forceRestart: Bool) {
prefetch(aroundByteOffset: offset, forceRestart: forceRestart, startsAtWindowStart: false)
}
private func prefetch(
aroundByteOffset offset: Int64,
forceRestart: Bool,
explicitWindow: HTTPByteRange? = nil,
startsAtWindowStart: Bool
) {
if !forceRestart, activePrefetchWindow?.contains(offset) == true, prefetchTask?.isCancelled == false {
return
}
prefetchTask?.cancel()
let window = explicitWindow ?? targetWindow(aroundByteOffset: offset)
activePrefetchWindow = window
activePrefetchPreferredOffset = offset
evictOverBudget(protecting: window)
guard !store.hasData(for: window) else {
activePrefetchWindow = nil
activePrefetchPreferredOffset = nil
return
}
prefetchTask = Task(priority: startsAtWindowStart ? .userInitiated : .utility) { [weak self] in
guard let self else {
return
}
for chunk in self.prefetchChunks(in: window, preferredOffset: offset, startsAtWindowStart: startsAtWindowStart) {
guard !Task.isCancelled else {
return
}
1 unmodified line
do {
let data = try await fetcher.fetch(range: chunk)
store.insert(data: data, at: chunk.start)
evictOverBudget(protecting: window)
#if DEBUG
print("[DreamioRangeCache] fetched range=\(chunk.start)-\(chunk.end) bytes=\(data.count)")
#endif
37 unmodified lines
}
private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {
targetWindow(aroundByteOffset: offset, minimumBehind: prefetchChunkSize)
}
private func targetWindow(aroundByteOffset offset: Int64, minimumBehind: Int64) -> HTTPByteRange {
let bytesPerSecond = estimatedBytesPerSecond()
let behind = max(minimumBehind, bytesPerSecond * 30)
let ahead = max(prefetchChunkSize * 2, bytesPerSecond * 60)
let raw = clamp(HTTPByteRange(start: offset - behind, end: offset + ahead))
return HTTPByteRange(start: alignedChunkStart(for: raw.start), end: alignedChunkEnd(for: raw.end))
}
func prefetchChunks(in window: HTTPByteRange, preferredOffset offset: Int64) -> [HTTPByteRange] {
prefetchChunks(in: window, preferredOffset: offset, startsAtWindowStart: false)
}
func prefetchChunks(in window: HTTPByteRange, preferredOffset offset: Int64, startsAtWindowStart: Bool) -> [HTTPByteRange] {
let boundedOffset = max(window.start, min(window.end, offset))
let windowStart = alignedChunkStart(for: window.start)
let preferredStart = startsAtWindowStart ? windowStart : alignedChunkStart(for: boundedOffset)
var chunks: [HTTPByteRange] = []
var cursor = preferredStart
3 unmodified lines
cursor = chunk.end + 1
}
cursor = windowStart
while cursor < preferredStart {
let chunk = HTTPByteRange(start: cursor, end: min(preferredStart - 1, cursor + prefetchChunkSize - 1))
chunks.append(chunk)
3 unmodified lines
return chunks
}
private func evictOverBudget(protecting range: HTTPByteRange) {
let headerRange = HTTPByteRange(start: 0, end: min(contentLength - 1, prefetchChunkSize - 1))
let tailStart = max(0, contentLength - (4 * prefetchChunkSize))
let tailRange = HTTPByteRange(start: tailStart, end: contentLength - 1)
let protectedRanges = [range, recentSeekRange, recentForegroundRange, activePrefetchWindow, headerRange, tailRange].compactMap { $0 }
let evicted = store.evict(toByteBudget: cacheByteBudget, preserving: protectedRanges)
#if DEBUG
if !evicted.isEmpty {
print("[DreamioRangeCache] evicted reason=budget ranges=\(evicted.map { "\($0.start)-\($0.end)" }.joined(separator: ",")) protected=\(protectedRanges.map { "\($0.start)-\($0.end)" }.joined(separator: ","))")
}
#endif
}
private func alignedChunkStart(for offset: Int64) -> Int64 {
max(0, (offset / prefetchChunkSize) * prefetchChunkSize)
}
private func alignedChunkEnd(for offset: Int64) -> Int64 {
min(contentLength - 1, alignedChunkStart(for: offset) + prefetchChunkSize - 1)
}
private func estimatedBytesPerSecond() -> Int64 {
let duration = durationProvider()
guard duration > 1 else {

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
-2+9
150 unmodified lines
151
152
153
154
155
156
157
13 unmodified lines
171
172
173
174
175
176
177
426 unmodified lines
604
605
606
607
608
609
150 unmodified lines
return
}
let clamped = max(0, min(1, position))
rangeCacheSession?.prefetch(aroundByteOffset: rangeCacheSession?.byteOffset(for: clamped) ?? 0)
#if DEBUG
if let byteOffset = rangeCacheSession?.byteOffset(for: clamped) {
print("[DreamioVLC] seek targetPosition=\(clamped) byteOffset=\(byteOffset) mode=local-cache")
13 unmodified lines
let nextTime = max(0, min(duration, currentTime + seconds))
if duration > 0 {
let nextPosition = Float(nextTime / duration)
rangeCacheSession?.prefetch(aroundByteOffset: rangeCacheSession?.byteOffset(for: nextPosition) ?? 0)
#if DEBUG
if let byteOffset = rangeCacheSession?.byteOffset(for: nextPosition) {
print("[DreamioVLC] jump seconds=\(seconds) target=\(nextTime) byteOffset=\(byteOffset) mode=local-cache")
426 unmodified lines
#if canImport(MobileVLCKit)
extension VLCNativePlaybackBackend: VLCMediaPlayerDelegate {
func mediaPlayerStateChanged(_ aNotification: Notification) {
#if DEBUG
logPlaybackStateIfNeeded(stateName(mediaPlayer.state))
#endif
150 unmodified lines
151
152
153
154
155
156
157
13 unmodified lines
171
172
173
174
175
176
177
426 unmodified lines
604
605
606
607
608
609
610
611
612
613
614
615
616
150 unmodified lines
return
}
let clamped = max(0, min(1, position))
rangeCacheSession?.prefetchForSeek(aroundByteOffset: rangeCacheSession?.byteOffset(for: clamped) ?? 0)
#if DEBUG
if let byteOffset = rangeCacheSession?.byteOffset(for: clamped) {
print("[DreamioVLC] seek targetPosition=\(clamped) byteOffset=\(byteOffset) mode=local-cache")
13 unmodified lines
let nextTime = max(0, min(duration, currentTime + seconds))
if duration > 0 {
let nextPosition = Float(nextTime / duration)
rangeCacheSession?.prefetchForSeek(aroundByteOffset: rangeCacheSession?.byteOffset(for: nextPosition) ?? 0)
#if DEBUG
if let byteOffset = rangeCacheSession?.byteOffset(for: nextPosition) {
print("[DreamioVLC] jump seconds=\(seconds) target=\(nextTime) byteOffset=\(byteOffset) mode=local-cache")
426 unmodified lines
#if canImport(MobileVLCKit)
extension VLCNativePlaybackBackend: VLCMediaPlayerDelegate {
func mediaPlayerStateChanged(_ aNotification: Notification) {
Task { @MainActor [weak self] in
self?.handleMediaPlayerStateChanged()
}
}
@MainActor
private func handleMediaPlayerStateChanged() {
#if DEBUG
logPlaybackStateIfNeeded(stateName(mediaPlayer.state))
#endif

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
-2+71
34 unmodified lines
35
36
37
38
39
40
41
42
291 unmodified lines
334
335
336
337
338
339
28 unmodified lines
368
369
370
371
372
373
32 unmodified lines
406
407
408
409
410
411
412
413
34 unmodified lines
testSparseRangeStoreHitPartialHitAndMiss()
testSparseRangeStoreEvictsOutsideWindow()
testSparseRangeStoreTrimsOverlappingWindow()
testRangeCacheSessionCapsResponseRange()
testRangeCachePrefetchPrioritizesSeekOffset()
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
291 unmodified lines
assert(store.data(for: HTTPByteRange(start: 0, end: 5)) == nil, "Expected trimmed bytes outside the window to be evicted")
}
private static func testRangeCacheSessionCapsResponseRange() {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]),
28 unmodified lines
])
}
private static func testRangeCacheForegroundMissReprioritizesPrefetch() async {
let queue = DispatchQueue(label: "dreamio.range-cache-test")
var requestedRanges: [String] = []
32 unmodified lines
let ranges = queue.sync { requestedRanges }
assert(ranges.contains("bytes=51818977-52867552"), "Expected foreground VLC range to be fetched")
assert(ranges.contains { range in
range.hasPrefix("bytes=51936225-")
}, "Expected prefetch to restart near VLC's foreground range, got \(ranges)")
session.cancelPrefetch()
MockURLProtocol.handler = nil
try? await Task.sleep(nanoseconds: 50_000_000)
34 unmodified lines
35
36
37
38
39
40
41
42
43
44
45
291 unmodified lines
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
28 unmodified lines
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
32 unmodified lines
475
476
477
478
479
480
481
482
34 unmodified lines
testSparseRangeStoreHitPartialHitAndMiss()
testSparseRangeStoreEvictsOutsideWindow()
testSparseRangeStoreTrimsOverlappingWindow()
testSparseRangeStoreEvictsByBudgetWhilePreservingUsefulRanges()
testRangeCacheSessionCapsResponseRange()
testRangeCachePrefetchPrioritizesSeekOffset()
testRangeCacheSeekPrimingIncludesObservedVLCStart()
testRangeCachePrefetchUsesGlobalChunkBoundaries()
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
291 unmodified lines
assert(store.data(for: HTTPByteRange(start: 0, end: 5)) == nil, "Expected trimmed bytes outside the window to be evicted")
}
private static func testSparseRangeStoreEvictsByBudgetWhilePreservingUsefulRanges() {
let store = SparseHTTPByteRangeStore()
store.insert(data: Data(repeating: 1, count: 4), at: 0)
store.insert(data: Data(repeating: 2, count: 4), at: 100)
store.insert(data: Data(repeating: 3, count: 4), at: 200)
let evicted = store.evict(
toByteBudget: 8,
preserving: [
HTTPByteRange(start: 0, end: 3),
HTTPByteRange(start: 190, end: 210)
]
)
assertEqual(evicted, [HTTPByteRange(start: 100, end: 103)])
assertEqual(store.cachedRanges, [
HTTPByteRange(start: 0, end: 3),
HTTPByteRange(start: 200, end: 203)
])
}
private static func testRangeCacheSessionCapsResponseRange() {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]),
28 unmodified lines
])
}
private static func testRangeCacheSeekPrimingIncludesObservedVLCStart() {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]),
contentLength: 711_080_522,
durationProvider: { 0 }
)
let estimatedOffset: Int64 = 213_615_760
let firstVLCRequest = HTTPByteRange(start: 212_942_432, end: 213_991_007)
let window = session.seekPrimeWindow(aroundByteOffset: estimatedOffset)
let chunks = session.prefetchChunks(
in: window,
preferredOffset: estimatedOffset,
startsAtWindowStart: true
)
let chunkContainingVLCStart = chunks.firstIndex { $0.contains(firstVLCRequest.start) }
let chunkContainingEstimatedOffset = chunks.firstIndex { $0.contains(estimatedOffset) }
assert(chunkContainingVLCStart != nil, "Expected seek priming to include VLC's first request start")
assert(chunkContainingEstimatedOffset != nil, "Expected seek priming to include the estimated offset")
assert(
chunkContainingVLCStart! <= chunkContainingEstimatedOffset!,
"Expected bytes behind the seek target to be primed before ahead chunks"
)
assertEqual(chunks[chunkContainingVLCStart!], HTTPByteRange(start: 212_860_928, end: 213_909_503))
}
private static func testRangeCachePrefetchUsesGlobalChunkBoundaries() {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]),
contentLength: 711_080_522,
durationProvider: { 0 }
)
let chunks = session.prefetchChunks(
in: HTTPByteRange(start: 213_278_260, end: 216_000_000),
preferredOffset: 213_615_760
)
assert(chunks.allSatisfy { $0.start % 1_048_576 == 0 }, "Expected prefetch chunk starts to use stable global 1 MB boundaries: \(chunks)")
assertEqual(chunks[0], HTTPByteRange(start: 212_860_928, end: 213_909_503))
}
private static func testRangeCacheForegroundMissReprioritizesPrefetch() async {
let queue = DispatchQueue(label: "dreamio.range-cache-test")
var requestedRanges: [String] = []
32 unmodified lines
let ranges = queue.sync { requestedRanges }
assert(ranges.contains("bytes=51818977-52867552"), "Expected foreground VLC range to be fetched")
assert(ranges.contains { range in
range.hasPrefix("bytes=52428800-")
}, "Expected prefetch to restart on a global chunk boundary near VLC's foreground range, got \(ranges)")
session.cancelPrefetch()
MockURLProtocol.handler = nil
try? await Task.sleep(nanoseconds: 50_000_000)

Expected Impact for End-Users

Short backward or forward skips in MKV/direct-file playback should resume with fewer full foreground misses, especially when VLC asks slightly before Dreamio’s estimated byte position.

Validation

Issues, Limitations, and Mitigations

Follow-up Work

New Changes as of 2026-05-26 00:40 EDT

Summary of changes

Adjusted the cache to follow VLC's actual foreground read area even when those reads are hits, and changed foreground partial misses to fetch stable aligned chunks synchronously.

Why this change was made

Device logs showed a backward jump estimating byte 15936567, while VLC continued reading from about 27165812 through the warmed cache. Because those reads were hits, prefetch stayed near the estimate until VLC reached the cache edge and began partial misses.

Code diffs

Dreamio/ProgressiveHTTPRangeCache.swift

Dreamio/ProgressiveHTTPRangeCache.swift
-2+32
375 unmodified lines
376
377
378
379
380
381
4 unmodified lines
386
387
388
389
390
391
392
393
106 unmodified lines
500
501
502
503
504
505
33 unmodified lines
539
540
541
542
543
544
375 unmodified lines
#if DEBUG
print("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")
#endif
return data
}
4 unmodified lines
#endif
cancelPrefetchIfNeeded(forForegroundRange: bounded)
for missingRange in missingRanges {
let data = try await fetcher.fetch(range: missingRange)
store.insert(data: data, at: missingRange.start)
}
prefetch(aroundByteOffset: bounded.end + 1, forceRestart: true)
return store.data(for: bounded) ?? Data()
106 unmodified lines
cancelPrefetch()
}
private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {
targetWindow(aroundByteOffset: offset, minimumBehind: prefetchChunkSize)
}
33 unmodified lines
return chunks
}
private func evictOverBudget(protecting range: HTTPByteRange) {
let headerRange = HTTPByteRange(start: 0, end: min(contentLength - 1, prefetchChunkSize - 1))
let tailStart = max(0, contentLength - (4 * prefetchChunkSize))
375 unmodified lines
376
377
378
379
380
381
382
4 unmodified lines
387
388
389
390
391
392
393
394
395
396
397
398
399
106 unmodified lines
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
33 unmodified lines
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
375 unmodified lines
#if DEBUG
print("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")
#endif
prefetchAheadIfForegroundMoved(to: bounded)
return data
}
4 unmodified lines
#endif
cancelPrefetchIfNeeded(forForegroundRange: bounded)
for missingRange in missingRanges {
for fetchRange in alignedChunks(covering: missingRange) where !store.hasData(for: fetchRange) {
let data = try await fetcher.fetch(range: fetchRange)
store.insert(data: data, at: fetchRange.start)
#if DEBUG
print("[DreamioRangeCache] foreground fetched range=\(fetchRange.start)-\(fetchRange.end) bytes=\(data.count)")
#endif
}
}
prefetch(aroundByteOffset: bounded.end + 1, forceRestart: true)
return store.data(for: bounded) ?? Data()
106 unmodified lines
cancelPrefetch()
}
private func prefetchAheadIfForegroundMoved(to range: HTTPByteRange) {
guard activePrefetchWindow?.contains(range.start) == true,
let preferredOffset = activePrefetchPreferredOffset,
abs(range.start - preferredOffset) >= responseChunkSize else {
return
}
#if DEBUG
print("[DreamioRangeCache] prefetch follow-foreground from=\(preferredOffset) to=\(range.end + 1)")
#endif
prefetch(aroundByteOffset: range.end + 1, forceRestart: true)
}
private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {
targetWindow(aroundByteOffset: offset, minimumBehind: prefetchChunkSize)
}
33 unmodified lines
return chunks
}
func alignedChunks(covering range: HTTPByteRange) -> [HTTPByteRange] {
let bounded = clamp(range)
var chunks: [HTTPByteRange] = []
var cursor = alignedChunkStart(for: bounded.start)
while cursor <= bounded.end {
let chunk = HTTPByteRange(start: cursor, end: min(contentLength - 1, cursor + prefetchChunkSize - 1))
chunks.append(chunk)
cursor = chunk.end + 1
}
return chunks
}
private func evictOverBudget(protecting range: HTTPByteRange) {
let headerRange = HTTPByteRange(start: 0, end: min(contentLength - 1, prefetchChunkSize - 1))
let tailStart = max(0, contentLength - (4 * prefetchChunkSize))

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
-1+59
39 unmodified lines
40
41
42
43
44
45
46
390 unmodified lines
437
438
439
440
441
442
30 unmodified lines
473
474
475
476
477
478
479
2 unmodified lines
482
483
484
485
486
487
39 unmodified lines
testRangeCachePrefetchPrioritizesSeekOffset()
testRangeCacheSeekPrimingIncludesObservedVLCStart()
testRangeCachePrefetchUsesGlobalChunkBoundaries()
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed")
390 unmodified lines
assertEqual(chunks[0], HTTPByteRange(start: 212_860_928, end: 213_909_503))
}
private static func testRangeCacheForegroundMissReprioritizesPrefetch() async {
let queue = DispatchQueue(label: "dreamio.range-cache-test")
var requestedRanges: [String] = []
30 unmodified lines
try? await Task.sleep(nanoseconds: 50_000_000)
let ranges = queue.sync { requestedRanges }
assert(ranges.contains("bytes=51818977-52867552"), "Expected foreground VLC range to be fetched")
assert(ranges.contains { range in
range.hasPrefix("bytes=52428800-")
}, "Expected prefetch to restart on a global chunk boundary near VLC's foreground range, got \(ranges)")
2 unmodified lines
try? await Task.sleep(nanoseconds: 50_000_000)
}
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)
39 unmodified lines
40
41
42
43
44
45
46
47
48
390 unmodified lines
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
30 unmodified lines
487
488
489
490
491
492
493
2 unmodified lines
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
39 unmodified lines
testRangeCachePrefetchPrioritizesSeekOffset()
testRangeCacheSeekPrimingIncludesObservedVLCStart()
testRangeCachePrefetchUsesGlobalChunkBoundaries()
testRangeCacheForegroundMissFetchesAlignedChunks()
await testRangeCacheForegroundMissReprioritizesPrefetch()
await testRangeCacheHitFollowsActualPostSeekReadArea()
await testRangeProbeFallsBackWhenServerIgnoresRange()
await testRangeFetcherPreservesHeaders()
print("StreamResolverTests passed")
390 unmodified lines
assertEqual(chunks[0], HTTPByteRange(start: 212_860_928, end: 213_909_503))
}
private static func testRangeCacheForegroundMissFetchesAlignedChunks() {
let session = ProgressiveHTTPRangeCacheSession(
fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]),
contentLength: 711_080_522,
durationProvider: { 0 }
)
let chunks = session.alignedChunks(covering: HTTPByteRange(start: 48_234_649, end: 49_185_907))
assertEqual(chunks, [HTTPByteRange(start: 48_234_496, end: 49_283_071)])
}
private static func testRangeCacheForegroundMissReprioritizesPrefetch() async {
let queue = DispatchQueue(label: "dreamio.range-cache-test")
var requestedRanges: [String] = []
30 unmodified lines
try? await Task.sleep(nanoseconds: 50_000_000)
let ranges = queue.sync { requestedRanges }
assert(ranges.contains("bytes=51380224-52428799"), "Expected foreground VLC miss to fetch aligned cache chunks")
assert(ranges.contains { range in
range.hasPrefix("bytes=52428800-")
}, "Expected prefetch to restart on a global chunk boundary near VLC's foreground range, got \(ranges)")
2 unmodified lines
try? await Task.sleep(nanoseconds: 50_000_000)
}
private static func testRangeCacheHitFollowsActualPostSeekReadArea() async {
let queue = DispatchQueue(label: "dreamio.range-cache-hit-follow-test")
var requestedRanges: [String] = []
MockURLProtocol.handler = { request in
let range = request.value(forHTTPHeaderField: "Range") ?? ""
queue.sync {
requestedRanges.append(range)
}
let byteRange = byteRange(fromHeader: range, contentLength: 80_000_000)
let response = HTTPURLResponse(
url: request.url!,
statusCode: 206,
httpVersion: nil,
headerFields: ["Content-Range": "bytes \(byteRange.start)-\(byteRange.end)/80000000"]
)!
return (Data(repeating: 1, count: Int(byteRange.length)), response)
}
let session = ProgressiveHTTPRangeCacheSession(
fetcher: HTTPRangeRemoteFetcher(
url: URL(string: "https://cdn.example.test/movie.mp4")!,
headers: [:],
session: mockSession()
),
contentLength: 80_000_000,
durationProvider: { 100 }
)
defer {
session.cancelPrefetch()
}
session.store.insert(data: Data(repeating: 7, count: 1_048_576), at: 27_165_812)
session.prefetchForSeek(aroundByteOffset: 15_936_567)
_ = try? await session.data(for: HTTPByteRange(start: 27_165_812, end: 28_214_387))
try? await Task.sleep(nanoseconds: 100_000_000)
let ranges = queue.sync { requestedRanges }
assert(ranges.contains { range in
range.hasPrefix("bytes=27262976-")
}, "Expected a cache hit far from the seek estimate to restart prefetch near VLC's real read area, got \(ranges)")
MockURLProtocol.handler = nil
try? await Task.sleep(nanoseconds: 50_000_000)
}
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-mi1.

New Changes as of May 26, 2026 at 10:52 AM EDT

Summary of changes

Updated the local HTTP range cache prefetch bookkeeping after reviewing fresh device logs from normal playback and a warmed-cache 15 second skip. Normal cold-cache misses are expected, but the warmed-cache skip showed the foreground read moving behind the active prefetch cursor while older prefetch state could still finish and mutate session state.

Why this change was made

The cache should follow the bytes VLC is actually reading after a skip, not keep stale prefetch state alive after a newer foreground read has reoriented the cache. This update makes canceled prefetch workers stop before inserting fetched bytes after cancellation and prevents stale workers from clearing newer active prefetch state.

Code diffs

Rendered with @pierre/diffs/ssr.

Dreamio/ProgressiveHTTPRangeCache.swift

Dreamio/ProgressiveHTTPRangeCache.swift
-2+14
349 unmodified lines
350
351
352
353
354
355
85 unmodified lines
441
442
443
444
445
446
15 unmodified lines
462
463
464
465
466
467
10 unmodified lines
478
479
480
481
482
483
1 unmodified line
485
486
487
488
489
490
21 unmodified lines
512
513
514
515
516
517
518
519
520
521
349 unmodified lines
private var prefetchTask: Task<Void, Never>?
private var activePrefetchWindow: HTTPByteRange?
private var activePrefetchPreferredOffset: Int64?
private var recentSeekRange: HTTPByteRange?
private var recentForegroundRange: HTTPByteRange?
85 unmodified lines
}
prefetchTask?.cancel()
let window = explicitWindow ?? targetWindow(aroundByteOffset: offset)
activePrefetchWindow = window
activePrefetchPreferredOffset = offset
15 unmodified lines
if !store.hasData(for: chunk) {
do {
let data = try await fetcher.fetch(range: chunk)
store.insert(data: data, at: chunk.start)
evictOverBudget(protecting: window)
#if DEBUG
10 unmodified lines
}
}
}
self.activePrefetchWindow = nil
self.activePrefetchPreferredOffset = nil
}
1 unmodified line
func cancelPrefetch() {
prefetchTask?.cancel()
activePrefetchWindow = nil
activePrefetchPreferredOffset = nil
}
21 unmodified lines
abs(range.start - preferredOffset) >= responseChunkSize else {
return
}
#if DEBUG
print("[DreamioRangeCache] prefetch follow-foreground from=\(preferredOffset) to=\(range.end + 1)")
#endif
prefetch(aroundByteOffset: range.end + 1, forceRestart: true)
}
private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {
349 unmodified lines
350
351
352
353
354
355
356
85 unmodified lines
442
443
444
445
446
447
448
449
15 unmodified lines
465
466
467
468
469
470
471
472
473
10 unmodified lines
484
485
486
487
488
489
490
491
492
1 unmodified line
494
495
496
497
498
499
500
21 unmodified lines
522
523
524
525
526
527
528
529
530
531
532
533
349 unmodified lines
private var prefetchTask: Task<Void, Never>?
private var activePrefetchWindow: HTTPByteRange?
private var activePrefetchPreferredOffset: Int64?
private var prefetchGeneration: UInt64 = 0
private var recentSeekRange: HTTPByteRange?
private var recentForegroundRange: HTTPByteRange?
85 unmodified lines
}
prefetchTask?.cancel()
prefetchGeneration += 1
let generation = prefetchGeneration
let window = explicitWindow ?? targetWindow(aroundByteOffset: offset)
activePrefetchWindow = window
activePrefetchPreferredOffset = offset
15 unmodified lines
if !store.hasData(for: chunk) {
do {
let data = try await fetcher.fetch(range: chunk)
guard !Task.isCancelled else {
return
}
store.insert(data: data, at: chunk.start)
evictOverBudget(protecting: window)
#if DEBUG
10 unmodified lines
}
}
}
guard self.prefetchGeneration == generation else {
return
}
self.activePrefetchWindow = nil
self.activePrefetchPreferredOffset = nil
}
1 unmodified line
func cancelPrefetch() {
prefetchTask?.cancel()
prefetchGeneration += 1
activePrefetchWindow = nil
activePrefetchPreferredOffset = nil
}
21 unmodified lines
abs(range.start - preferredOffset) >= responseChunkSize else {
return
}
let nextOffset = range.end + 1
#if DEBUG
let reason = nextOffset < preferredOffset ? "reanchor-foreground" : "follow-foreground"
print("[DreamioRangeCache] prefetch \(reason) from=\(preferredOffset) to=\(nextOffset)")
#endif
prefetch(aroundByteOffset: nextOffset, forceRestart: true)
}
private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {

Related issues or PRs

Beads issue dreamio-2hw.