fix local cache loopback port

This commit is contained in:
dirtydishes 2026-05-25 18:34:08 -04:00
parent e7a80df7cc
commit 151ae3ca5b
4 changed files with 77 additions and 15 deletions

View file

@ -1,4 +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-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-11s","title":"add progressive vlc range cache","description":"Implement Dreamio-owned progressive HTTP range cache for native VLC playback, including local loopback playback, diagnostics, and range cache tests.","status":"closed","priority":1,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-25T22:20:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T22:33:59Z","closed_at":"2026-05-25T22:33:59Z","close_reason":"Implemented progressive HTTP range cache, VLC loopback playback selection, diagnostics, and cache tests.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-6bv","title":"fix native stream proxy buffering after seek","description":"Investigate and fix VLC staying in buffering after native proxy-backed jump seeks. Logs show time remains pinned after jump while state repeatedly reports buffering.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T21:53:57Z","created_by":"dirtydishes","updated_at":"2026-05-25T21:55:32Z","started_at":"2026-05-25T21:54:01Z","closed_at":"2026-05-25T21:55:32Z","close_reason":"handled VLC buffering follow-up by supporting HEAD probes, moving fetch work off listener queue, reducing foreground range size, and locking cache access","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-6bv","title":"fix native stream proxy buffering after seek","description":"Investigate and fix VLC staying in buffering after native proxy-backed jump seeks. Logs show time remains pinned after jump while state repeatedly reports buffering.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T21:53:57Z","created_by":"dirtydishes","updated_at":"2026-05-25T21:55:32Z","started_at":"2026-05-25T21:54:01Z","closed_at":"2026-05-25T21:55:32Z","close_reason":"handled VLC buffering follow-up by supporting HEAD probes, moving fetch work off listener queue, reducing foreground range size, and locking cache access","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-dsp","title":"add local seek buffer proxy for native playback","description":"Implement a local HTTP range proxy/cache between VLC and direct-file streams so nearby seeks can use retained bytes, while preserving stream headers and subtitle behavior.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T20:18:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T20:22:41Z","started_at":"2026-05-25T20:18:47Z","closed_at":"2026-05-25T20:22:41Z","close_reason":"implemented local native stream cache proxy with range cache tests and successful simulator build","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-dsp","title":"add local seek buffer proxy for native playback","description":"Implement a local HTTP range proxy/cache between VLC and direct-file streams so nearby seeks can use retained bytes, while preserving stream headers and subtitle behavior.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T20:18:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T20:22:41Z","started_at":"2026-05-25T20:18:47Z","closed_at":"2026-05-25T20:22:41Z","close_reason":"implemented local native stream cache proxy with range cache tests and successful simulator build","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-ejh","title":"Preserve external subtitle language names in VLC captions menu","description":"VLC can surface externally attached subtitle slaves as generic Track N labels even though Dreamio already knows the OpenSubtitles language metadata. Preserve and apply that metadata when building the native captions menu so users can distinguish subtitle languages.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T17:46:38Z","created_by":"dirtydishes","updated_at":"2026-05-25T17:48:09Z","started_at":"2026-05-25T17:46:43Z","closed_at":"2026-05-25T17:48:09Z","close_reason":"Fixed by preserving known external subtitle display names for generic VLC subtitle tracks and expanding language-code aliases.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-ejh","title":"Preserve external subtitle language names in VLC captions menu","description":"VLC can surface externally attached subtitle slaves as generic Track N labels even though Dreamio already knows the OpenSubtitles language metadata. Preserve and apply that metadata when building the native captions menu so users can distinguish subtitle languages.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T17:46:38Z","created_by":"dirtydishes","updated_at":"2026-05-25T17:48:09Z","started_at":"2026-05-25T17:46:43Z","closed_at":"2026-05-25T17:48:09Z","close_reason":"Fixed by preserving known external subtitle display names for generic VLC subtitle tracks and expanding language-code aliases.","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -335,29 +335,79 @@ final class ProgressiveHTTPRangeCacheServer {
private var listener: NWListener? private var listener: NWListener?
private var port: UInt16? private var port: UInt16?
private var sessions: [String: ProgressiveHTTPRangeCacheSession] = [:] private var sessions: [String: ProgressiveHTTPRangeCacheSession] = [:]
private var startupContinuations: [CheckedContinuation<UInt16, Error>] = []
func localURL(for session: ProgressiveHTTPRangeCacheSession) throws -> URL { func localURL(for session: ProgressiveHTTPRangeCacheSession) async throws -> URL {
try startIfNeeded() let assignedPort = try await startIfNeeded()
sessions[session.id] = session sessions[session.id] = session
guard let port, guard let url = URL(string: "http://127.0.0.1:\(assignedPort)/stream/\(session.id)") else {
let url = URL(string: "http://127.0.0.1:\(port)/stream/\(session.id)") else {
throw HTTPRangeCacheError.serverUnavailable throw HTTPRangeCacheError.serverUnavailable
} }
return url return url
} }
private func startIfNeeded() throws { private func startIfNeeded() async throws -> UInt16 {
guard listener == nil else { if let port, port > 0 {
return return port
} }
let listener = try NWListener(using: .tcp, on: .any) return try await withCheckedThrowingContinuation { continuation in
listener.newConnectionHandler = { [weak self] connection in queue.async { [weak self] in
self?.handle(connection) guard let self else {
continuation.resume(throwing: HTTPRangeCacheError.serverUnavailable)
return
}
if let port = self.port, port > 0 {
continuation.resume(returning: port)
return
}
self.startupContinuations.append(continuation)
guard self.listener == nil else {
return
}
do {
let listener = try NWListener(using: .tcp, on: .any)
listener.newConnectionHandler = { [weak self] connection in
self?.handle(connection)
}
listener.stateUpdateHandler = { [weak self] state in
self?.handleListenerState(state)
}
self.listener = listener
listener.start(queue: self.queue)
} catch {
self.finishStartup(with: .failure(error))
}
}
}
}
private func handleListenerState(_ state: NWListener.State) {
switch state {
case .ready:
guard let rawPort = listener?.port?.rawValue, rawPort > 0 else {
finishStartup(with: .failure(HTTPRangeCacheError.serverUnavailable))
return
}
let assignedPort = UInt16(rawPort)
port = assignedPort
finishStartup(with: .success(assignedPort))
case .failed(let error):
finishStartup(with: .failure(error))
default:
break
}
}
private func finishStartup(with result: Result<UInt16, Error>) {
let continuations = startupContinuations
startupContinuations.removeAll()
continuations.forEach { continuation in
continuation.resume(with: result)
} }
listener.start(queue: queue)
self.listener = listener
self.port = listener.port.map { UInt16($0.rawValue) }
} }
private func handle(_ connection: NWConnection) { private func handle(_ connection: NWConnection) {
@ -449,6 +499,8 @@ final class ProgressiveHTTPRangeCacheServer {
} }
} }
extension ProgressiveHTTPRangeCacheServer: @unchecked Sendable {}
private extension NSLock { private extension NSLock {
func withLock<T>(_ body: () -> T) -> T { func withLock<T>(_ body: () -> T) -> T {
lock() lock()

View file

@ -84,7 +84,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
contentLength: contentLength, contentLength: contentLength,
durationProvider: { [weak self] in self?.duration ?? 0 } durationProvider: { [weak self] in self?.duration ?? 0 }
) )
let localURL = try ProgressiveHTTPRangeCacheServer.shared.localURL(for: session) let localURL = try await ProgressiveHTTPRangeCacheServer.shared.localURL(for: session)
await MainActor.run { await MainActor.run {
self.rangeCacheSession = session self.rangeCacheSession = session
session.prefetch(aroundByteOffset: 0) session.prefetch(aroundByteOffset: 0)

View file

@ -291,6 +291,14 @@
<section class="panel"><h2>Validation</h2><ul><li><code>xcrun swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Dreamio/ProgressiveHTTPRangeCache.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests</code> passed.</li><li><code>pod install</code> restored missing CocoaPods support files for the worktree.</li><li><code>xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' build</code> passed.</li></ul></section> <section class="panel"><h2>Validation</h2><ul><li><code>xcrun swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Dreamio/ProgressiveHTTPRangeCache.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests</code> passed.</li><li><code>pod install</code> restored missing CocoaPods support files for the worktree.</li><li><code>xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' build</code> passed.</li></ul></section>
<section class="panel"><h2>Issues, Limitations, and Mitigations</h2><ul><li>Manual device validation against the original problematic stream is still needed because local playback quality depends on real server range behavior.</li><li>The first version intentionally does not cache HLS/live streams; those remain on direct VLC playback because their model is playlist/segment based.</li><li>The loopback server is intentionally small and per-process. If future playback needs multiple concurrent videos, session lifecycle cleanup should be tightened further.</li></ul></section> <section class="panel"><h2>Issues, Limitations, and Mitigations</h2><ul><li>Manual device validation against the original problematic stream is still needed because local playback quality depends on real server range behavior.</li><li>The first version intentionally does not cache HLS/live streams; those remain on direct VLC playback because their model is playlist/segment based.</li><li>The loopback server is intentionally small and per-process. If future playback needs multiple concurrent videos, session lifecycle cleanup should be tightened further.</li></ul></section>
<section class="panel"><h2>Follow-up Work</h2><ul><li>Run the manual 15-second seek validation on device with the stream that produced the buffering logs.</li><li>Add an end-to-end local server integration test that opens the loopback URL and verifies repeated range reuse through the full server path.</li><li>Consider exposing cache counters in a debug overlay if native playback diagnostics continue to be a focus.</li></ul></section> <section class="panel"><h2>Follow-up Work</h2><ul><li>Run the manual 15-second seek validation on device with the stream that produced the buffering logs.</li><li>Add an end-to-end local server integration test that opens the loopback URL and verifies repeated range reuse through the full server path.</li><li>Consider exposing cache counters in a debug overlay if native playback diagnostics continue to be a focus.</li></ul></section>
<section class="panel"><h2>New Changes as of 2026-05-25 18:32 EDT</h2><h3>Summary of changes</h3><p>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.</p><h3>Why this change was made</h3><p>Device logs showed VLC opening <code>http://127.0.0.1:0/stream/...</code>. Port <code>0</code> 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.</p><h3>Code diffs</h3><pre><code>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.</code></pre><h3>Related issues or PRs</h3><p>Related to Beads issue <code>dreamio-11s</code> and branch <code>lavender/vlc-local-range-cache</code>.</p></section>
</main> </main>
</body> </body>
</html> </html>