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 doesnt support asset:// because:
	asset:// is Tauri’s built-in read-only protocol, backed by AppHandle::fs_scope().
		Its behavior is fixedyou cant control response handling.
		It doesnt 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:
		Its 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 automatically send Range headers such as: Range: bytes=102400- But asset:// can only return the entire file at once, not partial data, thus breaking seek and resume functionality.

🧠 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 element is hidden or transparent. • Codec: The format (e.g., H.264 10-bit) isn’t supported. • WebKit bug: Safari sometimes only plays the audio track.

⸻ ✅ 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.