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

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.

App struct
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.

Subscribe keyboard handle
    /* 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.

Keyboard handler
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.

Toggle play
 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.

Seek position
 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.
p
Toggling player to PAUSE mode
h
Player commands:
p - toggle play/pause,
s[value] - seek to relative position +/-[value] seconds (default 1.0),
h - show this help screen,
q - quit application.
p
Toggling player to PLAY mode
s+2
Seeking 2.000 seconds from now
s+20
Seeking 20.000 seconds from now
s-5
Seeking -5.000 seconds from now
p
Toggling player to PAUSE mode

Full source code

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: