Remove the legacy Tauri backend (src-tauri)

The shipping product is the pure-Rust app in lightningbeam-ui/; the old
JavaScript UI in src/ is browser-only (window.__TAURI__ is shimmed by
src/tauri_polyfill.js), so the src-tauri Tauri backend is dead. It also no
longer compiled (mid-refactor: missing video_server module, handler list
referencing deleted commands), and CI only ever built lightningbeam-ui.

- Delete src-tauri/ entirely (commands, native renderer/render-window,
  websocket frame streamers, Tauri config, generated schemas, icons).
- Relocate icon.icns into lightningbeam-ui/lightningbeam-editor/assets/icons/
  (the only src-tauri asset still needed) and repoint the editor's window-icon
  include_bytes! at assets/icons/256x256.png (was src-tauri/icons/icon.png).
- CI/packaging: drop the redundant icon-copy steps (PNGs are committed in the
  editor assets) and point macOS bundling at the relocated icns.
- package.json: drop @tauri-apps/* deps and the tauri script.
- Docs/.gitignore: drop src-tauri references.

daw-backend/ (shared audio engine) and the top-level WASM lightningbeam-core/
are untouched. Editor builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Skyler Lehmkuhl 2026-06-21 18:25:39 -04:00
parent 2c40858663
commit a3f18bdab6
46 changed files with 5 additions and 11125 deletions

View File

@ -130,14 +130,6 @@ jobs:
# LLVM/libclang for bindgen
echo "LIBCLANG_PATH=C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Setup icons
shell: bash
run: |
mkdir -p lightningbeam-ui/lightningbeam-editor/assets/icons
cp -f src-tauri/icons/32x32.png lightningbeam-ui/lightningbeam-editor/assets/icons/
cp -f src-tauri/icons/128x128.png lightningbeam-ui/lightningbeam-editor/assets/icons/
cp -f src-tauri/icons/icon.png lightningbeam-ui/lightningbeam-editor/assets/icons/256x256.png
- name: Stage factory presets
shell: bash
run: |
@ -327,7 +319,7 @@ jobs:
mkdir -p "$APP/Contents/Resources/presets"
cp lightningbeam-ui/target/release/lightningbeam-editor "$APP/Contents/MacOS/"
cp src-tauri/icons/icon.icns "$APP/Contents/Resources/lightningbeam-editor.icns"
cp lightningbeam-ui/lightningbeam-editor/assets/icons/icon.icns "$APP/Contents/Resources/lightningbeam-editor.icns"
cp -r lightningbeam-ui/lightningbeam-editor/assets/presets/* "$APP/Contents/Resources/presets/"
cat > "$APP/Contents/Info.plist" << EOF

2
.gitignore vendored
View File

@ -28,8 +28,6 @@ dist-ssr
.gitignore
# Build
src-tauri/gen
src-tauri/target
lightningbeam-core/target
daw-backend/target
target/

View File

@ -486,8 +486,7 @@ lightningbeam-2/
│ ├── synth/ # Synthesizers
│ └── midi/ # MIDI handling
├── src-tauri/ # Legacy Tauri backend
├── src/ # Legacy JavaScript frontend
├── src/ # Legacy JavaScript frontend (browser-only)
├── CONTRIBUTING.md # Contributor guide
├── ARCHITECTURE.md # This file
├── README.md # Project overview

View File

@ -122,8 +122,7 @@ lightningbeam-2/
│ │ ├── track.rs # Track management
│ │ └── project.rs # Project state
│ └── effects/ # Audio effects
├── src-tauri/ # Legacy Tauri backend
└── src/ # Legacy JavaScript frontend
└── src/ # Legacy JavaScript frontend (browser-only)
```
## Making Changes

View File

@ -140,7 +140,7 @@ fn main() -> eframe::Result {
}
// Load window icon
let icon_data = include_bytes!("../../../src-tauri/icons/icon.png");
let icon_data = include_bytes!("../assets/icons/256x256.png");
let icon_image = match image::load_from_memory(icon_data) {
Ok(img) => {
let rgba = img.to_rgba8();

View File

@ -4,12 +4,10 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"tauri": "tauri",
"test": "wdio run wdio.conf.js",
"test:watch": "wdio run wdio.conf.js --watch"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@wdio/cli": "^9.20.0",
"@wdio/globals": "^9.17.0",
"@wdio/local-runner": "8",
@ -19,9 +17,6 @@
},
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.10",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-log": "^2.2.0",
"ffmpeg": "^0.0.4",
"ffmpeg.js": "^4.2.9003"
}

View File

@ -20,9 +20,7 @@ pkgbuild: setup-icons
setup-icons:
@echo "==> Setting up icons..."
@mkdir -p $(EDITOR)/assets/icons
@cp -u $(REPO_ROOT)/src-tauri/icons/32x32.png $(EDITOR)/assets/icons/ 2>/dev/null || true
@cp -u $(REPO_ROOT)/src-tauri/icons/128x128.png $(EDITOR)/assets/icons/ 2>/dev/null || true
@cp -u $(REPO_ROOT)/src-tauri/icons/icon.png $(EDITOR)/assets/icons/256x256.png 2>/dev/null || true
@# Icons are committed under $(EDITOR)/assets/icons (32x32, 128x128, 256x256, icon.icns).
@echo " Icons ready"
clean:

6708
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +0,0 @@
[package]
name = "lightningbeam"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "lightningbeam_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
log = "0.4"
env_logger = "0.11"
# DAW backend integration
daw-backend = { path = "../daw-backend" }
cpal = "0.15"
rtrb = "0.3"
tokio = { version = "1", features = ["sync", "time"] }
# Video decoding
lru = "0.12"
# WebSocket for frame streaming (disable default features to remove tracing, but keep handshake)
tungstenite = { version = "0.20", default-features = false, features = ["handshake"] }
# Native rendering with wgpu
wgpu = "0.19"
winit = "0.29"
pollster = "0.3"
bytemuck = { version = "1.14", features = ["derive"] }
raw-window-handle = "0.6"
image = "0.24"
[target.'cfg(target_os = "macos")'.dependencies]
ffmpeg-next = { version = "8.0", features = ["build"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
ffmpeg-next = "8.0"
[profile.dev]
opt-level = 1 # Enable basic optimizations in debug mode for audio decoding performance
[profile.release]
opt-level = 3
lto = true

View File

@ -1,98 +0,0 @@
{
"metadata": {
"name": "Basic Sine",
"description": "Simple sine wave synthesizer with ADSR envelope. Great for learning the basics of subtractive synthesis.",
"author": "Lightningbeam",
"version": 1,
"tags": ["basic", "lead", "mono"]
},
"nodes": [
{
"id": 0,
"node_type": "MidiInput",
"parameters": {},
"position": [100.0, 200.0]
},
{
"id": 1,
"node_type": "MidiToCV",
"parameters": {},
"position": [300.0, 200.0]
},
{
"id": 2,
"node_type": "Oscillator",
"parameters": {
"0": 440.0,
"1": 0.7,
"2": 0.0
},
"position": [500.0, 150.0]
},
{
"id": 3,
"node_type": "ADSR",
"parameters": {
"0": 0.01,
"1": 0.1,
"2": 0.7,
"3": 0.3
},
"position": [500.0, 300.0]
},
{
"id": 4,
"node_type": "Gain",
"parameters": {
"0": 1.0
},
"position": [700.0, 200.0]
},
{
"id": 5,
"node_type": "AudioOutput",
"parameters": {},
"position": [900.0, 200.0]
}
],
"connections": [
{
"from_node": 0,
"from_port": 0,
"to_node": 1,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 2,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 3,
"to_port": 0
},
{
"from_node": 2,
"from_port": 0,
"to_node": 4,
"to_port": 0
},
{
"from_node": 3,
"from_port": 0,
"to_node": 4,
"to_port": 1
},
{
"from_node": 4,
"from_port": 0,
"to_node": 5,
"to_port": 0
}
],
"midi_targets": [0],
"output_node": 5
}

View File

@ -1,137 +0,0 @@
{
"metadata": {
"name": "Pluck",
"description": "Percussive pluck sound with fast attack and decay. Great for arpeggios, melodies, and rhythmic patterns.",
"author": "Lightningbeam",
"version": 1,
"tags": ["pluck", "lead", "percussive", "arpeggio"]
},
"nodes": [
{
"id": 0,
"node_type": "MidiInput",
"parameters": {},
"position": [100.0, 250.0]
},
{
"id": 1,
"node_type": "MidiToCV",
"parameters": {},
"position": [300.0, 250.0]
},
{
"id": 2,
"node_type": "Oscillator",
"parameters": {
"0": 440.0,
"1": 0.6,
"2": 2.0
},
"position": [500.0, 150.0]
},
{
"id": 3,
"node_type": "Filter",
"parameters": {
"0": 2000.0,
"1": 0.8,
"2": 0.0
},
"position": [700.0, 150.0]
},
{
"id": 4,
"node_type": "ADSR",
"parameters": {
"0": 0.001,
"1": 0.3,
"2": 0.0,
"3": 0.05
},
"position": [500.0, 350.0]
},
{
"id": 5,
"node_type": "ADSR",
"parameters": {
"0": 0.001,
"1": 0.4,
"2": 0.0,
"3": 0.1
},
"position": [700.0, 350.0]
},
{
"id": 6,
"node_type": "Gain",
"parameters": {
"0": 1.0
},
"position": [900.0, 200.0]
},
{
"id": 7,
"node_type": "AudioOutput",
"parameters": {},
"position": [1100.0, 200.0]
}
],
"connections": [
{
"from_node": 0,
"from_port": 0,
"to_node": 1,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 2,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 4,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 5,
"to_port": 0
},
{
"from_node": 2,
"from_port": 0,
"to_node": 3,
"to_port": 0
},
{
"from_node": 4,
"from_port": 0,
"to_node": 3,
"to_port": 1
},
{
"from_node": 3,
"from_port": 0,
"to_node": 6,
"to_port": 0
},
{
"from_node": 5,
"from_port": 0,
"to_node": 6,
"to_port": 1
},
{
"from_node": 6,
"from_port": 0,
"to_node": 7,
"to_port": 0
}
],
"midi_targets": [0],
"output_node": 7
}

View File

@ -1,145 +0,0 @@
{
"metadata": {
"name": "Poly Synth",
"description": "8-voice polyphonic synthesizer with sawtooth oscillator and ADSR envelope. Perfect for chords and complex harmonies.",
"author": "Lightningbeam",
"version": 1,
"tags": ["poly", "polyphonic", "synth", "chords"]
},
"nodes": [
{
"id": 0,
"node_type": "MidiInput",
"parameters": {},
"position": [100.0, 200.0]
},
{
"id": 1,
"node_type": "VoiceAllocator",
"parameters": {
"0": 8.0
},
"position": [400.0, 200.0],
"template_graph": {
"metadata": {
"name": "template",
"description": "",
"author": "",
"version": 1,
"tags": []
},
"nodes": [
{
"id": 0,
"node_type": "TemplateInput",
"parameters": {},
"position": [100.0, 200.0]
},
{
"id": 1,
"node_type": "MidiToCV",
"parameters": {},
"position": [300.0, 200.0]
},
{
"id": 2,
"node_type": "Oscillator",
"parameters": {
"0": 440.0,
"1": 0.7,
"2": 1.0
},
"position": [500.0, 150.0]
},
{
"id": 3,
"node_type": "ADSR",
"parameters": {
"0": 0.01,
"1": 0.2,
"2": 0.6,
"3": 0.3
},
"position": [500.0, 300.0]
},
{
"id": 4,
"node_type": "Gain",
"parameters": {
"0": 1.0
},
"position": [700.0, 200.0]
},
{
"id": 5,
"node_type": "TemplateOutput",
"parameters": {},
"position": [900.0, 200.0]
}
],
"connections": [
{
"from_node": 0,
"from_port": 0,
"to_node": 1,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 2,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 3,
"to_port": 0
},
{
"from_node": 2,
"from_port": 0,
"to_node": 4,
"to_port": 0
},
{
"from_node": 3,
"from_port": 0,
"to_node": 4,
"to_port": 1
},
{
"from_node": 4,
"from_port": 0,
"to_node": 5,
"to_port": 0
}
],
"midi_targets": [],
"output_node": 5
}
},
{
"id": 2,
"node_type": "AudioOutput",
"parameters": {},
"position": [700.0, 200.0]
}
],
"connections": [
{
"from_node": 0,
"from_port": 0,
"to_node": 1,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 2,
"to_port": 0
}
],
"midi_targets": [0],
"output_node": 2
}

View File

@ -1,137 +0,0 @@
{
"metadata": {
"name": "Sawtooth Bass",
"description": "Classic analog-style bass synth with sawtooth oscillator and resonant lowpass filter. Perfect for electronic music basslines.",
"author": "Lightningbeam",
"version": 1,
"tags": ["bass", "analog", "electronic", "mono"]
},
"nodes": [
{
"id": 0,
"node_type": "MidiInput",
"parameters": {},
"position": [100.0, 250.0]
},
{
"id": 1,
"node_type": "MidiToCV",
"parameters": {},
"position": [300.0, 250.0]
},
{
"id": 2,
"node_type": "Oscillator",
"parameters": {
"0": 110.0,
"1": 0.8,
"2": 1.0
},
"position": [500.0, 150.0]
},
{
"id": 3,
"node_type": "Filter",
"parameters": {
"0": 800.0,
"1": 2.5,
"2": 0.0
},
"position": [700.0, 150.0]
},
{
"id": 4,
"node_type": "ADSR",
"parameters": {
"0": 0.005,
"1": 0.2,
"2": 0.3,
"3": 0.1
},
"position": [500.0, 300.0]
},
{
"id": 5,
"node_type": "ADSR",
"parameters": {
"0": 0.005,
"1": 0.15,
"2": 0.6,
"3": 0.2
},
"position": [700.0, 350.0]
},
{
"id": 6,
"node_type": "Gain",
"parameters": {
"0": 1.2
},
"position": [900.0, 200.0]
},
{
"id": 7,
"node_type": "AudioOutput",
"parameters": {},
"position": [1100.0, 200.0]
}
],
"connections": [
{
"from_node": 0,
"from_port": 0,
"to_node": 1,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 2,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 4,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 5,
"to_port": 0
},
{
"from_node": 2,
"from_port": 0,
"to_node": 3,
"to_port": 0
},
{
"from_node": 4,
"from_port": 0,
"to_node": 3,
"to_port": 1
},
{
"from_node": 3,
"from_port": 0,
"to_node": 6,
"to_port": 0
},
{
"from_node": 5,
"from_port": 0,
"to_node": 6,
"to_port": 1
},
{
"from_node": 6,
"from_port": 0,
"to_node": 7,
"to_port": 0
}
],
"midi_targets": [0],
"output_node": 7
}

View File

@ -1,176 +0,0 @@
{
"metadata": {
"name": "Warm Pad",
"description": "Lush pad sound combining sawtooth and triangle waves with slow filter sweep and gentle attack. Ideal for ambient and cinematic music.",
"author": "Lightningbeam",
"version": 1,
"tags": ["pad", "ambient", "warm", "cinematic"]
},
"nodes": [
{
"id": 0,
"node_type": "MidiInput",
"parameters": {},
"position": [100.0, 300.0]
},
{
"id": 1,
"node_type": "MidiToCV",
"parameters": {},
"position": [300.0, 300.0]
},
{
"id": 2,
"node_type": "Oscillator",
"parameters": {
"0": 440.0,
"1": 0.5,
"2": 1.0
},
"position": [500.0, 150.0]
},
{
"id": 3,
"node_type": "Oscillator",
"parameters": {
"0": 440.0,
"1": 0.4,
"2": 3.0
},
"position": [500.0, 250.0]
},
{
"id": 4,
"node_type": "Mixer",
"parameters": {
"0": 0.5,
"1": 0.5,
"2": 0.0,
"3": 0.0
},
"position": [700.0, 200.0]
},
{
"id": 5,
"node_type": "Filter",
"parameters": {
"0": 1200.0,
"1": 1.0,
"2": 0.0
},
"position": [900.0, 200.0]
},
{
"id": 6,
"node_type": "ADSR",
"parameters": {
"0": 0.8,
"1": 1.0,
"2": 0.6,
"3": 1.5
},
"position": [700.0, 400.0]
},
{
"id": 7,
"node_type": "ADSR",
"parameters": {
"0": 0.5,
"1": 0.5,
"2": 0.8,
"3": 1.0
},
"position": [900.0, 400.0]
},
{
"id": 8,
"node_type": "Gain",
"parameters": {
"0": 0.8
},
"position": [1100.0, 250.0]
},
{
"id": 9,
"node_type": "AudioOutput",
"parameters": {},
"position": [1300.0, 250.0]
}
],
"connections": [
{
"from_node": 0,
"from_port": 0,
"to_node": 1,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 2,
"to_port": 0
},
{
"from_node": 1,
"from_port": 0,
"to_node": 3,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 6,
"to_port": 0
},
{
"from_node": 1,
"from_port": 1,
"to_node": 7,
"to_port": 0
},
{
"from_node": 2,
"from_port": 0,
"to_node": 4,
"to_port": 0
},
{
"from_node": 3,
"from_port": 0,
"to_node": 4,
"to_port": 1
},
{
"from_node": 4,
"from_port": 0,
"to_node": 5,
"to_port": 0
},
{
"from_node": 6,
"from_port": 0,
"to_node": 5,
"to_port": 1
},
{
"from_node": 5,
"from_port": 0,
"to_node": 8,
"to_port": 0
},
{
"from_node": 7,
"from_port": 0,
"to_node": 8,
"to_port": 1
},
{
"from_node": 8,
"from_port": 0,
"to_node": 9,
"to_port": 0
}
],
"midi_targets": [0],
"output_node": 9
}

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,77 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main",
"window*"
],
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-set-title",
"shell:allow-open",
"fs:default",
{
"identifier": "fs:allow-exists",
"allow": [
{
"path": "$HOME/*"
},
{
"path": "$DOCUMENT/*"
},
{
"path": "$DOWNLOAD/*"
},
{
"path": "$DESKTOP/*"
},
{
"path": "**/*"
}
]
},
{
"identifier": "fs:allow-app-write-recursive",
"allow": [
{
"path": "$HOME/*"
},
{
"path": "$DOCUMENT/*"
},
{
"path": "$DOWNLOAD/*"
},
{
"path": "$DESKTOP/*"
},
{
"path": "**/*"
}
]
},
{
"identifier": "fs:allow-app-read-recursive",
"allow": [
{
"path": "$HOME/*"
},
{
"path": "$DOCUMENT/*"
},
{
"path": "$DOWNLOAD/*"
},
{
"path": "$DESKTOP/*"
},
{
"path": "**/*"
}
]
},
"dialog:default"
]
}

