From 5dc84874dc45886b6b7ec6a924903d01718cbb19 Mon Sep 17 00:00:00 2001 From: TastelessVoid Date: Tue, 5 May 2026 18:15:35 +0200 Subject: [PATCH 1/4] (Tauri/fix):Android APK crashing and UnifiedPush not registering --- .gitignore | 2 +- src-tauri/gen/android/app/proguard-rules.pro | 37 +++++++++++++++++- .../android/app/src/main/AndroidManifest.xml | 3 ++ src-tauri/src/lib.rs | 16 ++++++-- .../notifications/SystemNotification.tsx | 4 +- .../notifications/UnifiedPushNotifications.ts | 38 +++++++++++++------ src/app/generated/tauri/.typecache | 6 +-- src/app/generated/tauri/commands.ts | 2 +- src/app/generated/tauri/index.ts | 2 +- src/app/generated/tauri/types.ts | 2 +- 10 files changed, 88 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 4699f9800..7ad97f72f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ devAssets .DS_Store .idea - +*.code-workspace .env .wrangler diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro index 481bb4348..b63990750 100644 --- a/src-tauri/gen/android/app/proguard-rules.pro +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -18,4 +18,39 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +# Keep Android bridge symbols used by Wry/Tauri JNI on release builds. +# Without these, R8 can rename/remove methods like WryActivity.getId(), +# which tao resolves by method name via JNI. +-keep class moe.sable.client.* { + native ; +} + +-keep class moe.sable.client.WryActivity { + public (...); + + void setWebView(moe.sable.client.RustWebView); + java.lang.Class getAppClass(...); + java.lang.String getVersion(); + int startActivity(...); + int getId(); +} + +-keep class moe.sable.client.Ipc { + public (...); + + @android.webkit.JavascriptInterface public ; +} + +-keep class moe.sable.client.RustWebView { + public (...); + + void loadUrlMainThread(...); + void loadHTMLMainThread(...); + void evalScript(...); +} + +-keep class moe.sable.client.RustWebChromeClient,moe.sable.client.RustWebViewClient { + public (...); +} \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index ec115b553..2e50b687b 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + @@ -26,6 +27,8 @@ + + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index adf77e133..4232017eb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -78,7 +78,7 @@ pub fn run() { #[cfg(mobile)] let builder = builder.plugin(tauri_plugin_edge_to_edge::init()); - builder + let app = builder .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_deep_link::init()) @@ -122,9 +122,17 @@ pub fn run() { #[cfg(windows)] desktop::windows::window_tracking::is_window_tracking_active, ]) - .build(tauri::generate_context!()) - .expect("error while building tauri application") - .run(|app, event| { + .build(tauri::generate_context!()); + + let Ok(app) = app else { + // On Android this function is called through JNI. Panicking here causes + // "panic_cannot_unwind" and aborts the whole process without a useful + // message in logcat. + eprintln!("failed to build tauri application: {app:?}"); + return; + }; + + app.run(|app, event| { #[cfg(desktop)] desktop::tray::handle_run_event(app, event); diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx index d9e5a97ba..a48a8f5f7 100644 --- a/src/app/features/settings/notifications/SystemNotification.tsx +++ b/src/app/features/settings/notifications/SystemNotification.tsx @@ -592,6 +592,7 @@ function BackgroundPushNotificationSetting() { registration: { endpoint: string; instance: string; + gatewayUrl?: string; distributor?: string; pubKeySet?: { pubKey: string; @@ -603,7 +604,8 @@ function BackgroundPushNotificationSetting() { endpoint: registration.endpoint, instance: registration.instance, appId: effectivePushTransport.unifiedPushAppID?.trim() ?? DEFAULT_UNIFIED_PUSH_APP_ID, - gatewayUrl: effectivePushTransport.unifiedPushGatewayUrl?.trim() ?? undefined, + gatewayUrl: + registration.gatewayUrl ?? effectivePushTransport.unifiedPushGatewayUrl?.trim() ?? undefined, status: 'registered', distributor: distributorOverride ?? registration.distributor, permissionState: 'granted', diff --git a/src/app/features/settings/notifications/UnifiedPushNotifications.ts b/src/app/features/settings/notifications/UnifiedPushNotifications.ts index bd40857fa..16292316b 100644 --- a/src/app/features/settings/notifications/UnifiedPushNotifications.ts +++ b/src/app/features/settings/notifications/UnifiedPushNotifications.ts @@ -33,13 +33,25 @@ const unifiedPushLog = createDebugLogger('unifiedpush'); * Falls back to the configured or public UP gateway. * Note: pushNotifyUrl (Sygnal) is NOT suitable — only a proper UP gateway works. */ -async function discoverGateway(upEndpoint: string, unifiedPushGateway?: string): Promise { - try { - const probeUrl = new URL(upEndpoint); - probeUrl.pathname = '/_matrix/push/v1/notify'; - probeUrl.search = ''; - const res = await fetch(probeUrl.toString()); - if (res.ok) { +async function discoverGateway( + upEndpoint: string, + unifiedPushGateway?: string, + upInstance?: string +): Promise { + const probeCandidates = [upInstance, upEndpoint].filter( + (candidate): candidate is string => !!candidate?.trim() + ); + + for (const candidate of probeCandidates) { + try { + const probeUrl = new URL(candidate); + probeUrl.pathname = '/_matrix/push/v1/notify'; + probeUrl.search = ''; + const res = await fetch(probeUrl.toString()); + if (!res.ok) { + continue; + } + const body = await res.json(); if ( body?.gateway === 'matrix' || @@ -47,10 +59,11 @@ async function discoverGateway(upEndpoint: string, unifiedPushGateway?: string): ) { return probeUrl.toString(); } + } catch { + // probe failed } - } catch { - // probe failed } + return unifiedPushGateway ?? UP_PUBLIC_GATEWAY; } @@ -85,6 +98,7 @@ export type EnableUnifiedPushResult = status: 'registered'; endpoint: string; instance: string; + gatewayUrl: string; distributor: string; pubKeySet?: { pubKey: string; @@ -132,7 +146,7 @@ export async function tryEnableUnifiedPush( const { endpoint, instance, pubKeySet } = registration; const resolvedConfig = resolveUnifiedPushPusherConfig(config); - const gatewayUrl = await discoverGateway(endpoint, resolvedConfig.gatewayUrl); + const gatewayUrl = await discoverGateway(endpoint, resolvedConfig.gatewayUrl, instance); const pusherData: Record = { url: gatewayUrl, @@ -160,6 +174,7 @@ export async function tryEnableUnifiedPush( status: 'registered', endpoint, instance, + gatewayUrl, distributor: registration.distributor, pubKeySet, }; @@ -168,7 +183,7 @@ export async function tryEnableUnifiedPush( export async function enableUnifiedPush( mx: MatrixClient, config?: UnifiedPushTransportConfigInput -): Promise<{ endpoint: string; instance: string }> { +): Promise<{ endpoint: string; instance: string; gatewayUrl: string }> { const result = await tryEnableUnifiedPush(mx, config); if (result.status !== 'registered') { throw new Error(result.error ?? 'UnifiedPush registration failed'); @@ -177,6 +192,7 @@ export async function enableUnifiedPush( return { endpoint: result.endpoint, instance: result.instance, + gatewayUrl: result.gatewayUrl, }; } diff --git a/src/app/generated/tauri/.typecache b/src/app/generated/tauri/.typecache index 2b07bd3d6..d14fbec66 100644 --- a/src/app/generated/tauri/.typecache +++ b/src/app/generated/tauri/.typecache @@ -1,8 +1,8 @@ { "version": 2, - "commands_hash": "fc2eef5fe637d84", - "structs_hash": "558120cf32bcd332", + "commands_hash": "eb32eda4bc83b382", + "structs_hash": "a284d88145e0b10b", "events_hash": "7904dd65836c8ff4", "config_hash": "c72a07caa5bc6ed4", - "combined_hash": "6c5e9d69794c83ae" + "combined_hash": "9a335f76f3f4c201" } \ No newline at end of file diff --git a/src/app/generated/tauri/commands.ts b/src/app/generated/tauri/commands.ts index e8f54a530..d655f2762 100644 --- a/src/app/generated/tauri/commands.ts +++ b/src/app/generated/tauri/commands.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-04-12T10:38:31.125771300+00:00 + * Generated at: 2026-05-04T20:53:05.987838956+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate diff --git a/src/app/generated/tauri/index.ts b/src/app/generated/tauri/index.ts index 62c5361e2..871d9cf1a 100644 --- a/src/app/generated/tauri/index.ts +++ b/src/app/generated/tauri/index.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-04-12T10:38:31.126453100+00:00 + * Generated at: 2026-05-04T20:53:05.988109318+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate diff --git a/src/app/generated/tauri/types.ts b/src/app/generated/tauri/types.ts index 53dd15000..0a885d749 100644 --- a/src/app/generated/tauri/types.ts +++ b/src/app/generated/tauri/types.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-04-12T10:38:31.125008800+00:00 + * Generated at: 2026-05-04T20:53:05.987256613+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate From 2c38acb8cb2ea20505a014a3cb453b1119fd4575 Mon Sep 17 00:00:00 2001 From: TastelessVoid Date: Tue, 5 May 2026 18:39:04 +0200 Subject: [PATCH 2/4] Fixed ESLint errors --- .../notifications/UnifiedPushNotifications.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/app/features/settings/notifications/UnifiedPushNotifications.ts b/src/app/features/settings/notifications/UnifiedPushNotifications.ts index 16292316b..0fcbf34d3 100644 --- a/src/app/features/settings/notifications/UnifiedPushNotifications.ts +++ b/src/app/features/settings/notifications/UnifiedPushNotifications.ts @@ -42,15 +42,13 @@ async function discoverGateway( (candidate): candidate is string => !!candidate?.trim() ); - for (const candidate of probeCandidates) { + const probeCandidate = async (candidate: string): Promise => { try { const probeUrl = new URL(candidate); probeUrl.pathname = '/_matrix/push/v1/notify'; probeUrl.search = ''; const res = await fetch(probeUrl.toString()); - if (!res.ok) { - continue; - } + if (!res.ok) return undefined; const body = await res.json(); if ( @@ -60,11 +58,23 @@ async function discoverGateway( return probeUrl.toString(); } } catch { - // probe failed + // Probe failed (network error, invalid URL, etc) } - } + return undefined; + }; + + const probeAtIndex = async (index: number): Promise => { + if (index >= probeCandidates.length) return undefined; + + const candidate = probeCandidates[index]; + const result = await probeCandidate(candidate); + if (result) return result; + + return probeAtIndex(index + 1); + }; - return unifiedPushGateway ?? UP_PUBLIC_GATEWAY; + const discoveredGateway = await probeAtIndex(0); + return discoveredGateway ?? unifiedPushGateway ?? UP_PUBLIC_GATEWAY; } const UP_REGISTER_TIMEOUT_MS = 30_000; From d780c452252ab113b508cb36163384d468c6b6d0 Mon Sep 17 00:00:00 2001 From: TastelessVoid Date: Fri, 15 May 2026 01:16:40 +0200 Subject: [PATCH 3/4] Fix Tauri desktop builds and media caching --- src-tauri/src/main.rs | 10 +++++ src/app/pages/client/ClientRoot.tsx | 63 +++++++++++++++++++++-------- src/app/pages/client/SyncStatus.tsx | 2 +- src/app/utils/mediaCache.ts | 17 +++++++- src/app/utils/mediaTransport.ts | 3 +- src/client/initMatrix.ts | 18 +++++---- 6 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d42d2ed28..d88f86cd0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,6 +16,16 @@ fn main() { if std::env::var_os("__NV_DISABLE_EXPLICIT_SYNC").is_none() { std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1"); } + + // WebKit2GTK can hit compositor/DMABUF bugs + // https://github.com/tauri-apps/tauri/issues/14424 + // https://github.com/tauri-apps/tauri/issues/9394 + if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() { + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + } + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } } app_lib::run(); diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index d20d8cd56..10931d8b2 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -197,6 +197,7 @@ export function ClientRoot({ children }: ClientRootProps) { const loadedUserIdRef = useRef(undefined); const syncStartTimeRef = useRef(performance.now()); const firstSyncReadyRef = useRef(false); + const loadingStateLoggedRef = useRef<'unknown' | 'true' | 'false'>('unknown'); const [loadState, loadMatrix, setLoadState] = useAsyncCallback( useCallback(async () => { @@ -227,7 +228,7 @@ export function ClientRoot({ children }: ClientRootProps) { const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined; - const [startState, startMatrix] = useAsyncCallback( + const [startState, startMatrix, setStartState] = useAsyncCallback( useCallback( (m) => startClient(m, { @@ -239,6 +240,19 @@ export function ClientRoot({ children }: ClientRootProps) { ) ); + useEffect(() => { + if (loadState.status === AsyncStatus.Loading) { + firstSyncReadyRef.current = false; + syncStartTimeRef.current = performance.now(); + } + if (loadState.status !== AsyncStatus.Success) { + setStartState((prev) => { + if (prev.status === AsyncStatus.Idle) return prev; + return { status: AsyncStatus.Idle }; + }); + } + }, [loadState.status, setStartState]); + useEffect(() => { if (!activeSession) return; if (loadedUserIdRef.current && loadedUserIdRef.current !== activeSession.userId) { @@ -289,6 +303,32 @@ export function ClientRoot({ children }: ClientRootProps) { } }, [loadState, loadMatrix]); + useSyncState( + mx, + useCallback((state: string) => { + if (isClientReady(state)) { + if (!firstSyncReadyRef.current) { + firstSyncReadyRef.current = true; + try { + Sentry.metrics.distribution( + 'sable.sync.time_to_ready_ms', + performance.now() - syncStartTimeRef.current + ); + } catch { + // no-op: never block loading gate on telemetry + } + } + setLoading(false); + } + }, []) + ); + + useEffect(() => { + const next = loading ? 'true' : 'false'; + if (loadingStateLoggedRef.current === next) return; + loadingStateLoggedRef.current = next; + }, [loading]); + useEffect(() => { if (mx && !mx.clientRunning) { startMatrix(mx); @@ -302,21 +342,12 @@ export function ClientRoot({ children }: ClientRootProps) { } }, [mx]); - useSyncState( - mx, - useCallback((state: string) => { - if (isClientReady(state)) { - if (!firstSyncReadyRef.current) { - firstSyncReadyRef.current = true; - Sentry.metrics.distribution( - 'sable.sync.time_to_ready_ms', - performance.now() - syncStartTimeRef.current - ); - } - setLoading(false); - } - }, []) - ); + useEffect(() => { + if (startState.status !== AsyncStatus.Success || !mx) return; + const syncState = mx.getSyncState(); + if (!isClientReady(syncState)) return; + setLoading(false); + }, [startState.status, mx]); // Set matrix client context: homeserver and sync type (not PII) useEffect(() => { diff --git a/src/app/pages/client/SyncStatus.tsx b/src/app/pages/client/SyncStatus.tsx index 04b6bed09..9dd077f6f 100644 --- a/src/app/pages/client/SyncStatus.tsx +++ b/src/app/pages/client/SyncStatus.tsx @@ -38,7 +38,7 @@ const isSyncStatusDemoEnabled = (): boolean => { export function SyncStatus({ mx }: SyncStatusProps) { const [stateData, setStateData] = useState({ - current: null, + current: mx.getSyncState(), previous: undefined, }); const [demoIndex, setDemoIndex] = useState(0); diff --git a/src/app/utils/mediaCache.ts b/src/app/utils/mediaCache.ts index 8e8f21976..1f013c768 100644 --- a/src/app/utils/mediaCache.ts +++ b/src/app/utils/mediaCache.ts @@ -10,11 +10,24 @@ async function openCache(): Promise { } } +function getCacheRequest(url: string): Request { + const isAbsoluteHttpUrl = /^https?:\/\//i.test(url); + if (!isAbsoluteHttpUrl) { + return new Request(`https://sable-media-cache.invalid/${encodeURIComponent(url)}`); + } + + try { + return new Request(url); + } catch { + return new Request(`https://sable-media-cache.invalid/${encodeURIComponent(url)}`); + } +} + export async function getFromMediaCache(url: string): Promise { const cache = await openCache(); if (!cache) return undefined; try { - const response = await cache.match(url); + const response = await cache.match(getCacheRequest(url)); if (!response) return undefined; return await response.blob(); } catch { @@ -43,7 +56,7 @@ export async function putInMediaCache(url: string, blob: Blob): Promise { if (!cache) return; try { await cache.put( - url, + getCacheRequest(url), new Response(blob, { headers: { 'Content-Type': blob.type || 'application/octet-stream' }, }) diff --git a/src/app/utils/mediaTransport.ts b/src/app/utils/mediaTransport.ts index e98964e5e..b80035fec 100644 --- a/src/app/utils/mediaTransport.ts +++ b/src/app/utils/mediaTransport.ts @@ -175,7 +175,8 @@ async function fetchMediaResponse( async function fetchMediaBlobInternal(url: string, options?: MediaTransportOptions): Promise { const cacheMode = options?.cache ?? 'default'; - const scopedCacheKey = getScopedMediaCacheKey(url, resolveSessionScope(options)); + const resolvedScope = resolveSessionScope(options); + const scopedCacheKey = getScopedMediaCacheKey(url, resolvedScope); if (cacheMode === 'default') { const cachedBlob = await getFromMediaCache(scopedCacheKey); diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 7206bd6ef..4f27fb6a7 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -476,13 +476,17 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig): data?: ISyncStateData ) => { classicSyncCount += 1; - Sentry.metrics.count('sable.sync.cycle', 1, { attributes: { transport: 'classic', state } }); - debugLog.info('sync', `Classic sync state: ${state}`, { - state, - prevState: prevState ?? 'null', - syncNumber: classicSyncCount, - error: data?.error?.message, - }); + if (prevState !== state) { + Sentry.metrics.count('sable.sync.cycle', 1, { + attributes: { transport: 'classic', state }, + }); + debugLog.info('sync', `Classic sync state: ${state}`, { + state, + prevState: prevState ?? 'null', + syncNumber: classicSyncCount, + error: data?.error?.message, + }); + } if (state === SyncState.Error || state === SyncState.Reconnecting) { debugLog.warn('sync', `Classic sync problem: ${state}`, { state, From 68d02a153b4249684f6303920c38eb42d350244a Mon Sep 17 00:00:00 2001 From: TastelessVoid Date: Fri, 15 May 2026 16:23:54 +0200 Subject: [PATCH 4/4] Add keystore to gitignore, fixed AppImage failing due to GStreamer plugin discovery failing --- .gitignore | 5 ++++- src-tauri/src/main.rs | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ad97f72f..23c7e1093 100644 --- a/.gitignore +++ b/.gitignore @@ -40,9 +40,12 @@ dist-js src-tauri/target/ # Tauri Android build outputs src-tauri/gen/android/app/build/ +# Tauri Android release keystore +*.jks # Tauri-typegen cache .typecache # Worktrees -.worktrees \ No newline at end of file +.worktrees + diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d88f86cd0..af842c605 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,6 +4,8 @@ fn main() { #[cfg(target_os = "linux")] unsafe { + use std::path::{Path, PathBuf}; + // Tao/Tauri Wayland decorations are don't respect server side decorations, forcing GTK onto X11/XWayland for now. // https://github.com/tauri-apps/tao/issues/1046 // https://github.com/tauri-apps/tauri/issues/11856 @@ -26,6 +28,52 @@ fn main() { if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } + + // AppImage can fail to discover host GStreamer plugins/scanner. Probe + // common distro layouts, but don't override explicit user config. + // Not finding these plugings prevents Sable from launching correctly. + // Maybe there's a better way to do this? + let plugin_dirs = [ + "/usr/lib/gstreamer-1.0", + "/usr/lib64/gstreamer-1.0", + "/usr/local/lib/gstreamer-1.0", + "/usr/local/lib64/gstreamer-1.0", + "/usr/lib/x86_64-linux-gnu/gstreamer-1.0", + "/usr/lib/aarch64-linux-gnu/gstreamer-1.0", + "/run/host/usr/lib/gstreamer-1.0", + "/run/host/usr/lib64/gstreamer-1.0", + ]; + let resolved_plugin_dir = plugin_dirs.iter().find(|dir| Path::new(dir).exists()); + + if std::env::var_os("GST_PLUGIN_SYSTEM_PATH_1_0").is_none() { + if let Some(dir) = resolved_plugin_dir { + std::env::set_var("GST_PLUGIN_SYSTEM_PATH_1_0", dir); + } + } + if std::env::var_os("GST_PLUGIN_PATH_1_0").is_none() { + if let Some(dir) = resolved_plugin_dir { + std::env::set_var("GST_PLUGIN_PATH_1_0", dir); + } + } + if std::env::var_os("GST_PLUGIN_SCANNER").is_none() { + let mut scanner_candidates: Vec = vec![ + PathBuf::from("/usr/lib/gstreamer-1.0/gst-plugin-scanner"), + PathBuf::from("/usr/lib64/gstreamer-1.0/gst-plugin-scanner"), + PathBuf::from("/usr/libexec/gstreamer-1.0/gst-plugin-scanner"), + PathBuf::from("/usr/lib/x86_64-linux-gnu/gstreamer-1.0/gst-plugin-scanner"), + PathBuf::from("/usr/lib/aarch64-linux-gnu/gstreamer-1.0/gst-plugin-scanner"), + PathBuf::from("/run/host/usr/lib/gstreamer-1.0/gst-plugin-scanner"), + PathBuf::from("/run/host/usr/lib64/gstreamer-1.0/gst-plugin-scanner"), + ]; + + if let Some(path_env) = std::env::var_os("PATH") { + scanner_candidates.extend(std::env::split_paths(&path_env).map(|p| p.join("gst-plugin-scanner"))); + } + + if let Some(scanner) = scanner_candidates.iter().find(|path| path.exists()) { + std::env::set_var("GST_PLUGIN_SCANNER", scanner.as_os_str()); + } + } } app_lib::run();