diff --git a/docs/src/content/docs/guides/linux-media.mdx b/docs/src/content/docs/guides/linux-media.mdx
new file mode 100644
index 000000000..5786c0b32
--- /dev/null
+++ b/docs/src/content/docs/guides/linux-media.mdx
@@ -0,0 +1,189 @@
+---
+title: Linux Media Playback
+sidebar:
+ order: 12
+---
+
+import {Badge} from '@astrojs/starlight/components';
+
+Relevant Platforms:
+
+
+On Linux, Wails applications use WebKitGTK for rendering web content. WebKitGTK delegates media playback (video and audio) to GStreamer, which handles the actual decoding and rendering of media files.
+
+## The GStreamer Protocol Issue
+
+GStreamer operates independently from WebKit's custom URI scheme handling. While Wails registers a `wails://` protocol handler with WebKit for serving your application's assets, GStreamer doesn't have access to this handler. This means when an HTML `` or `` element tries to load a media file from your bundled assets, GStreamer cannot resolve the `wails://` URL.
+
+This is a [known upstream WebKit issue](https://bugs.webkit.org/show_bug.cgi?id=146351) that has been open since 2015.
+
+### Symptoms
+
+Without the workaround, you might experience:
+- Video elements showing a black screen or failing to load
+- Audio elements remaining silent
+- Browser console errors about failed media requests
+- Media elements stuck in a "loading" state
+
+## Automatic Workaround
+
+Wails automatically works around this limitation by injecting JavaScript that intercepts media elements and converts their sources to blob URLs. This happens transparently - you don't need to modify your frontend code.
+
+### How It Works
+
+1. When your application loads, Wails injects platform-specific JavaScript
+2. This JavaScript monitors the DOM for `` and `` elements
+3. When a media element is found with a relative or `wails://` URL, the script:
+ - Fetches the media file using the standard `fetch()` API (which works with the Wails asset server)
+ - Creates a blob URL from the response
+ - Replaces the element's `src` with the blob URL
+4. GStreamer can then play the blob URL normally
+
+### What Gets Intercepted
+
+The workaround intercepts:
+- `` elements with `src` attributes
+- `` elements with `src` attributes
+- `` elements inside `` and `` elements
+- Elements added dynamically via JavaScript
+- Elements with `src` set via JavaScript after page load
+
+## Configuration Options
+
+You can configure the GStreamer workaround behavior using `LinuxOptions`:
+
+```go
+app := application.New(application.Options{
+ Name: "My App",
+ Linux: application.LinuxOptions{
+ // Disable the GStreamer workaround entirely
+ DisableGStreamerFix: false,
+
+ // Enable caching of blob URLs for better performance
+ EnableGStreamerCaching: true,
+ },
+})
+```
+
+### DisableGStreamerFix
+
+Set to `true` to disable the automatic media interception. You might want to do this if:
+- You're only using external media URLs (http/https)
+- You're implementing your own media handling solution
+- You're debugging media-related issues
+
+```go
+Linux: application.LinuxOptions{
+ DisableGStreamerFix: true,
+},
+```
+
+### EnableGStreamerCaching
+
+When enabled, blob URLs are cached so that the same media file doesn't need to be fetched and converted multiple times. This improves performance when:
+- The same media is played repeatedly
+- Multiple elements reference the same media file
+- Media elements are recreated (e.g., in a SPA with component remounting)
+
+```go
+Linux: application.LinuxOptions{
+ EnableGStreamerCaching: true,
+},
+```
+
+:::note[Memory Considerations]
+Enabling caching keeps blob URLs in memory for the lifetime of the page. For applications with many large media files, consider whether the memory trade-off is worthwhile for your use case.
+:::
+
+## Working with Media Files
+
+### Bundled Assets
+
+Place media files in your frontend's public or assets directory:
+
+```
+frontend/
+ public/
+ video.mp4
+ audio.ogg
+ src/
+ main.js
+```
+
+Reference them with relative URLs:
+
+```html
+
+
+
+
+
+
+
+```
+
+### Supported Formats
+
+The supported media formats depend on your GStreamer installation. Common formats include:
+- Video: MP4 (H.264), WebM (VP8/VP9), OGG (Theora)
+- Audio: MP3, OGG (Vorbis), WAV, FLAC
+
+To check available codecs on your system:
+
+```bash
+gst-inspect-1.0 --version
+gst-inspect-1.0 | grep -i decoder
+```
+
+### Installing Codecs
+
+On Ubuntu/Debian:
+```bash
+sudo apt install gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly
+```
+
+On Fedora:
+```bash
+sudo dnf install gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-plugins-ugly-free
+```
+
+On Arch Linux:
+```bash
+sudo pacman -S gst-plugins-good gst-plugins-bad gst-plugins-ugly
+```
+
+## Example Application
+
+See the [audio-video example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/audio-video) for a complete working example that demonstrates:
+- Video playback with bundled MP4 files
+- Audio playback with bundled MP3 files
+- Using the `@wailsio/runtime` npm module
+- Proper project structure for media assets
+
+## Troubleshooting
+
+### Media Not Playing
+
+1. **Check GStreamer installation**: Ensure GStreamer and required plugins are installed
+2. **Check the console**: Look for JavaScript errors or failed network requests
+3. **Verify file paths**: Ensure media files are in the correct location and accessible
+4. **Check codec support**: Verify GStreamer has the necessary decoder for your media format
+
+### Performance Issues
+
+If media playback is sluggish:
+1. Enable caching with `EnableGStreamerCaching: true`
+2. Consider using more efficient codecs (e.g., H.264 instead of VP9)
+3. Reduce media file sizes through compression
+
+### Debugging
+
+To see what the media interceptor is doing, check the browser console. The interceptor logs its actions when media sources are converted.
+
+You can also temporarily disable the fix to compare behavior:
+
+```go
+Linux: application.LinuxOptions{
+ DisableGStreamerFix: true,
+},
+```
diff --git a/docs/src/content/docs/quick-start/installation.mdx b/docs/src/content/docs/quick-start/installation.mdx
index 20bdc51ba..dbfa3df5e 100644
--- a/docs/src/content/docs/quick-start/installation.mdx
+++ b/docs/src/content/docs/quick-start/installation.mdx
@@ -144,18 +144,33 @@ If `wails3 doctor` passes, you're done. [Skip to First App →](/quick-start/fir
sudo apt update
sudo apt install build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev
```
+
+ **For audio/video playback** (optional but recommended):
+ ```bash
+ sudo apt install gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly
+ ```
```bash
sudo dnf install gcc pkg-config gtk3-devel webkit2gtk4.0-devel
```
+
+ **For audio/video playback** (optional but recommended):
+ ```bash
+ sudo dnf install gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-plugins-ugly-free
+ ```
```bash
sudo pacman -S base-devel gtk3 webkit2gtk
```
+
+ **For audio/video playback** (optional but recommended):
+ ```bash
+ sudo pacman -S gst-plugins-good gst-plugins-bad gst-plugins-ugly
+ ```
diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md
index 8e4648038..29c625c27 100644
--- a/v3/UNRELEASED_CHANGELOG.md
+++ b/v3/UNRELEASED_CHANGELOG.md
@@ -16,12 +16,15 @@ After processing, the content will be moved to the main changelog and this file
-->
## Added
+- Add `DisableGStreamerFix` and `EnableGStreamerCaching` options to `LinuxOptions` for controlling media playback workaround (#4412) by @leaanthony
+- Add `audio-video` example demonstrating HTML5 media playback with the `@wailsio/runtime` npm module by @leaanthony
## Changed
## Fixed
+- Fix video and audio playback on Linux/WebKitGTK by intercepting media elements and converting wails:// URLs to blob URLs (#4412) by @leaanthony
## Deprecated
diff --git a/v3/examples/audio-video/README.md b/v3/examples/audio-video/README.md
new file mode 100644
index 000000000..ceadb6446
--- /dev/null
+++ b/v3/examples/audio-video/README.md
@@ -0,0 +1,38 @@
+# Audio/Video Example
+
+This example demonstrates HTML5 audio and video playback using the `@wailsio/runtime` npm module.
+
+## Linux Notes
+
+On Linux, WebKitGTK uses GStreamer for media playback. GStreamer doesn't have a URI handler for the `wails://` protocol, which means media files served from the bundled assets won't play directly.
+
+Wails automatically works around this limitation by intercepting media elements and converting their sources to blob URLs. This happens transparently - you don't need to change your code.
+
+See the [Linux-specific documentation](https://wails.io/docs/guides/linux-media) for details on:
+- How the GStreamer workaround works
+- How to disable it if needed (`DisableGStreamerFix`)
+- How to enable caching for better performance (`EnableGStreamerCaching`)
+
+## Building
+
+```bash
+cd frontend
+npm install
+npm run build
+cd ..
+go build
+./audio-video
+```
+
+## Development
+
+For development with hot-reload:
+
+```bash
+# Terminal 1: Run Vite dev server
+cd frontend
+npm run dev
+
+# Terminal 2: Run Go app with dev server URL
+FRONTEND_DEVSERVER_URL=http://localhost:5173 go run .
+```
diff --git a/v3/examples/audio-video/frontend/dist/assets/index-DxRb0nj9.js b/v3/examples/audio-video/frontend/dist/assets/index-DxRb0nj9.js
new file mode 100644
index 000000000..daa86c9a0
--- /dev/null
+++ b/v3/examples/audio-video/frontend/dist/assets/index-DxRb0nj9.js
@@ -0,0 +1,6 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))i(s);new MutationObserver(s=>{for(const r of s)if(r.type==="childList")for(const a of r.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function n(s){const r={};return s.integrity&&(r.integrity=s.integrity),s.referrerPolicy&&(r.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?r.credentials="include":s.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(s){if(s.ep)return;s.ep=!0;const r=n(s);fetch(s.href,r)}})();const G="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";function X(e=21){let t="",n=e|0;for(;n--;)t+=G[Math.random()*64|0];return t}const Y=window.location.origin+"/wails/runtime",H=Object.freeze({Call:0,Clipboard:1,Application:2,Events:3,ContextMenu:4,Dialog:5,Window:6,Screens:7,System:8,Browser:9,CancelCall:10,IOS:11});let V=X();function B(e,t=""){return function(n,i=null){return $(e,n,t,i)}}async function $(e,t,n,i){var s,r;let a=new URL(Y),f={object:e,method:t};i!=null&&(f.args=i);let d={"x-wails-client-id":V,"Content-Type":"application/json"};n&&(d["x-wails-window-name"]=n);let c=await fetch(a,{method:"POST",headers:d,body:JSON.stringify(f)});if(!c.ok)throw new Error(await c.text());return((r=(s=c.headers.get("Content-Type"))===null||s===void 0?void 0:s.indexOf("application/json"))!==null&&r!==void 0?r:-1)!==-1?c.json():c.text()}B(H.System);const x=function(){var e,t,n,i,s,r;try{if(!((t=(e=window.chrome)===null||e===void 0?void 0:e.webview)===null||t===void 0)&&t.postMessage)return window.chrome.webview.postMessage.bind(window.chrome.webview);if(!((s=(i=(n=window.webkit)===null||n===void 0?void 0:n.messageHandlers)===null||i===void 0?void 0:i.external)===null||s===void 0)&&s.postMessage)return window.webkit.messageHandlers.external.postMessage.bind(window.webkit.messageHandlers.external);if(!((r=window.wails)===null||r===void 0)&&r.invoke)return a=>window.wails.invoke(typeof a=="string"?a:JSON.stringify(a))}catch{}return console.warn(`
+%c⚠️ Browser Environment Detected %c
+
+%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.
+More information at: https://v3.wails.io/learn/build/#using-a-browser-for-development
+`,"background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;","background: transparent;","color: #ffffff; font-style: italic; font-weight: bold;"),null}();function S(e){x==null||x(e)}function A(){var e,t;return((t=(e=window._wails)===null||e===void 0?void 0:e.environment)===null||t===void 0?void 0:t.OS)==="windows"}function J(){var e,t;return!!(!((t=(e=window._wails)===null||e===void 0?void 0:e.environment)===null||t===void 0)&&t.Debug)}function q(){return new MouseEvent("mousedown").buttons===0}function U(e){var t;return e.target instanceof HTMLElement?e.target:!(e.target instanceof HTMLElement)&&e.target instanceof Node&&(t=e.target.parentElement)!==null&&t!==void 0?t:document.body}document.addEventListener("DOMContentLoaded",()=>{});window.addEventListener("contextmenu",te);const K=B(H.ContextMenu),Q=0;function ee(e,t,n,i){K(Q,{id:e,x:t,y:n,data:i})}function te(e){const t=U(e),n=window.getComputedStyle(t).getPropertyValue("--custom-contextmenu").trim();if(n){e.preventDefault();const i=window.getComputedStyle(t).getPropertyValue("--custom-contextmenu-data");ee(n,e.clientX,e.clientY,i)}else ne(e,t)}function ne(e,t){if(J())return;switch(window.getComputedStyle(t).getPropertyValue("--default-contextmenu").trim()){case"show":return;case"hide":e.preventDefault();return}if(t.isContentEditable)return;const n=window.getSelection(),i=n&&n.toString().length>0;if(i)for(let s=0;s{P=e,P||(M=b=!1,u())};let I=!1;function oe(){var e,t;const n=(t=(e=window._wails)===null||e===void 0?void 0:e.environment)===null||t===void 0?void 0:t.OS;if(n==="ios"||n==="android")return!0;const i=navigator.userAgent||navigator.vendor||window.opera||"";return/android|iphone|ipad|ipod|iemobile|wpdesktop/i.test(i)}function N(){if(!I&&!oe()){window.addEventListener("mousedown",O,{capture:!0}),window.addEventListener("mousemove",O,{capture:!0}),window.addEventListener("mouseup",O,{capture:!0});for(const e of["click","contextmenu","dblclick"])window.addEventListener(e,ie,{capture:!0});I=!0}}N();document.addEventListener("DOMContentLoaded",N,{once:!0});let se=0;const W=window.setInterval(()=>{if(I){window.clearInterval(W);return}N(),++se>100&&window.clearInterval(W)},50);function ie(e){(y||b)&&(e.stopImmediatePropagation(),e.stopPropagation(),e.preventDefault())}const D=0,re=1,F=2;function O(e){let t,n=e.buttons;switch(e.type){case"mousedown":t=D,L||(n=p|1<{if(n.preventDefault(),n.dataTransfer&&n.dataTransfer.types.includes("Files")){t++;const i=document.elementFromPoint(n.clientX,n.clientY),s=j(i);l&&l!==s&&l.classList.remove(g),s?(s.classList.add(g),n.dataTransfer.dropEffect="copy",l=s):(n.dataTransfer.dropEffect="none",l=null)}},!1),e.addEventListener("dragover",n=>{n.preventDefault(),n.dataTransfer&&n.dataTransfer.types.includes("Files")&&(l?(l.classList.contains(g)||l.classList.add(g),n.dataTransfer.dropEffect="copy"):n.dataTransfer.dropEffect="none")},!1),e.addEventListener("dragleave",n=>{n.preventDefault(),n.dataTransfer&&n.dataTransfer.types.includes("Files")&&(t--,(t===0||n.relatedTarget===null||l&&!l.contains(n.relatedTarget))&&(l&&(l.classList.remove(g),l=null),t=0))},!1),e.addEventListener("drop",n=>{n.preventDefault(),t=0,l&&(l.classList.remove(g),l=null)},!1)}typeof window<"u"&&typeof document<"u"&&dt();window._wails=window._wails||{};window._wails.invoke=S;window._wails.handlePlatformFileDrop=Z.HandlePlatformFileDrop.bind(Z);fetch("/wails/platform.js").then(e=>e.text()).then(e=>{if(e&&e.trim().length>0){const t=document.createElement("script");t.textContent=e,document.head.appendChild(t)}}).catch(e=>{console.debug("[Wails] No platform-specific code to load:",e)}).finally(()=>{S("wails:runtime:ready")});const R=document.getElementById("video"),m=document.getElementById("video-status");R.addEventListener("loadeddata",()=>{m.className="status success",m.textContent="Loaded ("+R.duration.toFixed(1)+"s)"});R.addEventListener("error",()=>{m.className="status error",m.textContent="Failed to load"});const _=document.getElementById("audio"),w=document.getElementById("audio-status");_.addEventListener("loadeddata",()=>{w.className="status success",w.textContent="Loaded ("+_.duration.toFixed(1)+"s)"});_.addEventListener("error",()=>{w.className="status error",w.textContent="Failed to load"});setTimeout(()=>{m.classList.contains("pending")&&(m.className="status error",m.textContent="Timeout"),w.classList.contains("pending")&&(w.className="status error",w.textContent="Timeout")},5e3);
diff --git a/v3/examples/audio-video/frontend/dist/index.html b/v3/examples/audio-video/frontend/dist/index.html
new file mode 100644
index 000000000..9cddde2a8
--- /dev/null
+++ b/v3/examples/audio-video/frontend/dist/index.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ Audio/Video Example
+
+
+
+
+
+
Audio/Video Example
+
+
+
Video
+
+
+
+
Loading...
+
+
+
+
Audio
+
+
+
+
Loading...
+
+
+
+
+
diff --git a/v3/examples/audio-video/frontend/dist/wails.mp3 b/v3/examples/audio-video/frontend/dist/wails.mp3
new file mode 100644
index 000000000..962a1ca9b
Binary files /dev/null and b/v3/examples/audio-video/frontend/dist/wails.mp3 differ
diff --git a/v3/examples/audio-video/frontend/dist/wails.mp4 b/v3/examples/audio-video/frontend/dist/wails.mp4
new file mode 100644
index 000000000..d950cc39f
Binary files /dev/null and b/v3/examples/audio-video/frontend/dist/wails.mp4 differ
diff --git a/v3/examples/audio-video/frontend/index.html b/v3/examples/audio-video/frontend/index.html
new file mode 100644
index 000000000..81285e746
--- /dev/null
+++ b/v3/examples/audio-video/frontend/index.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ Audio/Video Example
+
+
+
+
+
Audio/Video Example
+
+
+
Video
+
+
+
+
Loading...
+
+
+
+
Audio
+
+
+
+
Loading...
+
+
+
+
+
+
diff --git a/v3/examples/audio-video/frontend/package-lock.json b/v3/examples/audio-video/frontend/package-lock.json
new file mode 100644
index 000000000..646470a38
--- /dev/null
+++ b/v3/examples/audio-video/frontend/package-lock.json
@@ -0,0 +1,965 @@
+{
+ "name": "audio-video-example",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "audio-video-example",
+ "version": "0.0.0",
+ "dependencies": {
+ "@wailsio/runtime": "file:../../../internal/runtime/desktop/@wailsio/runtime"
+ },
+ "devDependencies": {
+ "vite": "^5.0.0"
+ }
+ },
+ "../../../internal/runtime/desktop/@wailsio/runtime": {
+ "version": "3.0.0-alpha.75",
+ "license": "MIT",
+ "devDependencies": {
+ "happy-dom": "^17.1.1",
+ "promises-aplus-tests": "2.1.2",
+ "rimraf": "^5.0.5",
+ "typedoc": "^0.27.7",
+ "typedoc-plugin-markdown": "^4.4.2",
+ "typedoc-plugin-mdn-links": "^4.0.13",
+ "typedoc-plugin-missing-exports": "^3.1.0",
+ "typescript": "^5.7.3",
+ "vite": "^5.2.0",
+ "vitest": "^3.0.6"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@wailsio/runtime": {
+ "resolved": "../../../internal/runtime/desktop/@wailsio/runtime",
+ "link": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/v3/examples/audio-video/frontend/package.json b/v3/examples/audio-video/frontend/package.json
new file mode 100644
index 000000000..fe587a673
--- /dev/null
+++ b/v3/examples/audio-video/frontend/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "audio-video-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@wailsio/runtime": "file:../../../internal/runtime/desktop/@wailsio/runtime"
+ },
+ "devDependencies": {
+ "vite": "^5.0.0"
+ }
+}
diff --git a/v3/examples/audio-video/frontend/public/wails.mp3 b/v3/examples/audio-video/frontend/public/wails.mp3
new file mode 100644
index 000000000..962a1ca9b
Binary files /dev/null and b/v3/examples/audio-video/frontend/public/wails.mp3 differ
diff --git a/v3/examples/audio-video/frontend/public/wails.mp4 b/v3/examples/audio-video/frontend/public/wails.mp4
new file mode 100644
index 000000000..d950cc39f
Binary files /dev/null and b/v3/examples/audio-video/frontend/public/wails.mp4 differ
diff --git a/v3/examples/audio-video/frontend/src/main.js b/v3/examples/audio-video/frontend/src/main.js
new file mode 100644
index 000000000..9df93d1f9
--- /dev/null
+++ b/v3/examples/audio-video/frontend/src/main.js
@@ -0,0 +1,40 @@
+// Import the Wails runtime from npm
+import '@wailsio/runtime';
+
+// Setup media status listeners
+const video = document.getElementById('video');
+const videoStatus = document.getElementById('video-status');
+
+video.addEventListener('loadeddata', () => {
+ videoStatus.className = 'status success';
+ videoStatus.textContent = 'Loaded (' + video.duration.toFixed(1) + 's)';
+});
+
+video.addEventListener('error', () => {
+ videoStatus.className = 'status error';
+ videoStatus.textContent = 'Failed to load';
+});
+
+const audio = document.getElementById('audio');
+const audioStatus = document.getElementById('audio-status');
+
+audio.addEventListener('loadeddata', () => {
+ audioStatus.className = 'status success';
+ audioStatus.textContent = 'Loaded (' + audio.duration.toFixed(1) + 's)';
+});
+
+audio.addEventListener('error', () => {
+ audioStatus.className = 'status error';
+ audioStatus.textContent = 'Failed to load';
+});
+
+setTimeout(() => {
+ if (videoStatus.classList.contains('pending')) {
+ videoStatus.className = 'status error';
+ videoStatus.textContent = 'Timeout';
+ }
+ if (audioStatus.classList.contains('pending')) {
+ audioStatus.className = 'status error';
+ audioStatus.textContent = 'Timeout';
+ }
+}, 5000);
diff --git a/v3/examples/audio-video/frontend/vite.config.js b/v3/examples/audio-video/frontend/vite.config.js
new file mode 100644
index 000000000..b40142469
--- /dev/null
+++ b/v3/examples/audio-video/frontend/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ }
+})
diff --git a/v3/examples/audio-video/main.go b/v3/examples/audio-video/main.go
new file mode 100644
index 000000000..cdb6f7e49
--- /dev/null
+++ b/v3/examples/audio-video/main.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+ "embed"
+ "log"
+
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+//go:embed frontend/dist/*
+var assets embed.FS
+
+func main() {
+ app := application.New(application.Options{
+ Name: "Audio/Video Example",
+ Description: "A demo of HTML5 Audio/Video with the Wails runtime npm module",
+ Assets: application.AssetOptions{
+ Handler: application.BundledAssetFileServer(assets),
+ },
+ Mac: application.MacOptions{
+ ApplicationShouldTerminateAfterLastWindowClosed: true,
+ },
+ })
+
+ app.Window.NewWithOptions(application.WebviewWindowOptions{
+ Title: "Audio/Video Example",
+ Width: 900,
+ Height: 700,
+ })
+
+ err := app.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/v3/internal/assetserver/assetserver.go b/v3/internal/assetserver/assetserver.go
index 6ff1b169f..0cb76d385 100644
--- a/v3/internal/assetserver/assetserver.go
+++ b/v3/internal/assetserver/assetserver.go
@@ -1,12 +1,15 @@
package assetserver
import (
+ "bytes"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
+
+ "github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
)
const (
@@ -15,6 +18,19 @@ const (
HeaderAcceptLanguage = "accept-language"
)
+// Platform-specific options set during application initialization.
+var (
+ disableGStreamerFix bool
+ enableGStreamerCaching bool
+)
+
+// SetGStreamerOptions configures GStreamer workaround options on Linux.
+// This is called during application initialization.
+func SetGStreamerOptions(disable, enableCaching bool) {
+ disableGStreamerFix = disable
+ enableGStreamerCaching = enableCaching
+}
+
type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
}
@@ -94,6 +110,13 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH
//}
reqPath := req.URL.Path
+
+ // Handle internal Wails endpoints - these work regardless of user's handler
+ if strings.HasPrefix(reqPath, "/wails/") {
+ a.serveWailsEndpoint(rw, reqPath[6:]) // Strip "/wails" prefix
+ return
+ }
+
switch reqPath {
case "", "/", "/index.html":
// Cache the accept-language header
@@ -133,6 +156,40 @@ func (a *AssetServer) AttachServiceHandler(route string, handler http.Handler) {
a.services = append(a.services, service{route, handler})
}
+// serveWailsEndpoint handles internal /wails/* endpoints.
+func (a *AssetServer) serveWailsEndpoint(rw http.ResponseWriter, path string) {
+ rw.Header().Set(HeaderContentType, "application/javascript")
+ switch path {
+ case "/runtime.js":
+ rw.Write(bundledassets.RuntimeJS)
+ case "/platform.js":
+ rw.Write(getPlatformJS())
+ default:
+ rw.WriteHeader(http.StatusNotFound)
+ }
+}
+
+// getPlatformJS returns the platform-specific JavaScript based on current options.
+func getPlatformJS() []byte {
+ if platformJS == nil {
+ return nil
+ }
+
+ // If the fix is disabled, return empty
+ if disableGStreamerFix {
+ return nil
+ }
+
+ // If caching is not enabled, modify the JS to disable caching
+ if !enableGStreamerCaching {
+ return bytes.ReplaceAll(platformJS,
+ []byte("const ENABLE_CACHING = true;"),
+ []byte("const ENABLE_CACHING = false;"))
+ }
+
+ return platformJS
+}
+
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
err := ServeFile(rw, filename, blob)
if err != nil {
diff --git a/v3/internal/assetserver/assetserver_android.go b/v3/internal/assetserver/assetserver_android.go
index ce616f47e..015a8875f 100644
--- a/v3/internal/assetserver/assetserver_android.go
+++ b/v3/internal/assetserver/assetserver_android.go
@@ -10,3 +10,6 @@ var baseURL = url.URL{
Scheme: "https",
Host: "wails.localhost",
}
+
+// platformJS is empty on android - no platform-specific JS needed.
+var platformJS []byte
diff --git a/v3/internal/assetserver/assetserver_darwin.go b/v3/internal/assetserver/assetserver_darwin.go
index d7c647103..09c2a65da 100644
--- a/v3/internal/assetserver/assetserver_darwin.go
+++ b/v3/internal/assetserver/assetserver_darwin.go
@@ -8,3 +8,6 @@ var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}
+
+// platformJS is empty on darwin - no platform-specific JS needed.
+var platformJS []byte
diff --git a/v3/internal/assetserver/assetserver_ios.go b/v3/internal/assetserver/assetserver_ios.go
index afc05b221..051697a21 100644
--- a/v3/internal/assetserver/assetserver_ios.go
+++ b/v3/internal/assetserver/assetserver_ios.go
@@ -8,3 +8,6 @@ var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}
+
+// platformJS is empty on ios - no platform-specific JS needed.
+var platformJS []byte
diff --git a/v3/internal/assetserver/assetserver_linux.go b/v3/internal/assetserver/assetserver_linux.go
index ed579fad7..627a862e0 100644
--- a/v3/internal/assetserver/assetserver_linux.go
+++ b/v3/internal/assetserver/assetserver_linux.go
@@ -2,9 +2,15 @@
package assetserver
-import "net/url"
+import (
+ _ "embed"
+ "net/url"
+)
var baseURL = url.URL{
Scheme: "wails",
Host: "localhost",
}
+
+//go:embed assetserver_linux.js
+var platformJS []byte
diff --git a/v3/internal/assetserver/assetserver_linux.js b/v3/internal/assetserver/assetserver_linux.js
new file mode 100644
index 000000000..05e9d5272
--- /dev/null
+++ b/v3/internal/assetserver/assetserver_linux.js
@@ -0,0 +1,166 @@
+/*
+ * Wails Linux Media Interceptor
+ *
+ * On Linux, WebKitGTK uses GStreamer for media playback. GStreamer does not
+ * have a URI handler for the "wails://" protocol, causing video and audio
+ * elements to fail loading.
+ *
+ * This script intercepts media elements and converts wails:// URLs to blob URLs
+ * by fetching the content through fetch() (which works via WebKit's URI scheme
+ * handler) and creating object URLs.
+ *
+ * See: https://github.com/wailsapp/wails/issues/4412
+ * See: https://bugs.webkit.org/show_bug.cgi?id=146351
+ */
+(function() {
+ 'use strict';
+
+ // This constant is replaced by the server based on EnableGStreamerCaching option
+ const ENABLE_CACHING = true;
+
+ const blobUrlCache = ENABLE_CACHING ? new Map() : null;
+ const processingElements = new WeakSet();
+ const processingSourceElements = new WeakSet();
+
+ function shouldInterceptUrl(src) {
+ if (!src || src.startsWith('blob:') || src.startsWith('data:')) {
+ return false;
+ }
+ if (src.startsWith('wails://')) {
+ return true;
+ }
+ if (src.startsWith('/') || (!src.includes('://') && !src.startsWith('//'))) {
+ return true;
+ }
+ if (src.startsWith(window.location.origin)) {
+ return true;
+ }
+ return false;
+ }
+
+ function toAbsoluteUrl(src) {
+ if (src.startsWith('wails://') || src.startsWith('http://') || src.startsWith('https://')) {
+ return src;
+ }
+ if (src.startsWith('/')) {
+ return window.location.origin + src;
+ }
+ const base = window.location.href.substring(0, window.location.href.lastIndexOf('/') + 1);
+ return base + src;
+ }
+
+ async function convertToBlob(url) {
+ const absoluteUrl = toAbsoluteUrl(url);
+ if (ENABLE_CACHING && blobUrlCache.has(absoluteUrl)) {
+ return blobUrlCache.get(absoluteUrl);
+ }
+ const response = await fetch(absoluteUrl);
+ if (!response.ok) {
+ throw new Error('Failed to fetch media: ' + response.status + ' ' + response.statusText);
+ }
+ const blob = await response.blob();
+ const blobUrl = URL.createObjectURL(blob);
+ if (ENABLE_CACHING) {
+ blobUrlCache.set(absoluteUrl, blobUrl);
+ }
+ return blobUrl;
+ }
+
+ async function processSourceElement(source) {
+ if (processingSourceElements.has(source)) {
+ return;
+ }
+ const src = source.src || source.getAttribute('src');
+ if (!src || !shouldInterceptUrl(src)) {
+ return;
+ }
+ processingSourceElements.add(source);
+ try {
+ source.dataset.wailsOriginalSrc = src;
+ const blobUrl = await convertToBlob(src);
+ source.src = blobUrl;
+ console.debug('[Wails] Converted source element:', src);
+ } catch (err) {
+ console.error('[Wails] Failed to convert source element:', src, err);
+ }
+ }
+
+ async function processMediaElement(element) {
+ if (processingElements.has(element)) {
+ return;
+ }
+ const src = element.src || element.getAttribute('src');
+ if (src && shouldInterceptUrl(src)) {
+ processingElements.add(element);
+ try {
+ element.dataset.wailsOriginalSrc = src;
+ const blobUrl = await convertToBlob(src);
+ element.src = blobUrl;
+ console.debug('[Wails] Converted media element:', src);
+ } catch (err) {
+ console.error('[Wails] Failed to convert media element:', src, err);
+ }
+ }
+
+ const sources = element.querySelectorAll('source');
+ for (const source of sources) {
+ await processSourceElement(source);
+ }
+ if (sources.length > 0 && element.dataset.wailsOriginalSrc === undefined) {
+ element.load();
+ }
+ }
+
+ function scanForMediaElements() {
+ const mediaElements = document.querySelectorAll('video, audio');
+ mediaElements.forEach(function(element) {
+ processMediaElement(element);
+ });
+ }
+
+ function setupMutationObserver() {
+ const observer = new MutationObserver(function(mutations) {
+ for (const mutation of mutations) {
+ for (const node of mutation.addedNodes) {
+ if (node instanceof HTMLMediaElement) {
+ processMediaElement(node);
+ } else if (node instanceof Element) {
+ const mediaElements = node.querySelectorAll('video, audio');
+ mediaElements.forEach(function(el) {
+ processMediaElement(el);
+ });
+ }
+ }
+ if (mutation.type === 'attributes' &&
+ mutation.attributeName === 'src' &&
+ mutation.target instanceof HTMLMediaElement) {
+ processingElements.delete(mutation.target);
+ processMediaElement(mutation.target);
+ }
+ if (mutation.type === 'attributes' &&
+ mutation.attributeName === 'src' &&
+ mutation.target instanceof HTMLSourceElement) {
+ processingSourceElements.delete(mutation.target);
+ processSourceElement(mutation.target);
+ }
+ }
+ });
+
+ observer.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['src']
+ });
+ }
+
+ console.debug('[Wails] Enabling media interceptor for Linux/WebKitGTK');
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', scanForMediaElements);
+ } else {
+ scanForMediaElements();
+ }
+
+ setupMutationObserver();
+})();
diff --git a/v3/internal/assetserver/assetserver_windows.go b/v3/internal/assetserver/assetserver_windows.go
index 22deda4d2..015e4c21f 100644
--- a/v3/internal/assetserver/assetserver_windows.go
+++ b/v3/internal/assetserver/assetserver_windows.go
@@ -1,3 +1,5 @@
+//go:build windows
+
package assetserver
import "net/url"
@@ -6,3 +8,6 @@ var baseURL = url.URL{
Scheme: "http",
Host: "wails.localhost",
}
+
+// platformJS is empty on windows - no platform-specific JS needed.
+var platformJS []byte
diff --git a/v3/internal/assetserver/bundled_assetserver.go b/v3/internal/assetserver/bundled_assetserver.go
index 15297cd37..fa1203acc 100644
--- a/v3/internal/assetserver/bundled_assetserver.go
+++ b/v3/internal/assetserver/bundled_assetserver.go
@@ -1,10 +1,8 @@
package assetserver
import (
- "github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
"io/fs"
"net/http"
- "strings"
)
type BundledAssetServer struct {
@@ -18,16 +16,5 @@ func NewBundledAssetFileServer(fs fs.FS) *BundledAssetServer {
}
func (b *BundledAssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
- if strings.HasPrefix(req.URL.Path, "/wails/") {
- // Strip the /wails prefix
- req.URL.Path = req.URL.Path[6:]
- switch req.URL.Path {
- case "/runtime.js":
- rw.Header().Set("Content-Type", "application/javascript")
- rw.Write([]byte(bundledassets.RuntimeJS))
- return
- }
- return
- }
b.handler.ServeHTTP(rw, req)
}
diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
index 4a6f74c46..84874ac5d 100644
--- a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
+++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
@@ -70,4 +70,20 @@ window._wails.invoke = System.invoke;
// Binding ensures 'this' correctly refers to the current window instance
window._wails.handlePlatformFileDrop = Window.HandlePlatformFileDrop.bind(Window);
-System.invoke("wails:runtime:ready");
+// Load platform-specific code before signaling ready
+// This allows the backend to inject OS-specific fixes (e.g., Linux media interceptor)
+fetch("/wails/platform.js")
+ .then(response => response.text())
+ .then(code => {
+ if (code && code.trim().length > 0) {
+ const script = document.createElement("script");
+ script.textContent = code;
+ document.head.appendChild(script);
+ }
+ })
+ .catch(err => {
+ console.debug("[Wails] No platform-specific code to load:", err);
+ })
+ .finally(() => {
+ System.invoke("wails:runtime:ready");
+ });
diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go
index f0e14f47d..cc1ebb9e6 100644
--- a/v3/pkg/application/application_linux.go
+++ b/v3/pkg/application/application_linux.go
@@ -24,6 +24,7 @@ import (
"path/filepath"
"github.com/godbus/dbus/v5"
+ "github.com/wailsapp/wails/v3/internal/assetserver"
"github.com/wailsapp/wails/v3/internal/operatingsystem"
"github.com/wailsapp/wails/v3/pkg/events"
)
@@ -285,6 +286,12 @@ func newPlatformApp(parent *App) *linuxApp {
setProgramName(parent.options.Linux.ProgramName)
}
+ // Configure GStreamer workaround options for the asset server
+ assetserver.SetGStreamerOptions(
+ parent.options.Linux.DisableGStreamerFix,
+ parent.options.Linux.EnableGStreamerCaching,
+ )
+
return app
}
diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go
index 5e9eb57e2..3ca9f05f7 100644
--- a/v3/pkg/application/application_options.go
+++ b/v3/pkg/application/application_options.go
@@ -239,6 +239,17 @@ type LinuxOptions struct {
//
//[see the docs]: https://docs.gtk.org/glib/func.set_prgname.html
ProgramName string
+
+ // DisableGStreamerFix disables the workaround for GStreamer not supporting wails:// URLs.
+ // When false (default), video and audio elements with wails:// URLs are automatically
+ // converted to blob URLs via fetch() to enable media playback on WebKitGTK.
+ // See: https://bugs.webkit.org/show_bug.cgi?id=146351
+ DisableGStreamerFix bool
+
+ // EnableGStreamerCaching enables caching of blob URLs for the GStreamer fix.
+ // When true, blob URLs are cached to avoid re-fetching the same media.
+ // Default is false since the data is already in memory on the server side.
+ EnableGStreamerCaching bool
}
/********* iOS Options *********/