Playback Controls¶
Goal¶
This tutorial covers the playback of a video. Here, you will learn to:
Read values from user input to create an interactive player
Play and pause the video as per user input
Seek (move to a specific timestamp) the video to user-selected values
Prerequisites¶
Walkthrough¶
This is a slightly bigger source code than before. To avoid the boilerplate,
not every single chunk of code is commented if it has been listed previously.
Also, for the first time, we introduce the App
structure, used to pass the
application context across our functions.
typedef struct
{
GMainLoop *main_loop;
guint keyboard_source_id;
FluPlayer *player;
} App;
Our main
function initializes the Fluendo SDK and opens the video. It
doesn’t start playing it, though. This time, we want to control that manually.
To do so, we subscribe to the FLU_PLAYER_EVENT_ERROR
event to provide
feedback to the user in case something happens. Apart from that, we also set a
listener for FLU_PLAYER_EVENT_STREAM_STATE_CHANGED
, similarly to what we
did in our Event Management tutorial. Both listeners are the
same as in the previous tutorial, so we won’t reiterate on those. The new and
most important part of main
is where we use g_io_channel_unix_new
and g_io_add_watch
to set a keyboard handler (__handle_keyboard
) that is executed in our main
loop. This enables us to capture keyboard strokes and run different logic
depending on the key that is pressed. This is why on the cleanup side we need
to call g_remove_source
to unsubscribe the listener.
/* Capture keystrokes for control de play */
g_print("To see the list of available commands, press h<Enter>.\n\n");
GIOChannel *io_stdin = g_io_channel_unix_new(fileno(stdin));
app.keyboard_source_id = g_io_add_watch(io_stdin, G_IO_IN, (GIOFunc)_handle_keyboard, &app);
g_io_channel_unref(io_stdin);
/* Start running the main_loop. This will run the loop until
g_main_loop_quit is called on it. */
g_main_loop_run(app.main_loop);
/* Remove the keyboard handler, close the player, unref it, unref the main loop
and shutdown the Fluendo SDK */
g_source_remove(app.keyboard_source_id);
flu_player_close(app.player);
flu_player_unref(app.player);
g_main_loop_unref(app.main_loop);
flu_shutdown();
Now, the keyboard handler is quite straightforward. Through
g_io_channel_read_line,
we capture all characters typed (including the terminating character, which is
why we need to set the last byte to 0) and react differently on each one to
either toggle the play status or to seek for a position. Finally, we need to
free the resources allocated for the string and through the return TRUE
we
state as usual in GLib listeners that we want to keep registered to the event.
static gboolean _handle_keyboard(GIOChannel *source, GIOCondition cond, gpointer data)
{
gchar *str = NULL;
gsize terminator_pos = 0;
App *app = (App *)data;
if (g_io_channel_read_line(source, &str, NULL, &terminator_pos, NULL) == G_IO_STATUS_NORMAL)
{
str[terminator_pos] = 0;
switch (str[0])
{
/* Quit app */
case 'q':
g_print("Exiting...\n");
g_main_loop_quit(app->main_loop);
break;
/* Play/Pause current video */
case 'p':
_toggle_play(app);
break;
/* Seek to relative position +/-[value] seconds */
case 's':
setlocale(LC_NUMERIC, "C");
_seek_position(app, atof(str + 1));
break;
default:
g_print("Player commands:\n"
" p - toggle play/pause,\n"
" s[value] - seek to relative position +/-[value] seconds (default 1.0),\n"
" h - show this help screen,\n"
" q - quit application.\n");
break;
}
}
g_free(str);
return TRUE;
}
The _toggle_play
function switches the playing state from play to pause and
vice versa. It does so using flu_player_state_get to get a
FluPlayerState. Then,
depending on the returned value, it calls either flu_player_play or flu_player_pause to change its state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static void _toggle_play(App *app)
{
FluPlayerState state = flu_player_state_get(app->player);
if ((state == FLU_PLAYER_STATE_STOPPED) || (state == FLU_PLAYER_STATE_PAUSED))
{
g_print("Toggling player to PLAY mode\n");
flu_player_play(app->player);
}
else if (state == FLU_PLAYER_STATE_PLAYING)
{
g_print("Toggling player to PAUSE mode\n");
flu_player_pause(app->player);
}
}
|
Regarding seeking for a specific position, we need to get the current position using flu_player_position_get. Then, we add the offset passed by the user and use the resulting value to call flu_player_position_set.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | static void _seek_position(App *app, double offset)
{
gint64 position;
if (offset == 0)
{
offset = 1;
}
g_print("Seeking %.3f seconds from now\n", offset);
if (!flu_player_position_get(app->player, &position))
{
g_print("Cannot get current player position\n");
}
else
{
position += (gint64)(TIME_SECOND * offset);
if (position < 0)
{
position = 0;
}
if (!flu_player_position_set(app->player, position, TRUE))
{
g_print("Cannot set player position to " TIME_FORMAT "\n", TIME_ARGS(position));
}
}
}
|
- Output::
- Info: video stream 0x0x7f2868006090 has become active.Info: audio stream 0x0x7f2868006210 has become active.pToggling player to PAUSE modehPlayer commands:p - toggle play/pause,s[value] - seek to relative position +/-[value] seconds (default 1.0),h - show this help screen,q - quit application.pToggling player to PLAY modes+2Seeking 2.000 seconds from nows+20Seeking 20.000 seconds from nows-5Seeking -5.000 seconds from nowpToggling player to PAUSE mode
Full source code¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | #include <fluendo-sdk.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#define TIME_SECOND (G_USEC_PER_SEC * G_GINT64_CONSTANT(1000))
#define TIME_FORMAT "%u:%02u:%02u.%09u"
#define TIME_ARGS(t) (t > 0) ? (guint)(((guint64)(t)) / (TIME_SECOND * 60 * 60)) : 0, \
(t > 0) ? (guint)((((guint64)(t)) / (TIME_SECOND * 60)) % 60) : 0, \
(t > 0) ? (guint)((((guint64)(t)) / TIME_SECOND) % 60) : 0, \
(t > 0) ? (guint)(((guint64)(t)) % TIME_SECOND) : 0
typedef struct
{
GMainLoop *main_loop;
guint keyboard_source_id;
FluPlayer *player;
} App;
static gboolean _player_on_error(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
FluPlayerEventError *ev = (FluPlayerEventError *)event;
App *app = (App *)data;
g_print("Error: %s\nDetailed description: %s.\n", ev->error->message, ev->dbg);
g_main_loop_quit(app->main_loop);
return TRUE;
}
static gboolean _player_on_eos(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
App *app = (App *)data;
g_print("EOS received\n");
g_main_loop_quit(app->main_loop);
return TRUE;
}
static const gchar *_stream_type_name(FluStream *stream)
{
switch (flu_stream_type_get(stream))
{
case FLU_STREAM_TYPE_VIDEO:
return "video";
case FLU_STREAM_TYPE_AUDIO:
return "audio";
case FLU_STREAM_TYPE_TEXT:
return "text";
case FLU_STREAM_TYPE_DATA:
return "data";
default:
return "unknown";
}
}
static gboolean _player_on_stream_state_changed(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
FluPlayerEventStreamStateChanged *ev = (FluPlayerEventStreamStateChanged *)event;
App *app = (App *)data;
if (ev->stream && !flu_stream_is_pending(ev->stream))
{
gboolean active = flu_stream_is_active(ev->stream);
g_print("Info: %s stream 0x%p has become %s.\n",
_stream_type_name(ev->stream),
ev->stream,
active ? "active" : "inactive");
if (!active && g_main_loop_is_running(app->main_loop))
{
g_print("Window closed, exiting...\n");
g_main_loop_quit(app->main_loop);
}
}
return TRUE;
}
static void _toggle_play(App *app)
{
FluPlayerState state = flu_player_state_get(app->player);
if ((state == FLU_PLAYER_STATE_STOPPED) || (state == FLU_PLAYER_STATE_PAUSED))
{
g_print("Toggling player to PLAY mode\n");
flu_player_play(app->player);
}
else if (state == FLU_PLAYER_STATE_PLAYING)
{
g_print("Toggling player to PAUSE mode\n");
flu_player_pause(app->player);
}
}
static void _seek_position(App *app, double offset)
{
gint64 position;
if (offset == 0)
{
offset = 1;
}
g_print("Seeking %.3f seconds from now\n", offset);
if (!flu_player_position_get(app->player, &position))
{
g_print("Cannot get current player position\n");
}
else
{
position += (gint64)(TIME_SECOND * offset);
if (position < 0)
{
position = 0;
}
if (!flu_player_position_set(app->player, position, TRUE))
{
g_print("Cannot set player position to " TIME_FORMAT "\n", TIME_ARGS(position));
}
}
}
static gboolean _handle_keyboard(GIOChannel *source, GIOCondition cond, gpointer data)
{
gchar *str = NULL;
gsize terminator_pos = 0;
App *app = (App *)data;
if (g_io_channel_read_line(source, &str, NULL, &terminator_pos, NULL) == G_IO_STATUS_NORMAL)
{
str[terminator_pos] = 0;
switch (str[0])
{
/* Quit app */
case 'q':
g_print("Exiting...\n");
g_main_loop_quit(app->main_loop);
break;
/* Play/Pause current video */
case 'p':
_toggle_play(app);
break;
/* Seek to relative position +/-[value] seconds */
case 's':
setlocale(LC_NUMERIC, "C");
_seek_position(app, atof(str + 1));
break;
default:
g_print("Player commands:\n"
" p - toggle play/pause,\n"
" s[value] - seek to relative position +/-[value] seconds (default 1.0),\n"
" h - show this help screen,\n"
" q - quit application.\n");
break;
}
}
g_free(str);
return TRUE;
}
int main(int argc, const char **argv)
{
/* Struct for store the app info */
App app = {0};
/* Initialize the Fluendo SDK and create the player */
flu_initialize();
app.player = flu_player_new();
/* Set the URI to the first argument if given */
const char *uri = "http://ftp.halifax.rwth-aachen.de/blender/demo/movies/ToS/tears_of_steel_720p.mov";
if (argc > 1)
{
uri = argv[1];
g_print("Setting video stream URI: %s\n", uri);
}
/* Ask the player only to open the video */
flu_player_uri_open(app.player, uri);
/* Create the main loop that will handle the events we subscribe to */
app.main_loop = g_main_loop_new(NULL, FALSE);
/* Subscribe to the desired events, passing main_loop to the ones that need it */
flu_player_event_listener_add(app.player, FLU_PLAYER_EVENT_ERROR, _player_on_error, &app);
flu_player_event_listener_add(app.player, FLU_PLAYER_EVENT_EOS, _player_on_eos, &app);
flu_player_event_listener_add(app.player, FLU_PLAYER_EVENT_STREAM_STATE_CHANGED, _player_on_stream_state_changed, &app);
/* Capture keystrokes for control de play */
g_print("To see the list of available commands, press h<Enter>.\n\n");
GIOChannel *io_stdin = g_io_channel_unix_new(fileno(stdin));
app.keyboard_source_id = g_io_add_watch(io_stdin, G_IO_IN, (GIOFunc)_handle_keyboard, &app);
g_io_channel_unref(io_stdin);
/* Start running the main_loop. This will run the loop until
g_main_loop_quit is called on it. */
g_main_loop_run(app.main_loop);
/* Remove the keyboard handler, close the player, unref it, unref the main loop
and shutdown the Fluendo SDK */
g_source_remove(app.keyboard_source_id);
flu_player_close(app.player);
flu_player_unref(app.player);
g_main_loop_unref(app.main_loop);
flu_shutdown();
return 0;
}
|
You can download it here.
Building¶
This source code along with the rest of tutorials can be compiled using the following commands.
On Linux:
mkdir fluendo-sdk-tutorials && cd fluendo-sdk-tutorials
meson /opt/fluendo-sdk/share/doc/fluendo-sdk/tutorials/src
ninja
On Windows:
mkdir fluendo-sdk-tutorials
cd fluendo-sdk-tutorials
meson C:\fluendo-sdk\<version>\<x86/x86_64>\share\doc\fluendo-sdk\tutorials\src
ninja
To generate a Visual Studio project, you can pass the --backend=vs
option
to meson.
Conclusions¶
Recapitulating everything this tutorial covered, you have learned:
How to create and unreference a new io channel to read user input using g_io_channel_unix_new and g_io_channel_unref
How to set a listener for user input using g_io_add_watch and remove it using g_remove_source
How to read a user input using g_io_channel_read_line to create an interactive application
How to get the current FluPlayerState of the player using flu_player_state_get
How to play and pause the player using flu_player_play and flu_player_pause
How to get and set the current position of the video using flu_player_position_get and flu_player_position_set