dreamio/docs/turns/2026-05-25-guard-native-playback-availability.html

218 lines
9.4 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Guard Native Playback Availability</title>
<style>
:root {
color-scheme: light;
--ink: oklch(23% 0.024 282);
--muted: oklch(48% 0.03 282);
--paper: oklch(98% 0.007 285);
--panel: oklch(95% 0.012 285);
--line: oklch(84% 0.026 285);
--accent: oklch(56% 0.16 292);
--warn: oklch(62% 0.14 58);
--good: oklch(54% 0.13 160);
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.55;
}
main {
max-width: 1040px;
margin: 0 auto;
padding: 56px 28px 72px;
}
header {
margin-bottom: 34px;
}
h1 {
max-width: 820px;
margin: 0 0 16px;
font-size: clamp(2rem, 4vw, 3.8rem);
line-height: 1;
letter-spacing: 0;
}
h2 {
margin: 34px 0 10px;
font-size: 1.25rem;
}
p {
max-width: 74ch;
}
ul {
max-width: 78ch;
padding-left: 1.2rem;
}
code {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.95em;
background: var(--panel);
padding: 0.08rem 0.28rem;
border-radius: 4px;
}
pre {
max-width: 100%;
overflow: auto;
border: 1px solid var(--line);
border-radius: 8px;
background: oklch(99% 0.004 285);
padding: 16px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.88rem;
line-height: 1.45;
}
.summary {
max-width: 76ch;
color: var(--muted);
font-size: 1.08rem;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
}
.pill {
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 10px;
color: var(--muted);
background: oklch(96% 0.01 285);
font-size: 0.9rem;
}
.callout {
max-width: 78ch;
border: 1px solid oklch(78% 0.08 58);
background: oklch(95% 0.035 68);
border-radius: 8px;
padding: 14px 16px;
}
.ok {
border-color: oklch(78% 0.08 160);
background: oklch(95% 0.032 160);
}
</style>
</head>
<body>
<main>
<header>
<h1>Guard Native Playback Availability</h1>
<p class="summary">Dreamio now checks whether the MobileVLCKit-backed native player is actually linked before presenting the full-screen native player. Raw project builds stay buildable, but they now show a setup alert instead of opening a black player that can only fail.</p>
<div class="meta">
<span class="pill">Beads: dreamio-2k5</span>
<span class="pill">Native playback</span>
<span class="pill">CocoaPods setup</span>
<span class="pill">2026-05-25</span>
</div>
</header>
<section>
<h2>Summary</h2>
<p>Fixed the unavailable native playback build path by exposing a build-time availability check on <code>VLCNativePlaybackBackend</code> and using it before Dreamio presents native playback. CocoaPods was installed through Homebrew, <code>pod install</code> was run, and the generated workspace now links MobileVLCKit.</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added <code>VLCNativePlaybackBackend.isAvailable</code>, backed by the same <code>canImport(MobileVLCKit)</code> compile condition as the real VLC implementation.</li>
<li>Updated <code>DreamioWebViewController</code> to check native backend availability before resolving and presenting the native player.</li>
<li>Added an actionable setup alert for builds that do not link <code>MobileVLCKit</code>.</li>
<li>Updated the README to explain that the exact unavailable-build message means the binary was built without the CocoaPods workspace.</li>
<li>Installed CocoaPods 1.16.2 with Homebrew and ran <code>pod install</code>, generating <code>Dreamio.xcworkspace</code> and <code>Podfile.lock</code> with MobileVLCKit 3.7.3.</li>
<li>Disabled Xcode user script sandboxing for the project so CocoaPods can embed MobileVLCKit during the framework copy phase.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>The repository has a <code>Podfile</code> declaring <code>MobileVLCKit</code>, but this checkout did not have a generated <code>Pods/</code> directory or <code>Dreamio.xcworkspace</code>. In that state, Swift takes the fallback compile path where <code>canImport(MobileVLCKit)</code> is false. Before this change, Dreamio could still present the native player, which then displayed the generic fallback error.</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The fallback backend remains intact so opening <code>Dreamio.xcodeproj</code> directly still compiles.</li>
<li>The guard runs before stream resolution, avoiding unnecessary resolver network work when native playback cannot succeed in the current build.</li>
<li>The duplicate playback key is cleared when the guard blocks playback, so the user can retry after rebuilding the app correctly.</li>
<li>The generated workspace references <code>Dreamio.xcodeproj</code> and <code>Pods/Pods.xcodeproj</code>. The <code>Pods/</code> directory remains ignored, while <code>Podfile.lock</code> and workspace metadata are tracked.</li>
<li><code>ENABLE_USER_SCRIPT_SANDBOXING</code> is set to <code>NO</code> because the CocoaPods embed frameworks script uses <code>rsync</code> to copy the MobileVLCKit framework into the app bundle.</li>
<li>No public app-facing API changed.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p><code>@pierre/diffs</code> is installed as a library dependency, but its package does not expose a runnable CLI in this checkout, and <code>npx @pierre/diffs --help</code> failed with "could not determine executable to run." The plain diff below is the fallback snippet for the core behavior change.</p>
<pre><code>diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift
@@ -368,6 +368,12 @@ final class DreamioWebViewController: UIViewController {
@MainActor
private func resolveAndPresentNativePlayback(_ request: NativePlaybackRequest) async {
+ guard VLCNativePlaybackBackend.isAvailable else {
+ lastNativePlaybackURL = nil
+ showNativePlaybackUnavailableAlert()
+ return
+ }
+
do {
let resolved = try await streamResolver.resolve(request: request)
diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift
@@ -5,6 +5,14 @@ import MobileVLCKit
#endif
final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
+ static var isAvailable: Bool {
+#if canImport(MobileVLCKit)
+ true
+#else
+ false
+#endif
+ }
+
let view = UIView()
var onReady: (() -&gt; Void)?
var onFailure: ((Error) -&gt; Void)?</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>Users who accidentally run a raw <code>.xcodeproj</code> build will see a clear CocoaPods setup message instead of a black native player with an unavailable-build failure. Users who build from <code>Dreamio.xcworkspace</code> with <code>MobileVLCKit</code> linked should continue into VLC-backed direct-file playback.</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Ran <code>swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-tests &amp;&amp; /tmp/dreamio-stream-tests</code>: passed.</li>
<li>Ran <code>swiftc -typecheck Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift</code>: passed.</li>
<li>Ran <code>git diff --check</code>: passed.</li>
<li>Ran <code>HOMEBREW_NO_AUTO_UPDATE=1 brew install cocoapods</code>: passed, installing CocoaPods 1.16.2.</li>
<li>Ran <code>pod --version &amp;&amp; pod install</code>: passed, installing MobileVLCKit 3.7.3 and generating the workspace.</li>
<li>Ran <code>DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -sdk iphonesimulator build</code>: passed.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<div class="callout">The simulator workspace build passes. Real-device playback validation is still required for the actual VLC-backed stream behavior.</div>
<ul>
<li>The global <code>xcode-select</code> value still points at Command Line Tools because changing it requires sudo. Command-line builds can use <code>DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer</code>.</li>
<li>The <code>Pods/</code> directory is intentionally ignored by git, so another checkout should run <code>pod install</code> after pulling.</li>
<li>The native player still depends on MobileVLCKit behavior once the workspace build is available.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>On device, select a direct MKV, AVI, or WebM stream and confirm the VLC-backed player starts.</li>
</ul>
</section>
</main>
</body>
</html>