diff --git a/tools/ges-launcher-kb.c b/tools/ges-launcher-kb.c new file mode 100644 index 0000000000..ffaafde777 --- /dev/null +++ b/tools/ges-launcher-kb.c @@ -0,0 +1,293 @@ +/* GStreamer command line playback testing utility - keyboard handling helpers + * + * Copyright (C) 2013 Tim-Philipp Müller + * Copyright (C) 2013 Centricular 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 "ges-launcher-kb.h" + +#include +#include +#include + +#ifdef G_OS_UNIX +#include +#include +#endif + +#ifdef G_OS_WIN32 +#include +#include +#endif + +#include + +/* This is all not thread-safe, but doesn't have to be really */ +static GstPlayKbFunc kb_callback; +static gpointer kb_callback_data; + +#ifdef G_OS_UNIX +static struct termios term_settings; +static gboolean term_settings_saved = FALSE; +static gulong io_watch_id; + +static gboolean +gst_play_kb_io_cb (GIOChannel * ioc, GIOCondition cond, gpointer user_data) +{ + GIOStatus status; + + if (cond & G_IO_IN) { + gchar buf[16] = { 0, }; + gsize read; + + status = g_io_channel_read_chars (ioc, buf, sizeof (buf) - 1, &read, NULL); + if (status == G_IO_STATUS_ERROR) + return FALSE; + if (status == G_IO_STATUS_NORMAL) { + if (kb_callback) + kb_callback (buf, kb_callback_data); + } + } + + return TRUE; /* call us again */ +} + +gboolean +gst_play_kb_set_key_handler (GstPlayKbFunc kb_func, gpointer user_data) +{ + GIOChannel *ioc; + + if (!isatty (STDIN_FILENO)) { + GST_INFO ("stdin is not connected to a terminal"); + return FALSE; + } + + if (io_watch_id > 0) { + g_source_remove (io_watch_id); + io_watch_id = 0; + } + + if (kb_func == NULL && term_settings_saved) { + /* restore terminal settings */ + if (tcsetattr (STDIN_FILENO, TCSAFLUSH, &term_settings) == 0) + term_settings_saved = FALSE; + else + g_warning ("could not restore terminal attributes"); + + setvbuf (stdin, NULL, _IOLBF, 0); + } + + if (kb_func != NULL) { + struct termios new_settings; + + if (!term_settings_saved) { + if (tcgetattr (STDIN_FILENO, &term_settings) != 0) { + g_warning ("could not save terminal attributes"); + return FALSE; + } + term_settings_saved = TRUE; + + /* Echo off, canonical mode off, extended input processing off */ + new_settings = term_settings; + new_settings.c_lflag &= ~(ECHO | ICANON | IEXTEN); + new_settings.c_cc[VMIN] = 0; + new_settings.c_cc[VTIME] = 0; + + if (tcsetattr (STDIN_FILENO, TCSAFLUSH, &new_settings) != 0) { + g_warning ("Could not set terminal state"); + return FALSE; + } + setvbuf (stdin, NULL, _IONBF, 0); + } + } + + ioc = g_io_channel_unix_new (STDIN_FILENO); + + io_watch_id = g_io_add_watch_full (ioc, G_PRIORITY_DEFAULT, G_IO_IN, + (GIOFunc) gst_play_kb_io_cb, user_data, NULL); + g_io_channel_unref (ioc); + + kb_callback = kb_func; + kb_callback_data = user_data; + + return TRUE; +} + +#elif defined(G_OS_WIN32) + +typedef struct +{ + GThread *thread; + HANDLE event_handle; + HANDLE console_handle; + gboolean closing; + GMutex lock; +} Win32KeyHandler; + +static Win32KeyHandler *win32_handler = NULL; + +static gboolean +gst_play_kb_source_cb (Win32KeyHandler * handler) +{ + HANDLE h_input = handler->console_handle; + INPUT_RECORD buffer; + DWORD n; + + if (PeekConsoleInput (h_input, &buffer, 1, &n) && n == 1) { + ReadConsoleInput (h_input, &buffer, 1, &n); + + if (buffer.EventType == KEY_EVENT && buffer.Event.KeyEvent.bKeyDown) { + gchar key_val[2] = { 0 }; + + switch (buffer.Event.KeyEvent.wVirtualKeyCode) { + case VK_RIGHT: + kb_callback (GST_PLAY_KB_ARROW_RIGHT, kb_callback_data); + break; + case VK_LEFT: + kb_callback (GST_PLAY_KB_ARROW_LEFT, kb_callback_data); + break; + case VK_UP: + kb_callback (GST_PLAY_KB_ARROW_UP, kb_callback_data); + break; + case VK_DOWN: + kb_callback (GST_PLAY_KB_ARROW_DOWN, kb_callback_data); + break; + default: + key_val[0] = buffer.Event.KeyEvent.uChar.AsciiChar; + kb_callback (key_val, kb_callback_data); + break; + } + } + } + + return G_SOURCE_REMOVE; +} + +static gpointer +gst_play_kb_win32_thread (gpointer user_data) +{ + Win32KeyHandler *handler = (Win32KeyHandler *) user_data; + HANDLE handles[2]; + + handles[0] = handler->event_handle; + handles[1] = handler->console_handle; + + if (!kb_callback) + return NULL; + + while (TRUE) { + DWORD ret = WaitForMultipleObjects (2, handles, FALSE, INFINITE); + + if (ret == WAIT_FAILED) { + GST_WARNING ("WaitForMultipleObject Failed"); + return NULL; + } + + g_mutex_lock (&handler->lock); + if (handler->closing) { + g_mutex_unlock (&handler->lock); + + return NULL; + } + g_mutex_unlock (&handler->lock); + + g_idle_add ((GSourceFunc) gst_play_kb_source_cb, handler); + } + + return NULL; +} + +gboolean +gst_play_kb_set_key_handler (GstPlayKbFunc kb_func, gpointer user_data) +{ + gint fd = _fileno (stdin); + + if (!_isatty (fd)) { + GST_INFO ("stdin is not connected to a terminal"); + return FALSE; + } + + if (win32_handler) { + g_mutex_lock (&win32_handler->lock); + win32_handler->closing = TRUE; + g_mutex_unlock (&win32_handler->lock); + + SetEvent (win32_handler->event_handle); + g_thread_join (win32_handler->thread); + CloseHandle (win32_handler->event_handle); + + g_mutex_clear (&win32_handler->lock); + g_free (win32_handler); + win32_handler = NULL; + } + + if (kb_func) { + SECURITY_ATTRIBUTES sec_attrs; + + sec_attrs.nLength = sizeof (SECURITY_ATTRIBUTES); + sec_attrs.lpSecurityDescriptor = NULL; + sec_attrs.bInheritHandle = FALSE; + + win32_handler = g_new0 (Win32KeyHandler, 1); + + /* create cancellable event handle */ + win32_handler->event_handle = CreateEvent (&sec_attrs, TRUE, FALSE, NULL); + + if (!win32_handler->event_handle) { + GST_WARNING ("Couldn't create event handle"); + g_free (win32_handler); + win32_handler = NULL; + + return FALSE; + } + + win32_handler->console_handle = GetStdHandle (STD_INPUT_HANDLE); + if (!win32_handler->console_handle) { + GST_WARNING ("Couldn't get console handle"); + CloseHandle (win32_handler->event_handle); + g_free (win32_handler); + win32_handler = NULL; + + return FALSE; + } + + g_mutex_init (&win32_handler->lock); + win32_handler->thread = + g_thread_new ("gst-play-kb", gst_play_kb_win32_thread, win32_handler); + } + + kb_callback = kb_func; + kb_callback_data = user_data; + + return TRUE; +} + +#else + +gboolean +gst_play_kb_set_key_handler (GstPlayKbFunc key_func, gpointer user_data) +{ + GST_FIXME ("Keyboard handling for this OS needs to be implemented"); + return FALSE; +} + +#endif /* !G_OS_UNIX */ diff --git a/tools/ges-launcher-kb.h b/tools/ges-launcher-kb.h new file mode 100644 index 0000000000..7dab0edd77 --- /dev/null +++ b/tools/ges-launcher-kb.h @@ -0,0 +1,35 @@ +/* GStreamer command line playback testing utility - keyboard handling helpers + * + * Copyright (C) 2013 Tim-Philipp Müller + * Copyright (C) 2013 Centricular 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. + */ +#ifndef __GST_PLAY_KB_INCLUDED__ +#define __GST_PLAY_KB_INCLUDED__ + +#include + +#define GST_PLAY_KB_ARROW_UP "\033[A" +#define GST_PLAY_KB_ARROW_DOWN "\033[B" +#define GST_PLAY_KB_ARROW_RIGHT "\033[C" +#define GST_PLAY_KB_ARROW_LEFT "\033[D" + +typedef void (*GstPlayKbFunc) (const gchar * kb_input, gpointer user_data); + +gboolean gst_play_kb_set_key_handler (GstPlayKbFunc kb_func, gpointer user_data); + +#endif /* __GST_PLAY_KB_INCLUDED__ */ diff --git a/tools/ges-launcher.c b/tools/ges-launcher.c index 92bc5b05db..2549c9dab9 100644 --- a/tools/ges-launcher.c +++ b/tools/ges-launcher.c @@ -31,6 +31,18 @@ #include "ges-launcher.h" #include "ges-validate.h" #include "utils.h" +#include "ges-launcher-kb.h" + +typedef enum +{ + GST_PLAY_TRICK_MODE_NONE = 0, + GST_PLAY_TRICK_MODE_DEFAULT, + GST_PLAY_TRICK_MODE_DEFAULT_NO_AUDIO, + GST_PLAY_TRICK_MODE_KEY_UNITS, + GST_PLAY_TRICK_MODE_KEY_UNITS_NO_AUDIO, + GST_PLAY_TRICK_MODE_INSTANT_RATE, + GST_PLAY_TRICK_MODE_LAST +} GstPlayTrickMode; struct _GESLauncherPrivate { @@ -41,6 +53,11 @@ struct _GESLauncherPrivate guint signal_watch_id; #endif GESLauncherParsedOptions parsed_options; + + GstPlayTrickMode trick_mode; + gdouble rate; + + GstState desired_state; /* as per user interaction, PAUSED or PLAYING */ }; G_DEFINE_TYPE_WITH_PRIVATE (GESLauncher, ges_launcher, G_TYPE_APPLICATION); @@ -56,6 +73,240 @@ static const gchar *HELP_SUMMARY = " `ges-launch-1.0 --inspect-action-type` for the available commands.\n\n" " By default, ges-launch-1.0 is in \"playback-mode\"."; +static gboolean +play_do_seek (GESLauncher * self, gint64 pos, gdouble rate, + GstPlayTrickMode mode) +{ + GstSeekFlags seek_flags; + GstEvent *seek; + + seek_flags = 0; + + switch (mode) { + case GST_PLAY_TRICK_MODE_DEFAULT: + seek_flags |= GST_SEEK_FLAG_TRICKMODE; + break; + case GST_PLAY_TRICK_MODE_DEFAULT_NO_AUDIO: + seek_flags |= GST_SEEK_FLAG_TRICKMODE | GST_SEEK_FLAG_TRICKMODE_NO_AUDIO; + break; + case GST_PLAY_TRICK_MODE_KEY_UNITS: + seek_flags |= GST_SEEK_FLAG_TRICKMODE_KEY_UNITS; + break; + case GST_PLAY_TRICK_MODE_KEY_UNITS_NO_AUDIO: + seek_flags |= + GST_SEEK_FLAG_TRICKMODE_KEY_UNITS | GST_SEEK_FLAG_TRICKMODE_NO_AUDIO; + break; + case GST_PLAY_TRICK_MODE_NONE: + default: + break; + } + + /* See if we can do an instant rate change (not changing dir) */ + if (mode & GST_PLAY_TRICK_MODE_INSTANT_RATE && rate * self->priv->rate > 0) { + seek = gst_event_new_seek (rate, GST_FORMAT_TIME, + seek_flags | GST_SEEK_FLAG_INSTANT_RATE_CHANGE, + GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE, + GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE); + if (gst_element_send_event (GST_ELEMENT (self->priv->pipeline), seek)) { + goto done; + } + } + + /* No instant rate change, need to do a flushing seek */ + seek_flags |= GST_SEEK_FLAG_FLUSH; + if (rate >= 0) + seek = gst_event_new_seek (rate, GST_FORMAT_TIME, + seek_flags | GST_SEEK_FLAG_ACCURATE, + /* start */ GST_SEEK_TYPE_SET, pos, + /* stop */ GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE); + else + seek = gst_event_new_seek (rate, GST_FORMAT_TIME, + seek_flags | GST_SEEK_FLAG_ACCURATE, + /* start */ GST_SEEK_TYPE_SET, 0, + /* stop */ GST_SEEK_TYPE_SET, pos); + + if (!gst_element_send_event (GST_ELEMENT (self->priv->pipeline), seek)) + return FALSE; + +done: + self->priv->rate = rate; + self->priv->trick_mode = mode & ~GST_PLAY_TRICK_MODE_INSTANT_RATE; + return TRUE; +} + +static void +restore_terminal (void) +{ + gst_play_kb_set_key_handler (NULL, NULL); +} + +static void +toggle_paused (GESLauncher * self) +{ + if (self->priv->desired_state == GST_STATE_PLAYING) + self->priv->desired_state = GST_STATE_PAUSED; + else + self->priv->desired_state = GST_STATE_PLAYING; + + gst_element_set_state (GST_ELEMENT (self->priv->pipeline), + self->priv->desired_state); +} + +static void +relative_seek (GESLauncher * self, gdouble percent) +{ + gint64 pos = -1, step, dur; + + g_return_if_fail (percent >= -1.0 && percent <= 1.0); + + if (!gst_element_query_position (GST_ELEMENT (self->priv->pipeline), + GST_FORMAT_TIME, &pos)) + goto seek_failed; + + if (!gst_element_query_duration (GST_ELEMENT (self->priv->pipeline), + GST_FORMAT_TIME, &dur)) { + goto seek_failed; + } + + step = dur * percent; + if (ABS (step) < GST_SECOND) + step = (percent < 0) ? -GST_SECOND : GST_SECOND; + + pos = pos + step; + if (pos > dur) { + gst_print ("\n%s\n", "Reached end of self list."); + g_application_quit (G_APPLICATION (self)); + } else { + if (pos < 0) + pos = 0; + + play_do_seek (self, pos, self->priv->rate, self->priv->trick_mode); + } + + return; + +seek_failed: + { + gst_print ("\nCould not seek.\n"); + } +} + +static gboolean +play_set_rate_and_trick_mode (GESLauncher * self, gdouble rate, + GstPlayTrickMode mode) +{ + gint64 pos = -1; + + g_return_val_if_fail (rate != 0, FALSE); + + if (!gst_element_query_position (GST_ELEMENT (self->priv->pipeline), + GST_FORMAT_TIME, &pos)) + return FALSE; + + return play_do_seek (self, pos, rate, mode); +} + +static void +play_set_playback_rate (GESLauncher * self, gdouble rate) +{ + GstPlayTrickMode mode = self->priv->trick_mode; + + if (play_set_rate_and_trick_mode (self, rate, mode)) { + gst_print ("Playback rate: %.2f", rate); + gst_print (" \n"); + } else { + gst_print ("\n"); + gst_print ("Could not change playback rate to %.2f", rate); + gst_print (".\n"); + } +} + +static void +play_set_relative_playback_rate (GESLauncher * self, gdouble rate_step, + gboolean reverse_direction) +{ + gdouble new_rate = self->priv->rate + rate_step; + + play_set_playback_rate (self, new_rate); +} + +static const gchar * +trick_mode_get_description (GstPlayTrickMode mode) +{ + switch (mode) { + case GST_PLAY_TRICK_MODE_NONE: + return "normal playback, trick modes disabled"; + case GST_PLAY_TRICK_MODE_DEFAULT: + return "trick mode: default"; + case GST_PLAY_TRICK_MODE_DEFAULT_NO_AUDIO: + return "trick mode: default, no audio"; + case GST_PLAY_TRICK_MODE_KEY_UNITS: + return "trick mode: key frames only"; + case GST_PLAY_TRICK_MODE_KEY_UNITS_NO_AUDIO: + return "trick mode: key frames only, no audio"; + default: + break; + } + return "unknown trick mode"; +} + +static void +play_switch_trick_mode (GESLauncher * self) +{ + GstPlayTrickMode new_mode = ++self->priv->trick_mode; + const gchar *mode_desc; + + if (new_mode == GST_PLAY_TRICK_MODE_LAST) + new_mode = GST_PLAY_TRICK_MODE_NONE; + + mode_desc = trick_mode_get_description (new_mode); + + if (play_set_rate_and_trick_mode (self, self->priv->rate, new_mode)) { + gst_print ("Rate: %.2f (%s) \n", self->priv->rate, + mode_desc); + } else { + gst_print ("\nCould not change trick mode to %s.\n", mode_desc); + } +} + +static void +print_keyboard_help (void) +{ + static struct + { + const gchar *key_desc; + const gchar *key_help; + } key_controls[] = { + { + "space", "pause/unpause"}, { + "q or ESC", "quit"}, { + "\342\206\222", "seek forward"}, { + "\342\206\220", "seek backward"}, { + "+", "increase playback rate"}, { + "-", "decrease playback rate"}, { + "t", "enable/disable trick modes"}, { + "s", "change subtitle track"}, { + "0", "seek to beginning"}, { + "k", "show keyboard shortcuts"},}; + guint i, chars_to_pad, desc_len, max_desc_len = 0; + + gst_print ("\n\n%s\n\n", "Interactive mode - keyboard controls:"); + + for (i = 0; i < G_N_ELEMENTS (key_controls); ++i) { + desc_len = g_utf8_strlen (key_controls[i].key_desc, -1); + max_desc_len = MAX (max_desc_len, desc_len); + } + ++max_desc_len; + + for (i = 0; i < G_N_ELEMENTS (key_controls); ++i) { + chars_to_pad = max_desc_len - g_utf8_strlen (key_controls[i].key_desc, -1); + gst_print ("\t%s", key_controls[i].key_desc); + gst_print ("%-*s: ", chars_to_pad, ""); + gst_print ("%s\n", key_controls[i].key_help); + } + gst_print ("\n"); +} + static gboolean _parse_track_type (const gchar * option_name, const gchar * value, GESLauncherParsedOptions * opts, GError ** error) @@ -971,6 +1222,10 @@ ges_launcher_parse_options (GESLauncher * self, "Embed nested timelines when saving.", } , + {"no-interactive", 0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, + &opts->interactive, + "Disable interactive control via the keyboard", NULL} + , {NULL} }; @@ -1090,6 +1345,68 @@ done: return res; } +static void +keyboard_cb (const gchar * key_input, gpointer user_data) +{ + GESLauncher *self = (GESLauncher *) user_data; + gchar key = '\0'; + + /* only want to switch/case on single char, not first char of string */ + if (key_input[0] != '\0' && key_input[1] == '\0') + key = g_ascii_tolower (key_input[0]); + + switch (key) { + case 'k': + print_keyboard_help (); + break; + case ' ': + toggle_paused (self); + break; + case 'q': + case 'Q': + g_application_quit (G_APPLICATION (self)); + break; + case '+': + if (ABS (self->priv->rate) < 2.0) + play_set_relative_playback_rate (self, 0.1, FALSE); + else if (ABS (self->priv->rate) < 4.0) + play_set_relative_playback_rate (self, 0.5, FALSE); + else + play_set_relative_playback_rate (self, 1.0, FALSE); + break; + case '-': + if (ABS (self->priv->rate) <= 2.0) + play_set_relative_playback_rate (self, -0.1, FALSE); + else if (ABS (self->priv->rate) <= 4.0) + play_set_relative_playback_rate (self, -0.5, FALSE); + else + play_set_relative_playback_rate (self, -1.0, FALSE); + break; + case 't': + play_switch_trick_mode (self); + break; + case 27: /* ESC */ + if (key_input[1] == '\0') { + g_application_quit (G_APPLICATION (self)); + break; + } + case '0': + play_do_seek (self, 0, self->priv->rate, self->priv->trick_mode); + break; + default: + if (strcmp (key_input, GST_PLAY_KB_ARROW_RIGHT) == 0) { + relative_seek (self, +0.08); + } else if (strcmp (key_input, GST_PLAY_KB_ARROW_LEFT) == 0) { + relative_seek (self, -0.01); + } else { + GST_INFO ("keyboard input:"); + for (; *key_input != '\0'; ++key_input) + GST_INFO (" code %3d", *key_input); + } + break; + } +} + static void _startup (GApplication * application) { @@ -1107,6 +1424,15 @@ _startup (GApplication * application) goto done; } + if (opts->interactive && !opts->outputuri) { + if (gst_play_kb_set_key_handler (keyboard_cb, self)) { + gst_print ("Press 'k' to see a list of keyboard shortcuts.\n"); + atexit (restore_terminal); + } else { + gst_print ("Interactive keyboard handling in terminal not available.\n"); + } + } + if (opts->list_transitions) { _print_transition_list (); goto done; @@ -1202,6 +1528,10 @@ ges_launcher_init (GESLauncher * self) self->priv = ges_launcher_get_instance_private (self); self->priv->parsed_options.track_types = GES_TRACK_TYPE_AUDIO | GES_TRACK_TYPE_VIDEO; + self->priv->parsed_options.interactive = TRUE; + self->priv->desired_state = GST_STATE_PLAYING; + self->priv->rate = 1.0; + self->priv->trick_mode = GST_PLAY_TRICK_MODE_NONE; } gint diff --git a/tools/ges-launcher.h b/tools/ges-launcher.h index d50d8f41f3..4021b14286 100644 --- a/tools/ges-launcher.h +++ b/tools/ges-launcher.h @@ -54,6 +54,7 @@ typedef struct gboolean disable_validate; gboolean ignore_eos; + gboolean interactive; } GESLauncherParsedOptions; struct _GESLauncher { diff --git a/tools/meson.build b/tools/meson.build index cb5113997a..f0726abd68 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -7,7 +7,7 @@ if gstvalidate_dep.found() endif ges_launch = executable('ges-launch-@0@'.format(apiversion), - 'ges-validate.c', 'ges-launch.c', 'ges-launcher.c', 'utils.c', + 'ges-validate.c', 'ges-launch.c', 'ges-launcher.c', 'utils.c', 'ges-launcher-kb.c', c_args : [ges_tool_args], dependencies : deps, install: true