Event Management

Goal

This tutorial explains to you how to receive events emitted by the Fluendo SDK to build a more advanced, robust and complete player. This time, you will learn to:

  • Subscribe to events

  • Show any error that may arise

  • Close the player when the video finishes or the window is closed

Prerequisites

  • Hello World

  • Knowledge of the Observer Pattern. Essentially, it is a pattern commonly used to receive information asynchronously when some events are triggered to avoid actively polling.

Walkthrough

We start initializing the SDK, opening the URI of the video stream and playing it.

Initialize the Fluendo SDK start playing
    flu_initialize();
    FluPlayer *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 to open the video and start playing it */
    flu_player_uri_open(player, uri);
    flu_player_play(player);

Now, we are going to subscribe to a handful set of interesting events that the player emits. Each event gives us different information that we are going to use to make our application more user-friendly. To register the observers (also known as listeners) to the events, we use the flu_player_event_listener method. It takes a FluPlayerListener along with an opaque pointer meant to pass user data to the listener. This pointer is a common way of getting the context from within a callback to avoid relying on global variables.

Since we don’t want to have to implement our own loop in our main thread that waits until some event happens before exiting, we are going to make use of GMainLoop. This convenient utility is nothing else than a loop will be relied upon in the following tutorials and it’s often used in GLib and GTK+ applications. We create it using g_main_loop_new and start it through g_main_loop_run to have our main thread running until some listener asks to stop it via g_main_loop_quit.

Subscribe to events
    GMainLoop *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(player, FLU_PLAYER_EVENT_ERROR, _player_on_error, main_loop);
    flu_player_event_listener_add(player, FLU_PLAYER_EVENT_EOS, _player_on_eos, main_loop);
    flu_player_event_listener_add(player, FLU_PLAYER_EVENT_STREAM_STATE_CHANGED, _player_on_stream_state_changed, main_loop);

    /* Start running the main_loop. This will run the loop until
       g_main_loop_quit is called on it. */
    g_main_loop_run(main_loop);

We are setting listeners for several FluPlayerEventType. Let’s go through every kind of event along with the listener registered.

Attention

The context from which each listener is called depends on the emitter of the event. Since different listeners for different events can be called from different threads, you need to be extra cautious in what you do within these listeners as you may need to protect for data races and pay attention that the methods you call are thread-safe.

FLU_PLAYER_EVENT_ERROR

FLU_PLAYER_EVENT_ERROR is emitted when an error happens in the player. This allows us to provide to the user a human-readable error. As stated before, the __player_on_error callback (as well as any other listener) receives 3 arguments: the FluPlayer, the FluPlayerEvent and the user data provided when subscribing. The first thing that we need to do in the event handler (yet another way of calling the listener) is to retrieve the information from FluPlayerEvent. It is a C union that conveys all the kind of events that FluPlayer can emit. This way, we can define generic functions that take FluPlayerEvent as an argument, without the need to have a different method for each of the events.

Hence, for each FluPlayerEventType we subscribe to for a particular listener, we can collect the information it carries by interpreting the correct type of event. Almost every FluPlayerEventType maps 1:1 to each FluPlayerEvent, with few exceptions of events that don’t need to provide any extra information. For our FLU_PLAYER_EVENT_ERROR, we need to interpret it (a cast in the C world) as a FluPlayerEventError. In this listener are going to show the short (GError->message) and long (dbg) description of the error. We also want to ask the main loop to quit because we consider for this tutorial all errors are unrecoverable. To do this, we cast the data pointer to the actual type we passed when registering to the event (GMainLoop) and call g_main_loop_quit. Lastly, we need to return either TRUE or FALSE from the listener. With FALSE, the listener will be unsubscribed from the observer.

Error listener
static gboolean _player_on_error(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
    FluPlayerEventError *ev = (FluPlayerEventError *)event;
    GMainLoop *main_loop = (GMainLoop *)data;

    g_print("Error: %s\nDetailed description: %s.\n", ev->error->message, ev->dbg);
    g_main_loop_quit(main_loop);

    return TRUE;
}
Example of error reported
Error: Not Found
Detailed description: gstsouphttpsrc.c(1195): gst_soup_http_src_parse_status ():/GstPipeline:pipeline0/GstURIDecodeBin:uridecodebin0/GstSoupHTTPSrc:source:
Not Found (404), URL: http://nonexistingmedia.

FLU_PLAYER_EVENT_EOS

The second event we subscribed to is FLU_PLAYER_EVENT_EOS, which is emitted when the EOS (End Of Stream) is reached. For this case, there is no FluEventType really associated since this is a self-explanatory event. All we want to do when this happens is quitting the loop.

