This is the first article in the series of the articles where we are going to build video player - player. The idea is simple, we will create a minimal video player program and then extend the functionalities of it via "extension articles". Let's see how things will go! I will update the following list as I publish the article(s) in this series.
- Let's Build Video Player
tl;dc: Refer this gist to get the final version of player.c code.
You need following:
- GCC -
sudo apt install build-essential - GTK4 -
sudo apt install libgtk-4-dev - GStreamer & plugins -
sudo apt install libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good
Go ahead and create a file with name player.c and write the following stub code.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
If you need brief explanation on this code, refer the Let's Build Terminal Pt. 1 article. Although, it is from "Let's Build Terminal" series but the first part is generic enough to understand such a code.
Use following command to compile and run the program.
gcc player.c `pkg-config --cflags --libs gtk4` && ./a.out
Let's add the header bar. The plan is to have a "Open" button in header bar that allow us to select the video file to open and play.
We can create a header bar using gtk_header_bar_new() function and then give a title to it using gtk_window_set_titlebar() function to set the header bar as title bar of the window.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
Now, go ahead and create a button using gtk_button_new_with_label() function and then pack it to the header bar.
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
Run the program and it should produce output like this.
When user click on the button, it should open a file dialog to select the video. We need to pass the window instance as user data because it is needed to load the video into the window.
#include <gtk/gtk.h> static void _on_open_clicked(GtkWidget* button, gpointer data) { } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
Let's cast and save the window passed as user data and create a file dialog using gtk_file_dialog_new() function.
#include <gtk/gtk.h> static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
In order to actually see the file dialog, we need to call the gtk_file_dialog_open() function. We need to pass 5 arguments - dialog itself, window as parent, NULL as cancellable operation, callback function, and user data.
#include <gtk/gtk.h> static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
We need to define the _on_file_opened() function. But is a asnyc callback. Due to this it accepts these parameters - source object that has started asnyc operation (file dialog) of type GObject*, result of the operation of type GAsyncResult*, and user data.
#include <gtk/gtk.h> static void _on_file_opened(GObject* source_object, GAsyncResult* res, gpointer data) { } static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
Again, let's get the values of dialog and window from the function parameters.
#include <gtk/gtk.h> static void _on_file_opened(GObject* source_object, GAsyncResult* res, gpointer data) { GtkFileDialog* file_dialog = GTK_FILE_DIALOG(source_object); GtkWindow* window = GTK_WINDOW(data); } static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
We need to call the gtk_file_dialog_open_finish() function. This function finish the gtk_file_dialog_open() function and return either the selected file or an error.
#include <gtk/gtk.h> static void _on_file_opened(GObject* source_object, GAsyncResult* res, gpointer data) { GtkFileDialog* file_dialog = GTK_FILE_DIALOG(source_object); GtkWindow* window = GTK_WINDOW(data); GFile* file = NULL; GError* error = NULL; file = gtk_file_dialog_open_finish(file_dialog, res, &error); } static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
If we get the error, we need to stop and free the error memory using g_error_free(). We can also print the error message using g_printerr() function.
#include <gtk/gtk.h> static void _on_file_opened(GObject* source_object, GAsyncResult* res, gpointer data) { GtkFileDialog* file_dialog = GTK_FILE_DIALOG(source_object); GtkWindow* window = GTK_WINDOW(data); GFile* file = NULL; GError* error = NULL; file = gtk_file_dialog_open_finish(file_dialog, res, &error); if (error) { g_printerr("Error opening file %s\n", error->message); g_error_free(error); return; } } static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
But, if we have file, we can proceed with playing the video on the window. Let's create a video widget using gtk_video_new() function and then set it as child of the window.
#include <gtk/gtk.h> static void _on_file_opened(GObject* source_object, GAsyncResult* res, gpointer data) { GtkFileDialog* file_dialog = GTK_FILE_DIALOG(source_object); GtkWindow* window = GTK_WINDOW(data); GFile* file = NULL; GError* error = NULL; file = gtk_file_dialog_open_finish(file_dialog, res, &error); if (error) { g_printerr("Error opening file %s\n", error->message); g_error_free(error); return; } if (file) { GtkWidget* video = gtk_video_new(); gtk_window_set_child(window, video); } } static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
Let's create a media stream from the given file using gtk_media_fil_new_from_file() and set this steam to the video using gtk_video_set_media_stream(). Don't forget to unref the file object.
#include <gtk/gtk.h> static void _on_file_opened(GObject* source_object, GAsyncResult* res, gpointer data) { GtkFileDialog* file_dialog = GTK_FILE_DIALOG(source_object); GtkWindow* window = GTK_WINDOW(data); GFile* file = NULL; GError* error = NULL; file = gtk_file_dialog_open_finish(file_dialog, res, &error); if (error) { g_printerr("Error opening file %s\n", error->message); g_error_free(error); return; } if (file) { GtkWidget* video = gtk_video_new(); GtkMediaStream* media = GTK_MEDIA_STREAM(gtk_media_file_new_for_file(file)); gtk_video_set_media_stream(GTK_VIDEO(video), media); gtk_window_set_child(window, video); g_object_unref(file); } } static void _on_open_clicked(GtkWidget* button, gpointer data) { GtkWindow* window = GTK_WINDOW(data); GtkFileDialog* file_dialog = gtk_file_dialog_new(); gtk_file_dialog_open(file_dialog, window, NULL, _on_file_opened, window); } static void activate(GtkApplication* app, gpointer data) { GtkWidget* window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Player"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 800); // Header bar. GtkWidget* header_bar = gtk_header_bar_new(); gtk_header_bar_set_title_widget(GTK_HEADER_BAR(header_bar), gtk_label_new("Player")); // Button. GtkWidget* button = gtk_button_new_with_label("Open"); g_signal_connect(button, "clicked", G_CALLBACK(_on_open_clicked), window); gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button); gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char** argv) { GtkApplication* app = gtk_application_new("dev.marichi.player", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); return status; }
And with these changes, we now have the video player. Go ahead and run the program to confirm the video player!
Yay!

