docs: add Codex prompt for Chromecast integration
This commit is contained in:
1 parent
1895812830
commit
bcfd9bbf48
2 files changed
+382
-27
No files matched your search
@@ -1,37 +1,58 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="static/logo.png" alt="QBZ-NIX logo" width="180" />
|
||||||
|
</p>
|
||||||
|
|
||||||
# QBZ-NIX
|
# QBZ-NIX
|
||||||
|
|
||||||
Native Qobuz client for Linux with Hi-Fi audio support.
|
Free and open source (FOSS) Qobuz client for Linux with native, high-fidelity playback. QBZ-NIX removes browser sample-rate limits and delivers hi-res audio through a purpose-built Rust playback engine.
|
||||||
|
|
||||||
## Why?
|
## Why QBZ-NIX
|
||||||
|
|
||||||
Browser-based players (Chromium, Firefox) have a hardcoded sample rate cap (~48kHz). Qobuz offers up to 192kHz Hi-Res content. QBZ-NIX bypasses this limitation with native audio playback.
|
Browsers cap audio output around 48 kHz, while Qobuz streams up to 192 kHz. QBZ-NIX uses a native playback pipeline so your system and DAC can receive the original resolution, with proper device control, caching, and media integration.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core
|
### Streaming and Playback
|
||||||
- Native audio playback (no browser limitations)
|
- Qobuz authentication and full catalog search (albums, tracks, artists, playlists).
|
||||||
- Search, browse albums, artists, tracks
|
- Native decoding for FLAC and MP3 with real-time playback state updates.
|
||||||
- Playback queue management
|
- Quality selection with automatic fallback across Qobuz tiers.
|
||||||
- Quality selection with automatic fallback
|
- Audio device enumeration and per-device output selection.
|
||||||
|
- Gapless-ready playback pipeline with precise position tracking.
|
||||||
|
|
||||||
### Audio
|
### Queue and Library
|
||||||
- Support for all Qobuz quality tiers (MP3, CD, Hi-Res, Ultra Hi-Res)
|
- Queue management with shuffle, repeat, and history navigation.
|
||||||
- Sample rate / bit depth display
|
- In-memory audio cache with LRU eviction and next-track prefetching.
|
||||||
- DAC passthrough mode for bit-perfect playback
|
- Favorites and playlists from your Qobuz account.
|
||||||
- Multiple audio backends (PipeWire, ALSA, PulseAudio)
|
- Local library backend: directory scanning, metadata extraction, CUE sheet parsing, and SQLite indexing.
|
||||||
|
|
||||||
### Planned
|
### Integrations
|
||||||
- Playlist management (local + Qobuz sync)
|
- MPRIS media controls and media key support on Linux.
|
||||||
- Favorites synchronization
|
- Desktop notifications for track changes.
|
||||||
- Equalizer
|
- Last.fm scrobbling and now-playing updates.
|
||||||
- Last.fm integration
|
- Shareable Qobuz URLs and universal SongLink links (Odesli).
|
||||||
- Lyrics display
|
|
||||||
|
### Interface
|
||||||
|
- Now playing, queue panel, and full-screen playback views.
|
||||||
|
- Focus mode for distraction-free listening.
|
||||||
|
- English and Spanish localization.
|
||||||
|
|
||||||
|
## Open Source
|
||||||
|
|
||||||
|
QBZ-NIX is MIT-licensed and fully open source. No telemetry, no lock-in, and no hidden services. Just a clean, transparent player built for Linux audio fans.
|
||||||
|
|
||||||
|
## Inspiration
|
||||||
|
|
||||||
|
QBZ-NIX draws inspiration from projects like qobuz-dl, and from the broader Linux audio community that values open tools and high-fidelity playback.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Backend:** Rust + Tauri
|
- **Desktop:** Rust + Tauri 2
|
||||||
- **Frontend:** Svelte + TypeScript
|
- **Frontend:** SvelteKit + TypeScript + Vite
|
||||||
- **Audio:** cpal/rodio with ALSA direct support
|
- **Audio:** rodio + symphonia
|
||||||
|
- **Networking:** reqwest
|
||||||
|
- **Local library:** walkdir + lofty + rusqlite
|
||||||
|
- **Integrations:** souvlaki (MPRIS), notify-rust, SongLink (Odesli)
|
||||||
|
- **UX:** svelte-i18n, lucide-svelte
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -39,7 +60,7 @@ Browser-based players (Chromium, Firefox) have a hardcoded sample rate cap (~48k
|
|||||||
|
|
||||||
- Rust (latest stable)
|
- Rust (latest stable)
|
||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
- Linux with audio support (PipeWire or PulseAudio)
|
- Linux with audio support (PipeWire, ALSA, or PulseAudio)
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
@@ -58,15 +79,19 @@ npm run tauri build
|
|||||||
|
|
||||||
```
|
```
|
||||||
qbz-nix/
|
qbz-nix/
|
||||||
├── src/ # Frontend (Svelte)
|
├── src/ # Frontend (SvelteKit)
|
||||||
├── src-tauri/
|
├── src-tauri/
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── api/ # Qobuz API client
|
│ ├── api/ # Qobuz API client
|
||||||
│ ├── player/ # Audio playback
|
│ ├── player/ # Audio playback engine
|
||||||
│ ├── queue/ # Queue management
|
│ ├── queue/ # Queue management
|
||||||
│ ├── config/ # Settings persistence
|
│ ├── cache/ # Audio cache and prefetch
|
||||||
|
│ ├── library/ # Local library backend
|
||||||
|
│ ├── lastfm/ # Last.fm integration
|
||||||
|
│ ├── share/ # SongLink / share utilities
|
||||||
|
│ ├── media_controls/ # MPRIS integration
|
||||||
│ └── commands/ # Tauri IPC commands
|
│ └── commands/ # Tauri IPC commands
|
||||||
└── static/ # Static assets
|
└── static/ # Static assets and logo
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
# Codex Task: Chromecast Integration
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
QBZ is a native Qobuz streaming client for Linux built with Tauri 2.0 + Rust backend + SvelteKit frontend. We need to add Chromecast support using the `rust-cast` crate.
|
||||||
|
|
||||||
|
**Reference document**: See `docs/CASTING_RESEARCH.md` for full analysis.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement Chromecast audio casting so users can stream music to Google Cast devices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ QBZ Frontend │────▶│ Tauri Commands │────▶│ Cast Module │
|
||||||
|
│ (SvelteKit) │ │ (lib.rs) │ │ (rust-cast) │
|
||||||
|
└─────────────────┘ └──────────────────┘ └────────┬────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────┼─────────┐
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ mDNS Device │ │ Local HTTP │ │
|
||||||
|
│ │ Discovery │ │ Audio Server │ │
|
||||||
|
│ └──────────────┘ └────────┬────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└───────────────────────────────┼───────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Chromecast │
|
||||||
|
│ Device │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
Create files under `src-tauri/src/cast/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri/src/cast/
|
||||||
|
├── mod.rs # Module exports + CastState
|
||||||
|
├── discovery.rs # Device discovery via mDNS
|
||||||
|
├── device.rs # CastDevice wrapper + connection
|
||||||
|
├── media_server.rs # Local HTTP server for streaming
|
||||||
|
├── commands.rs # Tauri commands
|
||||||
|
└── errors.rs # Error types
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Dependencies & Module Setup
|
||||||
|
|
||||||
|
### Cargo.toml additions:
|
||||||
|
```toml
|
||||||
|
# Chromecast
|
||||||
|
rust-cast = "0.21"
|
||||||
|
mdns-sd = "0.11" # For device discovery
|
||||||
|
tiny_http = "0.12" # Simple HTTP server for streaming
|
||||||
|
```
|
||||||
|
|
||||||
|
### mod.rs structure:
|
||||||
|
```rust
|
||||||
|
pub mod commands;
|
||||||
|
pub mod device;
|
||||||
|
pub mod discovery;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod media_server;
|
||||||
|
|
||||||
|
pub use commands::CastState;
|
||||||
|
pub use device::CastDevice;
|
||||||
|
pub use discovery::DeviceDiscovery;
|
||||||
|
pub use errors::CastError;
|
||||||
|
pub use media_server::MediaServer;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Device Discovery (`discovery.rs`)
|
||||||
|
|
||||||
|
Use `mdns-sd` crate to discover Chromecast devices on the local network.
|
||||||
|
|
||||||
|
### Data Model:
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DiscoveredDevice {
|
||||||
|
pub id: String, // Unique device ID
|
||||||
|
pub name: String, // Friendly name (e.g., "Living Room TV")
|
||||||
|
pub model: String, // Device model
|
||||||
|
pub ip: String, // IP address
|
||||||
|
pub port: u16, // Cast port (usually 8009)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions needed:
|
||||||
|
- `start_discovery()` - Start mDNS discovery in background
|
||||||
|
- `stop_discovery()` - Stop discovery
|
||||||
|
- `get_discovered_devices()` - Return list of found devices
|
||||||
|
|
||||||
|
### mDNS service type:
|
||||||
|
- `_googlecast._tcp.local.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Cast Device Connection (`device.rs`)
|
||||||
|
|
||||||
|
Wrapper around `rust-cast::CastDevice` for managing connections.
|
||||||
|
|
||||||
|
### Functions needed:
|
||||||
|
```rust
|
||||||
|
impl CastDeviceConnection {
|
||||||
|
/// Connect to a Chromecast by IP and port
|
||||||
|
pub fn connect(ip: &str, port: u16) -> Result<Self, CastError>;
|
||||||
|
|
||||||
|
/// Disconnect from device
|
||||||
|
pub fn disconnect(&mut self) -> Result<(), CastError>;
|
||||||
|
|
||||||
|
/// Get device status
|
||||||
|
pub fn get_status(&self) -> Result<DeviceStatus, CastError>;
|
||||||
|
|
||||||
|
/// Launch media receiver app
|
||||||
|
pub fn launch_media_app(&mut self) -> Result<String, CastError>;
|
||||||
|
|
||||||
|
/// Load media URL for playback
|
||||||
|
pub fn load_media(&mut self, url: &str, content_type: &str, metadata: MediaMetadata) -> Result<(), CastError>;
|
||||||
|
|
||||||
|
/// Play/pause/stop controls
|
||||||
|
pub fn play(&mut self) -> Result<(), CastError>;
|
||||||
|
pub fn pause(&mut self) -> Result<(), CastError>;
|
||||||
|
pub fn stop(&mut self) -> Result<(), CastError>;
|
||||||
|
|
||||||
|
/// Volume control (0.0 - 1.0)
|
||||||
|
pub fn set_volume(&mut self, volume: f32) -> Result<(), CastError>;
|
||||||
|
|
||||||
|
/// Seek to position
|
||||||
|
pub fn seek(&mut self, position_secs: f64) -> Result<(), CastError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct MediaMetadata {
|
||||||
|
pub title: String,
|
||||||
|
pub artist: String,
|
||||||
|
pub album: String,
|
||||||
|
pub artwork_url: Option<String>,
|
||||||
|
pub duration_secs: Option<u64>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Local Media Server (`media_server.rs`)
|
||||||
|
|
||||||
|
HTTP server to stream audio to Chromecast. The Cast device will fetch audio from this server.
|
||||||
|
|
||||||
|
### Requirements:
|
||||||
|
- Serve audio files via HTTP
|
||||||
|
- Support range requests (for seeking)
|
||||||
|
- Run on dynamic port, return URL to cast device
|
||||||
|
- Support both local files and in-memory audio data
|
||||||
|
|
||||||
|
### Interface:
|
||||||
|
```rust
|
||||||
|
pub struct MediaServer {
|
||||||
|
port: u16,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaServer {
|
||||||
|
/// Start server on available port
|
||||||
|
pub fn start() -> Result<Self, CastError>;
|
||||||
|
|
||||||
|
/// Stop server
|
||||||
|
pub fn stop(&mut self);
|
||||||
|
|
||||||
|
/// Get base URL (e.g., "http://192.168.1.100:8080")
|
||||||
|
pub fn base_url(&self) -> String;
|
||||||
|
|
||||||
|
/// Register audio data to serve (returns path like "/audio/123")
|
||||||
|
pub fn register_audio(&mut self, id: u64, data: Vec<u8>, content_type: &str) -> String;
|
||||||
|
|
||||||
|
/// Register local file to serve
|
||||||
|
pub fn register_file(&mut self, id: u64, file_path: &str) -> Result<String, CastError>;
|
||||||
|
|
||||||
|
/// Get full URL for registered audio
|
||||||
|
pub fn get_audio_url(&self, id: u64) -> Option<String>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content types:
|
||||||
|
- FLAC: `audio/flac`
|
||||||
|
- WAV: `audio/wav`
|
||||||
|
- ALAC/M4A: `audio/mp4`
|
||||||
|
- AIFF: `audio/aiff`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Tauri Commands (`commands.rs`)
|
||||||
|
|
||||||
|
### CastState:
|
||||||
|
```rust
|
||||||
|
pub struct CastState {
|
||||||
|
pub discovery: Arc<Mutex<DeviceDiscovery>>,
|
||||||
|
pub connection: Arc<Mutex<Option<CastDeviceConnection>>>,
|
||||||
|
pub media_server: Arc<Mutex<MediaServer>>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands to implement:
|
||||||
|
```rust
|
||||||
|
// Discovery
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_start_discovery(state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_stop_discovery(state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_get_devices(state: State<'_, CastState>) -> Result<Vec<DiscoveredDevice>, String>;
|
||||||
|
|
||||||
|
// Connection
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_connect(device_id: String, state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_disconnect(state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_get_status(state: State<'_, CastState>) -> Result<CastStatus, String>;
|
||||||
|
|
||||||
|
// Playback
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_play_track(
|
||||||
|
track_id: u64,
|
||||||
|
metadata: MediaMetadata,
|
||||||
|
state: State<'_, CastState>,
|
||||||
|
app_state: State<'_, AppState>, // For accessing audio cache
|
||||||
|
) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_play_local_track(
|
||||||
|
track_id: i64,
|
||||||
|
state: State<'_, CastState>,
|
||||||
|
library_state: State<'_, LibraryState>,
|
||||||
|
) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_play(state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_pause(state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_stop(state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_seek(position_secs: f64, state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cast_set_volume(volume: f32, state: State<'_, CastState>) -> Result<(), String>;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Integration with lib.rs
|
||||||
|
|
||||||
|
Add to `src-tauri/src/lib.rs`:
|
||||||
|
1. `pub mod cast;`
|
||||||
|
2. Initialize `CastState` in `run()`
|
||||||
|
3. Add `.manage(cast_state)`
|
||||||
|
4. Register all cast commands in `invoke_handler`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
### DO NOT modify these files (they're complex and tightly integrated):
|
||||||
|
- `src-tauri/src/player/mod.rs`
|
||||||
|
- `src-tauri/src/queue/mod.rs`
|
||||||
|
- `src-tauri/src/commands/playback.rs`
|
||||||
|
- Any frontend files (`src/`)
|
||||||
|
|
||||||
|
### Testing approach:
|
||||||
|
1. First test discovery: ensure devices are found
|
||||||
|
2. Test connection to device
|
||||||
|
3. Test media server with a simple audio file
|
||||||
|
4. Test full flow: play track to Chromecast
|
||||||
|
|
||||||
|
### Error handling:
|
||||||
|
- Use `CastError` enum with variants for Discovery, Connection, Media, Protocol errors
|
||||||
|
- All commands return `Result<T, String>` for Tauri compatibility
|
||||||
|
|
||||||
|
### Network considerations:
|
||||||
|
- Media server must bind to 0.0.0.0 (not localhost) so Chromecast can reach it
|
||||||
|
- Need to determine local IP address for the Cast device to connect back
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
After implementation, these commands should work:
|
||||||
|
```typescript
|
||||||
|
// Frontend can call:
|
||||||
|
await invoke('cast_start_discovery');
|
||||||
|
const devices = await invoke('cast_get_devices');
|
||||||
|
await invoke('cast_connect', { deviceId: devices[0].id });
|
||||||
|
await invoke('cast_play_track', { trackId: 123, metadata: {...} });
|
||||||
|
await invoke('cast_pause');
|
||||||
|
await invoke('cast_set_volume', { volume: 0.5 });
|
||||||
|
await invoke('cast_disconnect');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog Format
|
||||||
|
|
||||||
|
When done, provide a changelog like:
|
||||||
|
```
|
||||||
|
- src-tauri/Cargo.toml — added rust-cast, mdns-sd, tiny_http dependencies
|
||||||
|
- src-tauri/src/cast/errors.rs — CastError enum
|
||||||
|
- src-tauri/src/cast/discovery.rs — mDNS device discovery
|
||||||
|
- src-tauri/src/cast/device.rs — CastDeviceConnection wrapper
|
||||||
|
- src-tauri/src/cast/media_server.rs — HTTP server for audio streaming
|
||||||
|
- src-tauri/src/cast/commands.rs — Tauri commands + CastState
|
||||||
|
- src-tauri/src/cast/mod.rs — module exports
|
||||||
|
- src-tauri/src/lib.rs — wiring (mod cast, manage, commands)
|
||||||
|
```
|
||||||
Reference in new issue
Block a user