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
Prerequisites¶
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.
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.
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.
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.
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.
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¶
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:
How to let the user choose among the different kinds of video and audio devices present in the system
How to create a new Recorder using flu_recorder_new
How to tell the recorder that we want to save the output to a file by subscribing to
FLU_RECORDER_EVENT_REQUEST_SAVE_MODE
and writing the desired saving mode to FluRecorderEventRequestSaveModeHow to set the proper device configuration for both video and audio using flu_device_config_set
How to wire both video and audio streams to the recorder using flu_recorder_connect_device
How to start recording using flu_recorder_record and stop and flush the media file through flu_recorder_stop