Optimize MPV polling with single batch query every 200ms
All checks were successful
Build and Release / build-and-release (push) Successful in 1m18s
All checks were successful
Build and Release / build-and-release (push) Successful in 1m18s
Replace separate property queries with unified batch fetching: - Consolidate position, duration, and metadata into one IPC call - Reduce polling from 100ms to 200ms (5 FPS) - Remove complex timeout handling in favor of simple blocking reads - Remove unused is_idle, is_paused, and get_property methods This eliminates status bar flashing and reduces CPU usage.
This commit is contained in:
@@ -119,84 +119,75 @@ impl Player {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_property(&mut self, property: &str) -> Option<Value> {
|
||||
// Try to connect - if respawning or connection fails, return None
|
||||
if let Err(e) = self.connect() {
|
||||
tracing::debug!("Failed to connect for property '{}': {}", property, e);
|
||||
return None;
|
||||
fn get_properties_batch(&mut self, properties: &[&str]) -> std::collections::HashMap<String, Value> {
|
||||
let mut results = std::collections::HashMap::new();
|
||||
|
||||
// Try to connect
|
||||
if self.connect().is_err() {
|
||||
return results;
|
||||
}
|
||||
|
||||
let cmd = json!({
|
||||
"command": ["get_property", property],
|
||||
"request_id": 1
|
||||
});
|
||||
|
||||
if let Some(ref mut socket) = self.socket {
|
||||
let msg = format!("{}\n", cmd);
|
||||
|
||||
// Write command
|
||||
if let Err(e) = socket.write_all(msg.as_bytes()) {
|
||||
tracing::warn!("Failed to write get_property command for '{}': {}", property, e);
|
||||
self.socket = None;
|
||||
return None;
|
||||
// Send all property requests at once with unique request_ids
|
||||
for (idx, property) in properties.iter().enumerate() {
|
||||
let cmd = json!({
|
||||
"command": ["get_property", property],
|
||||
"request_id": idx + 1
|
||||
});
|
||||
let msg = format!("{}\n", cmd);
|
||||
if socket.write_all(msg.as_bytes()).is_err() {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to read response with timeout
|
||||
// Read all responses
|
||||
// IMPORTANT: Socket is non-blocking, need to set blocking mode for read
|
||||
socket.set_nonblocking(false).ok();
|
||||
socket.set_read_timeout(Some(Duration::from_millis(100))).ok();
|
||||
|
||||
let cloned_socket = match socket.try_clone() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to clone socket for '{}': {}", property, e);
|
||||
Err(_) => {
|
||||
socket.set_nonblocking(true).ok();
|
||||
return None;
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
// Set timeout on cloned socket too (clone doesn't copy settings)
|
||||
cloned_socket.set_nonblocking(false).ok();
|
||||
cloned_socket.set_read_timeout(Some(Duration::from_millis(100))).ok();
|
||||
|
||||
let mut reader = BufReader::new(cloned_socket);
|
||||
let mut response = String::new();
|
||||
if let Err(e) = reader.read_line(&mut response) {
|
||||
tracing::debug!("Failed to read response for '{}': {}", property, e);
|
||||
socket.set_nonblocking(true).ok();
|
||||
return None;
|
||||
|
||||
// Read responses - stop if we timeout or hit an error
|
||||
for _ in 0..properties.len() {
|
||||
let mut response = String::new();
|
||||
if reader.read_line(&mut response).is_err() {
|
||||
// Timeout or error - stop reading
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse response
|
||||
if let Ok(parsed) = serde_json::from_str::<Value>(&response) {
|
||||
// Check for success
|
||||
if let Some(error) = parsed.get("error").and_then(|e| e.as_str()) {
|
||||
if error == "success" {
|
||||
// Get request_id to match with property
|
||||
if let Some(req_id) = parsed.get("request_id").and_then(|id| id.as_i64()) {
|
||||
let idx = (req_id - 1) as usize;
|
||||
if idx < properties.len() {
|
||||
if let Some(data) = parsed.get("data") {
|
||||
results.insert(properties[idx].to_string(), data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore non-blocking mode
|
||||
socket.set_nonblocking(true).ok();
|
||||
|
||||
// Parse and validate response
|
||||
let parsed: Value = match serde_json::from_str(&response) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to parse JSON response for '{}': {} (response: {})", property, e, response.trim());
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Check for errors in response
|
||||
// MPV returns {"error": "success"} when there's NO error
|
||||
if let Some(error) = parsed.get("error").and_then(|e| e.as_str()) {
|
||||
if error != "success" {
|
||||
tracing::debug!("MPV returned error for '{}': {}", property, error);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate request_id matches (should be 1)
|
||||
if let Some(req_id) = parsed.get("request_id").and_then(|id| id.as_i64()) {
|
||||
if req_id != 1 {
|
||||
tracing::warn!("Request ID mismatch for '{}': expected 1, got {}", property, req_id);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed.get("data").cloned();
|
||||
}
|
||||
|
||||
None
|
||||
results
|
||||
}
|
||||
|
||||
pub fn play(&mut self, path: &Path) -> Result<()> {
|
||||
@@ -242,84 +233,73 @@ impl Player {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_properties(&mut self) {
|
||||
// Update position
|
||||
if let Some(val) = self.get_property("time-pos") {
|
||||
pub fn update_all_properties(&mut self) {
|
||||
// Fetch ALL properties in one batch
|
||||
let results = self.get_properties_batch(&[
|
||||
"time-pos",
|
||||
"duration",
|
||||
"metadata/by-key/artist",
|
||||
"metadata/by-key/ARTIST",
|
||||
"metadata/by-key/album",
|
||||
"metadata/by-key/ALBUM",
|
||||
"metadata/by-key/title",
|
||||
"metadata/by-key/TITLE",
|
||||
"media-title",
|
||||
"audio-codec-name",
|
||||
"audio-bitrate",
|
||||
"audio-params/samplerate",
|
||||
"demuxer-cache-duration",
|
||||
]);
|
||||
|
||||
// Position
|
||||
if let Some(val) = results.get("time-pos") {
|
||||
if let Some(pos) = val.as_f64() {
|
||||
self.position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
// Update duration
|
||||
if let Some(val) = self.get_property("duration") {
|
||||
// Duration
|
||||
if let Some(val) = results.get("duration") {
|
||||
if let Some(dur) = val.as_f64() {
|
||||
self.duration = dur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_metadata(&mut self) {
|
||||
// Try to get artist directly
|
||||
if let Some(val) = self.get_property("metadata/by-key/artist") {
|
||||
self.artist = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
// Fallback to ARTIST (uppercase)
|
||||
if self.artist.is_none() {
|
||||
if let Some(val) = self.get_property("metadata/by-key/ARTIST") {
|
||||
self.artist = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
// Artist - try lowercase first, then uppercase
|
||||
self.artist = results.get("metadata/by-key/artist")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.or_else(|| results.get("metadata/by-key/ARTIST")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string())));
|
||||
|
||||
// Try to get album directly
|
||||
if let Some(val) = self.get_property("metadata/by-key/album") {
|
||||
self.album = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
// Fallback to ALBUM (uppercase)
|
||||
if self.album.is_none() {
|
||||
if let Some(val) = self.get_property("metadata/by-key/ALBUM") {
|
||||
self.album = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
// Album - try lowercase first, then uppercase
|
||||
self.album = results.get("metadata/by-key/album")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.or_else(|| results.get("metadata/by-key/ALBUM")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string())));
|
||||
|
||||
// Try to get title directly
|
||||
if let Some(val) = self.get_property("metadata/by-key/title") {
|
||||
self.media_title = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
// Fallback to TITLE (uppercase)
|
||||
if self.media_title.is_none() {
|
||||
if let Some(val) = self.get_property("metadata/by-key/TITLE") {
|
||||
self.media_title = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
// Title - try lowercase, then uppercase, then media-title
|
||||
self.media_title = results.get("metadata/by-key/title")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.or_else(|| results.get("metadata/by-key/TITLE")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string())))
|
||||
.or_else(|| results.get("media-title")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string())));
|
||||
|
||||
// Final fallback to media-title if metadata doesn't have title
|
||||
if self.media_title.is_none() {
|
||||
if let Some(val) = self.get_property("media-title") {
|
||||
self.media_title = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
// Audio codec
|
||||
self.audio_codec = results.get("audio-codec-name")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()));
|
||||
|
||||
// Update audio codec
|
||||
if let Some(val) = self.get_property("audio-codec-name") {
|
||||
self.audio_codec = val.as_str().map(|s| s.to_string());
|
||||
}
|
||||
// Audio bitrate (convert from bps to kbps)
|
||||
self.audio_bitrate = results.get("audio-bitrate")
|
||||
.and_then(|v| v.as_f64().map(|b| b / 1000.0));
|
||||
|
||||
// Update audio bitrate (convert from bps to kbps)
|
||||
if let Some(val) = self.get_property("audio-bitrate") {
|
||||
self.audio_bitrate = val.as_f64().map(|b| b / 1000.0);
|
||||
}
|
||||
// Sample rate
|
||||
self.sample_rate = results.get("audio-params/samplerate")
|
||||
.and_then(|v| v.as_i64());
|
||||
|
||||
// Update sample rate
|
||||
if let Some(val) = self.get_property("audio-params/samplerate") {
|
||||
self.sample_rate = val.as_i64();
|
||||
}
|
||||
|
||||
// Update cache duration (how many seconds are buffered ahead)
|
||||
if let Some(val) = self.get_property("demuxer-cache-duration") {
|
||||
self.cache_duration = val.as_f64();
|
||||
} else {
|
||||
self.cache_duration = None;
|
||||
}
|
||||
// Cache duration
|
||||
self.cache_duration = results.get("demuxer-cache-duration")
|
||||
.and_then(|v| v.as_f64());
|
||||
}
|
||||
|
||||
pub fn get_position(&self) -> Option<f64> {
|
||||
@@ -330,20 +310,14 @@ impl Player {
|
||||
Some(self.duration)
|
||||
}
|
||||
|
||||
pub fn is_idle(&mut self) -> Option<bool> {
|
||||
self.get_property("idle-active")
|
||||
.and_then(|v| v.as_bool())
|
||||
}
|
||||
|
||||
pub fn is_paused(&mut self) -> Option<bool> {
|
||||
self.get_property("pause")
|
||||
.and_then(|v| v.as_bool())
|
||||
}
|
||||
|
||||
pub fn get_player_state(&mut self) -> Option<crate::state::PlayerState> {
|
||||
use crate::state::PlayerState;
|
||||
let is_idle = self.is_idle()?;
|
||||
let is_paused = self.is_paused()?;
|
||||
|
||||
// Batch fetch both properties at once
|
||||
let results = self.get_properties_batch(&["idle-active", "pause"]);
|
||||
|
||||
let is_idle = results.get("idle-active").and_then(|v| v.as_bool())?;
|
||||
let is_paused = results.get("pause").and_then(|v| v.as_bool())?;
|
||||
|
||||
Some(if is_idle {
|
||||
PlayerState::Stopped
|
||||
|
||||
Reference in New Issue
Block a user