Webcam Recorder

Goal

This tutorial shows how the Recorder interface allows recording arbitrary streams coming from different sources. Reading it you will learn to:

  • Let the user select which webcam and microphone to record

  • Connect both streams (video for webcam, audio for microphone) into a recorder

  • Generate an MP4 file containing both audio and video

Walkthrough

First, we need a function to select a webcam and a microphone from those available in the system. Note the usage of flu_device_is_fake to filter fake devices from actual devices. Aside from this, the code is basically what you have seen in the Devices tutorial for enumerating devices.

Choosing a device
static FluDevice *_choose_device_of_type(FluDeviceType type)
{
    GList *devices = flu_device_list_get();
    GList *valid_devices = NULL;
    FluDevice *choosen = NULL;
    guint size = g_list_length(devices);
    if (size > 0)
    {
        /* Create a new list filtering valid devices of the specified type */
        for (GList *item = devices; item; item = item->next)
        {
            const FluDevice *device = (const FluDevice *)item->data;
            if (!flu_device_is_fake(device) && flu_device_type_get(device) == type)
            {
                valid_devices = g_list_append(valid_devices, item->data);
            }
        }
        /* We ask again and again until the user enters a valid device number */
        while (!choosen)
        {
            g_print("\n");
            gint idx = 0;
            for (GList *item = valid_devices; item; item = item->next)
            {
                g_print("%d. %s.\n", ++idx, flu_device_label_get((const FluDevice *)item->data));
            }
            g_print("\nChoose device : ");
            int option = (getchar() - '0') - 1;
            if (option >= 0 && option < idx)
            {
                choosen =
                    flu_device_ref((FluDevice *)g_list_nth_data(valid_devices, option));
            }
        }
    }
    else
    {
        g_print("No devices available.\n");
    }

    flu_device_list_free(devices);
    g_list_free(valid_devices);
    return choosen;
}

Then, we need a function to create a Recorder object using flu_recorder_new, adding the corresponding listeners to it, as discussed in the Event Management tutorial. Note that we indicate in the FluMediaInfo struct the container that will be used. We also take advantage of the gpointer parameter in flu_recorder_event_listener_add to pass the application’s context.

Creating a recorder
static FluRecorder *_create_recorder(AppContext *app_context)
{
    FluMediaInfo media_info = {0};
    media_info.format = FLU_MEDIA_INFO_FORMAT_MP4;

    GError *error = NULL;
    FluRecorder *recorder = flu_recorder_new(&media_info, &error);
    if (recorder)
    {
        flu_recorder_event_listener_add(recorder, FLU_RECORDER_EVENT_REQUEST_SAVE_MODE, _recorder_on_request_save_mode, NULL);
        flu_recorder_event_listener_add(recorder, FLU_RECORDER_EVENT_STATE, _recorder_on_state_changed, NULL);
        flu_recorder_event_listener_add(recorder, FLU_RECORDER_EVENT_ERROR, _recorder_on_error, app_context);
    }
    else
    {
        g_print("Error: cannot create recorder (%s)\n",
                error ? error->message : "undefined error");
        if (error)
        {
            g_error_free(error);
        }
    }

    return recorder;
}

For the listeners, we need to pay attention to the callback that we implement for FLU_RECORDER_EVENT_REQUEST_SAVE_MODE. When called by the recorder, we fill the FluRecorderEventRequestSaveMode event with the desired save mode and output filename.

Selecting save mode and output file
static gboolean _recorder_on_request_save_mode(FluRecorder *recorder, FluRecorderEvent *event, gpointer data)
{
    FluRecorderEventRequestSaveMode *ev = (FluRecorderEventRequestSaveMode *)event;
    ev->mode = FLU_RECORDER_SAVE_MODE_FILE;
    ev->data.file.filename = "output.mp4";
    return TRUE;
}

Finally, we need a function to connect the devices to the recorder. We need to fill the relevant fields in the FluDeviceConfig struct. Using flu_device_output_formats_get, we get the available formats in the source device. In this case, we select the first one as output for the sake of simplicity. It’s important to not keep a reference to the format but perform a deep copy using flu_stream_info_copy. Then, we specify a codec depending on the device type (webcam or microphone) and we call flu_device_config_set to set the configuration. Last but not least, we use flu_recorder_connect_device to make the actual connection.