View File

@ -1,104 +0,0 @@
extern crate ffmpeg_next as ffmpeg;
use std::env;
fn main() {
ffmpeg::init().unwrap();
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <video_file>", args[0]);
std::process::exit(1);
}
let path = &args[1];
let input = ffmpeg::format::input(path).expect("Failed to open video");
println!("=== VIDEO FILE INFORMATION ===");
println!("File: {}", path);
println!("Format: {}", input.format().name());
println!("Duration: {:.2}s", input.duration() as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE));
println!();
let video_stream = input.streams()
.best(ffmpeg::media::Type::Video)
.expect("No video stream found");
let stream_index = video_stream.index();
let time_base = f64::from(video_stream.time_base());
let duration = video_stream.duration() as f64 * time_base;
let fps = f64::from(video_stream.avg_frame_rate());
println!("=== VIDEO STREAM ===");
println!("Stream index: {}", stream_index);
println!("Time base: {} ({:.10})", video_stream.time_base(), time_base);
println!("Duration: {:.2}s", duration);
println!("FPS: {:.2}", fps);
println!("Frames: {}", video_stream.frames());
let context = ffmpeg::codec::context::Context::from_parameters(video_stream.parameters())
.expect("Failed to create context");
let decoder = context.decoder().video().expect("Failed to create decoder");
println!("Codec: {:?}", decoder.id());
println!("Resolution: {}x{}", decoder.width(), decoder.height());
println!("Pixel format: {:?}", decoder.format());
println!();
println!("=== SCANNING FRAMES ===");
println!("Timestamp (ts) | Time (s) | Key | Type");
println!("---------------|----------|-----|-----");
let mut input = ffmpeg::format::input(path).expect("Failed to reopen video");
let context = ffmpeg::codec::context::Context::from_parameters(
input.streams().best(ffmpeg::media::Type::Video).unwrap().parameters()
).expect("Failed to create context");
let mut decoder = context.decoder().video().expect("Failed to create decoder");
let mut frame_count = 0;
let mut keyframe_count = 0;
for (stream, packet) in input.packets() {
if stream.index() == stream_index {
let packet_pts = packet.pts().unwrap_or(0);
let packet_time = packet_pts as f64 * time_base;
let is_key = packet.is_key();
if is_key {
keyframe_count += 1;
}
// Print first 50 packets and all keyframes
if frame_count < 50 || is_key {
println!("{:14} | {:8.2} | {:3} | {:?}",
packet_pts,
packet_time,
if is_key { "KEY" } else { " " },
if is_key { "I-frame" } else { "P/B-frame" }
);
}
decoder.send_packet(&packet).ok();
let mut frame = ffmpeg::util::frame::Video::empty();
while decoder.receive_frame(&mut frame).is_ok() {
frame_count += 1;
}
}
}
// Flush decoder
decoder.send_eof().ok();
let mut frame = ffmpeg::util::frame::Video::empty();
while decoder.receive_frame(&mut frame).is_ok() {
frame_count += 1;
}
println!();
println!("=== SUMMARY ===");
println!("Total frames decoded: {}", frame_count);
println!("Total keyframes: {}", keyframe_count);
if keyframe_count > 0 {
println!("Average keyframe interval: {:.2} frames", frame_count as f64 / keyframe_count as f64);
println!("Average keyframe interval: {:.2}s", duration / keyframe_count as f64);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +0,0 @@
use std::net::TcpListener;
use std::sync::{Arc, Mutex};
use std::thread;
use tungstenite::{accept, Message};
pub struct FrameStreamer {
port: u16,
clients: Arc<Mutex<Vec<tungstenite::WebSocket<std::net::TcpStream>>>>,
}
impl FrameStreamer {
pub fn new() -> Result<Self, String> {
// Bind to localhost on a random available port
let listener = TcpListener::bind("127.0.0.1:0")
.map_err(|e| format!("Failed to create WebSocket listener: {}", e))?;
let port = listener.local_addr()
.map_err(|e| format!("Failed to get listener address: {}", e))?
.port();
// eprintln!("[Frame Streamer] WebSocket server started on port {}", port);
let clients = Arc::new(Mutex::new(Vec::new()));
let clients_clone = clients.clone();
// Spawn acceptor thread
thread::spawn(move || {
for stream in listener.incoming() {
match stream {
Ok(stream) => {
// eprintln!("[Frame Streamer] New WebSocket connection from {:?}", stream.peer_addr());
match accept(stream) {
Ok(websocket) => {
let mut clients = clients_clone.lock().unwrap();
clients.push(websocket);
// eprintln!("[Frame Streamer] Client connected, total clients: {}", clients.len());
}
Err(_e) => {
// eprintln!("[Frame Streamer] Failed to accept WebSocket: {}", e);
}
}
}
Err(_e) => {
// eprintln!("[Frame Streamer] Failed to accept connection: {}", e);
}
}
}
});
Ok(Self {
port,
clients,
})
}
pub fn port(&self) -> u16 {
self.port
}
/// Send a decoded frame to all connected clients
/// Frame format: [pool_index: u32][timestamp_ms: u32][width: u32][height: u32][rgba_data...]
pub fn send_frame(&self, pool_index: usize, timestamp: f64, width: u32, height: u32, rgba_data: &[u8]) {
let mut clients = self.clients.lock().unwrap();
// Build frame message (rgba_data is already in RGBA format from decoder)
let mut frame_msg = Vec::with_capacity(16 + rgba_data.len());
frame_msg.extend_from_slice(&(pool_index as u32).to_le_bytes());
frame_msg.extend_from_slice(&((timestamp * 1000.0) as u32).to_le_bytes());
frame_msg.extend_from_slice(&width.to_le_bytes());
frame_msg.extend_from_slice(&height.to_le_bytes());
frame_msg.extend_from_slice(rgba_data);
// Send to all clients, remove disconnected ones
clients.retain_mut(|client| {
match client.write_message(Message::Binary(frame_msg.clone())) {
Ok(_) => true,
Err(_e) => {
// eprintln!("[Frame Streamer] Client disconnected: {}", e);
false
}
}
});
}
}

View File

@ -1,230 +0,0 @@
use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
State,
},
response::IntoResponse,
routing::get,
Router,
};
use flume::{Sender, unbounded};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::time::Duration;
/// Playback state for a video pool
#[derive(Clone, Debug)]
pub struct PlaybackState {
pub is_playing: bool,
pub target_fps: f64,
pub current_time: f64,
}
/// Shared server state
pub struct ServerState {
/// Connected clients
clients: RwLock<Vec<Sender<Vec<u8>>>>,
/// Playback state per pool index
playback_state: RwLock<HashMap<usize, PlaybackState>>,
}
impl ServerState {
pub fn new() -> Self {
Self {
clients: RwLock::new(Vec::new()),
playback_state: RwLock::new(HashMap::new()),
}
}
/// Register a new client
pub async fn add_client(&self, sender: Sender<Vec<u8>>) {
let mut clients = self.clients.write().await;
clients.push(sender);
eprintln!("[Async Frame Streamer] Client registered, total: {}", clients.len());
}
/// Broadcast a frame to all connected clients
pub async fn broadcast_frame(&self, frame_data: Vec<u8>) {
let clients = self.clients.read().await;
// Send to all clients
for client in clients.iter() {
// Non-blocking send, drop frame if client is slow
let _ = client.try_send(frame_data.clone());
}
}
/// Remove disconnected clients
pub async fn cleanup_clients(&self) {
let mut clients = self.clients.write().await;
clients.retain(|client| !client.is_disconnected());
eprintln!("[Async Frame Streamer] Cleaned up clients, remaining: {}", clients.len());
}
/// Update playback state for a pool
pub async fn set_playback_state(&self, pool_index: usize, state: PlaybackState) {
let mut states = self.playback_state.write().await;
states.insert(pool_index, state);
}
/// Get playback state for a pool
pub async fn get_playback_state(&self, pool_index: usize) -> Option<PlaybackState> {
let states = self.playback_state.read().await;
states.get(&pool_index).cloned()
}
}
pub struct AsyncFrameStreamer {
port: u16,
state: Arc<ServerState>,
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
}
impl AsyncFrameStreamer {
pub async fn new() -> Result<Self, String> {
let state = Arc::new(ServerState::new());
// Create router with WebSocket upgrade handler
let app_state = state.clone();
let app = Router::new()
.route("/ws", get(ws_handler))
.with_state(app_state);
// Bind to localhost on a random port
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.map_err(|e| format!("Failed to bind: {}", e))?;
let port = listener
.local_addr()
.map_err(|e| format!("Failed to get address: {}", e))?
.port();
eprintln!("[Async Frame Streamer] WebSocket server starting on port {}", port);
// Spawn server task
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
axum::serve(listener, app)
.with_graceful_shutdown(async {
shutdown_rx.await.ok();
})
.await
.expect("Server error");
});
eprintln!("[Async Frame Streamer] Server started");
Ok(Self {
port,
state,
shutdown_tx: Some(shutdown_tx),
})
}
pub fn port(&self) -> u16 {
self.port
}
/// Send a frame to all connected clients for a specific pool
/// Frame format: [pool_index: u32][timestamp_ms: u32][width: u32][height: u32][rgba_data...]
pub async fn send_frame(&self, pool_index: usize, timestamp: f64, width: u32, height: u32, rgba_data: &[u8]) {
// Build frame message
let mut frame_msg = Vec::with_capacity(16 + rgba_data.len());
frame_msg.extend_from_slice(&(pool_index as u32).to_le_bytes());
frame_msg.extend_from_slice(&((timestamp * 1000.0) as u32).to_le_bytes());
frame_msg.extend_from_slice(&width.to_le_bytes());
frame_msg.extend_from_slice(&height.to_le_bytes());
frame_msg.extend_from_slice(rgba_data);
// Broadcast to all connected clients
self.state.broadcast_frame(frame_msg).await;
}
/// Start streaming frames for a pool at a target FPS
pub async fn start_stream(&self, pool_index: usize, fps: f64) {
let state = PlaybackState {
is_playing: true,
target_fps: fps,
current_time: 0.0,
};
self.state.set_playback_state(pool_index, state).await;
eprintln!("[Async Frame Streamer] Started streaming pool {} at {} FPS", pool_index, fps);
}
/// Stop streaming frames for a pool
pub async fn stop_stream(&self, pool_index: usize) {
if let Some(mut state) = self.state.get_playback_state(pool_index).await {
state.is_playing = false;
self.state.set_playback_state(pool_index, state).await;
eprintln!("[Async Frame Streamer] Stopped streaming pool {}", pool_index);
}
}
/// Seek to a specific time in a pool
pub async fn seek(&self, pool_index: usize, timestamp: f64) {
if let Some(mut state) = self.state.get_playback_state(pool_index).await {
state.current_time = timestamp;
self.state.set_playback_state(pool_index, state).await;
}
}
}
impl Drop for AsyncFrameStreamer {
fn drop(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(());
}
}
}
/// WebSocket handler
async fn ws_handler(
ws: WebSocketUpgrade,
State(state): State<Arc<ServerState>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
/// Handle individual WebSocket connection
async fn handle_socket(mut socket: WebSocket, state: Arc<ServerState>) {
eprintln!("[Async Frame Streamer] New WebSocket connection");
// Create a channel for this client
let (tx, rx) = unbounded::<Vec<u8>>();
// Register this client
state.add_client(tx).await;
// Spawn task to send frames to this client
let mut rx = rx;
let mut send_task = tokio::spawn(async move {
while let Ok(frame) = rx.recv_async().await {
if socket.send(Message::Binary(frame)).await.is_err() {
eprintln!("[Async Frame Streamer] Failed to send frame to client");
break;
}
}
eprintln!("[Async Frame Streamer] Send task ended");
});
// Keep connection alive with ping/pong
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
tokio::select! {
_ = interval.tick() => {
// Connection alive, no need to ping in this simple implementation
}
_ = &mut send_task => {
eprintln!("[Async Frame Streamer] Send task completed, closing connection");
break;
}
}
}
// Cleanup
state.cleanup_clients().await;
}

