messenger-gtk

Gtk+3 graphical user interfaces for GNUnet Messenger
Log | Files | Refs | Submodules | README | LICENSE

play_media.c (19385B)


      1 /*
      2    This file is part of GNUnet.
      3    Copyright (C) 2022--2025 GNUnet e.V.
      4 
      5    GNUnet is free software: you can redistribute it and/or modify it
      6    under the terms of the GNU Affero General Public License as published
      7    by the Free Software Foundation, either version 3 of the License,
      8    or (at your option) any later version.
      9 
     10    GNUnet is distributed in the hope that it will be useful, but
     11    WITHOUT ANY WARRANTY; without even the implied warranty of
     12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13    Affero General Public License for more details.
     14 
     15    You should have received a copy of the GNU Affero General Public License
     16    along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18    SPDX-License-Identifier: AGPL3.0-or-later
     19  */
     20 /*
     21  * @author Tobias Frisch
     22  * @file ui/play_media.c
     23  */
     24 
     25 #include "play_media.h"
     26 
     27 #include "../application.h"
     28 #include "../ui.h"
     29 #include "../util.h"
     30 #include <glib-2.0/glib.h>
     31 
     32 gboolean
     33 ui_play_media_window_supports_file_extension(const gchar *filename)
     34 {
     35   if (!filename)
     36     return FALSE;
     37 
     38   const char* extension = strrchr(filename, '.');
     39 
     40   if (!extension)
     41     return FALSE;
     42 
     43   if (0 == g_strcmp0(extension, ".mkv"))
     44     return TRUE;
     45   if (0 == g_strcmp0(extension, ".mp4"))
     46     return TRUE;
     47   if (0 == g_strcmp0(extension, ".webm"))
     48     return TRUE;
     49 
     50   return FALSE;
     51 }
     52 
     53 static void
     54 handle_back_button_click(UNUSED GtkButton *button,
     55 			                   gpointer user_data)
     56 {
     57   g_assert(user_data);
     58 
     59   GtkWindow *window = GTK_WINDOW(user_data);
     60   gtk_window_close(window);
     61 }
     62 
     63 static void
     64 _set_media_controls_sensivity(UI_PLAY_MEDIA_Handle *handle,
     65 			                        gboolean sensitive)
     66 {
     67   g_assert(handle);
     68 
     69   if (handle->play_pause_button)
     70     gtk_widget_set_sensitive(
     71       GTK_WIDGET(handle->play_pause_button),
     72       sensitive
     73     );
     74 
     75   if (handle->volume_button)
     76     gtk_widget_set_sensitive(
     77       GTK_WIDGET(handle->volume_button),
     78       sensitive
     79     );
     80 
     81   if (handle->timeline_scale)
     82     gtk_widget_set_sensitive(
     83       GTK_WIDGET(handle->timeline_scale),
     84       sensitive
     85     );
     86 }
     87 
     88 static void
     89 handle_timeline_scale_value_changed(GtkRange *range,
     90 				                            gpointer user_data);
     91 
     92 static void
     93 _set_signal_connection_of_timeline(UI_PLAY_MEDIA_Handle *handle,
     94 				                           gboolean connected)
     95 {
     96   g_assert(handle);
     97 
     98   if (!(handle->timeline_scale))
     99     return;
    100 
    101   if (connected == (handle->timeline_signal != 0))
    102     return;
    103 
    104   if (connected)
    105     handle->timeline_signal = g_signal_connect(
    106 	    handle->timeline_scale,
    107     	"value-changed",
    108     	G_CALLBACK(handle_timeline_scale_value_changed),
    109     	handle
    110     );
    111   else
    112   {
    113     g_signal_handler_disconnect(
    114       handle->timeline_scale,
    115       handle->timeline_signal
    116     );
    117 
    118     handle->timeline_signal = 0;
    119   }
    120 }
    121 
    122 static void
    123 _set_media_position(UI_PLAY_MEDIA_Handle *handle,
    124                     gint64 pos,
    125                     gint64 len,
    126                     gboolean include_scale)
    127 {
    128   g_assert(handle);
    129 
    130   const gdouble position = (
    131       len > 0? 1.0 * pos / len : 0.0
    132   );
    133 
    134   if (handle->timeline_label)
    135   {
    136     GString *str = g_string_new(NULL);
    137 
    138     guint pos_seconds = GST_TIME_AS_SECONDS(pos);
    139     guint len_seconds = GST_TIME_AS_SECONDS(len);
    140 
    141     g_string_append_printf(
    142       str,
    143       "%u:%02u / %u:%02u",
    144       pos_seconds / 60,
    145       pos_seconds % 60,
    146       len_seconds / 60,
    147       len_seconds % 60
    148     );
    149 
    150     ui_label_set_text(handle->timeline_label, str->str);
    151     g_string_free(str, TRUE);
    152   }
    153 
    154   if (handle->timeline_progress_bar)
    155     gtk_progress_bar_set_fraction(
    156       handle->timeline_progress_bar,
    157       1.0 * position
    158     );
    159 
    160   if ((!(handle->timeline_scale)) || (!include_scale))
    161     return;
    162 
    163   _set_signal_connection_of_timeline(handle, FALSE);
    164 
    165   gtk_range_set_value(
    166     GTK_RANGE(handle->timeline_scale),
    167     100.0 * position
    168   );
    169 
    170   _set_signal_connection_of_timeline(handle, TRUE);
    171 }
    172 
    173 static gboolean
    174 _adjust_playing_media_position(UI_PLAY_MEDIA_Handle *handle);
    175 
    176 static void
    177 _set_next_timeout_callback_of_timeline(UI_PLAY_MEDIA_Handle *handle)
    178 {
    179   g_assert(handle);
    180 
    181   handle->timeline = util_timeout_add(
    182     200,
    183     G_SOURCE_FUNC(_adjust_playing_media_position),
    184     handle
    185   );
    186 }
    187 
    188 static gboolean
    189 _adjust_playing_media_position(UI_PLAY_MEDIA_Handle *handle)
    190 {
    191   g_assert(handle);
    192 
    193   gint64 pos, len;
    194 
    195   handle->timeline = 0;
    196 
    197   if (!(handle->pipeline))
    198     return G_SOURCE_REMOVE;
    199 
    200   if (!gst_element_query_position(handle->pipeline, GST_FORMAT_TIME, &pos))
    201     return G_SOURCE_REMOVE;
    202 
    203   if (!gst_element_query_duration(handle->pipeline, GST_FORMAT_TIME, &len))
    204     return G_SOURCE_REMOVE;
    205 
    206   _set_media_position(handle, pos, len, TRUE);
    207   _set_next_timeout_callback_of_timeline(handle);
    208   return G_SOURCE_REMOVE;
    209 }
    210 
    211 static void
    212 _set_timeout_callback_of_timeline(UI_PLAY_MEDIA_Handle *handle,
    213 				                          gboolean connected)
    214 {
    215   g_assert(handle);
    216   
    217   if (handle->timeline)
    218     util_source_remove(handle->timeline);
    219 
    220   if (connected)
    221     _set_next_timeout_callback_of_timeline(handle);
    222   else
    223     handle->timeline = 0;
    224 }
    225 
    226 static void
    227 _set_media_state(UI_PLAY_MEDIA_Handle *handle,
    228 		             gboolean playing)
    229 {
    230   g_assert(handle);
    231   
    232   if (handle->play_symbol_stack)
    233     gtk_stack_set_visible_child_name(
    234         handle->play_symbol_stack,
    235 	playing? "pause_page" : "play_page"
    236     );
    237 
    238   _set_timeout_callback_of_timeline(handle, playing);
    239 }
    240 
    241 static void
    242 _disable_video_processing(UI_PLAY_MEDIA_Handle *handle,
    243 			                    gboolean drop_pipeline)
    244 {
    245   g_assert(handle);
    246 
    247   if (handle->preview_stack)
    248     gtk_stack_set_visible_child(handle->preview_stack, handle->fail_box);
    249 
    250   _set_media_controls_sensivity(handle, FALSE);
    251   _set_media_position(handle, 0, 0, TRUE);
    252   _set_media_state(handle, FALSE);
    253 
    254   if ((!(handle->pipeline)) || (!drop_pipeline))
    255     return;
    256 
    257   gst_element_set_state(handle->pipeline, GST_STATE_NULL);
    258 }
    259 
    260 static void
    261 _adjust_playing_media_state(UI_PLAY_MEDIA_Handle *handle, gboolean playing)
    262 {
    263   _set_media_state(handle, playing);
    264 
    265   if (handle->preview_stack)
    266     gtk_stack_set_visible_child(
    267       handle->preview_stack,
    268       handle->video_box
    269     );
    270 }
    271 
    272 static void
    273 _pause_playing_media(UI_PLAY_MEDIA_Handle *handle)
    274 {
    275   if (!(handle->pipeline))
    276     return;
    277 
    278   GstStateChangeReturn ret = gst_element_set_state(
    279       handle->pipeline,
    280       GST_STATE_PAUSED
    281   );
    282 
    283   if (GST_STATE_CHANGE_FAILURE == ret)
    284   {
    285     _disable_video_processing(handle, TRUE);
    286     return;
    287   }
    288 }
    289 
    290 static void
    291 _continue_playing_media(UI_PLAY_MEDIA_Handle *handle)
    292 {
    293   if (!(handle->pipeline))
    294     return;
    295 
    296   GstStateChangeReturn ret = gst_element_set_state(
    297       handle->pipeline,
    298       GST_STATE_PLAYING
    299   );
    300 
    301   if (GST_STATE_CHANGE_FAILURE == ret)
    302   {
    303     _disable_video_processing(handle, TRUE);
    304     return;
    305   }
    306 }
    307 
    308 static void
    309 handle_play_pause_button_click(GtkButton *button,
    310 			       gpointer user_data)
    311 {
    312   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) user_data;
    313 
    314   if (!(handle->play_symbol_stack))
    315     return;
    316 
    317   const gchar *page = gtk_stack_get_visible_child_name(
    318       handle->play_symbol_stack
    319   );
    320 
    321   if (0 == g_strcmp0(page, "pause_page"))
    322     _pause_playing_media(handle);
    323   else
    324     _continue_playing_media(handle);
    325 }
    326 
    327 static void
    328 handle_volume_button_value_changed(GtkScaleButton *button,
    329 				   double value,
    330 				   gpointer user_data)
    331 {
    332   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) user_data;
    333 
    334   if (!(handle->pipeline))
    335     return;
    336 
    337   g_object_set(
    338       G_OBJECT(handle->pipeline),
    339       "volume",
    340       value,
    341       NULL
    342   );
    343 
    344   g_object_set(
    345       G_OBJECT(handle->pipeline),
    346       "mute",
    347       (value <= 0.0),
    348       NULL
    349   );
    350 }
    351 
    352 static void
    353 handle_timeline_scale_value_changed(GtkRange *range,
    354 				    gpointer user_data)
    355 {
    356   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) user_data;
    357   gint64 pos, len;
    358 
    359   if (!(handle->pipeline))
    360     return;
    361 
    362   if (!gst_element_query_duration(handle->pipeline, GST_FORMAT_TIME, &len))
    363     return;
    364 
    365   pos = (gint64) (gtk_range_get_value(range) * len / 100);
    366 
    367   if (gst_element_seek_simple(handle->pipeline,
    368 			      GST_FORMAT_TIME,
    369 			      GST_SEEK_FLAG_FLUSH,
    370 			      pos))
    371     _set_media_position(handle, pos, len, FALSE);
    372 }
    373 
    374 static void
    375 handle_fullscreen_button_click(GtkButton *button,
    376 			       gpointer user_data)
    377 {
    378   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) user_data;
    379 
    380   gtk_revealer_set_reveal_child(handle->header_revealer, handle->fullscreen);
    381   hdy_flap_set_reveal_flap(handle->controls_flap, handle->fullscreen);
    382 
    383   handle->fullscreen = !(handle->fullscreen);
    384 
    385   if (!(handle->fullscreen))
    386     gtk_window_unfullscreen(GTK_WINDOW(handle->window));
    387 
    388   gtk_widget_hide(GTK_WIDGET(handle->window));
    389 
    390   gtk_window_set_type_hint(
    391       GTK_WINDOW(handle->window),
    392       handle->fullscreen?
    393 	  GDK_WINDOW_TYPE_HINT_NORMAL :
    394 	  GDK_WINDOW_TYPE_HINT_DIALOG
    395   );
    396 
    397   gtk_window_set_modal(GTK_WINDOW(handle->window), !(handle->fullscreen));
    398 
    399   gtk_window_set_position(
    400       GTK_WINDOW(handle->window),
    401       handle->fullscreen? GTK_WIN_POS_NONE : GTK_WIN_POS_CENTER_ON_PARENT
    402   );
    403 
    404   gtk_window_set_transient_for(
    405       GTK_WINDOW(handle->window),
    406       handle->fullscreen? NULL : handle->parent
    407   );
    408 
    409   gtk_widget_show_all(GTK_WIDGET(handle->window));
    410   hdy_flap_set_reveal_flap(handle->controls_flap, !(handle->fullscreen));
    411 
    412   if (handle->fullscreen)
    413     gtk_window_fullscreen(GTK_WINDOW(handle->window));
    414   else
    415   {
    416     if (handle->motion_lost)
    417       util_source_remove(handle->motion_lost);
    418 
    419     handle->motion_lost = 0;
    420   }
    421 
    422   gtk_stack_set_visible_child_name(
    423       handle->fullscreen_symbol_stack,
    424       handle->fullscreen? "scale_down_page" : "scale_up_page"
    425   );
    426 }
    427 
    428 static gboolean
    429 handle_media_motion_lost(gpointer user_data)
    430 {
    431   g_assert(user_data);
    432 
    433   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) user_data;
    434 
    435   handle->motion_lost = 0;
    436 
    437   if (!(hdy_flap_get_reveal_flap(handle->controls_flap)))
    438     return G_SOURCE_REMOVE;
    439 
    440   hdy_flap_set_reveal_flap(handle->controls_flap, FALSE);
    441   return G_SOURCE_REMOVE;
    442 }
    443 
    444 static gboolean
    445 handle_media_motion_notify(GtkWidget *widget,
    446 			   GdkEvent *event,
    447 			   gpointer user_data)
    448 {
    449   g_assert(user_data);
    450 
    451   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) user_data;
    452 
    453   if (hdy_flap_get_reveal_flap(handle->controls_flap))
    454     return G_SOURCE_REMOVE;
    455 
    456   if (handle->motion_lost)
    457     util_source_remove(handle->motion_lost);
    458 
    459   hdy_flap_set_reveal_flap(handle->controls_flap, TRUE);
    460 
    461   if (!(handle->fullscreen))
    462     return G_SOURCE_REMOVE;
    463 
    464   handle->motion_lost = util_timeout_add_seconds(
    465       3,
    466       G_SOURCE_FUNC(handle_media_motion_lost),
    467       handle
    468   );
    469 
    470   return G_SOURCE_REMOVE;
    471 }
    472 
    473 static void
    474 handle_window_destroy(UNUSED GtkWidget *window,
    475 		                  gpointer user_data)
    476 {
    477   g_assert(user_data);
    478 
    479   ui_play_media_window_cleanup((UI_PLAY_MEDIA_Handle*) user_data);
    480 }
    481 
    482 static void
    483 msg_error_cb(UNUSED GstBus *bus,
    484              GstMessage *msg,
    485              gpointer data)
    486 {
    487   g_assert((msg) && (data));
    488 
    489   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) data;
    490 
    491   GError* error;
    492   gst_message_parse_error(msg, &error, NULL);
    493 
    494   if (!error)
    495     fprintf(stderr, "ERROR: Unknown error\n");
    496   else if (error->message)
    497     fprintf(stderr, "ERROR: %s (%d)\n", error->message, error->code);
    498   else
    499     fprintf(stderr, "ERROR: Unknown error (%d)\n", error->code);
    500 
    501   _disable_video_processing(handle, TRUE);
    502 }
    503 
    504 static void
    505 msg_eos_cb(UNUSED GstBus *bus,
    506            UNUSED GstMessage *msg,
    507            gpointer data)
    508 {
    509   g_assert(data);
    510 
    511   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) data;
    512 
    513   if (GST_MESSAGE_SRC(msg) != GST_OBJECT(handle->pipeline))
    514     return;
    515 
    516   if (handle->timeline_scale)
    517     gtk_range_set_value(GTK_RANGE(handle->timeline_scale), 0.0);
    518 
    519   _adjust_playing_media_state(handle, FALSE);
    520 }
    521 
    522 static void
    523 msg_state_changed_cb(UNUSED GstBus *bus,
    524                      GstMessage *msg,
    525                      gpointer data)
    526 {
    527   g_assert((msg) && (data));
    528 
    529   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) data;
    530 
    531   GstState old_state, new_state, pending_state;
    532   gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
    533 
    534   if (GST_MESSAGE_SRC(msg) != GST_OBJECT(handle->pipeline))
    535     return;
    536 
    537   if (!(handle->sink))
    538   {
    539     _disable_video_processing(handle, FALSE);
    540     return;
    541   }
    542 
    543   if (GST_STATE_READY == new_state)
    544     _set_media_controls_sensivity(handle, TRUE);
    545 
    546   if ((GST_STATE_PLAYING != new_state) && (GST_STATE_PAUSED != new_state))
    547     return;
    548 
    549   _adjust_playing_media_state(handle, GST_STATE_PLAYING == new_state);
    550 }
    551 
    552 static void
    553 msg_buffering_cb(UNUSED GstBus *bus,
    554                  GstMessage *msg,
    555                  gpointer data)
    556 {
    557   g_assert((msg) && (data));
    558 
    559   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) data;
    560 
    561   gint percent = 0;
    562   gst_message_parse_buffering(msg, &percent);
    563 
    564   GstStateChangeReturn ret = gst_element_set_state(
    565       handle->pipeline,
    566       (percent < 100? GST_STATE_PAUSED : GST_STATE_PLAYING)
    567   );
    568 
    569   if (ret == GST_STATE_CHANGE_FAILURE)
    570     _disable_video_processing(handle, TRUE);
    571 }
    572 
    573 static void
    574 _setup_gst_pipeline(UI_PLAY_MEDIA_Handle *handle)
    575 {
    576   g_assert(handle);
    577 
    578   handle->pipeline = gst_element_factory_make("playbin", NULL);
    579 
    580   if (!(handle->pipeline))
    581     return;
    582 
    583   handle->sink = gst_element_factory_make("gtksink", "vsink");
    584 
    585   if (!(handle->sink))
    586     return;
    587 
    588   g_object_set(
    589       G_OBJECT(handle->pipeline),
    590       "video-sink",
    591       handle->sink,
    592       NULL
    593   );
    594 
    595   GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(handle->pipeline));
    596 
    597   if (!bus)
    598     return;
    599 
    600   gst_bus_add_signal_watch(bus);
    601 
    602   g_signal_connect(
    603       G_OBJECT(bus),
    604       "message::error",
    605       (GCallback) msg_error_cb,
    606       handle
    607   );
    608 
    609   g_signal_connect(
    610       G_OBJECT(bus),
    611       "message::eos",
    612       (GCallback) msg_eos_cb,
    613       handle
    614   );
    615 
    616   g_signal_connect(
    617       G_OBJECT(bus),
    618       "message::state-changed",
    619       (GCallback) msg_state_changed_cb,
    620       handle
    621   );
    622 
    623   g_signal_connect(
    624       G_OBJECT(bus),
    625       "message::buffering",
    626       (GCallback) msg_buffering_cb,
    627       handle
    628   );
    629 
    630   gst_object_unref(bus);
    631 }
    632 
    633 static void*
    634 _ui_play_media_video_thread(void *args)
    635 {
    636   g_assert(args);
    637 
    638   UI_PLAY_MEDIA_Handle *handle = (UI_PLAY_MEDIA_Handle*) args;
    639   _continue_playing_media(handle);
    640   return NULL;
    641 }
    642 
    643 void
    644 ui_play_media_window_init(MESSENGER_Application *app,
    645                           UI_PLAY_MEDIA_Handle *handle)
    646 {
    647   g_assert((app) && (handle));
    648 
    649   _setup_gst_pipeline(handle);
    650 
    651   handle->parent = GTK_WINDOW(app->ui.messenger.main_window);
    652 
    653   handle->builder = ui_builder_from_resource(
    654     application_get_resource_path(app, "ui/play_media.ui")
    655   );
    656 
    657   handle->window = HDY_WINDOW(
    658     gtk_builder_get_object(handle->builder, "play_media_window")
    659   );
    660 
    661   gtk_window_set_position(
    662     GTK_WINDOW(handle->window),
    663     GTK_WIN_POS_CENTER_ON_PARENT
    664   );
    665 
    666   gtk_window_set_transient_for(
    667     GTK_WINDOW(handle->window),
    668     handle->parent
    669   );
    670 
    671   handle->header_revealer = GTK_REVEALER(
    672     gtk_builder_get_object(handle->builder, "header_revealer")
    673   );
    674 
    675   handle->title_bar = HDY_HEADER_BAR(
    676     gtk_builder_get_object(handle->builder, "title_bar")
    677   );
    678 
    679   handle->back_button = GTK_BUTTON(
    680     gtk_builder_get_object(handle->builder, "back_button")
    681   );
    682 
    683   g_signal_connect(
    684     handle->back_button,
    685     "clicked",
    686     G_CALLBACK(handle_back_button_click),
    687     handle->window
    688   );
    689 
    690   handle->controls_flap = HDY_FLAP(
    691     gtk_builder_get_object(handle->builder, "controls_flap")
    692   );
    693 
    694   handle->preview_stack = GTK_STACK(
    695     gtk_builder_get_object(handle->builder, "preview_stack")
    696   );
    697 
    698   handle->fail_box = GTK_WIDGET(
    699     gtk_builder_get_object(handle->builder, "fail_box")
    700   );
    701 
    702   handle->video_box = GTK_WIDGET(
    703     gtk_builder_get_object(handle->builder, "video_box")
    704   );
    705 
    706   GtkWidget *widget;
    707   if (handle->sink)
    708     g_object_get(handle->sink, "widget", &widget, NULL);
    709   else
    710     widget = NULL;
    711 
    712   if (widget)
    713   {
    714     gtk_box_pack_start(
    715       GTK_BOX(handle->video_box),
    716       widget,
    717       true,
    718       true,
    719       0
    720     );
    721 
    722     g_object_unref(widget);
    723     gtk_widget_realize(widget);
    724 
    725     gtk_widget_show_all(handle->video_box);
    726   }
    727 
    728   handle->play_pause_button = GTK_BUTTON(
    729     gtk_builder_get_object(handle->builder, "play_pause_button")
    730   );
    731 
    732   handle->play_symbol_stack = GTK_STACK(
    733     gtk_builder_get_object(handle->builder, "play_symbol_stack")
    734   );
    735 
    736   g_signal_connect(
    737     handle->play_pause_button,
    738     "clicked",
    739     G_CALLBACK(handle_play_pause_button_click),
    740     handle
    741   );
    742 
    743   handle->volume_button = GTK_VOLUME_BUTTON(
    744     gtk_builder_get_object(handle->builder, "volume_button")
    745   );
    746 
    747   g_signal_connect(
    748     handle->volume_button,
    749     "value-changed",
    750     G_CALLBACK(handle_volume_button_value_changed),
    751     handle
    752   );
    753 
    754   handle->timeline_label = GTK_LABEL(
    755     gtk_builder_get_object(handle->builder, "timeline_label")
    756   );
    757 
    758   handle->timeline_progress_bar = GTK_PROGRESS_BAR(
    759     gtk_builder_get_object(handle->builder, "timeline_progress_bar")
    760   );
    761 
    762   handle->timeline_scale = GTK_SCALE(
    763     gtk_builder_get_object(handle->builder, "timeline_scale")
    764   );
    765 
    766   _set_signal_connection_of_timeline(handle, handle->sink? TRUE : FALSE);
    767 
    768   handle->settings_button = GTK_BUTTON(
    769     gtk_builder_get_object(handle->builder, "settings_button")
    770   );
    771 
    772   handle->fullscreen_button = GTK_BUTTON(
    773     gtk_builder_get_object(handle->builder, "fullscreen_button")
    774   );
    775 
    776   handle->fullscreen_symbol_stack = GTK_STACK(
    777     gtk_builder_get_object(handle->builder, "fullscreen_symbol_stack")
    778   );
    779 
    780   g_signal_connect(
    781     handle->fullscreen_button,
    782     "clicked",
    783     G_CALLBACK(handle_fullscreen_button_click),
    784     handle
    785   );
    786 
    787   g_signal_connect(
    788     handle->window,
    789     "motion-notify-event",
    790     G_CALLBACK(handle_media_motion_notify),
    791     handle
    792   );
    793 
    794   gtk_widget_add_events(
    795     GTK_WIDGET(handle->window),
    796     GDK_POINTER_MOTION_HINT_MASK |
    797     GDK_POINTER_MOTION_MASK
    798   );
    799 
    800   g_signal_connect(
    801     handle->window,
    802     "destroy",
    803     G_CALLBACK(handle_window_destroy),
    804     handle
    805   );
    806 
    807   gtk_scale_button_set_value(
    808     GTK_SCALE_BUTTON(handle->volume_button),
    809     1.0
    810   );
    811 
    812   gtk_widget_show_all(GTK_WIDGET(handle->window));
    813 }
    814 
    815 void
    816 ui_play_media_window_update(UI_PLAY_MEDIA_Handle *handle,
    817                             const gchar *uri,
    818                             const struct GNUNET_CHAT_File *file)
    819 {
    820   g_assert((handle) && (uri));
    821 
    822   if (handle->video_tid)
    823     pthread_join(handle->video_tid, NULL);
    824 
    825   if (!(handle->pipeline))
    826     return;
    827 
    828   _disable_video_processing(handle, TRUE);
    829   g_object_set(G_OBJECT(handle->pipeline), "uri", uri, NULL);
    830 
    831   const gchar *filename;
    832 
    833   if (file)
    834     filename = GNUNET_CHAT_file_get_name(file);
    835   else
    836     filename = uri;
    837 
    838   hdy_header_bar_set_subtitle(
    839     handle->title_bar,
    840     filename? filename : ""
    841   );
    842 
    843   pthread_create(
    844     &(handle->video_tid),
    845     NULL,
    846     _ui_play_media_video_thread,
    847     handle
    848   );
    849 }
    850 
    851 void
    852 ui_play_media_window_cleanup(UI_PLAY_MEDIA_Handle *handle)
    853 {
    854   g_assert(handle);
    855 
    856   if (handle->video_tid)
    857     pthread_join(handle->video_tid, NULL);
    858 
    859   g_object_unref(handle->builder);
    860 
    861   if (handle->timeline)
    862     util_source_remove(handle->timeline);
    863 
    864   if (handle->motion_lost)
    865     util_source_remove(handle->motion_lost);
    866 
    867   if (handle->pipeline)
    868   {
    869     gst_element_set_state(handle->pipeline, GST_STATE_NULL);
    870     gst_object_unref(GST_OBJECT(handle->pipeline));
    871   }
    872 
    873   memset(handle, 0, sizeof(*handle));
    874 }