docs/internal/youtube-desktop.md
Status: Confirmed impossible. All JS/CSS approaches exhausted. Date: February 2026 Affects: Live News panel, Live Webcams panel (macOS desktop app) Root cause: WKWebView blocks programmatic media playback in cross-origin iframes without a direct user gesture.
tauri://localhost (main webview)
└── http://localhost:PORT (sidecar youtube-embed handler)
└── https://www.youtube.com/embed/VIDEO_ID (YouTube IFrame Player)
Three nested origins. The YouTube player sits inside a sidecar-served HTML page, which sits inside the Tauri WKWebView. The user gesture context is lost crossing each origin boundary.
allow="autoplay" iframe attributeWhat: Set allow="autoplay; encrypted-media" on the sidecar's <iframe> embedding YouTube.
Why it fails: WKWebView does not honor the allow attribute for media autoplay policy. This attribute works in Chromium-based browsers but is ignored by WebKit's media policy engine.
What: Used a MutationObserver in the sidecar embed HTML to watch for YT.Player's dynamically-created <iframe> and force allow="autoplay" onto it at creation time.
Result: The attribute was successfully added (confirmed via console logs), but WKWebView's autoplay policy operates at a layer below HTML attributes — it checks the media element's playback context, not iframe permissions.
What: After onReady, immediately call player.mute() + player.playVideo(), then retry at 500ms and 1500ms intervals.
Why it fails: In Chromium, muted autoplay is always allowed. In WKWebView, even muted playback requires a user gesture in cross-origin iframe contexts. playVideo() silently does nothing — no error thrown, no state change.
Permissions-Policy: autoplay=* response headerWhat: Added Permissions-Policy: autoplay=*, encrypted-media=* to the sidecar's HTTP response for the embed page.
Why it fails: Permissions-Policy is a Chromium feature. WebKit/WKWebView does not implement this header for media autoplay decisions.
http://localhost (vs http://127.0.0.1)What: Per W3C spec, http://localhost is a secure context while http://127.0.0.1 is not. Changed all sidecar URLs from 127.0.0.1 to localhost hoping secure context would unlock autoplay.
Result: No effect. WKWebView's autoplay restriction is orthogonal to secure context status. The policy is about user gesture propagation, not HTTPS/secure origin.
What: Set all relevant playerVars: autoplay: 1, mute: 1, playsinline: 1, enablejsapi: 1, plus origin and widget_referrer matching the sidecar origin.
Result: YouTube's player respects these in Chromium. In WKWebView, the underlying <video> element's play() call is what gets blocked — playerVars just configure what YouTube attempts to do, not what WKWebView allows.
player.playVideo() from various timing contextsWhat: Called playVideo() from:
onReady callback (immediate)setTimeout at 500ms, 1500ms, 2000msplayer.mute()
Why it fails: None of these run in a user gesture context. JavaScript's event loop loses gesture propagation after any async boundary (setTimeout, Promise.then, await). The onReady callback itself is fired asynchronously by YouTube's API.What: After 2 seconds, if autoplay hasn't started (no YT.PlayerState.PLAYING or BUFFERING event), destroy and recreate the iframe.
Result: The fresh iframe loads but still cannot autoplay — same policy applies. Reloading doesn't create a user gesture.
What: Verified that wry 0.54.2 (Tauri's WebKit wrapper) sets mediaTypesRequiringUserActionForPlayback = [] (equivalent to .none), meaning autoplay should be allowed at the WKWebView level.
Result: This setting works for same-origin content. It does NOT override the cross-origin iframe media policy. The YouTube embed is cross-origin (youtube.com vs localhost), so the WKWebView-level permission is insufficient.
What: Added a full-screen overlay div inside the sidecar embed. On click, called player.playVideo() + player.unMute().
Result: This DOES work — the click is a genuine user gesture. However, it requires the user to click inside the sidecar iframe first, and the gesture doesn't propagate to the cross-origin YouTube iframe underneath. The overlay approach only works if the overlay itself triggers playVideo() via the JS API (which does work from a user gesture).
WKWebView's media autoplay policy:
mediaTypesRequiringUserActionForPlayback setting. If set to .none, autoplay works.await, setTimeout, Promise). Even onReady is async.There is an open Tauri issue (#13200) requesting exposure of mediaTypesRequiringUserActionForPlayback per-webview, but even with it exposed, it wouldn't help for cross-origin iframes.
The only way autoplay would work is if YouTube's embed was served from the same origin as the WKWebView — which would mean serving it from tauri://localhost, which is impossible since YouTube's player must load from youtube.com.
pointer-events: auto — user clicks YouTube's native play button directlypointer-events: none)The previous architecture served embeds from https://worldmonitor.app/api/youtube/embed. The parent was also https://worldmonitor.app. Same origin = WKWebView's autoplay policy respected the .none setting. When we moved embeds to the local sidecar (http://localhost:PORT), the parent became tauri://localhost — now cross-origin, breaking autoplay.
Trade-off: Reverting to Vercel-hosted embeds would restore autoplay but adds a cloud dependency for the desktop app (defeats offline/local-first goals) and adds latency.
| File | Change |
|---|---|
src-tauri/sidecar/local-api-server.mjs | MutationObserver, retry chain, overlay fixes, postMessage('*'), Permissions-Policy header |
src/styles/main.css | .webcam-iframe { pointer-events: auto } (was none), expand button styles |
src/components/LiveWebcamsPanel.ts | Expand button in grid cells, localhost embed URLs |
src/components/LiveNewsPanel.ts | localhost embed URLs, embedOrigin getter |
index.html | CSP frame-src added http://localhost:* |
src-tauri/tauri.conf.json | CSP frame-src added http://localhost:* |
tauri:// scheme to make it same-origin. Would require proxying YouTube's iframe API JS — legally and technically complex.