View File

@ -1,346 +0,0 @@
use std::{path::PathBuf, sync::{Arc, Mutex}};
use tauri_plugin_log::{Target, TargetKind};
use log::{trace, info, debug, warn, error};
use tracing_subscriber::EnvFilter;
use chrono::Local;
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder};
mod audio;
mod video;
mod video_server;
#[derive(Default)]
struct AppState {
counter: u32,
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn trace(msg: String) {
trace!("{}",msg);
}
#[tauri::command]
fn info(msg: String) {
info!("{}",msg);
}
#[tauri::command]
fn debug(msg: String) {
debug!("{}",msg);
}
#[tauri::command]
fn warn(msg: String) {
warn!("{}",msg);
}
#[tauri::command]
fn error(msg: String) {
error!("{}",msg);
}
#[tauri::command]
async fn open_folder_dialog(app: AppHandle, title: String) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let folder = app.dialog()
.file()
.set_title(&title)
.blocking_pick_folder();
Ok(folder.map(|path| path.to_string()))
}
#[tauri::command]
async fn read_folder_files(path: String) -> Result<Vec<String>, String> {
use std::fs;
let entries = fs::read_dir(&path)
.map_err(|e| format!("Failed to read directory: {}", e))?;
let audio_extensions = vec!["wav", "aif", "aiff", "flac", "mp3", "ogg"];
let mut files = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if audio_extensions.contains(&ext_str.as_str()) {
if let Some(filename) = path.file_name() {
files.push(filename.to_string_lossy().to_string());
}
}
}
}
}
Ok(files)
}
use tauri::PhysicalSize;
#[tauri::command]
async fn create_window(app: tauri::AppHandle, path: Option<String>) {
let state = app.state::<Mutex<AppState>>();
// Lock the mutex to get mutable access:
let mut state = state.lock().unwrap();
// Increment the counter and generate a unique window label
let window_label = format!("window{}", state.counter);
state.counter += 1;
// Build the new window with the unique label
let webview_window = WebviewWindowBuilder::new(&app, &window_label, WebviewUrl::App("index.html".into()))
.title("Lightningbeam")
.build()
.unwrap();
// Get the current monitor's screen size from the new window
if let Ok(Some(monitor)) = webview_window.current_monitor() {
let screen_size = monitor.size(); // Get the size of the monitor
let width = 4096;
let height = 4096;
// Set the window size to be the smaller of the specified size or the screen size
let new_width = width.min(screen_size.width as u32);
let new_height = height.min(screen_size.height as u32 - 100);
// Set the size using PhysicalSize
webview_window.set_size(tauri::Size::Physical(PhysicalSize::new(new_width, new_height)))
.expect("Failed to set window size");
} else {
eprintln!("Could not detect the current monitor.");
}
// Set the opened file if provided
if let Some(val) = path {
// Pass path data to the window via JavaScript
webview_window.eval(&format!("window.openedFiles = [\"{val}\"]")).unwrap();
// Set the window title if provided
webview_window.set_title(&val).expect("Failed to set window title");
}
}
fn handle_file_associations(app: AppHandle, files: Vec<PathBuf>) {
// -- Scope handling start --
// You can remove this block if you only want to know about the paths, but not actually "use" them in the frontend.
// This requires the `fs` tauri plugin and is required to make the plugin's frontend work:
use tauri_plugin_fs::FsExt;
let fs_scope = app.fs_scope();
// This is for the `asset:` protocol to work:
let asset_protocol_scope = app.asset_protocol_scope();
for file in &files {
// This requires the `fs` plugin:
let _ = fs_scope.allow_file(file);
// This is for the `asset:` protocol:
let _ = asset_protocol_scope.allow_file(file);
}
// -- Scope handling end --
let files = files
.into_iter()
.map(|f| {
let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash
format!("\"{file}\"",) // wrap in quotes for JS array
})
.collect::<Vec<_>>()
.join(",");
warn!("{}",files);
let window = app.get_webview_window("main").unwrap();
window.eval(&format!("window.openedFiles = [{files}]")).unwrap();
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let pkg_name = env!("CARGO_PKG_NAME").to_string();
// Initialize video HTTP server
let video_server = video_server::VideoServer::new()
.expect("Failed to start video server");
eprintln!("[App] Video server started on port {}", video_server.port());
tauri::Builder::default()
.manage(Mutex::new(AppState::default()))
.manage(Arc::new(Mutex::new(audio::AudioState::default())))
.manage(Arc::new(Mutex::new(video::VideoState::default())))
.manage(Arc::new(Mutex::new(video_server)))
.setup(|app| {
#[cfg(any(windows, target_os = "linux"))] // Windows/Linux needs different handling from macOS
{
let mut files = Vec::new();
// NOTICE: `args` may include URL protocol (`your-app-protocol://`)
// or arguments (`--`) if your app supports them.
// files may also be passed as `file://path/to/file`
for maybe_file in std::env::args().skip(1) {
// skip flags like -f or --flag
if maybe_file.starts_with('-') {
continue;
}
// handle `file://` path urls and skip other urls
if let Ok(url) = Url::parse(&maybe_file) {
// if let Ok(url) = url::Url::parse(&maybe_file) {
if let Ok(path) = url.to_file_path() {
files.push(path);
}
} else {
files.push(PathBuf::from(maybe_file))
}
}
handle_file_associations(app.handle().clone(), files);
}
#[cfg(debug_assertions)] // only include this code on debug builds
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
window.close_devtools();
}
Ok(())
})
.plugin(
tauri_plugin_log::Builder::new()
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
.format(|out, message, record| {
let date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
out.finish(format_args!(
"{}[{}] {}",
date,
record.level(),
message
))
})
.targets([
Target::new(TargetKind::Stdout),
// LogDir locations:
// Linux: /home/user/.local/share/org.lightningbeam.core/logs
// macOS: /Users/user/Library/Logs/org.lightningbeam.core/logs
// Windows: C:\Users\user\AppData\Local\org.lightningbeam.core\logs
Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }),
Target::new(TargetKind::Webview),
])
.build()
)
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
greet, trace, debug, info, warn, error, create_window,
audio::audio_init,
audio::audio_reset,
audio::audio_play,
audio::audio_stop,
audio::set_metronome_enabled,
audio::audio_seek,
audio::audio_test_beep,
audio::audio_set_track_parameter,
audio::audio_create_track,
audio::audio_load_file,
audio::audio_add_clip,
audio::audio_move_clip,
audio::audio_trim_clip,
audio::audio_extend_clip,
audio::audio_start_recording,
audio::audio_stop_recording,
audio::audio_pause_recording,
audio::audio_resume_recording,
audio::audio_start_midi_recording,
audio::audio_stop_midi_recording,
audio::audio_create_midi_clip,
audio::audio_add_midi_note,
audio::audio_load_midi_file,
audio::audio_get_midi_clip_data,
audio::audio_update_midi_clip_notes,
audio::audio_send_midi_note_on,
audio::audio_send_midi_note_off,
audio::audio_set_active_midi_track,
audio::audio_get_pool_file_info,
audio::audio_get_pool_waveform,
audio::graph_add_node,
audio::graph_add_node_to_template,
audio::graph_remove_node,
audio::graph_connect,
audio::graph_connect_in_template,
audio::graph_disconnect,
audio::graph_set_parameter,
audio::graph_set_output_node,
audio::graph_save_preset,
audio::graph_load_preset,
audio::graph_load_preset_from_json,
audio::graph_list_presets,
audio::graph_delete_preset,
audio::graph_get_state,
audio::graph_get_template_state,
audio::sampler_load_sample,
audio::multi_sampler_add_layer,
audio::multi_sampler_get_layers,
audio::multi_sampler_update_layer,
audio::multi_sampler_remove_layer,
audio::get_oscilloscope_data,
audio::automation_add_keyframe,
audio::automation_remove_keyframe,
audio::automation_get_keyframes,
audio::automation_set_name,
audio::automation_get_name,
audio::audio_serialize_pool,
audio::audio_load_pool,
audio::audio_resolve_missing_file,
audio::audio_serialize_track_graph,
audio::audio_load_track_graph,
audio::audio_export,
video::video_load_file,
video::video_get_frame,
video::video_get_frames_batch,
video::video_set_cache_size,
open_folder_dialog,
read_folder_files,
video::video_get_pool_info,
video::video_ipc_benchmark,
video::video_get_transcode_status,
video::video_allow_asset,
])
// .manage(window_counter)
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(
#[allow(unused_variables)]
|app, event| {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let tauri::RunEvent::Opened { urls } = event {
let app = app.clone();
let files = urls
.into_iter()
.filter_map(|url| url.to_file_path().ok())
.map(|f| {
let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash
format!("\"{file}\"",) // wrap in quotes for JS array
})
.collect::<Vec<_>>();
tauri::async_runtime::spawn(async move {
for path in files {
create_window(app.clone(), Some(path)).await;
}
});
}
},
);
tracing_subscriber::fmt().with_env_filter(EnvFilter::new(format!("{}=trace", pkg_name))).init();
}

