From a6122f9db4f0b97d3361f3daa97cdd9e874ae3ca Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 16 Jun 2026 23:51:19 +0200 Subject: [PATCH] fix(windows): transcode WebP cover to PNG for the media controls (#1102) Windows SMTC could not render our cached WebP album covers: souvlaki loads the file and SetThumbnail/set_metadata succeed, but the lock screen and Quick Settings media tile showed a blank cover, because the OS thumbnail decoder does not handle WebP even with the Store WebP extension installed. Transcode local file:// WebP covers to PNG (libwebp decode then image PNG encode, into a single reusable temp file) before handing them to the OS media controls, gated to Windows. macOS (ImageIO) and Linux pass through unchanged. (cherry picked from commit 76d028127d3a79627057e15e3838b2b186807e88) --- .../src/lib_commands/app_api/integration.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src-tauri/src/lib_commands/app_api/integration.rs b/src-tauri/src/lib_commands/app_api/integration.rs index 3009ff05..0d105af4 100644 --- a/src-tauri/src/lib_commands/app_api/integration.rs +++ b/src-tauri/src/lib_commands/app_api/integration.rs @@ -84,6 +84,15 @@ pub(crate) fn mpris_set_metadata( let duration = duration_secs.map(Duration::from_secs_f64); let mut guard = controls.lock().unwrap(); let Some(ctrl) = guard.as_mut() else { return Ok(()); }; + + // #1102: Windows SMTC cannot render our cached WebP covers. souvlaki loads + // the file and SetThumbnail/set_metadata succeed, but the lock screen and + // Quick-Settings media tile show a blank cover (the OS thumbnail decoder + // does not handle WebP, even with the Store WebP extension installed). + // Transcode local WebP covers to PNG for the OS media controls; macOS + // (ImageIO) decodes WebP fine, so other platforms pass through unchanged. + let cover_url = smtc_cover_url(cover_url); + ctrl.set_metadata(MediaMetadata { title: title.as_deref(), artist: artist.as_deref(), @@ -94,6 +103,48 @@ pub(crate) fn mpris_set_metadata( .map_err(|e| format!("MPRIS set_metadata failed: {e:?}")) } +/// Rewrite a cached WebP cover URL to a PNG the OS media controls can render. +/// Windows SMTC cannot decode WebP thumbnails (#1102); other platforms and any +/// non-`file://`/non-WebP URL pass through unchanged. +fn smtc_cover_url(cover_url: Option) -> Option { + #[cfg(target_os = "windows")] + { + if let Some(url) = cover_url.as_deref() { + if let Some(path) = url.strip_prefix("file://") { + let is_webp = std::path::Path::new(path) + .extension() + .is_some_and(|e| e.eq_ignore_ascii_case("webp")); + if is_webp { + match webp_file_to_temp_png(path) { + Ok(png) => return Some(format!("file://{png}")), + Err(e) => { + crate::app_eprintln!("[mpris] cover WebP->PNG transcode failed: {e}") + } + } + } + } + } + } + cover_url +} + +/// Decode a WebP file (libwebp, the same codec that wrote the cover cache) and +/// re-encode it as a PNG in the temp dir, returning the native path. A single +/// reusable file is fine: souvlaki reads it synchronously inside `set_metadata`, +/// and the controls mutex serializes calls so it is never written concurrently. +#[cfg(target_os = "windows")] +fn webp_file_to_temp_png(webp_path: &str) -> Result { + let bytes = std::fs::read(webp_path).map_err(|e| e.to_string())?; + let decoded = webp::Decoder::new(&bytes) + .decode() + .ok_or_else(|| "WebP decode returned None".to_string())?; + let img = decoded.to_image(); + let out = std::env::temp_dir().join("psysonic-smtc-cover.png"); + img.save_with_format(&out, image::ImageFormat::Png) + .map_err(|e| e.to_string())?; + Ok(out.to_string_lossy().into_owned()) +} + #[tauri::command] pub(crate) fn mpris_set_playback( controls: tauri::State,