| /* GStreamer command line playback testing utility |
| * |
| * Copyright (C) 2013 Tim-Philipp Müller <tim centricular net> |
| * Copyright (C) 2013 Collabora Ltd. |
| * |
| * 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., 51 Franklin St, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| |
| #include <gst/gst.h> |
| #include <gst/gst-i18n-app.h> |
| #include <gst/pbutils/pbutils.h> |
| #include <stdlib.h> |
| |
| GST_DEBUG_CATEGORY (play_debug); |
| #define GST_CAT_DEFAULT play_debug |
| |
| typedef struct |
| { |
| gchar **uris; |
| guint num_uris; |
| gint cur_idx; |
| |
| GstElement *playbin; |
| |
| GMainLoop *loop; |
| guint bus_watch; |
| guint timeout; |
| |
| /* missing plugin messages */ |
| GList *missing; |
| |
| gboolean buffering; |
| gboolean is_live; |
| |
| /* configuration */ |
| gboolean gapless; |
| } GstPlay; |
| |
| static gboolean play_bus_msg (GstBus * bus, GstMessage * msg, gpointer data); |
| static gboolean play_next (GstPlay * play); |
| static gboolean play_timeout (gpointer user_data); |
| static void play_about_to_finish (GstElement * playbin, gpointer user_data); |
| static void play_reset (GstPlay * play); |
| |
| static GstPlay * |
| play_new (gchar ** uris, const gchar * audio_sink, const gchar * video_sink, |
| gboolean gapless) |
| { |
| GstElement *sink; |
| GstPlay *play; |
| |
| play = g_new0 (GstPlay, 1); |
| |
| play->uris = uris; |
| play->num_uris = g_strv_length (uris); |
| play->cur_idx = -1; |
| |
| play->playbin = gst_element_factory_make ("playbin", "playbin"); |
| |
| if (audio_sink != NULL) { |
| sink = gst_element_factory_make (audio_sink, NULL); |
| if (sink != NULL) |
| g_object_set (play->playbin, "audio-sink", sink, NULL); |
| else |
| g_warning ("Couldn't create specified audio sink '%s'", audio_sink); |
| } |
| if (video_sink != NULL) { |
| sink = gst_element_factory_make (video_sink, NULL); |
| if (sink != NULL) |
| g_object_set (play->playbin, "video-sink", sink, NULL); |
| else |
| g_warning ("Couldn't create specified video sink '%s'", video_sink); |
| } |
| |
| play->loop = g_main_loop_new (NULL, FALSE); |
| |
| play->bus_watch = gst_bus_add_watch (GST_ELEMENT_BUS (play->playbin), |
| play_bus_msg, play); |
| |
| /* FIXME: make configurable incl. 0 for disable */ |
| play->timeout = g_timeout_add (100, play_timeout, play); |
| |
| play->missing = NULL; |
| play->buffering = FALSE; |
| play->is_live = FALSE; |
| |
| play->gapless = gapless; |
| if (gapless) { |
| g_signal_connect (play->playbin, "about-to-finish", |
| G_CALLBACK (play_about_to_finish), play); |
| } |
| |
| return play; |
| } |
| |
| static void |
| play_free (GstPlay * play) |
| { |
| play_reset (play); |
| |
| gst_element_set_state (play->playbin, GST_STATE_NULL); |
| gst_object_unref (play->playbin); |
| |
| g_source_remove (play->bus_watch); |
| g_source_remove (play->timeout); |
| g_main_loop_unref (play->loop); |
| |
| g_strfreev (play->uris); |
| g_free (play); |
| } |
| |
| /* reset for new file/stream */ |
| static void |
| play_reset (GstPlay * play) |
| { |
| g_list_foreach (play->missing, (GFunc) gst_message_unref, NULL); |
| play->missing = NULL; |
| |
| play->buffering = FALSE; |
| play->is_live = FALSE; |
| } |
| |
| /* returns TRUE if something was installed and we should restart playback */ |
| static gboolean |
| play_install_missing_plugins (GstPlay * play) |
| { |
| /* FIXME: implement: try to install any missing plugins we haven't |
| * tried to install before */ |
| return FALSE; |
| } |
| |
| static gboolean |
| play_bus_msg (GstBus * bus, GstMessage * msg, gpointer user_data) |
| { |
| GstPlay *play = user_data; |
| |
| switch (GST_MESSAGE_TYPE (msg)) { |
| case GST_MESSAGE_ASYNC_DONE: |
| g_print ("Prerolled.\r"); |
| if (play->missing != NULL && play_install_missing_plugins (play)) { |
| g_print ("New plugins installed, trying again...\n"); |
| --play->cur_idx; |
| play_next (play); |
| } |
| break; |
| case GST_MESSAGE_BUFFERING:{ |
| gint percent; |
| |
| if (!play->buffering) |
| g_print ("\n"); |
| |
| gst_message_parse_buffering (msg, &percent); |
| g_print ("%s %d%% \r", _("Buffering..."), percent); |
| |
| /* no state management needed for live pipelines */ |
| if (play->is_live) |
| break; |
| |
| if (percent == 100) { |
| /* a 100% message means buffering is done */ |
| if (play->buffering) { |
| play->buffering = FALSE; |
| gst_element_set_state (play->playbin, GST_STATE_PLAYING); |
| } |
| } else { |
| /* buffering... */ |
| if (!play->buffering) { |
| gst_element_set_state (play->playbin, GST_STATE_PAUSED); |
| play->buffering = TRUE; |
| } |
| } |
| break; |
| } |
| case GST_MESSAGE_LATENCY: |
| g_print ("Redistribute latency...\n"); |
| gst_bin_recalculate_latency (GST_BIN (play->playbin)); |
| break; |
| case GST_MESSAGE_REQUEST_STATE:{ |
| GstState state; |
| gchar *name; |
| |
| name = gst_object_get_path_string (GST_MESSAGE_SRC (msg)); |
| |
| gst_message_parse_request_state (msg, &state); |
| |
| g_print ("Setting state to %s as requested by %s...\n", |
| gst_element_state_get_name (state), name); |
| |
| gst_element_set_state (play->playbin, state); |
| g_free (name); |
| break; |
| } |
| case GST_MESSAGE_EOS: |
| /* print final position at end */ |
| play_timeout (play); |
| g_print ("\n"); |
| /* and switch to next item in list */ |
| if (!play_next (play)) { |
| g_print ("Reached end of play list.\n"); |
| g_main_loop_quit (play->loop); |
| } |
| break; |
| case GST_MESSAGE_WARNING:{ |
| GError *err; |
| gchar *dbg = NULL; |
| |
| gst_message_parse_warning (msg, &err, &dbg); |
| g_printerr ("WARNING %s\n", err->message); |
| if (dbg != NULL) |
| g_printerr ("WARNING debug information: %s\n", dbg); |
| g_error_free (err); |
| g_free (dbg); |
| break; |
| } |
| case GST_MESSAGE_ERROR:{ |
| GError *err; |
| gchar *dbg; |
| |
| gst_message_parse_error (msg, &err, &dbg); |
| g_printerr ("ERROR %s for %s\n", err->message, play->uris[play->cur_idx]); |
| if (dbg != NULL) |
| g_printerr ("ERROR debug information: %s\n", dbg); |
| g_error_free (err); |
| g_free (dbg); |
| |
| if (play->missing != NULL && play_install_missing_plugins (play)) { |
| g_print ("New plugins installed, trying again...\n"); |
| --play->cur_idx; |
| play_next (play); |
| break; |
| } |
| /* try next item in list then */ |
| if (!play_next (play)) { |
| g_print ("Reached end of play list.\n"); |
| g_main_loop_quit (play->loop); |
| } |
| break; |
| } |
| default: |
| if (gst_is_missing_plugin_message (msg)) { |
| gchar *desc; |
| |
| desc = gst_missing_plugin_message_get_description (msg); |
| g_print ("Missing plugin: %s\n", desc); |
| g_free (desc); |
| play->missing = g_list_append (play->missing, gst_message_ref (msg)); |
| } |
| break; |
| } |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| play_timeout (gpointer user_data) |
| { |
| GstPlay *play = user_data; |
| gint64 pos = -1, dur = -1; |
| |
| if (play->buffering) |
| return TRUE; |
| |
| gst_element_query_position (play->playbin, GST_FORMAT_TIME, &pos); |
| gst_element_query_duration (play->playbin, GST_FORMAT_TIME, &dur); |
| |
| if (pos >= 0 && dur > 0) { |
| gchar dstr[32], pstr[32]; |
| |
| /* FIXME: pretty print in nicer format */ |
| g_snprintf (pstr, 32, "%" GST_TIME_FORMAT, GST_TIME_ARGS (pos)); |
| pstr[9] = '\0'; |
| g_snprintf (dstr, 32, "%" GST_TIME_FORMAT, GST_TIME_ARGS (dur)); |
| dstr[9] = '\0'; |
| g_print ("%s / %s\r", pstr, dstr); |
| } |
| |
| return TRUE; |
| } |
| |
| static gchar * |
| play_uri_get_display_name (GstPlay * play, const gchar * uri) |
| { |
| gchar *loc; |
| |
| if (gst_uri_has_protocol (uri, "file")) { |
| loc = g_filename_from_uri (uri, NULL, NULL); |
| } else if (gst_uri_has_protocol (uri, "pushfile")) { |
| loc = g_filename_from_uri (uri + 4, NULL, NULL); |
| } else { |
| loc = g_strdup (uri); |
| } |
| |
| /* Maybe additionally use glib's filename to display name function */ |
| return loc; |
| } |
| |
| /* returns FALSE if we have reached the end of the playlist */ |
| static gboolean |
| play_next (GstPlay * play) |
| { |
| GstStateChangeReturn sret; |
| const gchar *next_uri; |
| gchar *loc; |
| |
| if (++play->cur_idx >= play->num_uris) |
| return FALSE; |
| |
| gst_element_set_state (play->playbin, GST_STATE_READY); |
| play_reset (play); |
| |
| next_uri = play->uris[play->cur_idx]; |
| loc = play_uri_get_display_name (play, next_uri); |
| g_print ("Now playing %s\n", loc); |
| g_free (loc); |
| |
| g_object_set (play->playbin, "uri", next_uri, NULL); |
| |
| sret = gst_element_set_state (play->playbin, GST_STATE_PLAYING); |
| switch (sret) { |
| case GST_STATE_CHANGE_FAILURE: |
| /* ignore, we should get an error message posted on the bus */ |
| break; |
| case GST_STATE_CHANGE_NO_PREROLL: |
| g_print ("Pipeline is live.\n"); |
| play->is_live = TRUE; |
| break; |
| case GST_STATE_CHANGE_ASYNC: |
| g_print ("Prerolling...\r"); |
| break; |
| default: |
| break; |
| } |
| |
| return TRUE; |
| } |
| |
| static void |
| play_about_to_finish (GstElement * playbin, gpointer user_data) |
| { |
| GstPlay *play = user_data; |
| const gchar *next_uri; |
| gchar *loc; |
| guint next_idx; |
| |
| if (!play->gapless) |
| return; |
| |
| next_idx = play->cur_idx + 1; |
| if (next_idx >= play->num_uris) |
| return; |
| |
| next_uri = play->uris[next_idx]; |
| loc = play_uri_get_display_name (play, next_uri); |
| g_print ("About to finish, preparing next title: %s\n", loc); |
| g_free (loc); |
| |
| g_object_set (play->playbin, "uri", next_uri, NULL); |
| play->cur_idx = next_idx; |
| } |
| |
| static void |
| do_play (GstPlay * play) |
| { |
| gint i; |
| |
| /* dump playlist */ |
| for (i = 0; i < play->num_uris; ++i) |
| GST_INFO ("%4u : %s", i, play->uris[i]); |
| |
| if (!play_next (play)) |
| return; |
| |
| g_main_loop_run (play->loop); |
| } |
| |
| static void |
| add_to_playlist (GPtrArray * playlist, const gchar * filename) |
| { |
| GDir *dir; |
| gchar *uri; |
| |
| if (gst_uri_is_valid (filename)) { |
| g_ptr_array_add (playlist, g_strdup (filename)); |
| return; |
| } |
| |
| if ((dir = g_dir_open (filename, 0, NULL))) { |
| const gchar *entry; |
| |
| /* FIXME: sort entries for each directory? */ |
| while ((entry = g_dir_read_name (dir))) { |
| gchar *path; |
| |
| path = g_strconcat (filename, G_DIR_SEPARATOR_S, entry, NULL); |
| add_to_playlist (playlist, path); |
| g_free (path); |
| } |
| |
| g_dir_close (dir); |
| return; |
| } |
| |
| uri = gst_filename_to_uri (filename, NULL); |
| if (uri != NULL) |
| g_ptr_array_add (playlist, uri); |
| else |
| g_warning ("Could not make URI out of filename '%s'", filename); |
| } |
| |
| int |
| main (int argc, char **argv) |
| { |
| GstPlay *play; |
| GPtrArray *playlist; |
| gboolean print_version = FALSE; |
| gboolean gapless = FALSE; |
| gchar **filenames = NULL; |
| gchar *audio_sink = NULL; |
| gchar *video_sink = NULL; |
| gchar **uris; |
| guint num, i; |
| GError *err = NULL; |
| GOptionContext *ctx; |
| GOptionEntry options[] = { |
| {"version", 0, 0, G_OPTION_ARG_NONE, &print_version, |
| N_("Print version information and exit"), NULL}, |
| {"videosink", 0, 0, G_OPTION_ARG_STRING, &video_sink, |
| N_("Video sink to use (default is autovideosink)"), NULL}, |
| {"audiosink", 0, 0, G_OPTION_ARG_STRING, &audio_sink, |
| N_("Audio sink to use (default is autoaudiosink)"), NULL}, |
| {"gapless", 0, 0, G_OPTION_ARG_NONE, &gapless, |
| N_("Enable gapless playback"), NULL}, |
| {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL}, |
| {NULL} |
| }; |
| |
| #ifdef ENABLE_NLS |
| bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); |
| bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); |
| textdomain (GETTEXT_PACKAGE); |
| #endif |
| |
| g_set_prgname ("gst-play-" GST_API_VERSION); |
| |
| ctx = g_option_context_new ("FILE1|URI1 [FILE2|URI2] [FILE3|URI3] ..."); |
| g_option_context_add_main_entries (ctx, options, GETTEXT_PACKAGE); |
| g_option_context_add_group (ctx, gst_init_get_option_group ()); |
| if (!g_option_context_parse (ctx, &argc, &argv, &err)) { |
| g_print ("Error initializing: %s\n", GST_STR_NULL (err->message)); |
| return 1; |
| } |
| g_option_context_free (ctx); |
| |
| GST_DEBUG_CATEGORY_INIT (play_debug, "play", 0, "gst-play"); |
| |
| if (print_version) { |
| gchar *version_str; |
| |
| version_str = gst_version_string (); |
| g_print ("%s version %s\n", g_get_prgname (), PACKAGE_VERSION); |
| g_print ("%s\n", version_str); |
| g_print ("%s\n", GST_PACKAGE_ORIGIN); |
| g_free (version_str); |
| return 0; |
| } |
| |
| if (filenames == NULL || *filenames == NULL) { |
| g_printerr (_("Usage: %s FILE1|URI1 [FILE2|URI2] [FILE3|URI3] ..."), |
| "gst-play-" GST_API_VERSION); |
| g_printerr ("\n\n"), |
| g_printerr ("%s\n\n", |
| _("You must provide at least one filename or URI to play.")); |
| return 1; |
| } |
| |
| playlist = g_ptr_array_new (); |
| |
| /* fill playlist */ |
| num = g_strv_length (filenames); |
| for (i = 0; i < num; ++i) { |
| GST_LOG ("command line argument: %s", filenames[i]); |
| add_to_playlist (playlist, filenames[i]); |
| } |
| g_strfreev (filenames); |
| |
| g_ptr_array_add (playlist, NULL); |
| |
| /* play */ |
| uris = (gchar **) g_ptr_array_free (playlist, FALSE); |
| play = play_new (uris, audio_sink, video_sink, gapless); |
| |
| do_play (play); |
| |
| /* clean up */ |
| play_free (play); |
| |
| return 0; |
| } |