View File

@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
lightningbeam_lib::run();
}

View File

@ -1,161 +0,0 @@
use std::sync::Arc;
use std::thread;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy},
window::WindowBuilder,
};
#[cfg(target_os = "linux")]
use winit::platform::x11::EventLoopBuilderExtX11;
use crate::renderer::Renderer;
/// Events that can be sent to the render window thread
#[derive(Debug, Clone)]
pub enum RenderEvent {
UpdateGradient { top: [f32; 4], bottom: [f32; 4] },
SetPosition { x: i32, y: i32 },
SetSize { width: u32, height: u32 },
RequestRedraw,
Close,
}
/// Handle to control the render window from other threads
pub struct RenderWindowHandle {
proxy: EventLoopProxy<RenderEvent>,
}
impl RenderWindowHandle {
/// Update the gradient colors
pub fn update_gradient(&self, top: [f32; 4], bottom: [f32; 4]) {
let _ = self.proxy.send_event(RenderEvent::UpdateGradient { top, bottom });
}
/// Set window position
pub fn set_position(&self, x: i32, y: i32) {
let _ = self.proxy.send_event(RenderEvent::SetPosition { x, y });
}
/// Set window size
pub fn set_size(&self, width: u32, height: u32) {
let _ = self.proxy.send_event(RenderEvent::SetSize { width, height });
}
/// Request a redraw
pub fn request_redraw(&self) {
let _ = self.proxy.send_event(RenderEvent::RequestRedraw);
}
/// Close the render window
pub fn close(&self) {
let _ = self.proxy.send_event(RenderEvent::Close);
}
}
/// Spawn the render window in a separate thread
pub fn spawn_render_window(
x: i32,
y: i32,
width: u32,
height: u32,
) -> Result<RenderWindowHandle, String> {
let (tx, rx) = std::sync::mpsc::channel();
thread::spawn(move || {
let mut event_loop_builder = EventLoopBuilder::with_user_event();
// On Linux, allow event loop on any thread (not just main thread)
#[cfg(target_os = "linux")]
{
event_loop_builder.with_any_thread(true);
}
let event_loop: EventLoop<RenderEvent> = event_loop_builder.build().unwrap();
let proxy = event_loop.create_proxy();
// Send the proxy back to the main thread
tx.send(proxy.clone()).unwrap();
let window = WindowBuilder::new()
.with_title("Lightningbeam Renderer")
.with_inner_size(winit::dpi::PhysicalSize::new(width, height))
.with_position(winit::dpi::PhysicalPosition::new(x, y))
.with_decorations(false) // No title bar
.with_transparent(false) // Opaque background
.with_resizable(false)
.build(&event_loop)
.unwrap();
let window = Arc::new(window);
// Initialize renderer (async operation)
let mut renderer = pollster::block_on(Renderer::new(window.clone()));
event_loop.run(move |event, elwt| {
elwt.set_control_flow(ControlFlow::Wait);
match event {
Event::UserEvent(render_event) => match render_event {
RenderEvent::UpdateGradient { top, bottom } => {
eprintln!("[RenderWindow] Updating gradient: {:?} -> {:?}", top, bottom);
renderer.update_gradient(top, bottom);
window.request_redraw();
}
RenderEvent::SetPosition { x, y } => {
eprintln!("[RenderWindow] Setting position: ({}, {})", x, y);
let _ = window.set_outer_position(winit::dpi::PhysicalPosition::new(x, y));
}
RenderEvent::SetSize { width, height } => {
eprintln!("[RenderWindow] Setting size: {}x{}", width, height);
let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(width, height));
}
RenderEvent::RequestRedraw => {
window.request_redraw();
}
RenderEvent::Close => {
eprintln!("[RenderWindow] Closing render window");
elwt.exit();
}
},
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
elwt.exit();
}
Event::WindowEvent {
event: WindowEvent::Resized(physical_size),
..
} => {
renderer.resize(physical_size);
window.request_redraw();
}
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
match renderer.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => renderer.resize(window.inner_size()),
Err(wgpu::SurfaceError::OutOfMemory) => {
eprintln!("Out of memory!");
elwt.exit();
}
Err(e) => eprintln!("Render error: {:?}", e),
}
}
_ => {}
}
}).expect("Event loop error");
});
// Wait for the proxy to be sent back
let proxy = rx.recv().map_err(|e| format!("Failed to receive proxy: {}", e))?;
Ok(RenderWindowHandle { proxy })
}

