/* GStreamer base utils library plugin install support for applications * Copyright (C) 2007 Tim-Philipp Müller * Copyright (C) 2006 Ryan Lortie * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /** * SECTION:gstpbutilsinstallplugins * @short_description: Missing plugin installation support for applications * * * Overview * * Using this API, applications can request the installation of missing * GStreamer plugins. These may be missing decoders/demuxers or encoders/muxers * for a certain format, sources or sinks for a certain URI protocol * (e.g. 'http'), or certain elements known by their element factory name * ('audioresample'). * * * Whether plugin installation is supported or not depends on the operating * system and/or distribution in question. The vendor of the operating system * needs to make sure the necessary hooks and mechanisms are in place for * plugin installation to work. See below for more detailed information. * * * From the application perspective, plugin installation is usually triggered * either * * * when the application itself has found that it wants or needs to install a * certain element * * * when the application has been notified by an element (such as playbin or * decodebin) that one or more plugins are missing and * the application has decided that it wants to install one or more of those * missing plugins * * * * Detail Strings * * The install functions in this section all take one or more 'detail strings'. * These detail strings contain information about the type of plugin that * needs to be installed (decoder, encoder, source, sink, or named element), * and some additional information such GStreamer version used and a * human-readable description of the component to install for user dialogs. * * * Applications should not concern themselves with the composition of the * string itself. They should regard the string as if it was a shared secret * between GStreamer and the plugin installer application. * * * Detail strings can be obtained using the function * gst_missing_plugin_message_get_installer_detail() on a missing-plugin * message. Such a message will either have been found by the application on * a pipeline's #GstBus, or the application will have created it itself using * gst_missing_element_message_new(), gst_missing_decoder_message_new(), * gst_missing_encoder_message_new(), gst_missing_uri_sink_message_new(), or * gst_missing_uri_source_message_new(). * * Plugin Installation from the Application Perspective * * For each GStreamer element/plugin/component that should be installed, the * application needs one of those 'installer detail' string mentioned in the * previous section. This string can be obtained, as already mentioned above, * from a missing-plugin message using the function * gst_missing_plugin_message_get_installer_detail(). The missing-plugin * message is either posted by another element and then found on the bus * by the application, or the application has created it itself as described * above. * * * The application will then call gst_install_plugins_async(), passing a * NULL-terminated array of installer detail strings, and a function that * should be called when the installation of the plugins has finished * (successfully or not). Optionally, a #GstInstallPluginsContext created * with gst_install_plugins_context_new() may be passed as well. This way * additional optional arguments like the application window's XID can be * passed to the external installer application. * * * gst_install_plugins_async() will return almost immediately, with the * return code indicating whether plugin installation was started or not. * If the necessary hooks for plugin installation are in place and an * external installer application has in fact been called, the passed in * function will be called with a result code as soon as the external installer * has finished. If the result code indicates that new plugins have been * installed, the application will want to call gst_update_registry() so the * run-time plugin registry is updated and the new plugins are made available * to the application. * * A Gtk/GLib main loop must be running in order for the result function to * be called when the external installer has finished. If this is not the case, * make sure to regularly call * * g_main_context_iteration (NULL,FALSE); * * from your code. * * * Plugin Installation from the Vendor/Distribution Perspective * * 1. Installer hook * * * When GStreamer applications initiate plugin installation via * gst_install_plugins_async() or gst_install_plugins_sync(), a pre-defined * helper application will be called. * * * The exact path of the helper application to be called is set at compile * time, usually by the ./configure script based on the * install prefix. For a normal package build into the /usr * prefix, this will usually default to * /usr/libexec/gst-install-plugins-helper or * /usr/lib/gst-install-plugins-helper. * * * Vendors/distros who want to support GStreamer plugin installation should * either provide such a helper script/application or use the * ./configure option * --with-install-plugins-helper=/path/to/installer to * make GStreamer call an installer of their own directly. * * * It is strongly recommended that vendors provide a small helper application * as interlocutor to the real installer though, even more so if command line * argument munging is required to transform the command line arguments * passed by GStreamer to the helper application into arguments that are * understood by the real installer. * * * The helper application path defined at compile time can be overriden at * runtime by setting the GST_INSTALL_PLUGINS_HELPER * environment variable. This can be useful for testing/debugging purposes. * * * 2. Arguments passed to the install helper * * * GStreamer will pass the following arguments to the install helper (this is * in addition to the path of the executable itself, which is by convention * argv[0]): * * * none to many optional arguments in the form of * --foo-bar=val. Example: * --transient-for=XID where XID is the X Window ID of * the main window of the calling application (so the installer can make * itself transient to that window). Unknown optional arguments should * be ignored by the installer. * * * one 'installer detail string' argument for each plugin to be installed; * these strings will have a gstreamer prefix; the * exact format of the detail string is explained below * * * * * 3. Detail string describing the missing plugin * * * The string is in UTF-8 encoding and is made up of several fields, separated * by '|' characters (but neither the first nor the last character is a '|'). * The fields are: * * * plugin system identifier, ie. "gstreamer" * * This identifier determines the format of the rest of the detail string. * Automatic plugin installers should not process detail strings with * unknown identifiers. This allows other plugin-based libraries to use * the same mechanism for their automatic plugin installation needs, or * for the format to be changed should it turn out to be insufficient. * * * plugin system version, e.g. "0.10" * * This is required so that when there is a GStreamer-0.12 or GStreamer-1.0 * at some point in future, the different major versions can still co-exist * and use the same plugin install mechanism in the same way. * * * application identifier, e.g. "totem" * * This may also be in the form of "pid/12345" if the program name can't * be obtained for some reason. * * * human-readable localised description of the required component, * e.g. "Vorbis audio decoder" * * * identifier string for the required component (see below for details about * how to map this to the package/plugin that needs installing), e.g. * * * urisource-$(PROTOCOL_REQUIRED), e.g. urisource-http or urisource-mms * * * element-$(ELEMENT_REQUIRED), e.g. element-videoconvert * * * decoder-$(CAPS_REQUIRED), e.g. (do read below for more details!): * * decoder-audio/x-vorbis * decoder-application/ogg * decoder-audio/mpeg, mpegversion=(int)4 * decoder-video/mpeg, systemstream=(boolean)true, mpegversion=(int)2 * * * encoder-$(CAPS_REQUIRED), e.g. encoder-audio/x-vorbis * * * * * optional further fields not yet specified * * * * * An entire ID string might then look like this, for example: * * gstreamer|0.10|totem|Vorbis audio decoder|decoder-audio/x-vorbis * * * * Plugin installers parsing this ID string should expect further fields also * separated by '|' symbols and either ignore them, warn the user, or error * out when encountering them. * * * Those unfamiliar with the GStreamer 'caps' system should note a few things * about the caps string used in the above decoder/encoder case: * * * the first part ("video/mpeg") of the caps string is a GStreamer media * type and not a MIME type. Wherever possible, the * GStreamer media type will be the same as the corresponding MIME type, * but often it is not. * * * a caps string may or may not have additional comma-separated fields * of various types (as seen in the examples above) * * * the caps string of a 'required' component (as above) will always have * fields with fixed values, whereas an introspected string (see below) * may have fields with non-fixed values. Compare for example: * * * audio/mpeg, mpegversion=(int)4 vs. * audio/mpeg, mpegversion=(int){2, 4} * * * video/mpeg, mpegversion=(int)2 vs. * video/mpeg, systemstream=(boolean){ true, false}, mpegversion=(int)[1, 2] * * * * * * * 4. Exit codes the installer should return * * * The installer should return one of the following exit codes when it exits: * * * 0 if all of the requested plugins could be installed * (#GST_INSTALL_PLUGINS_SUCCESS) * * * 1 if no appropriate installation candidate for any of the requested * plugins could be found. Only return this if nothing has been installed * (#GST_INSTALL_PLUGINS_NOT_FOUND) * * * 2 if an error occured during the installation. The application will * assume that the user will already have seen an error message by the * installer in this case and will usually not show another one * (#GST_INSTALL_PLUGINS_ERROR) * * * 3 if some of the requested plugins could be installed, but not all * (#GST_INSTALL_PLUGINS_PARTIAL_SUCCESS) * * * 4 if the user aborted the installation (#GST_INSTALL_PLUGINS_USER_ABORT) * * * * * 5. How to map the required detail string to packages * * * It is up to the vendor to find mechanism to map required components from * the detail string to the actual packages/plugins to install. This could * be a hardcoded list of mappings, for example, or be part of the packaging * system metadata. * * * GStreamer plugin files can be introspected for this information. The * gst-inspect utility has a special command line option * that will output information similar to what is required. For example * * $ gst-inspect-0.10 --print-plugin-auto-install-info /path/to/libgstvorbis.so * * should output something along the lines of * * decoder-audio/x-vorbis * element-vorbisdec * element-vorbisenc * element-vorbisparse * element-vorbistag * encoder-audio/x-vorbis * * Note that in the encoder and decoder case the introspected caps can be more * complex with additional fields, e.g. * audio/mpeg,mpegversion=(int){2,4}, so they will not * always exactly match the caps wanted by the application. It is up to the * installer to deal with this (either by doing proper caps intersection using * the GStreamer #GstCaps API, or by only taking into account the media type). * * * Another potential source of problems are plugins such as ladspa or * libvisual where the list of elements depends on the installed * ladspa/libvisual plugins at the time. This is also up to the distribution * to handle (but usually not relevant for playback applications). * * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "install-plugins.h" #include #ifdef HAVE_SYS_TYPES_H #include #endif #ifdef HAVE_SYS_WAIT_H #include #endif #include /* best effort to make things compile and possibly even work on win32 */ #ifndef WEXITSTATUS # define WEXITSTATUS(status) ((((guint)(status)) & 0xff00) >> 8) #endif #ifndef WIFEXITED # define WIFEXITED(status) ((((guint)(status)) & 0x7f) == 0) #endif static gboolean install_in_progress; /* FALSE */ /* private struct */ struct _GstInstallPluginsContext { guint xid; }; /** * gst_install_plugins_context_set_xid: * @ctx: a #GstInstallPluginsContext * @xid: the XWindow ID (XID) of the top-level application * * This function is for X11-based applications (such as most Gtk/Qt * applications on linux/unix) only. You can use it to tell the external * installer the XID of your main application window. That way the installer * can make its own window transient to your application window during the * installation. * * If set, the XID will be passed to the installer via a --transient-for=XID * command line option. * * Gtk+/Gnome application should be able to obtain the XID of the top-level * window like this: * * ##include <gtk/gtk.h> * ##ifdef GDK_WINDOWING_X11 * ##include <gdk/gdkx.h> * ##endif * ... * ##ifdef GDK_WINDOWING_X11 * xid = GDK_WINDOW_XWINDOW (GTK_WIDGET (application_window)->window); * ##endif * ... * */ void gst_install_plugins_context_set_xid (GstInstallPluginsContext * ctx, guint xid) { g_return_if_fail (ctx != NULL); ctx->xid = xid; } /** * gst_install_plugins_context_new: * * Creates a new #GstInstallPluginsContext. * * Returns: a new #GstInstallPluginsContext. Free with * gst_install_plugins_context_free() when no longer needed */ GstInstallPluginsContext * gst_install_plugins_context_new (void) { return g_new0 (GstInstallPluginsContext, 1); } /** * gst_install_plugins_context_free: * @ctx: a #GstInstallPluginsContext * * Frees a #GstInstallPluginsContext. */ void gst_install_plugins_context_free (GstInstallPluginsContext * ctx) { g_return_if_fail (ctx != NULL); g_free (ctx); } static GstInstallPluginsContext * gst_install_plugins_context_copy (GstInstallPluginsContext * ctx) { GstInstallPluginsContext *ret; ret = gst_install_plugins_context_new (); ret->xid = ctx->xid; return ret; } G_DEFINE_BOXED_TYPE (GstInstallPluginsContext, gst_install_plugins_context, (GBoxedCopyFunc) gst_install_plugins_context_copy, (GBoxedFreeFunc) gst_install_plugins_context_free); static const gchar * gst_install_plugins_get_helper (void) { const gchar *helper; helper = g_getenv ("GST_INSTALL_PLUGINS_HELPER"); if (helper == NULL) helper = GST_INSTALL_PLUGINS_HELPER; GST_LOG ("Using plugin install helper '%s'", helper); return helper; } static gboolean ptr_array_contains_string (GPtrArray * arr, const gchar * s) { gint i; for (i = 0; i < arr->len; ++i) { if (strcmp ((const char *) g_ptr_array_index (arr, i), s) == 0) return TRUE; } return FALSE; } static gboolean gst_install_plugins_spawn_child (const gchar * const *details, GstInstallPluginsContext * ctx, GPid * child_pid, gint * exit_status) { GPtrArray *arr; gboolean ret; GError *err = NULL; gchar **argv, xid_str[64] = { 0, }; arr = g_ptr_array_new (); /* argv[0] = helper path */ g_ptr_array_add (arr, (gchar *) gst_install_plugins_get_helper ()); /* add any additional command line args from the context */ if (ctx != NULL && ctx->xid != 0) { g_snprintf (xid_str, sizeof (xid_str), "--transient-for=%u", ctx->xid); g_ptr_array_add (arr, xid_str); } /* finally, add the detail strings, but without duplicates */ while (details != NULL && details[0] != NULL) { if (!ptr_array_contains_string (arr, details[0])) g_ptr_array_add (arr, (gpointer) details[0]); ++details; } /* and NULL-terminate */ g_ptr_array_add (arr, NULL); argv = (gchar **) arr->pdata; if (child_pid == NULL && exit_status != NULL) { install_in_progress = TRUE; ret = g_spawn_sync (NULL, argv, NULL, (GSpawnFlags) 0, NULL, NULL, NULL, NULL, exit_status, &err); install_in_progress = FALSE; } else if (child_pid != NULL && exit_status == NULL) { install_in_progress = TRUE; ret = g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, child_pid, &err); } else { g_return_val_if_reached (FALSE); } if (!ret) { GST_ERROR ("Error spawning plugin install helper: %s", err->message); g_error_free (err); } g_ptr_array_free (arr, TRUE); return ret; } static GstInstallPluginsReturn gst_install_plugins_return_from_status (gint status) { GstInstallPluginsReturn ret; /* did we exit cleanly? */ if (!WIFEXITED (status)) { ret = GST_INSTALL_PLUGINS_CRASHED; } else { ret = (GstInstallPluginsReturn) WEXITSTATUS (status); /* did the helper return an invalid status code? */ if (((guint) ret) >= GST_INSTALL_PLUGINS_STARTED_OK && ret != GST_INSTALL_PLUGINS_INTERNAL_FAILURE) { ret = GST_INSTALL_PLUGINS_INVALID; } } GST_LOG ("plugin installer exited with status 0x%04x = %s", status, gst_install_plugins_return_get_name (ret)); return ret; } typedef struct { GstInstallPluginsResultFunc func; gpointer user_data; } GstInstallPluginsAsyncHelper; static void gst_install_plugins_installer_exited (GPid pid, gint status, gpointer data) { GstInstallPluginsAsyncHelper *helper; GstInstallPluginsReturn ret; install_in_progress = FALSE; helper = (GstInstallPluginsAsyncHelper *) data; ret = gst_install_plugins_return_from_status (status); GST_LOG ("calling plugin install result function %p", helper->func); helper->func (ret, helper->user_data); g_free (helper); } /** * gst_install_plugins_async: * @details: (array zero-terminated=1) (transfer none): NULL-terminated array * of installer string details (see below) * @ctx: (allow-none): a #GstInstallPluginsContext, or NULL * @func: (scope async): the function to call when the installer program returns * @user_data: (closure): the user data to pass to @func when called, or NULL * * Requests plugin installation without blocking. Once the plugins have been * installed or installation has failed, @func will be called with the result * of the installation and your provided @user_data pointer. * * This function requires a running GLib/Gtk main loop. If you are not * running a GLib/Gtk main loop, make sure to regularly call * g_main_context_iteration(NULL,FALSE). * * The installer strings that make up @detail are typically obtained by * calling gst_missing_plugin_message_get_installer_detail() on missing-plugin * messages that have been caught on a pipeline's bus or created by the * application via the provided API, such as gst_missing_element_message_new(). * * It is possible to request the installation of multiple missing plugins in * one go (as might be required if there is a demuxer for a certain format * installed but no suitable video decoder and no suitable audio decoder). * * Returns: result code whether an external installer could be started */ GstInstallPluginsReturn gst_install_plugins_async (const gchar * const *details, GstInstallPluginsContext * ctx, GstInstallPluginsResultFunc func, gpointer user_data) { GstInstallPluginsAsyncHelper *helper; GPid pid; g_return_val_if_fail (details != NULL, GST_INSTALL_PLUGINS_INTERNAL_FAILURE); g_return_val_if_fail (func != NULL, GST_INSTALL_PLUGINS_INTERNAL_FAILURE); if (install_in_progress) return GST_INSTALL_PLUGINS_INSTALL_IN_PROGRESS; /* if we can't access our helper, don't bother */ if (!g_file_test (gst_install_plugins_get_helper (), G_FILE_TEST_IS_EXECUTABLE)) return GST_INSTALL_PLUGINS_HELPER_MISSING; if (!gst_install_plugins_spawn_child (details, ctx, &pid, NULL)) return GST_INSTALL_PLUGINS_INTERNAL_FAILURE; helper = g_new (GstInstallPluginsAsyncHelper, 1); helper->func = func; helper->user_data = user_data; g_child_watch_add (pid, gst_install_plugins_installer_exited, helper); return GST_INSTALL_PLUGINS_STARTED_OK; } /** * gst_install_plugins_sync: * @details: (array zero-terminated=1) (transfer none): NULL-terminated array * of installer string details * @ctx: (allow-none): a #GstInstallPluginsContext, or NULL * * Requests plugin installation and block until the plugins have been * installed or installation has failed. * * This function should almost never be used, it only exists for cases where * a non-GLib main loop is running and the user wants to run it in a separate * thread and marshal the result back asynchronously into the main thread * using the other non-GLib main loop. You should almost always use * gst_install_plugins_async() instead of this function. * * Returns: the result of the installation. */ GstInstallPluginsReturn gst_install_plugins_sync (const gchar * const *details, GstInstallPluginsContext * ctx) { gint status; g_return_val_if_fail (details != NULL, GST_INSTALL_PLUGINS_INTERNAL_FAILURE); if (install_in_progress) return GST_INSTALL_PLUGINS_INSTALL_IN_PROGRESS; /* if we can't access our helper, don't bother */ if (!g_file_test (gst_install_plugins_get_helper (), G_FILE_TEST_IS_EXECUTABLE)) return GST_INSTALL_PLUGINS_HELPER_MISSING; if (!gst_install_plugins_spawn_child (details, ctx, NULL, &status)) return GST_INSTALL_PLUGINS_INTERNAL_FAILURE; return gst_install_plugins_return_from_status (status); } /** * gst_install_plugins_return_get_name: * @ret: the return status code * * Convenience function to return the descriptive string associated * with a status code. This function returns English strings and * should not be used for user messages. It is here only to assist * in debugging. * * Returns: a descriptive string for the status code in @ret */ const gchar * gst_install_plugins_return_get_name (GstInstallPluginsReturn ret) { switch (ret) { case GST_INSTALL_PLUGINS_SUCCESS: return "success"; case GST_INSTALL_PLUGINS_NOT_FOUND: return "not-found"; case GST_INSTALL_PLUGINS_ERROR: return "install-error"; case GST_INSTALL_PLUGINS_CRASHED: return "installer-exit-unclean"; case GST_INSTALL_PLUGINS_PARTIAL_SUCCESS: return "partial-success"; case GST_INSTALL_PLUGINS_USER_ABORT: return "user-abort"; case GST_INSTALL_PLUGINS_STARTED_OK: return "started-ok"; case GST_INSTALL_PLUGINS_INTERNAL_FAILURE: return "internal-failure"; case GST_INSTALL_PLUGINS_HELPER_MISSING: return "helper-missing"; case GST_INSTALL_PLUGINS_INSTALL_IN_PROGRESS: return "install-in-progress"; case GST_INSTALL_PLUGINS_INVALID: return "invalid"; default: break; } return "(UNKNOWN)"; } /** * gst_install_plugins_installation_in_progress: * * Checks whether plugin installation (initiated by this application only) * is currently in progress. * * Returns: TRUE if plugin installation is in progress, otherwise FALSE */ gboolean gst_install_plugins_installation_in_progress (void) { return install_in_progress; } /** * gst_install_plugins_supported: * * Checks whether plugin installation is likely to be supported by the * current environment. This currently only checks whether the helper script * that is to be provided by the distribution or operating system vendor * exists. * * Returns: TRUE if plugin installation is likely to be supported. */ gboolean gst_install_plugins_supported (void) { return g_file_test (gst_install_plugins_get_helper (), G_FILE_TEST_IS_EXECUTABLE); }