Connect devices to the recorder
static gboolean _connect_device(FluRecorder *recorder, FluDevice *device)
{
    gboolean ret = FALSE;
    FluDeviceConfig device_config = {0};

    /* Get the device's supported formats */
    const GList *formats = flu_device_output_formats_get(device);
    if (!formats)
    {
        g_print("Error: device \"%s\" has no formats list\n",
                flu_device_name_get(device));
        goto cleanup;
    }

    /* Pick the first format */
    const FluStreamInfo *format_info = g_list_nth_data((GList *)formats, 0 /*first format*/);

    /* Fill FluDeviceConfig struct */
    device_config.type = flu_device_type_get(device);

    /* Important: deep copy the format info */
    flu_stream_info_copy(&device_config.output_format, format_info);

    /* Select the codec for output */
    switch (device_config.output_format.type)
    {
    case FLU_STREAM_TYPE_VIDEO:
        device_config.output_format.data.video.vcodec.type = FLU_STREAM_VIDEO_CODEC_H264;
        break;

    case FLU_STREAM_TYPE_AUDIO:
        device_config.output_format.data.audio.acodec = FLU_STREAM_AUDIO_CODEC_AAC;
        break;

    default:
        break;
    }

    if (!flu_device_config_set(device, &device_config))
    {
        g_print("Error: cannot set device \"%s\" configuration\n",
                flu_device_name_get(device));
        goto cleanup;
    }

    /* Connect the device with the selected configuration */
    GError *error = NULL;
    FluStream *stream = flu_recorder_connect_device(recorder, device, &device_config.output_format, &error);
    if (!stream)
    {
        g_print("Error: cannot connect device \"%s\" to recorder (%s)\n",
                flu_device_name_get(device),
                error ? error->message : "undefined error");
        if (error)
        {
            g_error_free(error);
        }
        goto cleanup;
    }

    flu_stream_unref(stream);
    ret = TRUE;

cleanup:
    flu_device_config_clear(&device_config);
    return ret;
}

Putting all together, we ask the user for a webcam and a microphone. Then, we create a recorder, connect both devices to it and we start the recording by calling flu_recorder_record. This changes the recorder status and starts recording audio and video from the sources until we quit the main loop. Then a call to flu_recorder_stop ends the recording and flushes the output file.

Main Function
int main(void)
{
    FluDevice *camera = NULL, *microphone = NULL;
    FluRecorder *recorder = NULL;
    gboolean error = TRUE;
    guint keyboard_source_id = 0;
    AppContext app_context = {0};

    /* Initialize Fluendo SDK */
    flu_initialize();

    /* Let the user choose a webcam */
    g_print("\nSelect a source webcam: \n");
    camera = _choose_device_of_type(FLU_DEVICE_TYPE_CAMERA);
    if (camera == NULL)
    {
        goto cleanup;
    }

    /* Let the user choose a microphone */
    g_print("\nSelect a source microphone: \n");
    microphone = _choose_device_of_type(FLU_DEVICE_TYPE_MICROPHONE);
    if (microphone == NULL)
    {
        goto cleanup;
    }

    /* Create the recorder */
    recorder = _create_recorder(&app_context);
    if (recorder == NULL)
    {
        goto cleanup;
    }

    /* Connect camera and microphone to the recorder */
    if (!_connect_device(recorder, camera))
    {
        goto cleanup;
    }
    if (!_connect_device(recorder, microphone))
    {
        goto cleanup;
    }

    /* Start the recorder */
    flu_recorder_record(recorder, NULL);

    /* Add an async watch for the keyboard */
    GIOChannel *io_stdin = g_io_channel_unix_new(fileno(stdin));
    keyboard_source_id = g_io_add_watch(io_stdin, G_IO_IN, (GIOFunc)_handle_keyboard, &app_context);
    g_io_channel_unref(io_stdin);

    /* Create a loop that will run until aborted */
    app_context.main_loop = g_main_loop_new(NULL, FALSE);
    g_print("Recording. Press 'q' to quit\n");
    g_main_loop_run(app_context.main_loop);

    /* Stop recording */
    flu_recorder_stop(recorder);
    error = app_context.recording_error;

cleanup:
    flu_device_unref(camera);
    flu_device_unref(microphone);
    flu_recorder_unref(recorder);
    g_main_loop_unref(app_context.main_loop);
    if (keyboard_source_id)
    {
        g_source_remove(keyboard_source_id);
    }
    /* Shutdowns library */
    flu_shutdown();
    return error ? 0 : 1;
}

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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#include <fluendo-sdk.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct _AppContext
{
    GMainLoop *main_loop;
    gboolean recording_error;
} AppContext;