View File

@ -1,293 +0,0 @@
use std::sync::Arc;
use wgpu::util::DeviceExt;
/// Vertex data for rendering
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 2],
color: [f32; 4],
}
impl Vertex {
fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
// Position
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
// Color
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
}
/// Main renderer state that manages the wgpu rendering pipeline
pub struct Renderer {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
size: winit::dpi::PhysicalSize<u32>,
render_pipeline: wgpu::RenderPipeline,
vertex_buffer: wgpu::Buffer,
num_vertices: u32,
}
impl Renderer {
/// Create a new renderer for the given window
pub async fn new(window: Arc<winit::window::Window>) -> Self {
let size = window.inner_size();
// Create wgpu instance
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
// Create surface from window
let surface = instance.create_surface(window.clone()).unwrap();
// Request adapter
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
// Request device and queue
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some("Lightningbeam Render Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
)
.await
.unwrap();
// Configure surface
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo, // VSync
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
// Create shader module
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Gradient Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/gradient.wgsl").into()),
});
// Create render pipeline
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[Vertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
});
// Create initial gradient vertices (two triangles forming a quad)
let vertices = Self::create_gradient_vertices();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
Self {
surface,
device,
queue,
config,
size,
render_pipeline,
vertex_buffer,
num_vertices: vertices.len() as u32,
}
}
/// Create vertices for a gradient quad covering the entire viewport
fn create_gradient_vertices() -> Vec<Vertex> {
vec![
// First triangle
Vertex {
position: [-1.0, 1.0],
color: [0.2, 0.3, 0.8, 1.0], // Blue at top
},
Vertex {
position: [-1.0, -1.0],
color: [0.6, 0.2, 0.8, 1.0], // Purple at bottom
},
Vertex {
position: [1.0, -1.0],
color: [0.6, 0.2, 0.8, 1.0], // Purple at bottom
},
// Second triangle
Vertex {
position: [-1.0, 1.0],
color: [0.2, 0.3, 0.8, 1.0], // Blue at top
},
Vertex {
position: [1.0, -1.0],
color: [0.6, 0.2, 0.8, 1.0], // Purple at bottom
},
Vertex {
position: [1.0, 1.0],
color: [0.2, 0.3, 0.8, 1.0], // Blue at top
},
]
}
/// Resize the renderer (call when window is resized)
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
self.config.width = new_size.width;
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
}
}
/// Render a frame
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.1,
b: 0.1,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.draw(0..self.num_vertices, 0..1);
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
/// Update gradient colors (for future customization)
pub fn update_gradient(&mut self, color_top: [f32; 4], color_bottom: [f32; 4]) {
let vertices = vec![
Vertex {
position: [-1.0, 1.0],
color: color_top,
},
Vertex {
position: [-1.0, -1.0],
color: color_bottom,
},
Vertex {
position: [1.0, -1.0],
color: color_bottom,
},
Vertex {
position: [-1.0, 1.0],
color: color_top,
},
Vertex {
position: [1.0, -1.0],
color: color_bottom,
},
Vertex {
position: [1.0, 1.0],
color: color_top,
},
];
self.queue
.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices));
}
}

