diff --git a/lib/gstreamerAvailability.js b/lib/gstreamerAvailability.js index 2fd86b7..6ccc23d 100644 --- a/lib/gstreamerAvailability.js +++ b/lib/gstreamerAvailability.js @@ -1,3 +1,67 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +/** + * True if a GStreamer plugin registry cache already exists for this user. + * A plain local directory listing — fast, unlike the scan below. + */ +function hasGstRegistryCache() { + const cacheDir = Gio.File.new_for_path( + GLib.build_filenamev([GLib.get_user_cache_dir(), 'gstreamer-1.0'])); + try { + const enumerator = cacheDir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null); + const hasEntry = enumerator.next_file(null) !== null; + enumerator.close(null); + return hasEntry; + } catch (e) { + return false; + } +} + +/** + * Gst.init() has no async variant. If its plugin registry cache is stale + * (e.g. right after a system update touched GStreamer packages), it + * synchronously rescans every plugin — spawning gst-plugin-scanner per + * plugin, and probing hardware for codec plugins — which can block + * gnome-shell's single main thread (it *is* the compositor) for many + * seconds, exactly the conditions right after login. That froze the entire + * screen the same way the blocking UPower proxy lookup did. + * + * GST_REGISTRY_UPDATE=no makes Gst.init() trust the existing cache + * unconditionally instead of validating it, so it returns immediately. Only + * set when a cache already exists — an extension-lifetime-first-ever + * GStreamer run on this machine still needs one real scan to have anything + * to trust. + */ +function keepGstInitFast() { + if (hasGstRegistryCache()) + GLib.setenv('GST_REGISTRY_UPDATE', 'no', true); +} + +/** + * Refreshes the on-disk registry cache in a throwaway subprocess, so a + * stale cache (skipped above) doesn't stay stale forever and newly + * installed plugins eventually become visible. Runs fully out-of-process: + * even if a hardware probe inside it hangs, gnome-shell is unaffected. + */ +function refreshGstRegistryInBackground() { + try { + const launcher = new Gio.SubprocessLauncher( + Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE); + launcher.setenv('GST_REGISTRY_UPDATE', 'yes', true); + const proc = launcher.spawnv(['gst-inspect-1.0', '--exists', 'playbin']); + proc.wait_check_async(null, (source, result) => { + try { + source.wait_check_finish(result); + } catch (e) { + // Best-effort refresh; a missing binary or nonzero exit just leaves the cache as-is. + } + }); + } catch (e) { + // Best-effort refresh; a missing gst-inspect-1.0 just leaves the cache as-is. + } +} + /** * Loads the GObject-Introspection bindings the live wallpaper feature needs * (Gst, GstApp for the appsink signals, Cogl for uploading decoded frames) @@ -13,6 +77,8 @@ * checkGstreamerBaseAvailable() there instead. */ export async function loadGstreamerModules() { + keepGstInitFast(); + const [{default: Gst}, , {default: Cogl}] = await Promise.all([ import('gi://Gst?version=1.0'), import('gi://GstApp?version=1.0'), @@ -22,6 +88,8 @@ export async function loadGstreamerModules() { if (!Gst.is_initialized()) Gst.init(null); + refreshGstRegistryInBackground(); + return {Gst, Cogl}; } @@ -33,6 +101,8 @@ export async function loadGstreamerModules() { * failure here is a genuine, actionable problem, unlike a Cogl probe. */ export async function checkGstreamerBaseAvailable() { + keepGstInitFast(); + const [{default: Gst}] = await Promise.all([ import('gi://Gst?version=1.0'), import('gi://GstApp?version=1.0'), @@ -40,6 +110,8 @@ export async function checkGstreamerBaseAvailable() { if (!Gst.is_initialized()) Gst.init(null); + + refreshGstRegistryInBackground(); } export const GSTREAMER_INSTALL_HINT =