static gboolean _handle_keyboard(GIOChannel *source, GIOCondition cond, gpointer data)
{
    gchar *str = NULL;
    if (g_io_channel_read_line(source, &str, NULL, NULL, NULL) == G_IO_STATUS_NORMAL)
    {
        AppContext *ctx = (AppContext *)data;
        switch (str[0])
        {
        case 'q':
            g_main_loop_quit(ctx->main_loop);
            break;
        }

        g_free(str);
    }

    return TRUE;
}

static gboolean _recorder_on_request_save_mode(FluRecorder *recorder, FluRecorderEvent *event, gpointer data)
{
    FluRecorderEventRequestSaveMode *ev = (FluRecorderEventRequestSaveMode *)event;
    ev->mode = FLU_RECORDER_SAVE_MODE_FILE;
    ev->data.file.filename = "output.mp4";
    return TRUE;
}

static gboolean _recorder_on_state_changed(FluRecorder *recorder, FluRecorderEvent *event, gpointer data)
{
    FluRecorderEventState *ev = (FluRecorderEventState *)event;
    g_print("State changed to %s\n", flu_recorder_state_name_get(ev->state));
    return TRUE;
}

static gboolean _recorder_on_error(FluRecorder *recorder, FluRecorderEvent *event, gpointer data)
{
    AppContext *app_context = (AppContext *)data;
    FluRecorderEventError *ev = (FluRecorderEventError *)event;
    g_print("Error: %s %s\n", ev->error.error->message, ev->error.dbg);
    app_context->recording_error = TRUE;
    g_main_loop_quit(app_context->main_loop);
    return FALSE;
}

static FluRecorder *_create_recorder(AppContext *app_context)
{
    FluMediaInfo media_info = {0};
    media_info.format = FLU_MEDIA_INFO_FORMAT_MP4;

    GError *error = NULL;
    FluRecorder *recorder = flu_recorder_new(&media_info, &error);
    if (recorder)
    {
        flu_recorder_event_listener_add(recorder, FLU_RECORDER_EVENT_REQUEST_SAVE_MODE, _recorder_on_request_save_mode, NULL);
        flu_recorder_event_listener_add(recorder, FLU_RECORDER_EVENT_STATE, _recorder_on_state_changed, NULL);
        flu_recorder_event_listener_add(recorder, FLU_RECORDER_EVENT_ERROR, _recorder_on_error, app_context);
    }
    else
    {
        g_print("Error: cannot create recorder (%s)\n",
                error ? error->message : "undefined error");
        if (error)
        {
            g_error_free(error);
        }
    }

    return recorder;
}

static gboolean _connect_device(FluRecorder *recorder, FluDevice *device)
{
    gboolean ret = FALSE;
    FluDeviceConfig device_config = {0};

    /* Get the device's supported formats */
    const GList *formats = flu_device_output_formats_get(device);
    if (!formats)
    {
        g_print("Error: device \"%s\" has no formats list\n",
                flu_device_name_get(device));
        goto cleanup;
    }

    /* Pick the first format */
    const FluStreamInfo *format_info = g_list_nth_data((GList *)formats, 0 /*first format*/);

    /* Fill FluDeviceConfig struct */
    device_config.type = flu_device_type_get(device);

    /* Important: deep copy the format info */
    flu_stream_info_copy(&device_config.output_format, format_info);

    /* Select the codec for output */
    switch (device_config.output_format.type)
    {
    case FLU_STREAM_TYPE_VIDEO:
        device_config.output_format.data.video.vcodec.type = FLU_STREAM_VIDEO_CODEC_H264;
        break;

    case FLU_STREAM_TYPE_AUDIO:
        device_config.output_format.data.audio.acodec = FLU_STREAM_AUDIO_CODEC_AAC;
        break;

    default:
        break;
    }

    if (!flu_device_config_set(device, &device_config))
    {
        g_print("Error: cannot set device \"%s\" configuration\n",
                flu_device_name_get(device));
        goto cleanup;
    }

    /* Connect the device with the selected configuration */
    GError *error = NULL;
    FluStream *stream = flu_recorder_connect_device(recorder, device, &device_config.output_format, &error);
    if (!stream)
    {
        g_print("Error: cannot connect device \"%s\" to recorder (%s)\n",
                flu_device_name_get(device),
                error ? error->message : "undefined error");
        if (error)
        {
            g_error_free(error);
        }
        goto cleanup;
    }

    flu_stream_unref(stream);
    ret = TRUE;

cleanup:
    flu_device_config_clear(&device_config);
    return ret;
}

