From baec60829d6bc4463d588556a4619d34107759b8 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Wed, 27 May 2026 00:09:20 -0400 Subject: [PATCH] Start buffering before caption loading --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 1 + Dreamio/NativePlayerViewController.swift | 30 +++- ...05-27-start-buffering-before-captions.html | 140 ++++++++++++++++++ 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 docs/turns/2026-05-27-start-buffering-before-captions.html diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index bba1d40..fe9ec69 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -41,3 +41,4 @@ {"id":"int-fc9ecdb1","kind":"field_change","created_at":"2026-05-27T01:50:32.02792Z","actor":"dirtydishes","issue_id":"dreamio-ee1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed baseline UX audit in docs/native-player-ux-audit.md"}} {"id":"int-c8a14c48","kind":"field_change","created_at":"2026-05-27T01:56:02.08139Z","actor":"dirtydishes","issue_id":"dreamio-060","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented native player Liquid Glass UX improvements and validated simulator build."}} {"id":"int-8c109835","kind":"field_change","created_at":"2026-05-27T03:47:00.090296Z","actor":"dirtydishes","issue_id":"dreamio-e3u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented VLC playback state hardening, instrumentation, ready-once reporting, refresh throttling, tests, and turn documentation."}} +{"id":"int-3533f9f7","kind":"field_change","created_at":"2026-05-27T04:09:20.72451Z","actor":"dirtydishes","issue_id":"dreamio-ccn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented playback-first native startup and background parallel subtitle resolution."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1595d55..52c9960 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -46,6 +46,7 @@ {"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"dreamio-ccn","title":"Start media buffering before caption loading","description":"Ensure native media playback starts buffering immediately and subtitle resolution/attachment runs in the background so captions do not delay playback startup.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T04:07:30Z","created_by":"dirtydishes","updated_at":"2026-05-27T04:09:21Z","started_at":"2026-05-27T04:07:31Z","closed_at":"2026-05-27T04:09:21Z","close_reason":"Implemented playback-first native startup and background parallel subtitle resolution.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-e3u","title":"Harden VLC play pause synchronization","description":"Implement state-aware MobileVLCKit play/pause handling, instrumentation, readiness gating, conservative caching, and validation for pause/audio lag.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T03:43:10Z","created_by":"dirtydishes","updated_at":"2026-05-27T03:47:00Z","started_at":"2026-05-27T03:43:55Z","closed_at":"2026-05-27T03:47:00Z","close_reason":"Implemented VLC playback state hardening, instrumentation, ready-once reporting, refresh throttling, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-060","title":"Improve native player controls experience","description":"Implement Liquid Glass-inspired native player UI improvements, touch target updates, scrubbing feedback, gestures, loading and failure states, menu polish, accessibility, and validation.","acceptance_criteria":"Native player controls are modernized; touch targets and scrubbing improve; gestures, loading/failure affordances, menu labels, visual polish, device adaptation, and accessibility are implemented; build validation is run.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T01:51:52Z","created_by":"dirtydishes","updated_at":"2026-05-27T01:56:02Z","started_at":"2026-05-27T01:51:57Z","closed_at":"2026-05-27T01:56:02Z","close_reason":"Implemented native player Liquid Glass UX improvements and validated simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-ee1","title":"Audit native player UX baseline","description":"Audit the existing native player controls and document current user experience strengths, gaps, and implementation constraints before making UI changes.","acceptance_criteria":"Current NativePlayerViewController controls are reviewed; backend constraints are summarized; UX improvement opportunities are documented.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T01:49:46Z","created_by":"dirtydishes","updated_at":"2026-05-27T01:50:32Z","started_at":"2026-05-27T01:49:48Z","closed_at":"2026-05-27T01:50:32Z","close_reason":"Completed baseline UX audit in docs/native-player-ux-audit.md","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/NativePlayerViewController.swift b/Dreamio/NativePlayerViewController.swift index 0c8a670..a2f6e27 100644 --- a/Dreamio/NativePlayerViewController.swift +++ b/Dreamio/NativePlayerViewController.swift @@ -258,13 +258,24 @@ final class NativePlayerViewController: UIViewController { } private func resolveSubtitleCandidates(_ candidates: [SubtitleCandidate]) async -> [SubtitleCandidate] { - var resolved: [SubtitleCandidate] = [] - for candidate in candidates { - if let playableCandidate = await subtitleResolver.resolve(candidate) { - resolved.append(playableCandidate) + await withTaskGroup(of: (Int, SubtitleCandidate?).self) { group in + for (index, candidate) in candidates.enumerated() { + group.addTask { [subtitleResolver] in + (index, await subtitleResolver.resolve(candidate)) + } } + + var resolvedCandidates: [(index: Int, candidate: SubtitleCandidate)] = [] + for await (index, candidate) in group { + if let candidate { + resolvedCandidates.append((index, candidate)) + } + } + + return resolvedCandidates + .sorted { $0.index < $1.index } + .map(\.candidate) } - return resolved } private func configureBackend() { @@ -309,10 +320,17 @@ final class NativePlayerViewController: UIViewController { failureContainer.isHidden = true startStartupTimer() backend.play(request: request) - addSubtitleCandidates(request.subtitleCandidates) + startCaptionLoadingInBackground() revealControls() } + private func startCaptionLoadingInBackground() { + let queuedCount = addSubtitleCandidates(request.subtitleCandidates) +#if DEBUG + print("[DreamioNativePlayer] startup captions queued=\(queuedCount) total=\(request.subtitleCandidates.count) playbackAlreadyRequested=true") +#endif + } + private func startStartupTimer() { startupTimer?.invalidate() startupTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { [weak self] _ in diff --git a/docs/turns/2026-05-27-start-buffering-before-captions.html b/docs/turns/2026-05-27-start-buffering-before-captions.html new file mode 100644 index 0000000..f457249 --- /dev/null +++ b/docs/turns/2026-05-27-start-buffering-before-captions.html @@ -0,0 +1,140 @@ + + + + + + Start Buffering Before Captions + + + + + + +
+
+

Dreamio turn document

+

Start Buffering Before Captions

+

Native playback now makes the media open request the explicit first startup action, while caption resolution is queued as background work and resolved in parallel.

+
+ 2026-05-27 + Beads issue dreamio-ccn + Native playback +
+
+
+

Summary

Confirmed the startup path and tightened it so VLC begins opening the media before subtitle loading work starts. Caption candidates are still accepted at startup and later discovery time, but resolution no longer proceeds one by one.

+

Changes Made

  • Extracted startup caption work into startCaptionLoadingInBackground(), called only after backend.play(request:).
  • Changed subtitle candidate resolution to run with a task group, preserving the original candidate order after parallel resolution.
  • Added a debug log that records startup captions were queued after playback was requested.
+

Context

The existing startup path already presented native playback without waiting for late captions: DreamioWebViewController resolves and presents the player, then forwards buffered and newly discovered subtitle candidates. Inside NativePlayerViewController, playback was requested before captions were attached. This change makes that ordering explicit and reduces caption resolution latency while media buffering is underway.

+

Important Implementation Details

  • startPlayback() still starts the startup timeout, requests VLC playback, then reveals controls.
  • Caption loading remains non-blocking because addSubtitleCandidates queues work in a Swift Task.
  • Parallel subtitle resolution sorts by original index before attaching, so menu ordering remains stable.
+

Relevant Diff Snippets

Dreamio/NativePlayerViewController.swift · playback-first caption loading

Dreamio/NativePlayerViewController.swift
-6+24
257 unmodified lines
258
259
260
261
262
263
264
265
266
267
268
269
270
38 unmodified lines
309
310
311
312
313
314
315
316
317
318
257 unmodified lines
}
+
private func resolveSubtitleCandidates(_ candidates: [SubtitleCandidate]) async -> [SubtitleCandidate] {
var resolved: [SubtitleCandidate] = []
for candidate in candidates {
if let playableCandidate = await subtitleResolver.resolve(candidate) {
resolved.append(playableCandidate)
}
}
return resolved
}
+
private func configureBackend() {
38 unmodified lines
failureContainer.isHidden = true
startStartupTimer()
backend.play(request: request)
addSubtitleCandidates(request.subtitleCandidates)
revealControls()
}
+
private func startStartupTimer() {
startupTimer?.invalidate()
startupTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { [weak self] _ in
257 unmodified lines
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
38 unmodified lines
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
257 unmodified lines
}
+
private func resolveSubtitleCandidates(_ candidates: [SubtitleCandidate]) async -> [SubtitleCandidate] {
await withTaskGroup(of: (Int, SubtitleCandidate?).self) { group in
for (index, candidate) in candidates.enumerated() {
group.addTask { [subtitleResolver] in
(index, await subtitleResolver.resolve(candidate))
}
}
+
var resolvedCandidates: [(index: Int, candidate: SubtitleCandidate)] = []
for await (index, candidate) in group {
if let candidate {
resolvedCandidates.append((index, candidate))
}
}
+
return resolvedCandidates
.sorted { $0.index < $1.index }
.map(\.candidate)
}
}
+
private func configureBackend() {
38 unmodified lines
failureContainer.isHidden = true
startStartupTimer()
backend.play(request: request)
startCaptionLoadingInBackground()
revealControls()
}
+
private func startCaptionLoadingInBackground() {
let queuedCount = addSubtitleCandidates(request.subtitleCandidates)
#if DEBUG
print("[DreamioNativePlayer] startup captions queued=\(queuedCount) total=\(request.subtitleCandidates.count) playbackAlreadyRequested=true")
#endif
}
+
private func startStartupTimer() {
startupTimer?.invalidate()
startupTimer = Timer.scheduledTimer(withTimeInterval: 20, repeats: false) { [weak self] _ in

Diff rendered with @pierre/diffs/ssr using a single-file patch.

+

Expected Impact for End-Users

Streams should begin opening and buffering as soon as the native player starts. External captions can continue resolving and appearing shortly after playback starts, with less delay when multiple caption candidates need network resolution.

+

Validation

  • Passed: DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build.
  • Manual device playback validation remains recommended for real VLC buffering timing and external subtitle provider latency.
+

Issues, Limitations, and Mitigations

  • No user-facing loading copy was changed. The existing “Opening stream…” text remains scoped to media startup rather than captions.
  • Subtitle provider failures are still handled as missing captions, not media playback failures.
+

Follow-up Work

  • Add a dedicated UIKit test seam for startup ordering if the project introduces an iOS unit test target for NativePlayerViewController.
  • Manually verify on device with a stream that has several OpenSubtitles candidates.
+
+
+ +