Compare commits
2 Commits
a10fae75d2
...
3a44b96621
Author | SHA1 | Date | |
---|---|---|---|
3a44b96621 | |||
5da81e5f93 |
189
README.md
189
README.md
@@ -1,7 +1,188 @@
|
|||||||
# Tauri + Solid + Typescript
|
# Display Ambient Light Desktop App
|
||||||
|
|
||||||
This template should help get you started developing with Tauri, Solid and Typescript in Vite.
|
A desktop application built with Tauri 2.0 for ambient light control, supporting multi-monitor screen sampling and LED strip control to create immersive ambient lighting effects.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## ✨ Features
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
- 🖥️ **Multi-Monitor Support** - Automatic detection and configuration of multiple displays
|
||||||
|
- 🎨 **Real-time Screen Sampling** - High-performance screen content capture and color analysis
|
||||||
|
- 💡 **LED Strip Control** - Configurable LED strip layout and mapping support
|
||||||
|
- ⚖️ **White Balance Calibration** - Built-in white balance adjustment tool with fullscreen mode
|
||||||
|
- 🎛️ **Intuitive Configuration Interface** - Modern UI with drag-and-drop configuration support
|
||||||
|
- 🔧 **Hardware Integration** - Display brightness control and audio device management
|
||||||
|
- 📡 **Network Communication** - UDP and WebSocket communication support
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **Framework**: [Solid.js](https://solidjs.com/) - High-performance reactive UI framework
|
||||||
|
- **Build Tool**: [Vite](https://vitejs.dev/) - Fast frontend build tool
|
||||||
|
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) + [DaisyUI](https://daisyui.com/) - Modern UI component library
|
||||||
|
- **Routing**: [@solidjs/router](https://github.com/solidjs/solid-router) - Client-side routing
|
||||||
|
- **Language**: TypeScript - Type-safe JavaScript
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
- **Framework**: [Tauri 2.0](https://tauri.app/) - Cross-platform desktop app framework
|
||||||
|
- **Language**: Rust - High-performance systems programming language
|
||||||
|
- **Screen Capture**: [screen-capture-kit](https://crates.io/crates/screen-capture-kit) - macOS native screen capture
|
||||||
|
- **Display Control**: [ddc-hi](https://crates.io/crates/ddc-hi) - DDC/CI display control
|
||||||
|
- **Audio**: [coreaudio-rs](https://crates.io/crates/coreaudio-rs) - macOS audio system integration
|
||||||
|
- **Networking**: tokio + tokio-tungstenite - Async network communication
|
||||||
|
|
||||||
|
## 📋 System Requirements
|
||||||
|
|
||||||
|
- **Operating System**: macOS 13.0+ (primary supported platform)
|
||||||
|
- **Memory**: 4GB+ recommended
|
||||||
|
- **Graphics**: Hardware-accelerated graphics card
|
||||||
|
- **Network**: For device discovery and communication
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Install Rust**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
source ~/.cargo/env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Node.js and pnpm**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Node.js (recommended using nvm)
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||||
|
nvm install node
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
npm install -g pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install Tauri CLI**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install @tauri-apps/cli@next
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. **Clone the project**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd display-ambient-light/desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start development server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the application
|
||||||
|
pnpm tauri build
|
||||||
|
|
||||||
|
# Build artifacts are located in src-tauri/target/release/bundle/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Application Interface
|
||||||
|
|
||||||
|
### Main Pages
|
||||||
|
|
||||||
|
1. **System Info** (`/info`) - Display system and hardware information
|
||||||
|
2. **Display Info** (`/displays`) - Monitor status and configuration
|
||||||
|
3. **LED Strip Configuration** (`/led-strips-configuration`) - LED strip layout and mapping configuration
|
||||||
|
4. **White Balance** (`/white-balance`) - Color calibration and white balance adjustment
|
||||||
|
|
||||||
|
### Core Features
|
||||||
|
|
||||||
|
- **Real-time Screen Preview** - WebSocket streaming of screen content
|
||||||
|
- **LED Mapping Configuration** - Visual configuration of LED strip positions and quantities
|
||||||
|
- **Color Calibration** - RGB adjustment panel with fullscreen comparison mode
|
||||||
|
- **Device Management** - Automatic discovery and management of LED control devices
|
||||||
|
|
||||||
|
## 🔧 Configuration Files
|
||||||
|
|
||||||
|
Application configuration is stored in the user directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/Library/Application Support/cc.ivanli.ambient-light.desktop/
|
||||||
|
├── config.toml # Main configuration file
|
||||||
|
├── led_strips.json # LED strip configuration
|
||||||
|
└── color_calibration.json # Color calibration data
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Development Guide
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
desktop/
|
||||||
|
├── src/ # Frontend source code (Solid.js)
|
||||||
|
│ ├── components/ # UI components
|
||||||
|
│ ├── stores/ # State management
|
||||||
|
│ ├── models/ # Data models
|
||||||
|
│ └── contexts/ # React Context
|
||||||
|
├── src-tauri/ # Backend source code (Rust)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── ambient_light/ # Ambient light control
|
||||||
|
│ │ ├── display/ # Display management
|
||||||
|
│ │ ├── rpc/ # Network communication
|
||||||
|
│ │ └── screenshot/ # Screen capture
|
||||||
|
│ └── tauri.conf.json # Tauri configuration
|
||||||
|
└── package.json # Frontend dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. **Frontend Development**: Modify files under `src/`, supports hot reload
|
||||||
|
2. **Backend Development**: Modify files under `src-tauri/src/`, requires dev server restart
|
||||||
|
3. **Configuration Changes**: Restart required after modifying `tauri.conf.json`
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
|
||||||
|
- Use browser developer tools to debug frontend
|
||||||
|
- Use `console.log` and Rust's `println!` for debugging
|
||||||
|
- Check Tauri console output for backend logs
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the project
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🔗 Related Links
|
||||||
|
|
||||||
|
- [Tauri Official Documentation](https://tauri.app/)
|
||||||
|
- [Solid.js Official Documentation](https://solidjs.com/)
|
||||||
|
- [Rust Official Documentation](https://doc.rust-lang.org/)
|
||||||
|
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
If you encounter issues or have suggestions, please:
|
||||||
|
|
||||||
|
- Create an [Issue](../../issues)
|
||||||
|
- Check the [Wiki](../../wiki) for more information
|
||||||
|
- Contact the developer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This application is primarily optimized for macOS platform, support for other platforms may be limited.
|
||||||
|
@@ -296,8 +296,19 @@ impl LedColorsPublisher {
|
|||||||
let mut buffer = Vec::<u8>::with_capacity(group_size * 3);
|
let mut buffer = Vec::<u8>::with_capacity(group_size * 3);
|
||||||
|
|
||||||
if group.end > group.start {
|
if group.end > group.start {
|
||||||
for i in group.pos - display_led_offset..group_size + group.pos - display_led_offset
|
// Prevent integer underflow by using saturating subtraction
|
||||||
{
|
let start_index = if group.pos >= display_led_offset {
|
||||||
|
group.pos - display_led_offset
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let end_index = if group.pos + group_size >= display_led_offset {
|
||||||
|
group_size + group.pos - display_led_offset
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in start_index..end_index {
|
||||||
if i < colors.len() {
|
if i < colors.len() {
|
||||||
let bytes = colors[i].as_bytes();
|
let bytes = colors[i].as_bytes();
|
||||||
buffer.append(&mut bytes.to_vec());
|
buffer.append(&mut bytes.to_vec());
|
||||||
@@ -308,10 +319,19 @@ impl LedColorsPublisher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for i in (group.pos - display_led_offset
|
// Prevent integer underflow by using saturating subtraction
|
||||||
..group_size + group.pos - display_led_offset)
|
let start_index = if group.pos >= display_led_offset {
|
||||||
.rev()
|
group.pos - display_led_offset
|
||||||
{
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let end_index = if group.pos + group_size >= display_led_offset {
|
||||||
|
group_size + group.pos - display_led_offset
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in (start_index..end_index).rev() {
|
||||||
if i < colors.len() {
|
if i < colors.len() {
|
||||||
let bytes = colors[i].as_bytes();
|
let bytes = colors[i].as_bytes();
|
||||||
buffer.append(&mut bytes.to_vec());
|
buffer.append(&mut bytes.to_vec());
|
||||||
|
@@ -143,12 +143,12 @@ impl ScreenStreamManager {
|
|||||||
let mut last_process_time = Instant::now();
|
let mut last_process_time = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Check if stream still has subscribers
|
// Check if stream still has subscribers and is still running
|
||||||
let should_continue = {
|
let should_continue = {
|
||||||
let streams_lock = streams.read().await;
|
let streams_lock = streams.read().await;
|
||||||
if let Some(stream_state) = streams_lock.get(&display_id) {
|
if let Some(stream_state) = streams_lock.get(&display_id) {
|
||||||
let state = stream_state.read().await;
|
let state = stream_state.read().await;
|
||||||
!state.subscribers.is_empty()
|
!state.subscribers.is_empty() && state.is_running
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ impl ScreenStreamManager {
|
|||||||
let mut state = stream_state.write().await;
|
let mut state = stream_state.write().await;
|
||||||
let changed = state.last_screenshot_hash.map_or(true, |hash| hash != frame_hash);
|
let changed = state.last_screenshot_hash.map_or(true, |hash| hash != frame_hash);
|
||||||
let elapsed_ms = state.last_force_send.elapsed().as_millis();
|
let elapsed_ms = state.last_force_send.elapsed().as_millis();
|
||||||
let force_send = elapsed_ms > 200; // Force send every 200ms for higher FPS
|
let force_send = elapsed_ms > 500; // Force send every 500ms for better CPU performance
|
||||||
|
|
||||||
if changed || force_send {
|
if changed || force_send {
|
||||||
state.last_screenshot_hash = Some(frame_hash);
|
state.last_screenshot_hash = Some(frame_hash);
|
||||||
@@ -338,8 +338,19 @@ impl ScreenStreamManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_stream(&self, display_id: u32) {
|
pub async fn stop_stream(&self, display_id: u32) {
|
||||||
|
log::info!("Stopping stream for display_id: {}", display_id);
|
||||||
let mut streams = self.streams.write().await;
|
let mut streams = self.streams.write().await;
|
||||||
|
|
||||||
|
if let Some(stream_state) = streams.get(&display_id) {
|
||||||
|
// Mark stream as not running to stop the processing task
|
||||||
|
let mut state = stream_state.write().await;
|
||||||
|
state.is_running = false;
|
||||||
|
log::info!("Marked stream as not running for display_id: {}", display_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the stream from the map
|
||||||
streams.remove(&display_id);
|
streams.remove(&display_id);
|
||||||
|
log::info!("Removed stream from manager for display_id: {}", display_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +451,7 @@ pub async fn handle_websocket_connection(
|
|||||||
log::info!("Starting stream with config: display_id={}, width={}, height={}",
|
log::info!("Starting stream with config: display_id={}, width={}, height={}",
|
||||||
config.display_id, config.target_width, config.target_height);
|
config.display_id, config.target_width, config.target_height);
|
||||||
let stream_manager = ScreenStreamManager::global().await;
|
let stream_manager = ScreenStreamManager::global().await;
|
||||||
|
let display_id_for_cleanup = config.display_id;
|
||||||
let mut frame_rx = match stream_manager.start_stream(config).await {
|
let mut frame_rx = match stream_manager.start_stream(config).await {
|
||||||
Ok(rx) => {
|
Ok(rx) => {
|
||||||
log::info!("Screen stream started successfully");
|
log::info!("Screen stream started successfully");
|
||||||
@@ -498,6 +510,11 @@ pub async fn handle_websocket_connection(
|
|||||||
_ = control_task => {},
|
_ = control_task => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up resources when connection ends
|
||||||
|
log::info!("WebSocket connection ending, cleaning up resources for display_id: {}", display_id_for_cleanup);
|
||||||
|
let stream_manager = ScreenStreamManager::global().await;
|
||||||
|
stream_manager.stop_stream(display_id_for_cleanup).await;
|
||||||
|
|
||||||
log::info!("WebSocket connection handler completed");
|
log::info!("WebSocket connection handler completed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@@ -189,8 +189,8 @@ impl ScreenshotManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sleep for a frame duration (15 FPS for better performance)
|
// Sleep for a frame duration (5 FPS for much better CPU performance)
|
||||||
sleep(Duration::from_millis(67)).await;
|
sleep(Duration::from_millis(200)).await;
|
||||||
yield_now().await;
|
yield_now().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user