static FluDevice *_choose_device_of_type(FluDeviceType type)
{
    GList *devices = flu_device_list_get();
    GList *valid_devices = NULL;
    FluDevice *choosen = NULL;
    guint size = g_list_length(devices);
    if (size > 0)
    {
        /* Create a new list filtering valid devices of the specified type */
        for (GList *item = devices; item; item = item->next)
        {
            const FluDevice *device = (const FluDevice *)item->data;
            if (!flu_device_is_fake(device) && flu_device_type_get(device) == type)
            {
                valid_devices = g_list_append(valid_devices, item->data);
            }
        }
        /* We ask again and again until the user enters a valid device number */
        while (!choosen)
        {
            g_print("\n");
            gint idx = 0;
            for (GList *item = valid_devices; item; item = item->next)
            {
                g_print("%d. %s.\n", ++idx, flu_device_label_get((const FluDevice *)item->data));
            }
            g_print("\nChoose device : ");
            int option = (getchar() - '0') - 1;
            if (option >= 0 && option < idx)
            {
                choosen =
                    flu_device_ref((FluDevice *)g_list_nth_data(valid_devices, option));
            }
        }
    }
    else
    {
        g_print("No devices available.\n");
    }

    flu_device_list_free(devices);
    g_list_free(valid_devices);
    return choosen;
}

int main(void)
{
    FluDevice *camera = NULL, *microphone = NULL;
    FluRecorder *recorder = NULL;
    gboolean error = TRUE;
    guint keyboard_source_id = 0;
    AppContext app_context = {0};

    /* Initialize Fluendo SDK */
    flu_initialize();

    /* Let the user choose a webcam */
    g_print("\nSelect a source webcam: \n");
    camera = _choose_device_of_type(FLU_DEVICE_TYPE_CAMERA);
    if (camera == NULL)
    {
        goto cleanup;
    }

    /* Let the user choose a microphone */
    g_print("\nSelect a source microphone: \n");
    microphone = _choose_device_of_type(FLU_DEVICE_TYPE_MICROPHONE);
    if (microphone == NULL)
    {
        goto cleanup;
    }

    /* Create the recorder */
    recorder = _create_recorder(&app_context);
    if (recorder == NULL)
    {
        goto cleanup;
    }

    /* Connect camera and microphone to the recorder */
    if (!_connect_device(recorder, camera))
    {
        goto cleanup;
    }
    if (!_connect_device(recorder, microphone))
    {
        goto cleanup;
    }

    /* Start the recorder */
    flu_recorder_record(recorder, NULL);

    /* Add an async watch for the keyboard */
    GIOChannel *io_stdin = g_io_channel_unix_new(fileno(stdin));
    keyboard_source_id = g_io_add_watch(io_stdin, G_IO_IN, (GIOFunc)_handle_keyboard, &app_context);
    g_io_channel_unref(io_stdin);

    /* Create a loop that will run until aborted */
    app_context.main_loop = g_main_loop_new(NULL, FALSE);
    g_print("Recording. Press 'q' to quit\n");
    g_main_loop_run(app_context.main_loop);

    /* Stop recording */
    flu_recorder_stop(recorder);
    error = app_context.recording_error;

cleanup:
    flu_device_unref(camera);
    flu_device_unref(microphone);
    flu_recorder_unref(recorder);
    g_main_loop_unref(app_context.main_loop);
    if (keyboard_source_id)
    {
        g_source_remove(keyboard_source_id);
    }
    /* Shutdowns library */
    flu_shutdown();
    return error ? 0 : 1;
}

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

After reading this tutorial, you should have clear visibility and knowledge about: