Folder: webcam-tauri-v2-ws-template
Tauri: 2.x
Renderer: Raw WebGL 1 / GLSL ES 1.0
Windows: Dedicated controls WebView + dedicated renderer WebView
Transport: Embedded loopback WebSocket relay on port 2727
Example 07 separates camera control from camera ownership.
The controls window chooses a device, changes uniforms, pauses the renderer, and requests fullscreen. Those instructions travel through a Rust WebSocket relay. The canvas window receives the instructions and performs every media and graphics operation locally:
Controls WebView
sliders / toggles / camera intent
│
│ JSON text messages
▼
Rust WebSocket relay · ws://127.0.0.1:2727
│
▼
Canvas WebView
getUserMedia permission
↓
hidden HTMLVideoElement
↓ decoded frame
texImage2D camera upload
↓
GLSL effect pass
↓
ping-pong feedback FBOs
↓
blit pass → visible canvas
│
└── telemetry → relay → controls
The MediaStream never crosses the WebSocket. A browser camera stream cannot be transferred from one Tauri WebView to another. The renderer must own the camera, video element, WebGL context, texture, and feedback history.
Example 06 teaches camera-to-texture processing in one window. Example 07 adds a realistic remote-control boundary without hiding the browser media pipeline.
It demonstrates:
src-tauri/src/main.rs starts two loopback listeners:
127.0.0.1:2727[::1]:2727Each WebView identifies itself with a hello message. The relay broadcasts text messages to every other client. It does not interpret camera frames or graphics data.
The same Rust file exposes toggle_canvas_fullscreen, which retrieves the configured canvas WebView with get_webview_window() and toggles native fullscreen.
src/controls.js owns the authoritative user-facing state:
When the canvas reconnects, the controls window resends the complete state and requests device enumeration. If camera intent was active, it asks the new canvas instance to start the selected device.
src/canvas.js owns:
navigator.mediaDevices;MediaStream;Important message families:
controls → canvas
state-sync complete parameter and pause state
param one changed uniform
camera-command enumerate, start, or stop
action pause, feedback reset, or fullscreen
canvas → controls
canvas-ready renderer is available for state restoration
camera-devices device IDs and labels
camera-status running, stopped, ended, or error
runtime renderer state
diagnostic one telemetry field
error-log camera, WebGL, or Tauri error details
paused-state authoritative renderer pause state
renderer-state canvas-keyboard reset reflected in controls
Slider traffic is coalesced with requestAnimationFrame, preventing a WebSocket message for every tiny pointer event.
The renderer compiles:
The effect pass writes to the current feedback framebuffer. The blit pass displays that already-processed texture without applying color correction a second time.
The six effect branches are:
webcam-tauri-v2-ws-template/
├── README.md
├── package.json
├── package-lock.json
├── src/
│ ├── controls.html
│ ├── controls.js
│ ├── canvas.html
│ ├── canvas.js
│ └── styles.css
└── src-tauri/
├── Cargo.toml
├── Cargo.lock
├── Info.plist
├── entitlements.plist
├── capabilities/
│ └── main-capability.json
├── src/
│ └── main.rs
└── tauri.conf.json
cd webcam-tauri-v2-ws-template
npm install
npm run dev
The controls and canvas windows open automatically. Start the camera from the controls window. The operating-system permission prompt belongs to the canvas window because that WebView calls getUserMedia().
npm run build
Tauri writes platform bundles under:
src-tauri/target/release/bundle/
Unsigned macOS applications may be blocked by Gatekeeper when distributed to another computer. Production distribution normally requires code signing and notarization.
Keyboard shortcuts in either window:
Space pause / resume
R reset parameters or renderer feedback
C start / stop camera
F toggle canvas fullscreen
The controls window displays telemetry generated by the renderer:
Render FPS and texture-upload FPS are intentionally separate. A 60 Hz render loop may repeatedly display a 30 fps camera, but the renderer should upload only newly decoded video frames.
The controls window is the authority for parameters and camera intent. The renderer requests a complete state after opening or reconnecting.
capabilities/main-capability.json.window.__TAURI__.core.invoke() for native fullscreen.get_webview_window("canvas").[workspace] resolver = "2" isolates this standalone project from parent Cargo workspaces.Info.plist includes NSCameraUsageDescription for macOS.entitlements.plist enables the macOS camera entitlement.NotReadableError.2 to avoid excessive framebuffer allocation.