View File

@ -1,26 +0,0 @@
// Vertex shader
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) color: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.clip_position = vec4<f32>(input.position, 0.0, 1.0);
output.color = input.color;
return output;
}
// Fragment shader
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
return input.color;
}

View File

@ -1,528 +0,0 @@
use std::sync::{Arc, Mutex};
use std::num::NonZeroUsize;
use ffmpeg_next as ffmpeg;
use lru::LruCache;
use daw_backend::WaveformPeak;
use image::RgbaImage;
use tauri::Manager;
#[derive(serde::Serialize, Clone)]
pub struct VideoFileMetadata {
pub pool_index: usize,
pub width: u32,
pub height: u32,
pub fps: f64,
pub duration: f64,
pub has_audio: bool,
pub audio_pool_index: Option<usize>,
pub audio_duration: Option<f64>,
pub audio_sample_rate: Option<u32>,
pub audio_channels: Option<u32>,
pub audio_waveform: Option<Vec<WaveformPeak>>,
}
struct VideoDecoder {
path: String,
width: u32, // Original video width
height: u32, // Original video height
output_width: u32, // Scaled output width
output_height: u32, // Scaled output height
fps: f64,
duration: f64,
time_base: f64,
stream_index: usize,
frame_cache: LruCache<i64, Vec<u8>>, // timestamp -> RGBA data
input: Option<ffmpeg::format::context::Input>,
decoder: Option<ffmpeg::decoder::Video>,
last_decoded_ts: i64, // Track the last decoded frame timestamp
}
impl VideoDecoder {
fn new(path: String, cache_size: usize, max_width: Option<u32>, max_height: Option<u32>) -> Result<Self, String> {
ffmpeg::init().map_err(|e| e.to_string())?;
let input = ffmpeg::format::input(&path)
.map_err(|e| format!("Failed to open video: {}", e))?;
let video_stream = input.streams()
.best(ffmpeg::media::Type::Video)
.ok_or("No video stream found")?;
let stream_index = video_stream.index();
let context_decoder = ffmpeg::codec::context::Context::from_parameters(
video_stream.parameters()
).map_err(|e| e.to_string())?;
let decoder = context_decoder.decoder().video()
.map_err(|e| e.to_string())?;
let width = decoder.width();
let height = decoder.height();
let time_base = f64::from(video_stream.time_base());
// Calculate output dimensions (scale down if larger than max)
let (output_width, output_height) = if let (Some(max_w), Some(max_h)) = (max_width, max_height) {
// Calculate scale to fit within max dimensions while preserving aspect ratio
let scale = (max_w as f32 / width as f32).min(max_h as f32 / height as f32).min(1.0);
((width as f32 * scale) as u32, (height as f32 * scale) as u32)
} else {
(width, height)
};
// Try to get duration from stream, fallback to container
let duration = if video_stream.duration() > 0 {
video_stream.duration() as f64 * time_base
} else if input.duration() > 0 {
input.duration() as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE)
} else {
// If no duration available, estimate from frame count and fps
let fps = f64::from(video_stream.avg_frame_rate());
if video_stream.frames() > 0 && fps > 0.0 {
video_stream.frames() as f64 / fps
} else {
0.0 // Unknown duration
}
};
let fps = f64::from(video_stream.avg_frame_rate());
Ok(Self {
path,
width,
height,
output_width,
output_height,
fps,
duration,
time_base,
stream_index,
frame_cache: LruCache::new(
NonZeroUsize::new(cache_size).unwrap()
),
input: None,
decoder: None,
last_decoded_ts: -1,
})
}
fn get_frame(&mut self, timestamp: f64) -> Result<Vec<u8>, String> {
use std::time::Instant;
let t_start = Instant::now();
// Convert timestamp to frame timestamp
let frame_ts = (timestamp / self.time_base) as i64;
// Check cache
if let Some(cached_frame) = self.frame_cache.get(&frame_ts) {
eprintln!("[Video Timing] Cache hit for ts={:.3}s ({}ms)", timestamp, t_start.elapsed().as_millis());
return Ok(cached_frame.clone());
}
let _t_after_cache = Instant::now();
// Determine if we need to seek
// Seek if: no decoder open, going backwards, or jumping forward more than 2 seconds
let need_seek = self.decoder.is_none()
|| frame_ts < self.last_decoded_ts
|| frame_ts > self.last_decoded_ts + (2.0 / self.time_base) as i64;
if need_seek {
let t_seek_start = Instant::now();
// Reopen input
let mut input = ffmpeg::format::input(&self.path)
.map_err(|e| format!("Failed to reopen video: {}", e))?;
// Seek to timestamp
input.seek(frame_ts, ..frame_ts)
.map_err(|e| format!("Seek failed: {}", e))?;
let context_decoder = ffmpeg::codec::context::Context::from_parameters(
input.streams().best(ffmpeg::media::Type::Video).unwrap().parameters()
).map_err(|e| e.to_string())?;
let decoder = context_decoder.decoder().video()
.map_err(|e| e.to_string())?;
self.input = Some(input);
self.decoder = Some(decoder);
self.last_decoded_ts = -1; // Reset since we seeked
eprintln!("[Video Timing] Seek took {}ms", t_seek_start.elapsed().as_millis());
}
let input = self.input.as_mut().unwrap();
let decoder = self.decoder.as_mut().unwrap();
// Decode frames until we find the one closest to our target timestamp
let mut best_frame_data: Option<Vec<u8>> = None;
let mut best_frame_ts: Option<i64> = None;
let t_decode_start = Instant::now();
let mut decode_count = 0;
let mut scale_time_ms = 0u128;
for (stream, packet) in input.packets() {
if stream.index() == self.stream_index {
decoder.send_packet(&packet)
.map_err(|e| e.to_string())?;
let mut frame = ffmpeg::util::frame::Video::empty();
while decoder.receive_frame(&mut frame).is_ok() {
decode_count += 1;
let current_frame_ts = frame.timestamp().unwrap_or(0);
self.last_decoded_ts = current_frame_ts; // Update last decoded position
// Check if this frame is closer to our target than the previous best
let is_better = match best_frame_ts {
None => true,
Some(best_ts) => {
(current_frame_ts - frame_ts).abs() < (best_ts - frame_ts).abs()
}
};
if is_better {
let t_scale_start = Instant::now();
// Convert to RGBA and scale to output size
let mut scaler = ffmpeg::software::scaling::context::Context::get(
frame.format(),
frame.width(),
frame.height(),
ffmpeg::format::Pixel::RGBA,
self.output_width,
self.output_height,
ffmpeg::software::scaling::flag::Flags::BILINEAR,
).map_err(|e| e.to_string())?;
let mut rgb_frame = ffmpeg::util::frame::Video::empty();
scaler.run(&frame, &mut rgb_frame)
.map_err(|e| e.to_string())?;
// Remove stride padding to create tightly packed RGBA data
let width = self.output_width as usize;
let height = self.output_height as usize;
let stride = rgb_frame.stride(0);
let row_size = width * 4; // RGBA = 4 bytes per pixel
let source_data = rgb_frame.data(0);
let mut packed_data = Vec::with_capacity(row_size * height);
for y in 0..height {
let row_start = y * stride;
let row_end = row_start + row_size;
packed_data.extend_from_slice(&source_data[row_start..row_end]);
}
scale_time_ms += t_scale_start.elapsed().as_millis();
best_frame_data = Some(packed_data);
best_frame_ts = Some(current_frame_ts);
}
// If we've reached or passed the target timestamp, we can stop
if current_frame_ts >= frame_ts {
// Found our frame, cache and return it
if let Some(data) = best_frame_data {
let total_time = t_start.elapsed().as_millis();
let decode_time = t_decode_start.elapsed().as_millis();
eprintln!("[Video Timing] ts={:.3}s | Decoded {} frames in {}ms | Scale: {}ms | Total: {}ms",
timestamp, decode_count, decode_time, scale_time_ms, total_time);
self.frame_cache.put(frame_ts, data.clone());
return Ok(data);
}
break;
}
}
}
}
eprintln!("[Video Decoder] ERROR: Failed to decode frame for timestamp {}", timestamp);
Err("Failed to decode frame".to_string())
}
}
pub struct VideoState {
pool: Vec<Arc<Mutex<VideoDecoder>>>,
next_pool_index: usize,
cache_size: usize,
}
impl Default for VideoState {
fn default() -> Self {
Self {
pool: Vec::new(),
next_pool_index: 0,
cache_size: 20, // Default cache size
}
}
}
#[tauri::command]
pub async fn video_load_file(
video_state: tauri::State<'_, Arc<Mutex<VideoState>>>,
audio_state: tauri::State<'_, Arc<Mutex<crate::audio::AudioState>>>,
path: String,
) -> Result<VideoFileMetadata, String> {
eprintln!("[Video] Loading file: {}", path);
ffmpeg::init().map_err(|e| e.to_string())?;
// Open input to check for audio stream
let mut input = ffmpeg::format::input(&path)
.map_err(|e| format!("Failed to open video: {}", e))?;
let audio_stream_opt = input.streams()
.best(ffmpeg::media::Type::Audio);
let has_audio = audio_stream_opt.is_some();
// Extract audio if present
let (audio_pool_index, audio_duration, audio_sample_rate, audio_channels, audio_waveform) = if has_audio {
let audio_stream = audio_stream_opt.unwrap();
let audio_index = audio_stream.index();
// Get audio properties
let context_decoder = ffmpeg::codec::context::Context::from_parameters(
audio_stream.parameters()
).map_err(|e| e.to_string())?;
let mut audio_decoder = context_decoder.decoder().audio()
.map_err(|e| e.to_string())?;
let sample_rate = audio_decoder.rate();
let channels = audio_decoder.channels() as u32;
// Decode all audio frames
let mut audio_samples: Vec<f32> = Vec::new();
for (stream, packet) in input.packets() {
if stream.index() == audio_index {
audio_decoder.send_packet(&packet)
.map_err(|e| e.to_string())?;
let mut audio_frame = ffmpeg::util::frame::Audio::empty();
while audio_decoder.receive_frame(&mut audio_frame).is_ok() {
// Convert audio to f32 planar format
let format = audio_frame.format();
let frame_channels = audio_frame.channels() as usize;
// Create resampler to convert to f32 planar
let mut resampler = ffmpeg::software::resampling::context::Context::get(
format,
audio_frame.channel_layout(),
sample_rate,
ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
audio_frame.channel_layout(),
sample_rate,
).map_err(|e| e.to_string())?;
let mut resampled_frame = ffmpeg::util::frame::Audio::empty();
resampler.run(&audio_frame, &mut resampled_frame)
.map_err(|e| e.to_string())?;
// Extract f32 samples (interleaved format)
let data_ptr = resampled_frame.data(0).as_ptr() as *const f32;
let total_samples = resampled_frame.samples() * frame_channels;
let samples_slice = unsafe {
std::slice::from_raw_parts(data_ptr, total_samples)
};
audio_samples.extend_from_slice(samples_slice);
}
}
}
// Flush audio decoder
audio_decoder.send_eof().map_err(|e| e.to_string())?;
let mut audio_frame = ffmpeg::util::frame::Audio::empty();
while audio_decoder.receive_frame(&mut audio_frame).is_ok() {
let format = audio_frame.format();
let frame_channels = audio_frame.channels() as usize;
let mut resampler = ffmpeg::software::resampling::context::Context::get(
format,
audio_frame.channel_layout(),
sample_rate,
ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed),
audio_frame.channel_layout(),
sample_rate,
).map_err(|e| e.to_string())?;
let mut resampled_frame = ffmpeg::util::frame::Audio::empty();
resampler.run(&audio_frame, &mut resampled_frame)
.map_err(|e| e.to_string())?;
let data_ptr = resampled_frame.data(0).as_ptr() as *const f32;
let total_samples = resampled_frame.samples() * frame_channels;
let samples_slice = unsafe {
std::slice::from_raw_parts(data_ptr, total_samples)
};
audio_samples.extend_from_slice(samples_slice);
}
// Calculate audio duration
let total_samples_per_channel = audio_samples.len() / channels as usize;
let audio_duration = total_samples_per_channel as f64 / sample_rate as f64;
// Generate waveform
let target_peaks = ((audio_duration * 300.0) as usize).clamp(1000, 20000);
let waveform = generate_waveform(&audio_samples, channels, target_peaks);
// Send audio to DAW backend
let mut audio_state_guard = audio_state.lock().unwrap();
let audio_pool_index = audio_state_guard.next_pool_index;
audio_state_guard.next_pool_index += 1;
if let Some(controller) = &mut audio_state_guard.controller {
controller.add_audio_file(
path.clone(),
audio_samples,
channels,
sample_rate,
);
}
drop(audio_state_guard);
(Some(audio_pool_index), Some(audio_duration), Some(sample_rate), Some(channels), Some(waveform))
} else {
(None, None, None, None, None)
};
// Create video decoder with max dimensions for playback (1920x1080)
// This scales videos to reduce data transfer over WebSocket
let mut video_state_guard = video_state.lock().unwrap();
let pool_index = video_state_guard.next_pool_index;
video_state_guard.next_pool_index += 1;
let decoder = VideoDecoder::new(path.clone(), video_state_guard.cache_size, Some(1920), Some(1080))?;
let metadata = VideoFileMetadata {
pool_index,
width: decoder.output_width, // Return scaled dimensions to JS
height: decoder.output_height,
fps: decoder.fps,
duration: decoder.duration,
has_audio,
audio_pool_index,
audio_duration,
audio_sample_rate,
audio_channels,
audio_waveform,
};
video_state_guard.pool.push(Arc::new(Mutex::new(decoder)));
Ok(metadata)
}
fn generate_waveform(audio_data: &[f32], channels: u32, target_peaks: usize) -> Vec<WaveformPeak> {
let total_samples = audio_data.len();
let samples_per_channel = total_samples / channels as usize;
let samples_per_peak = (samples_per_channel / target_peaks).max(1);
let mut waveform = Vec::new();
for peak_idx in 0..target_peaks {
let start_sample = peak_idx * samples_per_peak;
let end_sample = ((peak_idx + 1) * samples_per_peak).min(samples_per_channel);
if start_sample >= samples_per_channel {
break;
}
let mut min_val = 0.0f32;
let mut max_val = 0.0f32;
for sample_idx in start_sample..end_sample {
// Average across channels
let mut channel_sum = 0.0f32;
for ch in 0..channels as usize {
let idx = sample_idx * channels as usize + ch;
if idx < total_samples {
channel_sum += audio_data[idx];
}
}
let avg_sample = channel_sum / channels as f32;
min_val = min_val.min(avg_sample);
max_val = max_val.max(avg_sample);
}
waveform.push(WaveformPeak {
min: min_val,
max: max_val,
});
}
waveform
}
#[tauri::command]
pub async fn video_set_cache_size(
state: tauri::State<'_, Arc<Mutex<VideoState>>>,
cache_size: usize,
) -> Result<(), String> {
let mut video_state = state.lock().unwrap();
video_state.cache_size = cache_size;
Ok(())
}
#[tauri::command]
pub async fn video_get_pool_info(
state: tauri::State<'_, Arc<Mutex<VideoState>>>,
pool_index: usize,
) -> Result<(u32, u32, f64), String> {
let video_state = state.lock().unwrap();
let decoder = video_state.pool.get(pool_index)
.ok_or("Invalid pool index")?
.lock().unwrap();
Ok((
decoder.output_width, // Return scaled dimensions
decoder.output_height,
decoder.fps
))
}
/// Stream a decoded video frame over WebSocket (zero-copy performance testing)
#[tauri::command]
pub async fn video_stream_frame(
video_state: tauri::State<'_, Arc<Mutex<VideoState>>>,
frame_streamer: tauri::State<'_, Arc<Mutex<crate::frame_streamer::FrameStreamer>>>,
pool_index: usize,
timestamp: f64,
) -> Result<(), String> {
use std::time::Instant;
let t_start = Instant::now();
// Get decoder
let state = video_state.lock().unwrap();
let decoder = state.pool.get(pool_index)
.ok_or("Invalid pool index")?
.clone();
drop(state);
// Decode frame
let mut decoder = decoder.lock().unwrap();
let width = decoder.output_width;
let height = decoder.output_height;
let t_decode_start = Instant::now();
let rgba_data = decoder.get_frame(timestamp)?; // Note: get_frame returns RGBA, not RGB
let t_decode = t_decode_start.elapsed().as_micros();
drop(decoder);
// Stream over WebSocket
let t_stream_start = Instant::now();
let streamer = frame_streamer.lock().unwrap();
streamer.send_frame(pool_index, timestamp, width, height, &rgba_data);
let t_stream = t_stream_start.elapsed().as_micros();
drop(streamer);
// Commented out per-frame logging
// let t_total = t_start.elapsed().as_micros();
// eprintln!("[Video Stream] Frame {}x{} @ {:.2}s | Decode: {}μs | Stream: {}μs | Total: {}μs",
// width, height, timestamp, t_decode, t_stream, t_total);
Ok(())
}

View File

@ -1,63 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lightningbeam",
"version": "1.0.4-alpha",
"identifier": "org.lightningbeam.core",
"build": {
"frontendDist": "../src"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "Lightningbeam",
"width": 1500,
"height": 1024,
"dragDropEnabled": false,
"zoomHotkeysEnabled": false
}
],
"security": {
"csp": null,
"assetProtocol": {
"enable": true
}
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"../src/assets/instruments/**/*"
],
"linux": {
"appimage": {
"bundleMediaFramework": true,
"files": {}
},
"deb": {
"files": {}
},
"rpm": {
"epoch": 0,
"files": {},
"release": "1"
}
},
"fileAssociations": [
{
"ext": [
"beam"
],
"mimeType": "application/lightningbeam"
}
]
}
}

View File

@ -1,3 +0,0 @@
{
"version": "0.6.4-0"
}