Yuan's Blog
EN 中

Tauri Video Not Playing? The Ultimate Solution for “Unsupported URL” on macOS

When developing a local macOS app with Tauri, if you try to play a local video using <video>, you’ll likely encounter the following error: [Error] Failed to load resource: unsupported URL

Or you might see a black screen, hear audio only, or receive no visible error message.


❗ Common Failure Paths

  • convertFileSrc → ❌ WebKit does not support Range requests
  • fetch → blob → ❌ Triggers CORS; resource blocked
  • Modifying CSP → ❌ Does not solve the underlying protocol issue

🧠 Root Cause

  1. WebKit limitation: On macOS, Safari’s engine doesn’t support playing <video> via the asset:// protocol, though static assets like images still work.
  2. Range request failure: Videos inherently make Range requests (e.g., bytes=102400-), but asset:// cannot return partial content.
  3. CORS restriction: fetch("asset://...") is treated as cross-origin, and you can’t set Access-Control-Allow-Origin.
  4. Unregistered protocol: If the stream:// protocol isn’t registered with a handler in Rust, it will throw errors or fail silently.
  5. Rendering issues: Even if loading succeeds, videos may still show a black screen due to CSS or codec incompatibility.

🧪 Reproduction Example

fetch("asset://video.mp4")
  .then(res => res.blob())
  .then(blob => video.src = URL.createObjectURL(blob));
Result:
[Error] Failed to load resource: unsupported URL

WebKit doesn’t support asset:// because:
	•asset:// is Tauri’s built-in read-only protocol, backed by AppHandle::fs_scope().
	•	Its behavior is fixed—you can’t control response handling.
	•	It doesn’t support Range requests.
	•	It lacks proper MIME negotiation.
	•	It cannot add correct CORS headers.
	•	WebKit treats unknown protocols as unsupported URLs, especially in <video>, which requires strict handling.

✅ The correct approach: Create a custom stream:// protocol.

✅ WebKit supports stream:// because:
	•	It’s your own custom protocol registered in Tauri.
	•	You can tell WebKit exactly how to handle it:
	•	Set MIME type (to tell the browser “this is a video”).
	•	Support Range requests (for segmented video loading).
	•	Enable CORS headers.
	•	Add custom headers, caching, etc.

👉 In essence, stream:// is under your control, so you can make it behave like a protocol the browser loves.



✅ Step 1: Register the Protocol Handler in Rust
```Rust
tauri::Builder::default()
  .register_uri_scheme_protocol("stream", |request| {
    // 读取视频文件
    // 处理 Range 请求
    // 返回正确的 Content-Type(例如 video/mp4)和响应体
  })

✅ Step 2: Use the stream:// Protocol in Frontend <video controls src="stream://video.mp4" />

🧰 Why Images/Fonts Work but Videos Fail • Images, fonts, and small scripts are small, loaded once (no Range requests). • Videos and audio files are large, require segmented loading (Range), caching, and decoding.

When loading, elements like

🧠 How a Tauri App Works (Simplified)

🧩 Frontend: • Your React/Vue pages are bundled as static assets. • They are loaded in the WebView via asset://.

🔧 Backend: • Rust provides native capabilities (filesystem, FFmpeg, database, etc.). • You can register custom protocols (e.g., stream://) to emulate an HTTP-like service.

🎞️ Rendering: • macOS → WebKit (Safari engine) • Windows → WebView2 (Chromium

⸻

🙋‍♂️ 常见问题答疑(FAQ)

❓ Why can’t I just use file:/// or asset://?

Because these protocols: • Don’t support Range requests. • Can’t handle MIME negotiation. • Trigger CORS when using fetch, leading to blocked resources.

⸻

❓ Video has audio but black screen — why?

Possible causes: • CSS: The

⸻ ✅ Summary

💡 If you need to play large local files (videos/audio) in Tauri,you must implement a custom protocol handler that supports Range requests.This is the only way to bypass WebKit’s limitations and avoid CORS and MIME issues.