swiftc -parse-as-library Tests/StreamResolverTests.swift Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Dreamio/ProgressiveHTTPRangeCache.swift Dreamio/ExternalSubtitleTrackParser.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests: passed.DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -sdk iphonesimulator build: passed.hit, partial-miss, and uncached reads.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.
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.
375 unmodified lines3763773783793803814 unmodified lines386387388389390391392393106 unmodified lines50050150250350450533 unmodified lines539540541542543544375 unmodified lines#if DEBUGprint("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")#endifreturn data}+4 unmodified lines#endifcancelPrefetchIfNeeded(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 linescancelPrefetch()}+private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {targetWindow(aroundByteOffset: offset, minimumBehind: prefetchChunkSize)}33 unmodified linesreturn 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 lines3763773783793803813824 unmodified lines387388389390391392393394395396397398399106 unmodified lines50650750850951051151251351451551651751851952052152252333 unmodified lines557558559560561562563564565566567568569570571572573574375 unmodified lines#if DEBUGprint("[DreamioRangeCache] cache=hit range=\(bounded.start)-\(bounded.end)")#endifprefetchAheadIfForegroundMoved(to: bounded)return data}+4 unmodified lines#endifcancelPrefetchIfNeeded(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 DEBUGprint("[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 linescancelPrefetch()}+private func prefetchAheadIfForegroundMoved(to range: HTTPByteRange) {guard activePrefetchWindow?.contains(range.start) == true,let preferredOffset = activePrefetchPreferredOffset,abs(range.start - preferredOffset) >= responseChunkSize else {return}#if DEBUGprint("[DreamioRangeCache] prefetch follow-foreground from=\(preferredOffset) to=\(range.end + 1)")#endifprefetch(aroundByteOffset: range.end + 1, forceRestart: true)}+private func targetWindow(aroundByteOffset offset: Int64) -> HTTPByteRange {targetWindow(aroundByteOffset: offset, minimumBehind: prefetchChunkSize)}33 unmodified linesreturn 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))
39 unmodified lines40414243444546390 unmodified lines43743843944044144230 unmodified lines4734744754764774784792 unmodified lines48248348448548648739 unmodified linestestRangeCachePrefetchPrioritizesSeekOffset()testRangeCacheSeekPrimingIncludesObservedVLCStart()testRangeCachePrefetchUsesGlobalChunkBoundaries()await testRangeCacheForegroundMissReprioritizesPrefetch()await testRangeProbeFallsBackWhenServerIgnoresRange()await testRangeFetcherPreservesHeaders()print("StreamResolverTests passed")390 unmodified linesassertEqual(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 linestry? 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 inrange.hasPrefix("bytes=52428800-")}, "Expected prefetch to restart on a global chunk boundary near VLC's foreground range, got \(ranges)")2 unmodified linestry? 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 lines404142434445464748390 unmodified lines43944044144244344444544644744844945045145245345445545630 unmodified lines4874884894904914924932 unmodified lines49649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454539 unmodified linestestRangeCachePrefetchPrioritizesSeekOffset()testRangeCacheSeekPrimingIncludesObservedVLCStart()testRangeCachePrefetchUsesGlobalChunkBoundaries()testRangeCacheForegroundMissFetchesAlignedChunks()await testRangeCacheForegroundMissReprioritizesPrefetch()await testRangeCacheHitFollowsActualPostSeekReadArea()await testRangeProbeFallsBackWhenServerIgnoresRange()await testRangeFetcherPreservesHeaders()print("StreamResolverTests passed")390 unmodified linesassertEqual(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 linestry? 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 inrange.hasPrefix("bytes=52428800-")}, "Expected prefetch to restart on a global chunk boundary near VLC's foreground range, got \(ranges)")2 unmodified linestry? 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 inlet 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 inrange.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 = niltry? 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)
Beads issue dreamio-mi1.