EOS listener
static gboolean _player_on_eos(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
    GMainLoop *main_loop = (GMainLoop *)data;

    g_print("EOS received\n");
    g_main_loop_quit(main_loop);

    return TRUE;
}

FLU_PLAYER_EVENT_STREAM_STATE_CHANGED

The last event is FLU_PLAYER_EVENT_STREAM_STATE_CHANGED, that allow us to detect when the different streams are activated or deactivated.

Note

A single video container can have several streams of different kinds: video, audio, text or data. For instance, a movie could have different video qualities and tracks of audio for different languages, along with subtitles. To learn more about this, check out the Stream Management tutorial.

We use this listener to detect that the window has been closed, because it deactivates the video stream. The FluEventType sent to this event handler is the FluPlayerEventStreamStateChanged, that contains the FluStream that we use to query the state of the stream through the flu_stream_is_active method. We are also going to show the user some log when any stream state changes. In order to distinguish the type of stream we create a helper method called _stream_type_name that uses flu_stream_type_get.

Stream state changed
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;
    GMainLoop *main_loop = (GMainLoop *)data;

    if (ev->stream && !flu_stream_is_pending(ev->stream))
    {
        gboolean active = flu_stream_is_active(ev->stream);
        g_print("Info: %s stream %p has become %s.\n",
                _stream_type_name(ev->stream),
                ev->stream,
                active ? "active" : "inactive");

        if (!active && g_main_loop_is_running(main_loop))
        {
            g_print("Window closed, exiting...\n");
            g_main_loop_quit(main_loop);
        }
    }

    return TRUE;
}

Pay attention to the g_main_loop_is_running call to check that we have not previously attempted to quit. After closing the player, we may still receive this same event telling us that a different stream (such as audio) has also become inactive.

Output
Info: video stream 0x7fa230007000 has become active.
Info: audio stream 0x7fa230007180 has become active.
Info: video stream 0x7fa230007000 has become inactive.
Window closed, exiting…

Finally, we release the references to the objects we have worked with and shutdown the SDK.

Close the player and shutdown
    flu_player_close(player);
    flu_player_unref(player);
    g_main_loop_unref(main_loop);
    flu_shutdown();

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
#include <fluendo-sdk.h>

static gboolean _player_on_error(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
    FluPlayerEventError *ev = (FluPlayerEventError *)event;
    GMainLoop *main_loop = (GMainLoop *)data;

    g_print("Error: %s\nDetailed description: %s.\n", ev->error->message, ev->dbg);
    g_main_loop_quit(main_loop);

    return TRUE;
}

static gboolean _player_on_eos(FluPlayer *player, FluPlayerEvent *event, gpointer data)
{
    GMainLoop *main_loop = (GMainLoop *)data;

    g_print("EOS received\n");
    g_main_loop_quit(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;
    GMainLoop *main_loop = (GMainLoop *)data;

    if (ev->stream && !flu_stream_is_pending(ev->stream))
    {
        gboolean active = flu_stream_is_active(ev->stream);
        g_print("Info: %s stream %p has become %s.\n",
                _stream_type_name(ev->stream),
                ev->stream,
                active ? "active" : "inactive");

        if (!active && g_main_loop_is_running(main_loop))
        {
            g_print("Window closed, exiting...\n");
            g_main_loop_quit(main_loop);
        }
    }

    return TRUE;
}

int main(int argc, const char **argv)
{
    /* Initialize the Fluendo SDK and create the player */
    flu_initialize();
    FluPlayer *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 to open the video and start playing it */
    flu_player_uri_open(player, uri);
    flu_player_play(player);

    /* Create the main loop that will handle the events we subscribe to */
    GMainLoop *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(player, FLU_PLAYER_EVENT_ERROR, _player_on_error, main_loop);
    flu_player_event_listener_add(player, FLU_PLAYER_EVENT_EOS, _player_on_eos, main_loop);
    flu_player_event_listener_add(player, FLU_PLAYER_EVENT_STREAM_STATE_CHANGED, _player_on_stream_state_changed, main_loop);

    /* Start running the main_loop. This will run the loop until
       g_main_loop_quit is called on it. */
    g_main_loop_run(main_loop);

    /* Close the player, unref it, unref the main loop and shutdown the Fluendo SDK */
    flu_player_close(player);
    flu_player_unref(player);
    g_main_loop_unref(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

Summing up, this tutorial explained:

Continue to the next tutorial to learn how manage the video latency.