fix(linux): fix video and audio playback on WebKitGTK (#4412)

On Linux, WebKitGTK uses GStreamer for media playback. GStreamer doesn't
have a URI handler for the wails:// protocol, causing video and audio
elements to fail loading from bundled assets.

This fix automatically intercepts media elements and converts wails://
URLs to blob URLs by fetching content through fetch() (which works via
WebKit's URI scheme handler) and creating object URLs.

Changes:
- Add platform-specific JS injection via /wails/platform.js endpoint
- Move /wails/* route handling from BundledAssetServer to main AssetServer
  so it works regardless of user's asset handler choice
- Add DisableGStreamerFix and EnableGStreamerCaching options to LinuxOptions
- Update runtime to fetch /wails/platform.js before emitting ready event
- Add audio-video example demonstrating media playback with npm runtime
- Add Linux media playback documentation
- Add GStreamer codec dependencies to installation docs

See: https://bugs.webkit.org/show_bug.cgi?id=146351

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lea Anthony 2025-12-13 22:00:27 +11:00
commit cb377cc29f
27 changed files with 1716 additions and 15 deletions

View file

@ -0,0 +1,189 @@
---
title: Linux Media Playback
sidebar:
order: 12
---
import {Badge} from '@astrojs/starlight/components';
Relevant Platforms: <Badge text="Linux" variant="note" />
<br/>
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 `<video>` or `<audio>` 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 `<video>` and `<audio>` 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:
- `<video>` elements with `src` attributes
- `<audio>` elements with `src` attributes
- `<source>` elements inside `<video>` and `<audio>` 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
<video controls>
<source src="/video.mp4" type="video/mp4">
</video>
<audio controls>
<source src="/audio.ogg" type="audio/ogg">
</audio>
```
### 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,
},
```

View file

@ -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
```
</TabItem>
<TabItem label="Fedora">
```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
```
</TabItem>
<TabItem label="Arch">
```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
```
</TabItem>
<TabItem label="Other">

View file

@ -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
<!-- New features, capabilities, or enhancements -->
## Changed
<!-- Changes in existing functionality -->
## Fixed
- Fix video and audio playback on Linux/WebKitGTK by intercepting media elements and converting wails:// URLs to blob URLs (#4412) by @leaanthony
<!-- Bug fixes -->
## Deprecated

View file

@ -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 .
```

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio/Video Example</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
padding: 20px;
}
h1 { text-align: center; margin-bottom: 20px; color: #00d4ff; }
.container { max-width: 800px; margin: 0 auto; }
.section {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.section h2 { margin-bottom: 15px; color: #00d4ff; }
video, audio { width: 100%; border-radius: 8px; background: #000; }
video { max-height: 400px; }
.status {
margin-top: 10px;
padding: 8px 12px;
border-radius: 6px;
font-family: monospace;
}
.status.success { background: rgba(0, 255, 0, 0.2); color: #66ff66; }
.status.error { background: rgba(255, 0, 0, 0.2); color: #ff6666; }
.status.pending { background: rgba(255, 255, 0, 0.2); color: #ffff66; }
</style>
<script type="module" crossorigin src="/assets/index-DxRb0nj9.js"></script>
</head>
<body>
<div class="container">
<h1>Audio/Video Example</h1>
<div class="section">
<h2>Video</h2>
<video id="video" controls>
<source src="/wails.mp4" type="video/mp4">
</video>
<div id="video-status" class="status pending">Loading...</div>
</div>
<div class="section">
<h2>Audio</h2>
<audio id="audio" controls>
<source src="/wails.mp3" type="audio/mpeg">
</audio>
<div id="audio-status" class="status pending">Loading...</div>
</div>
</div>
</body>
</html>

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio/Video Example</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
padding: 20px;
}
h1 { text-align: center; margin-bottom: 20px; color: #00d4ff; }
.container { max-width: 800px; margin: 0 auto; }
.section {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.section h2 { margin-bottom: 15px; color: #00d4ff; }
video, audio { width: 100%; border-radius: 8px; background: #000; }
video { max-height: 400px; }
.status {
margin-top: 10px;
padding: 8px 12px;
border-radius: 6px;
font-family: monospace;
}
.status.success { background: rgba(0, 255, 0, 0.2); color: #66ff66; }
.status.error { background: rgba(255, 0, 0, 0.2); color: #ff6666; }
.status.pending { background: rgba(255, 255, 0, 0.2); color: #ffff66; }
</style>
</head>
<body>
<div class="container">
<h1>Audio/Video Example</h1>
<div class="section">
<h2>Video</h2>
<video id="video" controls>
<source src="/wails.mp4" type="video/mp4">
</video>
<div id="video-status" class="status pending">Loading...</div>
</div>
<div class="section">
<h2>Audio</h2>
<audio id="audio" controls>
<source src="/wails.mp3" type="audio/mpeg">
</audio>
<div id="audio-status" class="status pending">Loading...</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -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
}
}
}
}
}

View file

@ -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"
}
}

Binary file not shown.

Binary file not shown.

View file

@ -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);

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
export default defineConfig({
build: {
outDir: 'dist',
emptyOutDir: true,
}
})

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();
})();

View file

@ -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

View file

@ -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)
}

View file

@ -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");
});

View file

@ -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
}

View file

@ -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 *********/