feat(audio): add streaming-only mode backend
Add streaming_only setting that skips L1+L2 cache writes while still respecting offline cache (local-first). When enabled: - download_and_stream() skips final cache.insert() - Standard download path skips cache.insert() - Offline cache continues to work normally This reduces storage usage for users who prefer pure streaming.
This commit is contained in:
1 parent
eb9d9dbb15
commit
b4944aa527
3 files changed
+55
-14
No files matched your search
@@ -114,14 +114,14 @@ pub async fn play_track(
|
|||||||
// Not in any cache - check if streaming is enabled
|
// Not in any cache - check if streaming is enabled
|
||||||
log::info!("Track {} not in any cache, checking streaming setting...", track_id);
|
log::info!("Track {} not in any cache, checking streaming setting...", track_id);
|
||||||
|
|
||||||
// Check if stream_first_track is enabled
|
// Check streaming settings
|
||||||
let (stream_first_enabled, buffer_seconds) = {
|
let (stream_first_enabled, buffer_seconds, streaming_only) = {
|
||||||
let store = audio_settings.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
let store = audio_settings.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||||
match store.get_settings() {
|
match store.get_settings() {
|
||||||
Ok(settings) => (settings.stream_first_track, settings.stream_buffer_seconds),
|
Ok(settings) => (settings.stream_first_track, settings.stream_buffer_seconds, settings.streaming_only),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to get audio settings, using defaults: {}", e);
|
log::warn!("Failed to get audio settings, using defaults: {}", e);
|
||||||
(false, 3)
|
(false, 3, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -171,10 +171,17 @@ pub async fn play_track(
|
|||||||
let url = stream_url.url.clone();
|
let url = stream_url.url.clone();
|
||||||
let cache_clone = cache.clone();
|
let cache_clone = cache.clone();
|
||||||
let content_len = stream_info.content_length;
|
let content_len = stream_info.content_length;
|
||||||
|
let skip_cache = streaming_only;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
match download_and_stream(&url, buffer_writer, track_id, cache_clone, content_len).await {
|
match download_and_stream(&url, buffer_writer, track_id, cache_clone, content_len, skip_cache).await {
|
||||||
Ok(()) => log::info!("Track {} ready for instant replay from cache", track_id),
|
Ok(()) => {
|
||||||
Err(e) => log::error!("Streaming cache failed for track {}: {}", track_id, e),
|
if skip_cache {
|
||||||
|
log::info!("Track {} streaming complete (cache disabled)", track_id);
|
||||||
|
} else {
|
||||||
|
log::info!("Track {} ready for instant replay from cache", track_id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => log::error!("Streaming failed for track {}: {}", track_id, e),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,8 +203,12 @@ pub async fn play_track(
|
|||||||
let audio_data = download_audio(&stream_url.url).await?;
|
let audio_data = download_audio(&stream_url.url).await?;
|
||||||
let data_size = audio_data.len();
|
let data_size = audio_data.len();
|
||||||
|
|
||||||
// Cache it
|
// Cache it (unless streaming_only mode)
|
||||||
cache.insert(track_id, audio_data.clone());
|
if !streaming_only {
|
||||||
|
cache.insert(track_id, audio_data.clone());
|
||||||
|
} else {
|
||||||
|
log::info!("Streaming-only mode: skipping cache for track {}", track_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Play it
|
// Play it
|
||||||
state.player.play_data(audio_data, track_id)?;
|
state.player.play_data(audio_data, track_id)?;
|
||||||
@@ -491,13 +502,14 @@ fn extract_audio_format_from_header(data: &[u8]) -> Result<(u32, u16, u32), Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Download audio chunks and stream them to the buffer writer
|
/// Download audio chunks and stream them to the buffer writer
|
||||||
/// Also caches the complete data when download finishes
|
/// Also caches the complete data when download finishes (unless skip_cache is true)
|
||||||
async fn download_and_stream(
|
async fn download_and_stream(
|
||||||
url: &str,
|
url: &str,
|
||||||
writer: crate::player::BufferWriter,
|
writer: crate::player::BufferWriter,
|
||||||
track_id: u64,
|
track_id: u64,
|
||||||
cache: Arc<AudioCache>,
|
cache: Arc<AudioCache>,
|
||||||
content_length: u64,
|
content_length: u64,
|
||||||
|
skip_cache: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
@@ -594,9 +606,13 @@ async fn download_and_stream(
|
|||||||
avg_speed
|
avg_speed
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cache the complete file for future plays
|
// Cache the complete file for future plays (unless streaming-only mode)
|
||||||
log::info!("Caching track {} for future playback", track_id);
|
if skip_cache {
|
||||||
cache.insert(track_id, all_data);
|
log::info!("Streaming-only mode: skipping cache for track {}", track_id);
|
||||||
|
} else {
|
||||||
|
log::info!("Caching track {} for future playback", track_id);
|
||||||
|
cache.insert(track_id, all_data);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct AudioSettings {
|
|||||||
pub stream_first_track: bool,
|
pub stream_first_track: bool,
|
||||||
/// Initial buffer size in seconds before starting streaming playback (1-10, default 3)
|
/// Initial buffer size in seconds before starting streaming playback (1-10, default 3)
|
||||||
pub stream_buffer_seconds: u8,
|
pub stream_buffer_seconds: u8,
|
||||||
|
/// When true, skip L1+L2 cache writes (streaming-only mode). Offline cache still works.
|
||||||
|
pub streaming_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AudioSettings {
|
impl Default for AudioSettings {
|
||||||
@@ -34,6 +36,7 @@ impl Default for AudioSettings {
|
|||||||
alsa_hardware_volume: false, // Disabled by default (maximum compatibility)
|
alsa_hardware_volume: false, // Disabled by default (maximum compatibility)
|
||||||
stream_first_track: true, // Enabled by default for faster playback start
|
stream_first_track: true, // Enabled by default for faster playback start
|
||||||
stream_buffer_seconds: 3, // 3 seconds initial buffer
|
stream_buffer_seconds: 3, // 3 seconds initial buffer
|
||||||
|
streaming_only: false, // Disabled by default (cache tracks for instant replay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +81,7 @@ impl AudioSettingsStore {
|
|||||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN alsa_hardware_volume INTEGER DEFAULT 0", []);
|
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN alsa_hardware_volume INTEGER DEFAULT 0", []);
|
||||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN stream_first_track INTEGER DEFAULT 0", []);
|
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN stream_first_track INTEGER DEFAULT 0", []);
|
||||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN stream_buffer_seconds INTEGER DEFAULT 3", []);
|
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN stream_buffer_seconds INTEGER DEFAULT 3", []);
|
||||||
|
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN streaming_only INTEGER DEFAULT 0", []);
|
||||||
|
|
||||||
Ok(Self { conn })
|
Ok(Self { conn })
|
||||||
}
|
}
|
||||||
@@ -85,7 +89,7 @@ impl AudioSettingsStore {
|
|||||||
pub fn get_settings(&self) -> Result<AudioSettings, String> {
|
pub fn get_settings(&self) -> Result<AudioSettings, String> {
|
||||||
self.conn
|
self.conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate, backend_type, alsa_plugin, alsa_hardware_volume, stream_first_track, stream_buffer_seconds FROM audio_settings WHERE id = 1",
|
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate, backend_type, alsa_plugin, alsa_hardware_volume, stream_first_track, stream_buffer_seconds, streaming_only FROM audio_settings WHERE id = 1",
|
||||||
[],
|
[],
|
||||||
|row| {
|
|row| {
|
||||||
// Parse backend_type from JSON string
|
// Parse backend_type from JSON string
|
||||||
@@ -108,6 +112,7 @@ impl AudioSettingsStore {
|
|||||||
alsa_hardware_volume: row.get::<_, Option<i64>>(6)?.unwrap_or(0) != 0,
|
alsa_hardware_volume: row.get::<_, Option<i64>>(6)?.unwrap_or(0) != 0,
|
||||||
stream_first_track: row.get::<_, Option<i64>>(7)?.unwrap_or(0) != 0,
|
stream_first_track: row.get::<_, Option<i64>>(7)?.unwrap_or(0) != 0,
|
||||||
stream_buffer_seconds: row.get::<_, Option<i64>>(8)?.unwrap_or(3) as u8,
|
stream_buffer_seconds: row.get::<_, Option<i64>>(8)?.unwrap_or(3) as u8,
|
||||||
|
streaming_only: row.get::<_, Option<i64>>(9)?.unwrap_or(0) != 0,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -215,6 +220,16 @@ impl AudioSettingsStore {
|
|||||||
.map_err(|e| format!("Failed to set stream buffer seconds: {}", e))?;
|
.map_err(|e| format!("Failed to set stream buffer seconds: {}", e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_streaming_only(&self, enabled: bool) -> Result<(), String> {
|
||||||
|
self.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE audio_settings SET streaming_only = ?1 WHERE id = 1",
|
||||||
|
params![enabled as i64],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to set streaming only: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Thread-safe wrapper
|
/// Thread-safe wrapper
|
||||||
@@ -319,3 +334,12 @@ pub fn set_audio_stream_buffer_seconds(
|
|||||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||||
store.set_stream_buffer_seconds(seconds)
|
store.set_stream_buffer_seconds(seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_audio_streaming_only(
|
||||||
|
state: tauri::State<'_, AudioSettingsState>,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||||
|
store.set_streaming_only(enabled)
|
||||||
|
}
|
||||||
@@ -770,6 +770,7 @@ pub fn run() {
|
|||||||
config::audio_settings::set_audio_alsa_hardware_volume,
|
config::audio_settings::set_audio_alsa_hardware_volume,
|
||||||
config::audio_settings::set_audio_stream_first_track,
|
config::audio_settings::set_audio_stream_first_track,
|
||||||
config::audio_settings::set_audio_stream_buffer_seconds,
|
config::audio_settings::set_audio_stream_buffer_seconds,
|
||||||
|
config::audio_settings::set_audio_streaming_only,
|
||||||
// Audio backend commands
|
// Audio backend commands
|
||||||
commands::get_available_backends,
|
commands::get_available_backends,
|
||||||
commands::get_devices_for_backend,
|
commands::get_devices_for_backend,
|
||||||
|
|||||||
Reference in new issue
Block a user