diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index f1377d2..afa3c8c 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -46,3 +46,4 @@ {"id":"int-176e3ad2","kind":"field_change","created_at":"2026-05-26T04:14:19.812849Z","actor":"dirtydishes","issue_id":"dreamio-meh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed range-cache prefetch reprioritization so foreground VLC misses cancel stale speculative work and restart around VLC's actual requested byte range; added regression coverage for the observed jump mismatch."}} {"id":"int-56a87fde","kind":"field_change","created_at":"2026-05-26T04:35:35.693504Z","actor":"dirtydishes","issue_id":"dreamio-3pn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented backward-biased seek priming, global 1 MB range-cache chunk alignment, bounded protected eviction, partial foreground miss fetching/logging, main-actor VLC delegate handling, tests, and turn documentation."}} {"id":"int-91b3db21","kind":"field_change","created_at":"2026-05-26T04:40:10.299245Z","actor":"dirtydishes","issue_id":"dreamio-mi1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Used actual foreground VLC reads as prefetch follow signals on hits and changed foreground misses to fetch aligned chunks; added regression tests and updated the turn document."}} +{"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."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 45b452b..4249ce3 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,5 +1,6 @@ {"_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-2hw","title":"Fix range cache prefetch cursor after cached seek reads","description":"Skipping after the local range cache has warmed can leave prefetch following an older foreground cursor instead of the post-seek cached read position. Update the cache so cached foreground reads can reset the follow cursor and add regression coverage.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:45:44Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:47:44Z","started_at":"2026-05-26T04:46:36Z","closed_at":"2026-05-26T04:47:44Z","close_reason":"Fixed stale local range-cache prefetch state after cached seek reads and documented the validation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-mi1","title":"adapt vlc prefetch to actual post-seek reads","description":"Use real foreground VLC reads after a seek as a prefetch signal even when they are cache hits, and fetch aligned chunks for partial foreground misses so the cache warms ahead before VLC reaches the edge of retained data.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:38:14Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:40:10Z","started_at":"2026-05-26T04:38:16Z","closed_at":"2026-05-26T04:40:10Z","close_reason":"Used actual foreground VLC reads as prefetch follow signals on hits and changed foreground misses to fetch aligned chunks; added regression tests and updated the turn document.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-3pn","title":"reduce vlc seek buffering with range cache priming","description":"Improve VLC local range cache behavior after seek/jump by priming bytes behind the target, using stable global chunk boundaries, retaining useful cached ranges under a byte budget, and adding tests for the observed post-seek request pattern.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:31:46Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:35:36Z","started_at":"2026-05-26T04:31:51Z","closed_at":"2026-05-26T04:35:36Z","close_reason":"Implemented backward-biased seek priming, global 1 MB range-cache chunk alignment, bounded protected eviction, partial foreground miss fetching/logging, main-actor VLC delegate handling, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-meh","title":"Use VLC range requests to reprioritize seek prefetch","description":"Jump logs show duration-based byteOffset estimates can be far behind VLC's actual post-seek range requests, so prefetch keeps warming stale bytes while VLC buffers on higher cache misses.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T04:11:38Z","created_by":"dirtydishes","updated_at":"2026-05-26T04:14:20Z","started_at":"2026-05-26T04:11:40Z","closed_at":"2026-05-26T04:14:20Z","close_reason":"Fixed range-cache prefetch reprioritization so foreground VLC misses cancel stale speculative work and restart around VLC's actual requested byte range; added regression coverage for the observed jump mismatch.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/ProgressiveHTTPRangeCache.swift b/Dreamio/ProgressiveHTTPRangeCache.swift index 31d711e..fffdbc4 100644 --- a/Dreamio/ProgressiveHTTPRangeCache.swift +++ b/Dreamio/ProgressiveHTTPRangeCache.swift @@ -350,6 +350,7 @@ final class ProgressiveHTTPRangeCacheSession { private var prefetchTask: Task? private var activePrefetchWindow: HTTPByteRange? private var activePrefetchPreferredOffset: Int64? + private var prefetchGeneration: UInt64 = 0 private var recentSeekRange: HTTPByteRange? private var recentForegroundRange: HTTPByteRange? @@ -441,6 +442,8 @@ final class ProgressiveHTTPRangeCacheSession { } prefetchTask?.cancel() + prefetchGeneration += 1 + let generation = prefetchGeneration let window = explicitWindow ?? targetWindow(aroundByteOffset: offset) activePrefetchWindow = window activePrefetchPreferredOffset = offset @@ -462,6 +465,9 @@ final class ProgressiveHTTPRangeCacheSession { 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 @@ -478,6 +484,9 @@ final class ProgressiveHTTPRangeCacheSession { } } } + guard self.prefetchGeneration == generation else { + return + } self.activePrefetchWindow = nil self.activePrefetchPreferredOffset = nil } @@ -485,6 +494,7 @@ final class ProgressiveHTTPRangeCacheSession { func cancelPrefetch() { prefetchTask?.cancel() + prefetchGeneration += 1 activePrefetchWindow = nil activePrefetchPreferredOffset = nil } @@ -512,10 +522,12 @@ final class ProgressiveHTTPRangeCacheSession { abs(range.start - preferredOffset) >= responseChunkSize else { return } + let nextOffset = range.end + 1 #if DEBUG - print("[DreamioRangeCache] prefetch follow-foreground from=\(preferredOffset) to=\(range.end + 1)") + let reason = nextOffset < preferredOffset ? "reanchor-foreground" : "follow-foreground" + print("[DreamioRangeCache] prefetch \(reason) from=\(preferredOffset) to=\(nextOffset)") #endif - prefetch(aroundByteOffset: range.end + 1, forceRestart: true) + prefetch(aroundByteOffset: nextOffset, forceRestart: true) } private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange { diff --git a/docs/turns/2026-05-26-reduce-vlc-seek-buffering.html b/docs/turns/2026-05-26-reduce-vlc-seek-buffering.html index 8c8b0e5..0e313b0 100644 --- a/docs/turns/2026-05-26-reduce-vlc-seek-buffering.html +++ b/docs/turns/2026-05-26-reduce-vlc-seek-buffering.html @@ -439,6 +439,89 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fo
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.

+
+ \ No newline at end of file