1 /*
2  *  This file is part of GNOME Twitch - 'Enjoy Twitch on your GNU/Linux desktop'
3  *  Copyright © 2017 Vincent Szolnoky <vinszent@vinszent.com>
4  *
5  *  GNOME Twitch is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  GNOME Twitch is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with GNOME Twitch. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "gt-player.h"
20 #include "gt-win.h"
21 #include "gt-app.h"
22 #include "gt-enums.h"
23 #include "gt-chat.h"
24 #include "gt-http.h"
25 #include "gnome-twitch/gt-player-backend.h"
26 #include "utils.h"
27 #include <libpeas-gtk/peas-gtk.h>
28 #include <glib/gi18n.h>
29 #include <json-glib/json-glib.h>
30 
31 #define TAG "GtPlayer"
32 #include "gnome-twitch/gt-log.h"
33 
34 #define PLAYER_CONTROLS_REVEAL_HEIGHT 50
35 #define CHAT_RESIZE_HANDLE_SIZE 10
36 #define CHAT_MIN_WIDTH 290
37 #define CHAT_MIN_HEIGHT 200
38 
39 #define CHANNEL_SETTINGS_FILE g_build_filename(g_get_user_data_dir(), "gnome-twitch", "channel_settings.json", NULL);
40 #define CHANNEL_SETTINGS_FILE_VERSION 1
41 
42 #define LIVESTREAM_URI "https://api.twitch.tv/api/channels/%s/access_token"
43 #define VOD_URI "https://api.twitch.tv/api/vods/%s/access_token"
44 
45 typedef enum
46 {
47     MOUSE_POS_LEFT_HANDLE,
48     MOUSE_POS_RIGHT_HANDLE,
49     MOUSE_POS_TOP_HANDLE,
50     MOUSE_POS_BOTTOM_HANDLE,
51     MOUSE_POS_INSIDE,
52     MOUSE_POS_OUTSIDE,
53 } MousePos;
54 
55 typedef struct
56 {
57     GSimpleActionGroup* action_group;
58 
59     GtPlayerChannelSettings* cur_channel_settings;
60 
61     GtkWidget* player_box;
62     GtkWidget* offline_box;
63     GtkWidget* error_box;
64     GtkWidget* empty_box;
65     GtkWidget* player_overlay;
66     GtkWidget* docking_pane;
67     GtkWidget* chat_view;
68     GtkWidget* controls_revealer;
69     GtkWidget* buffer_revealer;
70     GtkWidget* buffer_label;
71     GtkWidget* player_widget;
72     GtkWidget* reload_button;
73 
74     GtkWidget* toggle_paused_button;
75     GtkWidget* volume_button;
76     GtkWidget* toggle_chat_button;
77     GtkWidget* edit_chat_button;
78     GtkWidget* toggle_fullscreen_button;
79     GtkWidget* seek_bar;
80     GtkWidget* seek_label;
81     GtkWidget* switch_live_button;
82     GtkWidget* stream_quality_box;
83 
84     GtPlayerBackend* backend;
85     PeasPluginInfo* backend_info;
86 
87     GtChannel* channel;
88     GtVOD* vod;
89 
90     GtPlayerMedium medium;
91 
92     JsonParser* json_parser;
93     GCancellable* cancel;
94 
95     gchar* vod_id;
96 
97     GtPlaylistEntryList* stream_qualities;
98     const GtPlaylistEntry* quality;
99 
100     gboolean paused;
101     gdouble volume;
102     gdouble prev_volume;
103     gdouble muted;
104     gboolean edit_chat;
105 
106     MousePos start_mouse_pos;
107     gboolean mouse_pressed;
108 
109     guint mouse_pressed_handler_id;
110     guint mouse_released_handler_id;
111     guint mouse_moved_handler_id;
112 
113     guint mouse_source;
114 } GtPlayerPrivate;
115 
116 G_DEFINE_TYPE_WITH_PRIVATE(GtPlayer, gt_player, GTK_TYPE_STACK)
117 
118 enum
119 {
120     PROP_0,
121     PROP_VOLUME,
122     PROP_MUTED,
123     PROP_CHANNEL,
124     PROP_VOD,
125     PROP_MEDIUM,
126     PROP_CHAT_VISIBLE,
127     PROP_CHAT_DOCKED,
128     PROP_CHAT_DARK_THEME,
129     PROP_CHAT_OPACITY,
130     PROP_DOCKED_HANDLE_POSITION,
131     PROP_EDIT_CHAT,
132     PROP_STREAM_QUALITY,
133     NUM_PROPS
134 };
135 
136 static GParamSpec* props[NUM_PROPS];
137 
138 static GHashTable* channel_settings_table;
139 
140 static const GEnumValue gt_player_medium_enum_values[] =
141 {
142     {GT_PLAYER_MEDIUM_NONE, "GT_PLAYER_MEDIUM_NONE", "none"},
143     {GT_PLAYER_MEDIUM_LIVESTREAM, "GT_PLAYER_MEDIUM_LIVESTREAM", "livestream"},
144     {GT_PLAYER_MEDIUM_VOD, "GT_PLAYER_MEDIUM_VOD", "vod"},
145 };
146 
147 GType
gt_player_medium_get_type()148 gt_player_medium_get_type()
149 {
150     static GType type = 0;
151 
152     if (type == 0)
153         type = g_enum_register_static("GtPlayerMedium", gt_player_medium_enum_values);
154 
155     return type;
156 }
157 
158 #define GT_TYPE_PLAYER_MEDIUM gt_player_medium_get_type()
159 
160 static void
load_channel_settings()161 load_channel_settings()
162 {
163     g_autofree gchar* fp = CHANNEL_SETTINGS_FILE;
164     g_autoptr(JsonParser) parser = json_parser_new();
165     g_autoptr(JsonReader) reader = NULL;
166     g_autoptr(GError) err = NULL;
167     gint version = 0;
168     gint count = 0;
169 
170     g_hash_table_remove_all(channel_settings_table);
171 
172     MESSAGE("Loading chat settings");
173 
174     if (!g_file_test(fp, G_FILE_TEST_EXISTS))
175     {
176         INFO("Chat settings file at '%s' doesn't exist", fp);
177         return;
178     }
179 
180     json_parser_load_from_file(parser, fp, &err);
181 
182     if (err)
183     {
184         WARNING("Unable to load chat settings because: %s", err->message);
185         return;
186     }
187 
188     reader = json_reader_new(json_parser_get_root(parser));
189 
190 /* FIXME: Propagate errors to UI once GtApp implements a startup error queue */
191 #define READ_JSON_VALUE(name, p)                                        \
192     if (json_reader_read_member(reader, name))                          \
193     {                                                                   \
194         p = _Generic(p,                                                 \
195             gboolean: json_reader_get_boolean_value(reader),            \
196             gdouble: json_reader_get_double_value(reader),              \
197             gint64: json_reader_get_int_value(reader));                 \
198     }                                                                   \
199     else                                                                \
200     {                                                                   \
201         WARNING("Couldn't read value '%s' from element '%d' in chat settings file." \
202             "Will assign default value.", name, i);                     \
203     }                                                                   \
204     json_reader_end_member(reader);                                     \
205 
206     if (json_reader_read_member(reader, "version"))
207     {
208         version = json_reader_get_int_value(reader);
209         INFO("Found version number '%d' for chat settings file", version);
210     }
211     else
212         INFO("No version number found, assuming version 0 for chat settings file");
213     json_reader_end_member(reader);
214 
215     if (version > 0) json_reader_read_member(reader, "chat-settings");
216 
217     count = json_reader_count_elements(reader);
218 
219     INFO("Reading '%d' elements from chat settings file", count);
220 
221     for (gint i = 0; i < count; i++)
222     {
223         GtPlayerChannelSettings* settings = NULL;
224         gchar* id = NULL;
225 
226         if (!json_reader_read_element(reader, i))
227         {
228             WARNING("Couldn't read element '%d' from chat settings file."
229                 "This item will be removed on next save.", i);
230             continue;
231         }
232 
233         if (json_reader_read_member(reader, version > 0 ? "id" : "name"))
234             id = g_strdup(json_reader_get_string_value(reader));
235         else
236         {
237             WARNING("Couldn't read value '%s' from element '%d' from chat settings file."
238                 "This item will be removed on next save.", version > 0 ? "id" : "name", i);
239             continue;
240         }
241         json_reader_end_member(reader);
242 
243         settings = gt_player_channel_settings_new();
244 
245         READ_JSON_VALUE("dark-theme", settings->dark_theme);
246         READ_JSON_VALUE("visible", settings->visible);
247         READ_JSON_VALUE("docked", settings->docked);
248         READ_JSON_VALUE("opacity", settings->opacity);
249         READ_JSON_VALUE("width", settings->width);
250         READ_JSON_VALUE("height", settings->height);
251         READ_JSON_VALUE("x-pos", settings->x_pos);
252         READ_JSON_VALUE("y-pos", settings->y_pos);
253         READ_JSON_VALUE("docked-handle-pos", settings->docked_handle_pos);
254 
255         json_reader_end_element(reader);
256 
257         g_hash_table_insert(channel_settings_table, id, settings);
258     }
259 
260     if (version > 0) json_reader_end_member(reader);
261 
262 #undef READ_JSON_VALUE
263 }
264 
265 static void
save_channel_settings()266 save_channel_settings()
267 {
268     g_autofree gchar* fp = CHANNEL_SETTINGS_FILE;
269     g_autoptr(JsonBuilder) builder = json_builder_new();
270     g_autoptr(JsonGenerator) gen = json_generator_new();
271     g_autoptr(JsonNode) final = NULL;
272     g_autoptr(GError) err = NULL;
273     GHashTableIter iter;
274     gchar* key;
275     GtPlayerChannelSettings* settings;
276 
277     MESSAGE("Saving chat settings");
278 
279     json_builder_begin_object(builder);
280 
281     json_builder_set_member_name(builder, "version");
282     json_builder_add_int_value(builder, CHANNEL_SETTINGS_FILE_VERSION);
283 
284     json_builder_set_member_name(builder, "chat-settings");
285     json_builder_begin_array(builder);
286 
287     g_hash_table_iter_init(&iter, channel_settings_table);
288 
289     while (g_hash_table_iter_next(&iter, (gpointer*) &key, (gpointer*) &settings))
290     {
291         json_builder_begin_object(builder);
292 
293         json_builder_set_member_name(builder, "id");
294         json_builder_add_string_value(builder, key);
295 
296         json_builder_set_member_name(builder, "dark-theme");
297         json_builder_add_boolean_value(builder, settings->dark_theme);
298 
299         json_builder_set_member_name(builder, "visible");
300         json_builder_add_boolean_value(builder, settings->visible);
301 
302         json_builder_set_member_name(builder, "docked");
303         json_builder_add_boolean_value(builder, settings->docked);
304 
305         json_builder_set_member_name(builder, "opacity");
306         json_builder_add_double_value(builder, settings->opacity);
307 
308         json_builder_set_member_name(builder, "width");
309         json_builder_add_double_value(builder, settings->width);
310 
311         json_builder_set_member_name(builder, "height");
312         json_builder_add_double_value(builder, settings->height);
313 
314         json_builder_set_member_name(builder, "x-pos");
315         json_builder_add_double_value(builder, settings->x_pos);
316 
317         json_builder_set_member_name(builder, "y-pos");
318         json_builder_add_double_value(builder, settings->y_pos);
319 
320         json_builder_set_member_name(builder, "docked-handle-pos");
321         json_builder_add_double_value(builder, settings->docked_handle_pos);
322 
323         json_builder_end_object(builder);
324     }
325 
326     json_builder_end_array(builder);
327 
328     json_builder_end_object(builder);
329 
330     final = json_builder_get_root(builder);
331 
332     json_generator_set_root(gen, final);
333     json_generator_to_file(gen, fp, &err);
334 
335     if (err)
336     {
337         WARNING("Unable to write chat settings file to '%s' because: %s",
338             fp, err->message);
339     }
340 }
341 
342 static void
finalise(GObject * obj)343 finalise(GObject* obj)
344 {
345     GtPlayer* self = GT_PLAYER(obj);
346     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
347 
348     G_OBJECT_CLASS(gt_player_parent_class)->finalize(obj);
349 
350     //TODO: Unref more stuff
351 
352     g_object_unref(priv->chat_view);
353     g_object_unref(priv->backend);
354     g_boxed_free(PEAS_TYPE_PLUGIN_INFO, priv->backend_info);
355 
356     g_settings_set_double(main_app->settings, "volume",
357                           priv->muted ? priv->prev_volume : priv->volume);
358 
359     MESSAGE("Finalise");
360 }
361 
362 static void
destroy_cb(GtkWidget * widget,gpointer udata)363 destroy_cb(GtkWidget* widget,
364            gpointer udata)
365 {
366     GtPlayer* self = GT_PLAYER(udata);
367     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
368 
369     g_object_unref(priv->action_group);
370 }
371 
372 static gboolean
update_chat_undocked_position(GtkOverlay * overlay,GtkWidget * widget,GdkRectangle * chat_alloc,gpointer udata)373 update_chat_undocked_position(GtkOverlay* overlay,
374     GtkWidget* widget, GdkRectangle* chat_alloc, gpointer udata)
375 {
376     GtPlayer* self = GT_PLAYER(udata);
377     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
378     GtkAllocation alloc;
379 
380     if (widget != priv->chat_view) return FALSE;
381 
382     /* NOTE: This is to shut GTK up */
383     gtk_widget_get_preferred_size(priv->chat_view, NULL, NULL);
384 
385     gtk_widget_get_allocation(GTK_WIDGET(self), &alloc);
386 
387     chat_alloc->width = ROUND(priv->cur_channel_settings->width*alloc.width);
388     chat_alloc->height = ROUND(priv->cur_channel_settings->height*alloc.height);
389     chat_alloc->x = ROUND(priv->cur_channel_settings->x_pos*(alloc.width - chat_alloc->width));
390     chat_alloc->y = ROUND(priv->cur_channel_settings->y_pos*(alloc.height - chat_alloc->height));
391 
392     return TRUE;
393 }
394 
395 static void
update_docked(GtPlayer * self)396 update_docked(GtPlayer* self)
397 {
398     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
399 
400     if (priv->cur_channel_settings->docked)
401     {
402         gdouble width;
403 
404         width = ROUND(gtk_widget_get_allocated_width(GTK_WIDGET(self)));
405 
406         if (width < 2.0)
407             return;
408 
409         if (gtk_widget_get_parent(priv->chat_view) == priv->player_overlay)
410             gtk_container_remove(GTK_CONTAINER(priv->player_overlay), priv->chat_view);
411 
412         if (gtk_widget_get_parent(priv->chat_view) == NULL)
413             gtk_paned_pack2(GTK_PANED(priv->docking_pane), priv->chat_view, FALSE, TRUE);
414 
415         g_object_set(priv->chat_view,
416             "opacity", 1.0,
417             NULL);
418 
419         g_object_set(self,
420             "edit-chat", FALSE,
421             NULL);
422 
423         gtk_paned_set_position(GTK_PANED(priv->docking_pane),
424             ROUND(priv->cur_channel_settings->docked_handle_pos*width));
425     }
426     else
427     {
428         if (gtk_widget_get_parent(priv->chat_view) == priv->docking_pane)
429             gtk_container_remove(GTK_CONTAINER(priv->docking_pane), priv->chat_view);
430 
431         if (gtk_widget_get_parent(priv->chat_view) == NULL)
432         {
433             gtk_overlay_add_overlay(GTK_OVERLAY(priv->player_overlay), priv->chat_view);
434             gtk_overlay_reorder_overlay(GTK_OVERLAY(priv->player_overlay), priv->chat_view, 0);
435         }
436 
437         g_object_set(priv->chat_view,
438             "opacity", priv->cur_channel_settings->opacity,
439             NULL);
440     }
441 }
442 
443 static void
update_muted(GtPlayer * self)444 update_muted(GtPlayer* self)
445 {
446     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
447 
448     if (priv->muted)
449     {
450         priv->prev_volume = priv->volume;
451         g_object_set(self, "volume", 0.0, NULL);
452     }
453     else
454     {
455         g_object_set(self, "volume", priv->prev_volume, NULL);
456     }
457 }
458 
459 static void
update_volume(GtPlayer * self)460 update_volume(GtPlayer* self)
461 {
462     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
463 
464     priv->muted = !(priv->volume > 0);
465     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_MUTED]);
466 }
467 
468 static void
player_backend_state_cb(GObject * source,GParamSpec * pspec,gpointer udata)469 player_backend_state_cb(GObject* source,
470     GParamSpec* pspec, gpointer udata)
471 {
472     RETURN_IF_FAIL(GT_IS_PLAYER_BACKEND(source));
473     RETURN_IF_FAIL(G_IS_PARAM_SPEC(pspec));
474     RETURN_IF_FAIL(GT_IS_PLAYER(udata));
475 
476     GtPlayer* self = GT_PLAYER(udata);
477     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
478     GtPlayerBackendState state = gt_player_backend_get_state(priv->backend);
479 
480     switch (state)
481     {
482         /* TODO: Update play/pause button state */
483         case GT_PLAYER_BACKEND_STATE_PLAYING:
484             gtk_revealer_set_reveal_child(GTK_REVEALER(priv->buffer_revealer), FALSE);
485 
486             priv->paused = FALSE;
487             gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->toggle_paused_button), !priv->paused);
488             break;
489         case GT_PLAYER_BACKEND_STATE_PAUSED:
490             priv->paused = TRUE;
491             gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->toggle_paused_button), !priv->paused);
492             break;
493         case GT_PLAYER_BACKEND_STATE_LOADING:
494             gtk_label_set_label(GTK_LABEL(priv->buffer_label), _("Loading"));
495             gtk_widget_set_visible(priv->buffer_revealer, TRUE);
496             gtk_revealer_set_reveal_child(GTK_REVEALER(priv->buffer_revealer), TRUE);
497             break;
498         case GT_PLAYER_BACKEND_STATE_BUFFERING:
499             gtk_label_set_label(GTK_LABEL(priv->buffer_label), _("Buffering"));
500             gtk_widget_set_visible(priv->buffer_revealer, TRUE);
501             gtk_revealer_set_reveal_child(GTK_REVEALER(priv->buffer_revealer), TRUE);
502             break;
503         case GT_PLAYER_BACKEND_STATE_STOPPED:
504             /* NOTE: Do nothing */
505             break;
506     }
507 
508     /* g_simple_action_set_state(priv->toggle_paused_action, arg); */
509 }
510 
511 static gboolean
motion_cb(GtkWidget * widget,GdkEventMotion * evt,gpointer udata)512 motion_cb(GtkWidget* widget,
513     GdkEventMotion* evt, gpointer udata)
514 {
515     GtPlayer* self = GT_PLAYER(udata);
516     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
517 
518     if (evt->y_root > gtk_widget_get_allocated_height(widget) - PLAYER_CONTROLS_REVEAL_HEIGHT)
519         gtk_revealer_set_reveal_child(GTK_REVEALER(priv->controls_revealer), TRUE);
520     else
521         gtk_revealer_set_reveal_child(GTK_REVEALER(priv->controls_revealer), FALSE);
522 
523     return GDK_EVENT_PROPAGATE;
524 }
525 
526 static gboolean
player_button_press_cb(GtkWidget * widget,GdkEventButton * evt,gpointer udata)527 player_button_press_cb(GtkWidget* widget,
528                        GdkEventButton* evt,
529                        gpointer udata)
530 {
531     if (evt->button == 1 && evt->type == GDK_2BUTTON_PRESS)
532         gt_win_toggle_fullscreen(GT_WIN_TOPLEVEL(widget));
533 
534     gtk_widget_grab_focus(widget);
535 
536     return GDK_EVENT_PROPAGATE;
537 }
538 
539 
540 static void
buffer_fill_cb(GObject * source,GParamSpec * pspec,gpointer udata)541 buffer_fill_cb(GObject* source,
542                   GParamSpec* pspec,
543                   gpointer udata)
544 {
545     GtPlayer* self = GT_PLAYER(udata);
546     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
547     gdouble perc;
548 
549     g_object_get(priv->backend, "buffer-fill", &perc, NULL);
550 
551     if (perc < 1.0)
552     {
553         g_autofree gchar* text;
554 
555         text = g_strdup_printf(_("Buffered %d%%"), (gint) (perc*100.0));
556         gtk_label_set_label(GTK_LABEL(priv->buffer_label), text);
557     }
558 }
559 
560 static void
revealer_revealed_cb(GObject * source,GParamSpec * pspec,gpointer udata)561 revealer_revealed_cb(GObject* source,
562                      GParamSpec* pspec,
563                      gpointer udata)
564 {
565     GtPlayer* self = GT_PLAYER(udata);
566     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
567 
568     if (!gtk_revealer_get_child_revealed(GTK_REVEALER(source)))
569         gtk_widget_set_visible(GTK_WIDGET(source), FALSE);
570 }
571 
572 static void
fullscreen_cb(GObject * source,GParamSpec * pspec,gpointer udata)573 fullscreen_cb(GObject* source,
574               GParamSpec* pspec,
575               gpointer udata)
576 {
577     GtPlayer* self = GT_PLAYER(udata);
578     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
579 
580     gtk_revealer_set_reveal_child(GTK_REVEALER(priv->controls_revealer), FALSE);
581 }
582 
583 static MousePos
get_mouse_pos(GtPlayer * self,gint x,gint y)584 get_mouse_pos(GtPlayer* self, gint x, gint y)
585 {
586     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
587     GtkAllocation alloc;
588 
589     gtk_widget_get_allocation(priv->chat_view, &alloc);
590 
591     gtk_widget_translate_coordinates(priv->chat_view, GTK_WIDGET(self),
592         alloc.x, alloc.y, &alloc.x, &alloc.y);
593 
594     gboolean inside_horizontally = alloc.x < x && x < alloc.x + alloc.width;
595     gboolean inside_vertically = alloc.y < y && y < alloc.y + alloc.height;
596 
597     if (alloc.x - CHAT_RESIZE_HANDLE_SIZE < x &&
598         x < alloc.x + CHAT_RESIZE_HANDLE_SIZE &&
599         inside_vertically)
600     {
601         return MOUSE_POS_LEFT_HANDLE;
602     }
603     else if (alloc.x + alloc.width - CHAT_RESIZE_HANDLE_SIZE < x &&
604         x < alloc.x + alloc.width + CHAT_RESIZE_HANDLE_SIZE &&
605         inside_vertically)
606     {
607         return MOUSE_POS_RIGHT_HANDLE;
608     }
609     else if (alloc.y - CHAT_RESIZE_HANDLE_SIZE < y &&
610         y < alloc.y + CHAT_RESIZE_HANDLE_SIZE &&
611         inside_horizontally)
612     {
613         return MOUSE_POS_TOP_HANDLE;
614     }
615     else if (alloc.y + alloc.height - CHAT_RESIZE_HANDLE_SIZE < y &&
616         y < alloc.y + alloc.height + CHAT_RESIZE_HANDLE_SIZE &&
617         inside_horizontally)
618     {
619         return MOUSE_POS_BOTTOM_HANDLE;
620     }
621     else if (inside_horizontally && inside_vertically)
622     {
623         return MOUSE_POS_INSIDE;
624     }
625     else
626     {
627         return MOUSE_POS_OUTSIDE;
628     }
629 }
630 
631 static gboolean
mouse_pressed_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)632 mouse_pressed_cb(GtkWidget* widget,
633     GdkEvent* evt, gpointer udata)
634 {
635     g_assert(GT_IS_PLAYER(udata));
636 
637     GtPlayer* self = GT_PLAYER(udata);
638     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
639 
640     if (((GdkEventButton*) evt)->type == GDK_2BUTTON_PRESS)
641     {
642         priv->mouse_pressed = FALSE;
643 
644         g_object_set(self, "edit-chat", FALSE, NULL);
645     }
646     else
647     {
648         GtkAllocation alloc;
649 
650         gtk_widget_get_allocation(priv->chat_view, &alloc);
651 
652         priv->start_mouse_pos = get_mouse_pos(self, evt->button.x, evt->button.y);
653 
654         if (priv->start_mouse_pos != MOUSE_POS_OUTSIDE)
655             priv->mouse_pressed = TRUE;
656     }
657 
658     return GDK_EVENT_PROPAGATE;
659 }
660 
661 static gboolean
mouse_released_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)662 mouse_released_cb(GtkWidget* widget,
663     GdkEvent* evt, gpointer udata)
664 {
665     g_assert(GT_IS_PLAYER(udata));
666 
667     GtPlayer* self = GT_PLAYER(udata);
668     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
669 
670     priv->mouse_pressed = FALSE;
671     priv->start_mouse_pos = MOUSE_POS_OUTSIDE;
672 
673     return GDK_EVENT_PROPAGATE;
674 }
675 
676 static gboolean
mouse_moved_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)677 mouse_moved_cb(GtkWidget* widget,
678     GdkEvent* evt, gpointer udata)
679 {
680     g_assert(GT_IS_PLAYER(udata));
681 
682     GtPlayer* self = GT_PLAYER(udata);
683     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
684 
685     MousePos pos;
686     gint x, y;
687 
688     x = ROUND(evt->button.x);
689     y = ROUND(evt->button.y);
690 
691     pos = get_mouse_pos(self, x, y);
692 
693     if (priv->mouse_pressed)
694     {
695         GtkAllocation alloc;
696 
697         gtk_widget_get_allocation(GTK_WIDGET(self), &alloc);
698 
699         gint width_request = ROUND(priv->cur_channel_settings->width*alloc.width);
700         gint height_request = ROUND(priv->cur_channel_settings->height*alloc.height);
701         gint margin_start = ROUND(priv->cur_channel_settings->x_pos*(alloc.width - width_request));
702         gint margin_top = ROUND(priv->cur_channel_settings->y_pos*(alloc.height - height_request));
703 
704         if (priv->start_mouse_pos == MOUSE_POS_LEFT_HANDLE && x >= 0)
705         {
706             width_request += margin_start - x;
707 
708             width_request = MAX(width_request, CHAT_MIN_WIDTH);
709 
710             margin_start = width_request == CHAT_MIN_WIDTH ? margin_start : x;
711         }
712         else if (priv->start_mouse_pos == MOUSE_POS_RIGHT_HANDLE &&
713             x <= gtk_widget_get_allocated_width(GTK_WIDGET(self)))
714         {
715             width_request += x - margin_start - width_request;
716 
717             width_request = MAX(width_request, CHAT_MIN_WIDTH);
718         }
719         else if (priv->start_mouse_pos == MOUSE_POS_TOP_HANDLE && y >= 0)
720         {
721             height_request += margin_top - y;
722 
723             height_request = MAX(height_request, CHAT_MIN_HEIGHT);
724 
725             margin_top = height_request == CHAT_MIN_HEIGHT ? margin_top : y;
726         }
727         else if (priv->start_mouse_pos == MOUSE_POS_BOTTOM_HANDLE &&
728             y <= gtk_widget_get_allocated_height(GTK_WIDGET(self)))
729         {
730             height_request += y - margin_top - height_request;
731 
732             height_request = MAX(height_request, CHAT_MIN_HEIGHT);
733         }
734         else if (priv->start_mouse_pos == MOUSE_POS_INSIDE)
735         {
736             margin_start = MIN(gtk_widget_get_allocated_width(GTK_WIDGET(self)) - width_request,
737                 MAX(0, x - width_request / 2));
738 
739             margin_top = MIN(gtk_widget_get_allocated_height(GTK_WIDGET(self)) - height_request,
740                 MAX(0, y - height_request / 2));
741         }
742 
743         priv->cur_channel_settings->x_pos = margin_start / (gdouble) (alloc.width - width_request);
744         priv->cur_channel_settings->y_pos = margin_top / (gdouble) (alloc.height - height_request);
745         priv->cur_channel_settings->width = width_request / (gdouble) alloc.width;
746         priv->cur_channel_settings->height = height_request / (gdouble) alloc.height;
747 
748         gtk_widget_queue_resize_no_redraw(priv->chat_view);
749 
750     }
751     else
752     {
753         g_autoptr(GdkCursor) cursor = NULL;
754 
755         switch (pos)
756         {
757             case MOUSE_POS_LEFT_HANDLE:
758                 cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_LEFT_SIDE);
759                 break;
760             case MOUSE_POS_RIGHT_HANDLE:
761                 cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_RIGHT_SIDE);
762                 break;
763             case MOUSE_POS_TOP_HANDLE:
764                 cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_TOP_SIDE);
765                 break;
766             case MOUSE_POS_BOTTOM_HANDLE:
767                 cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_BOTTOM_SIDE);
768                 break;
769             case MOUSE_POS_INSIDE:
770                 cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_FLEUR);
771                 break;
772             case MOUSE_POS_OUTSIDE:
773                 cursor = NULL;
774                 break;
775             default:
776                 g_assert_not_reached();
777         }
778 
779         gdk_window_set_cursor(evt->any.window, cursor);
780     }
781 
782     return GDK_EVENT_PROPAGATE;
783 }
784 static void
update_edit_chat(GtPlayer * self)785 update_edit_chat(GtPlayer* self)
786 {
787     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
788 
789     g_object_set(priv->player_box, "above-child", priv->edit_chat, NULL);
790 
791     if (priv->edit_chat)
792     {
793         priv->mouse_pressed_handler_id = g_signal_connect(self, "button-press-event",
794             G_CALLBACK(mouse_pressed_cb), self);
795 
796         priv->mouse_released_handler_id = g_signal_connect(self, "button-release-event",
797             G_CALLBACK(mouse_released_cb), self);
798 
799         priv->mouse_moved_handler_id = g_signal_connect(self, "motion-notify-event",
800             G_CALLBACK(mouse_moved_cb), self);
801 
802         ADD_STYLE_CLASS(self, "edit-chat");
803     }
804     else
805     {
806         if (priv->mouse_pressed_handler_id > 0)
807         {
808             g_signal_handler_disconnect(self, priv->mouse_pressed_handler_id);
809             priv->mouse_pressed_handler_id = 0;
810         }
811 
812         if (priv->mouse_released_handler_id > 0)
813         {
814             g_signal_handler_disconnect(self, priv->mouse_released_handler_id);
815             priv->mouse_pressed_handler_id = 0;
816         }
817 
818         if (priv->mouse_moved_handler_id > 0)
819         {
820             g_signal_handler_disconnect(self, priv->mouse_moved_handler_id);
821             priv->mouse_moved_handler_id = 0;
822         }
823 
824         REMOVE_STYLE_CLASS(self, "edit-chat");
825     }
826 }
827 
828 static void
medium_changed_cb(GObject * obj,GParamSpec * pspec,gpointer udata)829 medium_changed_cb(GObject* obj,
830     GParamSpec* pspec, gpointer udata)
831 {
832     RETURN_IF_FAIL(GT_IS_PLAYER(obj));
833     RETURN_IF_FAIL(G_IS_PARAM_SPEC(pspec));
834     RETURN_IF_FAIL(udata == NULL);
835 
836     GtPlayer* self = GT_PLAYER(obj);
837     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
838 
839     gboolean playing_livestream;
840 
841     switch (priv->medium)
842     {
843         case GT_PLAYER_MEDIUM_VOD:
844         case GT_PLAYER_MEDIUM_NONE:
845             playing_livestream = FALSE;
846             break;
847         case GT_PLAYER_MEDIUM_LIVESTREAM:
848             playing_livestream = TRUE;
849             break;
850         default:
851             RETURN_IF_REACHED();
852     }
853 
854     /* NOTE: Hide chat until we implement chat replay */
855     g_object_set(self, "chat-visible", playing_livestream, NULL);
856 
857     gtk_widget_set_visible(priv->toggle_paused_button, !playing_livestream);
858     gtk_widget_set_visible(priv->switch_live_button, !playing_livestream);
859     /* TODO: Until we implement chat replay, hide the toggle chat button */
860     gtk_widget_set_visible(priv->edit_chat_button, playing_livestream);
861     gtk_widget_set_visible(priv->toggle_chat_button, playing_livestream);
862 }
863 
864 static void
app_shutdown_cb(GApplication * app,gpointer udata)865 app_shutdown_cb(GApplication* app,
866     gpointer udata)
867 {
868     g_assert_null(udata);
869 
870     save_channel_settings();
871 }
872 
873 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)874 set_property(GObject* obj,
875              guint prop,
876              const GValue* val,
877              GParamSpec* pspec)
878 {
879     GtPlayer* self = GT_PLAYER(obj);
880     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
881 
882     switch (prop)
883     {
884         case PROP_VOLUME:
885             priv->volume = g_value_get_double(val);
886             update_volume(self);
887             break;
888         case PROP_MUTED:
889             priv->muted = g_value_get_boolean(val);
890             update_muted(self);
891             break;
892         case PROP_CHANNEL:
893             g_clear_object(&priv->channel);
894             priv->channel = g_value_dup_object(val);
895             break;
896         case PROP_VOD:
897             g_clear_object(&priv->vod);
898             priv->vod = g_value_dup_object(val);
899             break;
900         case PROP_CHAT_DOCKED:
901             priv->cur_channel_settings->docked = g_value_get_boolean(val);
902             update_docked(self);
903             break;
904         case PROP_CHAT_OPACITY:
905             priv->cur_channel_settings->opacity = g_value_get_double(val);
906             break;
907         case PROP_CHAT_VISIBLE:
908             priv->cur_channel_settings->visible = g_value_get_boolean(val);
909             break;
910         case PROP_CHAT_DARK_THEME:
911             priv->cur_channel_settings->dark_theme = g_value_get_boolean(val);
912             break;
913         case PROP_DOCKED_HANDLE_POSITION:
914             priv->cur_channel_settings->docked_handle_pos = g_value_get_double(val);
915             break;
916         case PROP_EDIT_CHAT:
917             priv->edit_chat = g_value_get_boolean(val);
918             update_edit_chat(self);
919             break;
920         case PROP_STREAM_QUALITY:
921             gt_player_set_quality(self, g_value_get_string(val));
922             break;
923         default:
924             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
925 
926     }
927 }
928 
929 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)930 get_property(GObject* obj,
931              guint prop,
932              GValue* val,
933              GParamSpec* pspec)
934 {
935     GtPlayer* self = GT_PLAYER(obj);
936     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
937 
938     switch (prop)
939     {
940         case PROP_VOLUME:
941             g_value_set_double(val, priv->volume);
942             break;
943         case PROP_MUTED:
944             g_value_set_boolean(val, priv->muted);
945             break;
946         case PROP_CHANNEL:
947             g_value_set_object(val, priv->channel);
948             break;
949         case PROP_VOD:
950             g_value_set_object(val, priv->vod);
951             break;
952         case PROP_MEDIUM:
953             g_value_set_enum(val, priv->medium);
954             break;
955         case PROP_CHAT_DOCKED:
956             if (priv->cur_channel_settings)
957                 g_value_set_boolean(val, priv->cur_channel_settings->docked);
958             else
959                 g_value_set_boolean(val, TRUE);
960             break;
961         case PROP_CHAT_OPACITY:
962             if (priv->cur_channel_settings)
963                 g_value_set_double(val, priv->cur_channel_settings->opacity);
964             else
965                 g_value_set_double(val, 1.0);
966             break;
967         case PROP_CHAT_VISIBLE:
968             if (priv->cur_channel_settings)
969                 g_value_set_boolean(val, priv->cur_channel_settings->visible);
970             else
971                 g_value_set_boolean(val, TRUE);
972             break;
973         case PROP_CHAT_DARK_THEME:
974             if (priv->cur_channel_settings)
975                 g_value_set_boolean(val, priv->cur_channel_settings->dark_theme);
976             else
977                 g_value_set_boolean(val, TRUE);
978             break;
979         case PROP_DOCKED_HANDLE_POSITION:
980             if (priv->cur_channel_settings)
981                 g_value_set_double(val, priv->cur_channel_settings->docked_handle_pos);
982             else
983                 g_value_set_double(val, 0.75);
984             break;
985         case PROP_EDIT_CHAT:
986             g_value_set_boolean(val, priv->edit_chat);
987             break;
988         case PROP_STREAM_QUALITY:
989             if (priv->quality)
990                 g_value_set_string(val, priv->quality->name);
991             else
992                 g_value_set_string(val, "");
993             break;
994         default:
995             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
996     }
997 }
998 
999 static gboolean
toggle_paused_cb(GtkWidget * button,GdkEvent * evt,gpointer udata)1000 toggle_paused_cb(GtkWidget* button,
1001     GdkEvent* evt, gpointer udata)
1002 {
1003     RETURN_VAL_IF_FAIL(GTK_IS_TOGGLE_BUTTON(button), GDK_EVENT_PROPAGATE);
1004     RETURN_VAL_IF_FAIL(GT_IS_PLAYER(udata), GDK_EVENT_PROPAGATE);
1005 
1006     GtPlayer* self = GT_PLAYER(udata);
1007     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1008 
1009     if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->toggle_paused_button)))
1010         gt_player_pause(self);
1011     else
1012         gt_player_play(self);
1013 
1014     return GDK_EVENT_PROPAGATE;
1015 }
1016 
1017 static gboolean
seek_bar_value_cb(GtkRange * range,GtkScrollType scroll_type,gdouble value,gpointer udata)1018 seek_bar_value_cb(GtkRange* range, GtkScrollType scroll_type,
1019     gdouble value, gpointer udata)
1020 {
1021     RETURN_VAL_IF_FAIL(GTK_IS_RANGE(range), GDK_EVENT_PROPAGATE);
1022     RETURN_VAL_IF_FAIL(GT_IS_PLAYER(udata), GDK_EVENT_PROPAGATE);
1023 
1024     GtPlayer* self = GT_PLAYER(udata);
1025     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1026 
1027     gt_player_backend_set_position(priv->backend, value);
1028 
1029     return GDK_EVENT_PROPAGATE;
1030 }
1031 
1032 static void
reload_button_cb(GtkButton * button,GtPlayer * self)1033 reload_button_cb(GtkButton* button,
1034     GtPlayer* self)
1035 {
1036     g_assert(GT_IS_PLAYER(self));
1037 
1038     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1039 
1040     //NOTE: Need to do this because of the way open_channel works
1041     //it will dereference channel before referencing it again
1042     gt_player_open_channel(self, g_object_ref(priv->channel));
1043 
1044     g_object_unref(priv->channel);
1045 }
1046 
1047 static void
realize_cb(GtkWidget * widget,gpointer udata)1048 realize_cb(GtkWidget* widget,
1049             gpointer udata)
1050 {
1051     GtPlayer* self = GT_PLAYER(udata);
1052     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1053     GtWin* win = GT_WIN_TOPLEVEL(self);
1054 
1055     g_assert(GT_IS_WIN(win));
1056 
1057     gtk_widget_insert_action_group(GTK_WIDGET(win), "player",
1058         G_ACTION_GROUP(priv->action_group));
1059 
1060     /* NOTE: To get the images for the  toggle buttons to display
1061      * correctly for the first time */
1062     g_object_notify(G_OBJECT(priv->toggle_fullscreen_button), "active");
1063     g_object_notify(G_OBJECT(priv->toggle_paused_button), "active");
1064 
1065     g_signal_connect(win, "notify::fullscreen", G_CALLBACK(fullscreen_cb), self);
1066 }
1067 
1068 static gboolean
hide_cursor_cb(gpointer udata)1069 hide_cursor_cb(gpointer udata)
1070 {
1071     GtPlayer* self = GT_PLAYER(udata);
1072     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1073     GdkCursor* cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_BLANK_CURSOR);
1074 
1075     gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(priv->player_widget)), cursor);
1076 
1077     priv->mouse_source = 0;
1078 
1079     return G_SOURCE_REMOVE;
1080 }
1081 
1082 static gboolean
motion_event_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)1083 motion_event_cb(GtkWidget* widget,
1084                 GdkEvent* evt,
1085                 gpointer udata)
1086 {
1087     GtPlayer* self = GT_PLAYER(udata);
1088     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1089     GdkCursor* cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_LEFT_PTR);
1090 
1091     gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
1092 
1093     if (!gt_win_is_fullscreen(GT_WIN_TOPLEVEL(self)))
1094         return G_SOURCE_REMOVE;
1095 
1096     if (priv->mouse_source)
1097         g_source_remove(priv->mouse_source);
1098 
1099     priv->mouse_source = g_timeout_add(1000, hide_cursor_cb, self);
1100 
1101     return G_SOURCE_REMOVE;
1102 }
1103 
1104 static gchar*
seek_bar_format_cb(GtkScale * scale,gdouble value,gpointer udata)1105 seek_bar_format_cb(GtkScale* scale,
1106     gdouble value, gpointer udata)
1107 {
1108     RETURN_VAL_IF_FAIL(GTK_IS_SCALE(scale), NULL);
1109     RETURN_VAL_IF_FAIL(udata == NULL, NULL);
1110 
1111     gint64 position = (gint64) value;
1112     gint64 duration = (gint64) gtk_adjustment_get_upper(gtk_range_get_adjustment(GTK_RANGE(scale)));
1113 
1114     return g_strdup_printf(_("%ld:%02ld / %ld:%02ld"),
1115         position / 60, position % 60, duration / 60, duration % 60);
1116 }
1117 
1118 static void
plugin_loaded_cb(PeasEngine * engine,PeasPluginInfo * info,gpointer udata)1119 plugin_loaded_cb(PeasEngine* engine,
1120                  PeasPluginInfo* info,
1121                  gpointer udata)
1122 {
1123     GtPlayer* self = GT_PLAYER(udata);
1124     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1125 
1126     if (peas_engine_provides_extension(engine, info, GT_TYPE_PLAYER_BACKEND))
1127     {
1128         PeasExtension* ext;
1129         GtkAdjustment* seek_adj = NULL;
1130 
1131         MESSAGEF("Loaded player backend '%s'", peas_plugin_info_get_name(info));
1132 
1133         if (priv->backend_info)
1134         {
1135             peas_engine_unload_plugin(main_app->players_engine,
1136                 priv->backend_info);
1137         }
1138 
1139         ext = peas_engine_create_extension(engine, info,
1140             GT_TYPE_PLAYER_BACKEND, NULL);
1141 
1142         priv->backend = GT_PLAYER_BACKEND(ext);
1143         priv->backend_info = g_boxed_copy(PEAS_TYPE_PLUGIN_INFO,
1144                                           info);
1145 
1146         g_signal_connect(priv->backend, "notify::buffer-fill",
1147             G_CALLBACK(buffer_fill_cb), self);
1148         g_signal_connect_object(priv->backend, "notify::state",
1149             G_CALLBACK(player_backend_state_cb), self, 0);
1150 
1151         g_object_bind_property(self, "volume", priv->backend, "volume",
1152             G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
1153         g_object_bind_property(priv->backend, "seekable", priv->seek_bar, "visible",
1154             G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
1155 
1156         seek_adj = gtk_range_get_adjustment(GTK_RANGE(priv->seek_bar));
1157         g_object_bind_property(priv->backend, "duration", seek_adj, "upper",
1158             G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
1159         g_object_bind_property(priv->backend, "position", seek_adj, "value",
1160             G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
1161 
1162         g_signal_connect_object(priv->seek_bar, "change-value",
1163             G_CALLBACK(seek_bar_value_cb), self, 0);
1164 
1165         gtk_stack_set_visible_child(GTK_STACK(self), priv->player_box);
1166 
1167         priv->player_widget = gt_player_backend_get_widget(priv->backend);
1168         gtk_widget_add_events(priv->player_widget, GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
1169         gtk_widget_set_can_focus(priv->player_widget, TRUE);
1170         gtk_container_add(GTK_CONTAINER(priv->player_overlay), priv->player_widget);
1171         gtk_widget_show_all(priv->player_overlay);
1172 
1173         g_signal_connect(priv->player_widget, "button-press-event", G_CALLBACK(player_button_press_cb), self);
1174         g_signal_connect(priv->player_widget, "motion-notify-event", G_CALLBACK(motion_event_cb), self);
1175 
1176         if (priv->channel)
1177             gt_player_open_channel(self, priv->channel);
1178     }
1179 }
1180 
1181 static void
plugin_unloaded_cb(PeasEngine * engine,PeasPluginInfo * info,gpointer udata)1182 plugin_unloaded_cb(PeasEngine* engine,
1183                    PeasPluginInfo* info,
1184                    gpointer udata)
1185 {
1186     GtPlayer* self = GT_PLAYER(udata);
1187     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1188 
1189     if (peas_engine_provides_extension(engine, info, GT_TYPE_PLAYER_BACKEND))
1190     {
1191         MESSAGEF("Unloaded player backend '%s'", peas_plugin_info_get_name(info));
1192 
1193         gtk_container_remove(GTK_CONTAINER(priv->player_overlay),
1194             gt_player_backend_get_widget(priv->backend));
1195 
1196         gtk_stack_set_visible_child(GTK_STACK(self), priv->empty_box);
1197 
1198         priv->player_widget = NULL;
1199 
1200         g_clear_object(&priv->backend);
1201 
1202         g_boxed_free(PEAS_TYPE_PLUGIN_INFO, priv->backend_info);
1203         priv->backend_info = NULL;
1204     }
1205 }
1206 
1207 #define SHOW_ERROR(primary, secondary, err)             \
1208     G_STMT_START                                        \
1209     {                                                   \
1210         GtWin* win = GT_WIN_TOPLEVEL(self);             \
1211         RETURN_IF_FAIL(GT_IS_WIN(win));                 \
1212                                                         \
1213         WARNING(secondary " because %s", err->message); \
1214                                                         \
1215         gt_win_show_error_message(win, _(primary),      \
1216             secondary " because: %s", err->message);    \
1217                                                         \
1218         return;                                         \
1219     } G_STMT_END
1220 
1221 static void
handle_playlist_response_cb(GtHTTP * http,gconstpointer res,gsize length,GError * error,gpointer udata)1222 handle_playlist_response_cb(GtHTTP* http,
1223     gconstpointer res, gsize length, GError* error, gpointer udata)
1224 {
1225     RETURN_IF_FAIL(GT_IS_HTTP(http));
1226     RETURN_IF_FAIL(udata != NULL);
1227 
1228     g_autoptr(GWeakRef) ref = udata;
1229     g_autoptr(GtPlayer) self = g_weak_ref_get(ref);
1230     g_autoptr(GtPlaylistEntryList) entries = NULL;
1231     g_autoptr(GError) err = NULL;
1232     g_autofree gchar* default_quality = NULL;
1233 
1234     if (!self) {TRACE("Unreffed while waiting"); return;}
1235 
1236     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1237 
1238     if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1239     {
1240         DEBUG("Cancelled");
1241         return;
1242     }
1243     else if (error)
1244         SHOW_ERROR("Unable to open stream/VOD", "Unable to handle playlist response", error);
1245 
1246     RETURN_IF_FAIL(length > 0);
1247 
1248     priv->quality = NULL;
1249     priv->stream_qualities = NULL;
1250 
1251     entries = utils_parse_playlist(res, &err);
1252 
1253     /* FIXME: Handle this */
1254     RETURN_IF_FAIL(g_list_length(entries) > 0);
1255 
1256     utils_container_clear(GTK_CONTAINER(priv->stream_quality_box));
1257 
1258     for (GList* l = entries; l != NULL; l = l->next)
1259     {
1260         RETURN_IF_FAIL(l->data != NULL);
1261 
1262         GtPlaylistEntry* entry = l->data; /* NOTE: Owned by entries list */
1263 
1264         /* NOTE: Skip over audio only streams */
1265         if (utils_str_empty(entry->resolution))
1266             continue;
1267 
1268         GtkWidget* button = NULL;
1269         GVariant* target_variant = NULL;
1270         gchar* text = NULL;
1271 
1272         button = gtk_model_button_new();
1273         target_variant = g_variant_new_string(entry->name);
1274         text = utils_str_capitalise(entry->name);
1275 
1276         g_object_set(button,
1277             "visible", TRUE,
1278             "action-name", "player.set_stream_quality",
1279             "action-target", target_variant,
1280             "text", g_dgettext("gnome-twich", text),
1281             NULL);
1282 
1283         gtk_container_add(GTK_CONTAINER(priv->stream_quality_box), button);
1284     }
1285 
1286     priv->stream_qualities = g_steal_pointer(&entries);
1287 
1288     if (err)
1289         SHOW_ERROR("Unable to open stream/VOD", "Unable to parse playlist data", err);
1290 
1291     default_quality = g_settings_get_string(main_app->settings, "default-quality");
1292 
1293     gt_player_set_quality(self, default_quality);
1294 
1295     priv->paused = FALSE;
1296 
1297     /* NOTE: To get the toggle button into the correct state when changing stream */
1298     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->toggle_paused_button), TRUE);
1299 }
1300 
1301 static void
process_access_token_json_cb(GObject * source,GAsyncResult * res,gpointer udata)1302 process_access_token_json_cb(GObject* source,
1303     GAsyncResult* res, gpointer udata)
1304 {
1305     RETURN_IF_FAIL(JSON_IS_PARSER(source));
1306     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
1307     RETURN_IF_FAIL(udata != NULL);
1308 
1309     g_autoptr(GWeakRef) ref = udata;
1310     g_autoptr(GtPlayer) self = g_weak_ref_get(ref);
1311 
1312     if (!self) {TRACE("Unreffed while waiting"); return;}
1313 
1314     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1315     g_autoptr(JsonReader) reader = NULL;
1316     g_autoptr(GError) err = NULL;
1317     g_autofree gchar* uri = NULL;
1318     g_autofree gchar* token = NULL;
1319     g_autofree gchar* sig = NULL;
1320     const gchar* id = NULL;
1321 
1322     json_parser_load_from_stream_finish(JSON_PARSER(source), res, &err);
1323 
1324     if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1325     {
1326         DEBUG("Cancelled");
1327         return;
1328     }
1329     else if (err)
1330         SHOW_ERROR("Unable to open stream/VOD", "Unable to process access token JSON", err);
1331 
1332     reader = json_reader_new(json_parser_get_root(JSON_PARSER(source)));
1333 
1334     if (!json_reader_read_member(reader, "token"))
1335         SHOW_ERROR("Unable to open stream/VOD", "Unable to process access token JSON", json_reader_get_error(reader));
1336     token = g_strdup(json_reader_get_string_value(reader));
1337     json_reader_end_member(reader);
1338 
1339     if (!json_reader_read_member(reader, "sig"))
1340         SHOW_ERROR("Unable to open stream/VOD", "Unable to process access token JSON", json_reader_get_error(reader));
1341     sig = g_strdup(json_reader_get_string_value(reader));
1342     json_reader_end_member(reader);
1343 
1344     if (g_strrstr(token, "channel_id"))
1345     {
1346         id = gt_channel_get_name(priv->channel);
1347 
1348         uri = g_strdup_printf("http://usher.twitch.tv/api/channel/hls/%s.m3u8?player=twitchweb&token=%s&sig=%s&allow_audio_only=true&allow_source=true&type=any&allow_spectre=true&p=%d",
1349             id, token, sig, g_random_int_range(0, 999999));
1350     }
1351     else if (g_strrstr(token, "vod_id"))
1352     {
1353         id = gt_vod_get_id(priv->vod);
1354 
1355         if (g_ascii_isalpha(id[0]))
1356             id = id + 1;
1357 
1358         uri = g_strdup_printf("https://usher.ttvnw.net/vod/%s.m3u8?nauth=%s&nauthsig=%s&allow_source=true&player_backend=html5",
1359             id, token, sig);
1360     }
1361     else
1362     {
1363         GtWin* win = GT_WIN_TOPLEVEL(self);
1364         RETURN_IF_FAIL(GT_IS_WIN(win));
1365 
1366         WARNING("Unable to open stream/VOD because: Unable to detect whether either stream or VOD");
1367 
1368         gt_win_show_error_message(win, _("Unable to open stream/VOD"),
1369             "Unable to open stream/VOD because: Unable to detect whether either stream/VOD");
1370 
1371         return;
1372     }
1373 
1374     gt_http_get_with_category(main_app->http, uri, "gt-player", GT_HTTP_TWITCH_HLS_HEADERS, priv->cancel,
1375         G_CALLBACK(handle_playlist_response_cb), g_steal_pointer(&ref), GT_HTTP_FLAG_RETURN_DATA);
1376 }
1377 
1378 static void
handle_access_token_response_cb(GtHTTP * http,gpointer res,GError * error,gpointer udata)1379 handle_access_token_response_cb(GtHTTP* http,
1380     gpointer res, GError* error, gpointer udata)
1381 {
1382     RETURN_IF_FAIL(GT_IS_HTTP(http));
1383     RETURN_IF_FAIL(udata != NULL);
1384 
1385     g_autoptr(GWeakRef) ref = udata;
1386     g_autoptr(GtPlayer) self = g_weak_ref_get(ref);
1387 
1388     if (!self) {TRACE("Unreffed while waiting"); return;}
1389 
1390     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1391 
1392     if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1393     {
1394         DEBUG("Cancelled");
1395         return;
1396     }
1397     else if (error)
1398         SHOW_ERROR("Unable to open VOD", "Unable to handle VOD access token response", error);
1399 
1400     RETURN_IF_FAIL(G_IS_INPUT_STREAM(res));
1401 
1402     json_parser_load_from_stream_async(priv->json_parser, res,
1403         priv->cancel, process_access_token_json_cb, g_steal_pointer(&ref));
1404 }
1405 
1406 static void
gt_player_class_init(GtPlayerClass * klass)1407 gt_player_class_init(GtPlayerClass* klass)
1408 {
1409     GObjectClass* object_class = G_OBJECT_CLASS(klass);
1410 
1411     object_class->finalize = finalise;
1412     object_class->get_property = get_property;
1413     object_class->set_property = set_property;
1414 
1415     props[PROP_VOLUME] = g_param_spec_double("volume", "Volume", "Current volume",
1416         0, 1.0, 0.3, G_PARAM_READWRITE);
1417 
1418     props[PROP_MUTED] = g_param_spec_boolean("muted", "Muted", "Whether muted",
1419         FALSE, G_PARAM_READWRITE);
1420 
1421     props[PROP_CHANNEL] = g_param_spec_object("channel", "Channel", "Current channel",
1422         GT_TYPE_CHANNEL, G_PARAM_READWRITE);
1423 
1424     props[PROP_VOD] = g_param_spec_object("vod", "VOD", "Current VOD",
1425         GT_TYPE_CHANNEL, G_PARAM_READWRITE);
1426 
1427     props[PROP_CHAT_VISIBLE] = g_param_spec_boolean("chat-visible", "Chat Visible", "Whether chat visible",
1428         TRUE, G_PARAM_READWRITE);
1429 
1430     props[PROP_CHAT_DOCKED] = g_param_spec_boolean("chat-docked", "Chat Docked", "Whether chat docked",
1431         TRUE, G_PARAM_READWRITE);
1432 
1433     props[PROP_CHAT_DARK_THEME] = g_param_spec_boolean("chat-dark-theme", "Chat Dark Theme", "Whether chat dark theme",
1434         TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
1435 
1436     props[PROP_CHAT_OPACITY] = g_param_spec_double("chat-opacity", "Chat Opacity", "Current chat opacity",
1437         0, 1.0, 1.0, G_PARAM_READWRITE);
1438 
1439     props[PROP_DOCKED_HANDLE_POSITION] = g_param_spec_double("docked-handle-position", "Docked Handle Position", "Current docked handle position",
1440         0, 1.0, 0, G_PARAM_READWRITE);
1441 
1442     props[PROP_EDIT_CHAT] = g_param_spec_boolean("edit-chat", "Edit chat", "Whether to edit chat",
1443         FALSE, G_PARAM_READWRITE);
1444 
1445     props[PROP_STREAM_QUALITY] = g_param_spec_string("stream-quality", "Stream quality", "Current stream quality",
1446         NULL, G_PARAM_READWRITE);
1447 
1448     props[PROP_MEDIUM] = g_param_spec_enum("medium", "Medium", "Current medium",
1449         GT_TYPE_PLAYER_MEDIUM, GT_PLAYER_MEDIUM_NONE, G_PARAM_READABLE);
1450 
1451     g_object_class_install_properties(object_class, NUM_PROPS, props);
1452 
1453     gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(klass), "/com/vinszent/GnomeTwitch/ui/gt-player.ui");
1454     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, empty_box);
1455     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, docking_pane);
1456     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, player_overlay);
1457     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, controls_revealer);
1458     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, buffer_revealer);
1459     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, buffer_label);
1460     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, player_box);
1461     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, offline_box);
1462     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, error_box);
1463     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, reload_button);
1464     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, volume_button);
1465     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, seek_bar);
1466     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, switch_live_button);
1467     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, toggle_paused_button);
1468     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, toggle_chat_button);
1469     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, edit_chat_button);
1470     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, toggle_fullscreen_button);
1471     gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS(klass), GtPlayer, stream_quality_box);
1472 
1473     channel_settings_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify) gt_player_channel_settings_free);
1474 
1475     load_channel_settings();
1476 }
1477 
1478 //Target -> source
1479 static gboolean
handle_position_from(GBinding * binding,const GValue * from,GValue * to,gpointer udata)1480 handle_position_from(GBinding* binding,
1481                      const GValue* from,
1482                      GValue* to,
1483                      gpointer udata)
1484 {
1485     GtPlayer* self = GT_PLAYER(udata);
1486     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1487     gint width = gtk_widget_get_allocated_width(priv->docking_pane);
1488     gint pos = g_value_get_int(from);
1489 
1490     g_value_set_double(to, (gdouble) pos / (gdouble) width);
1491 
1492     return TRUE;
1493 }
1494 
1495 
1496 //Source -> target
1497 static gboolean
handle_position_to(GBinding * binding,const GValue * from,GValue * to,gpointer udata)1498 handle_position_to(GBinding* binding,
1499                    const GValue* from,
1500                    GValue* to,
1501                    gpointer udata)
1502 {
1503     GtPlayer* self = GT_PLAYER(udata);
1504     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1505     gint width = gtk_widget_get_allocated_width(priv->docking_pane);
1506     gdouble mult = g_value_get_double(from);
1507 
1508     g_value_set_int(to, (gint) (width*mult));
1509 
1510     return TRUE;
1511 }
1512 
1513 static void
gt_player_init(GtPlayer * self)1514 gt_player_init(GtPlayer* self)
1515 {
1516     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1517     GAction* action;
1518 
1519     gtk_widget_init_template(GTK_WIDGET(self));
1520 
1521     gtk_widget_add_events(GTK_WIDGET(self), GDK_POINTER_MOTION_MASK);
1522 
1523     priv->paused = FALSE;
1524     priv->quality = NULL;
1525     priv->stream_qualities = NULL;
1526     priv->medium = GT_PLAYER_MEDIUM_NONE;
1527 
1528     priv->json_parser = json_parser_new();
1529     priv->cancel = NULL;
1530 
1531     priv->mouse_pressed = FALSE;
1532     priv->cur_channel_settings = gt_player_channel_settings_new();
1533 
1534     g_object_set(self, "volume",
1535                  g_settings_get_double(main_app->settings, "volume"),
1536                  NULL);
1537 
1538     g_object_ref(priv->empty_box);
1539 
1540     priv->chat_view = GTK_WIDGET(gt_chat_new());
1541     g_object_ref(priv->chat_view); //TODO: Unref in finalise
1542 
1543     g_signal_connect_object(priv->toggle_paused_button, "button-release-event",
1544         G_CALLBACK(toggle_paused_cb), self, 0);
1545 
1546     priv->action_group = g_simple_action_group_new();
1547 
1548     action = G_ACTION(g_property_action_new("toggle_chat", self, "chat-visible"));
1549     g_action_map_add_action(G_ACTION_MAP(priv->action_group), action);
1550     g_object_unref(action);
1551 
1552     action = G_ACTION(g_property_action_new("dock_chat", self, "chat-docked"));
1553     g_action_map_add_action(G_ACTION_MAP(priv->action_group), action);
1554     g_object_unref(action);
1555 
1556     action = G_ACTION(g_property_action_new("dark_theme_chat", self, "chat-dark-theme"));
1557     g_action_map_add_action(G_ACTION_MAP(priv->action_group), action);
1558     g_object_unref(action);
1559 
1560     action = G_ACTION(g_property_action_new("edit_chat", self, "edit-chat"));
1561     g_action_map_add_action(G_ACTION_MAP(priv->action_group), action);
1562     g_object_unref(action);
1563 
1564     action = G_ACTION(g_property_action_new("set_stream_quality", self, "stream-quality"));
1565     g_action_map_add_action(G_ACTION_MAP(priv->action_group), action);
1566     g_object_unref(action);
1567 
1568     g_object_bind_property_full(self, "docked-handle-position",
1569         priv->docking_pane, "position",
1570         G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
1571         handle_position_to, handle_position_from, self, NULL);
1572 
1573     g_object_bind_property(self, "chat-opacity", priv->chat_view, "opacity",
1574         G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
1575     g_object_bind_property(self, "chat-dark-theme", priv->chat_view, "dark-theme",
1576         G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
1577     g_object_bind_property(self, "chat-visible", priv->chat_view, "visible",
1578         G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
1579 
1580     g_object_bind_property(self, "volume", priv->volume_button, "value",
1581         G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
1582 
1583     utils_signal_connect_oneshot(self, "realize", G_CALLBACK(realize_cb), self);
1584     utils_signal_connect_oneshot_swapped(priv->docking_pane, "size-allocate", G_CALLBACK(update_docked), self);
1585     g_signal_connect(priv->buffer_revealer, "notify::child-revealed", G_CALLBACK(revealer_revealed_cb), self);
1586     g_signal_connect(priv->player_box, "motion-notify-event", G_CALLBACK(motion_cb), self);
1587     g_signal_connect_after(main_app->players_engine, "load-plugin", G_CALLBACK(plugin_loaded_cb), self);
1588     g_signal_connect(main_app->players_engine, "unload-plugin", G_CALLBACK(plugin_unloaded_cb), self);
1589     g_signal_connect(self, "destroy", G_CALLBACK(destroy_cb), self);
1590     g_signal_connect(priv->reload_button, "clicked", G_CALLBACK(reload_button_cb), self);
1591     g_signal_connect(main_app, "shutdown", G_CALLBACK(app_shutdown_cb), NULL);
1592     g_signal_connect_object(priv->player_overlay, "get-child-position", G_CALLBACK(update_chat_undocked_position), self, 0);
1593     g_signal_connect(self, "notify::medium", G_CALLBACK(medium_changed_cb), NULL);
1594     g_signal_connect_object(priv->switch_live_button, "clicked", G_CALLBACK(gt_player_play_livestream), self, G_CONNECT_SWAPPED);
1595     g_signal_connect(priv->seek_bar,"format-value", G_CALLBACK(seek_bar_format_cb), NULL);
1596 
1597     gchar** c;
1598     gchar** _c;
1599     for (_c = c = peas_engine_get_loaded_plugins(main_app->players_engine); *c != NULL; c++)
1600     {
1601         PeasPluginInfo* info = peas_engine_get_plugin_info(main_app->players_engine, *c);
1602 
1603         if (peas_engine_provides_extension(main_app->players_engine, info, GT_TYPE_PLAYER_BACKEND))
1604             plugin_loaded_cb(main_app->players_engine, info, self);
1605     }
1606 
1607     g_strfreev(_c);
1608 }
1609 
1610 void
gt_player_play(GtPlayer * self)1611 gt_player_play(GtPlayer* self)
1612 {
1613     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1614 
1615     if (!priv->backend)
1616         MESSAGE("Can't play, no backend loaded");
1617     else
1618     {
1619         MESSAGE("Playing");
1620         priv->paused = FALSE;
1621         gt_player_backend_play(priv->backend);
1622     }
1623 }
1624 
1625 void
gt_player_pause(GtPlayer * self)1626 gt_player_pause(GtPlayer* self)
1627 {
1628     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1629 
1630     if (!priv->backend)
1631         MESSAGE("Can't pause, no backend loaded");
1632     else
1633     {
1634         MESSAGE("Pausing");
1635         priv->paused = TRUE;
1636         gt_player_backend_pause(priv->backend);
1637     }
1638 }
1639 
1640 void
gt_player_stop(GtPlayer * self)1641 gt_player_stop(GtPlayer* self)
1642 {
1643     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1644 
1645     if (!priv->backend)
1646         MESSAGE("Can't stop, no backend loaded");
1647     else
1648     {
1649         MESSAGE("Stopping");
1650         gt_player_backend_stop(priv->backend);
1651     }
1652 }
1653 
1654 void
gt_player_open_channel(GtPlayer * self,GtChannel * chan)1655 gt_player_open_channel(GtPlayer* self, GtChannel* chan)
1656 {
1657     g_assert(GT_IS_PLAYER(self));
1658     g_assert(GT_IS_CHANNEL(chan));
1659 
1660     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1661     const gchar* id = gt_channel_get_id(chan);
1662     g_autofree gchar* uri = NULL;
1663 
1664     utils_refresh_cancellable(&priv->cancel);
1665 
1666     g_object_set(self, "channel", chan, NULL);
1667 
1668     if (!priv->backend)
1669     {
1670         MESSAGE("Can't open channel, no backend loaded");
1671 
1672         gtk_stack_set_visible_child(GTK_STACK(self), priv->empty_box);
1673 
1674         return;
1675     }
1676 
1677     gtk_stack_set_visible_child(GTK_STACK(self), priv->player_box);
1678 
1679     gtk_label_set_label(GTK_LABEL(priv->buffer_label), _("Loading stream"));
1680 
1681     gtk_widget_set_visible(priv->buffer_revealer, TRUE);
1682 
1683     if (!gtk_revealer_get_child_revealed(GTK_REVEALER(priv->buffer_revealer)))
1684         gtk_revealer_set_reveal_child(GTK_REVEALER(priv->buffer_revealer), TRUE);
1685 
1686     gt_chat_connect(GT_CHAT(priv->chat_view), chan);
1687 
1688     priv->cur_channel_settings = g_hash_table_lookup(channel_settings_table, id);
1689     if (!priv->cur_channel_settings)
1690     {
1691         priv->cur_channel_settings = gt_player_channel_settings_new();
1692         g_hash_table_insert(channel_settings_table,
1693             g_strdup(id), priv->cur_channel_settings);
1694     }
1695 
1696     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CHAT_VISIBLE]);
1697     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CHAT_OPACITY]);
1698     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DOCKED_HANDLE_POSITION]);
1699     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CHAT_DOCKED]);
1700     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CHAT_DARK_THEME]);
1701 
1702     update_docked(self);
1703 
1704     gt_win_inhibit_screensaver(GT_WIN_TOPLEVEL(self));
1705 
1706     gt_player_play_livestream(self);
1707 }
1708 
1709 void
gt_player_open_vod(GtPlayer * self,GtVOD * vod)1710 gt_player_open_vod(GtPlayer* self, GtVOD* vod)
1711 {
1712     RETURN_IF_FAIL(GT_IS_PLAYER(self));
1713     RETURN_IF_FAIL(GT_IS_VOD(vod));
1714 
1715     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1716     g_autofree gchar* uri = NULL;
1717     const gchar* vod_id = NULL;
1718 
1719     g_clear_object(&priv->vod);
1720     priv->vod = g_object_ref(vod);
1721     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_VOD]);
1722 
1723     utils_refresh_cancellable(&priv->cancel);
1724 
1725     gtk_stack_set_visible_child(GTK_STACK(self), priv->player_box);
1726 
1727     vod_id = gt_vod_get_id(priv->vod);
1728 
1729     if (g_ascii_isalpha(vod_id[0]))
1730         vod_id = vod_id + 1;
1731 
1732     priv->medium = GT_PLAYER_MEDIUM_VOD;
1733     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_MEDIUM]);
1734 
1735     uri = g_strdup_printf(VOD_URI, vod_id);
1736 
1737     gt_http_get_with_category(main_app->http, uri, "gt-player", DEFAULT_TWITCH_HEADERS, priv->cancel,
1738         G_CALLBACK(handle_access_token_response_cb), utils_weak_ref_new(self), GT_HTTP_FLAG_RETURN_STREAM);
1739 }
1740 
1741 void
gt_player_play_livestream(GtPlayer * self)1742 gt_player_play_livestream(GtPlayer* self)
1743 {
1744     RETURN_IF_FAIL(GT_IS_PLAYER(self));
1745 
1746     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1747     g_autofree gchar* uri = NULL;
1748 
1749     utils_refresh_cancellable(&priv->cancel);
1750 
1751     gt_player_stop(self);
1752 
1753     if (gt_channel_is_online(priv->channel))
1754     {
1755         priv->medium = GT_PLAYER_MEDIUM_LIVESTREAM;
1756 
1757         uri = g_strdup_printf(LIVESTREAM_URI, gt_channel_get_name(priv->channel));
1758 
1759         gt_http_get_with_category(main_app->http, uri, "gt-player", DEFAULT_TWITCH_HEADERS, priv->cancel,
1760             G_CALLBACK(handle_access_token_response_cb), utils_weak_ref_new(self), GT_HTTP_FLAG_RETURN_STREAM);
1761     }
1762     else
1763     {
1764         priv->medium = GT_PLAYER_MEDIUM_NONE;
1765         gtk_stack_set_visible_child(GTK_STACK(self), priv->offline_box);
1766     }
1767 
1768     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_MEDIUM]);
1769 }
1770 
1771 void
gt_player_close_channel(GtPlayer * self)1772 gt_player_close_channel(GtPlayer* self)
1773 {
1774     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1775 
1776     gt_chat_disconnect(GT_CHAT(priv->chat_view));
1777 
1778     g_object_set(self,
1779         "channel", NULL,
1780         "vod", NULL,
1781         "edit-chat", FALSE,
1782         NULL);
1783 
1784     gt_player_stop(self);
1785 
1786     gt_win_uninhibit_screensaver(GT_WIN_TOPLEVEL(self));
1787 }
1788 
1789 void
gt_player_set_quality(GtPlayer * self,const gchar * quality)1790 gt_player_set_quality(GtPlayer* self, const gchar* quality)
1791 {
1792     RETURN_IF_FAIL(GT_IS_PLAYER(self));
1793     RETURN_IF_FAIL(!utils_str_empty(quality));
1794 
1795     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1796 
1797     RETURN_IF_FAIL(g_list_length(priv->stream_qualities) > 0);
1798 
1799     priv->quality = NULL;
1800 
1801     for (GList* l = priv->stream_qualities; l != NULL; l = l->next)
1802     {
1803         const GtPlaylistEntry* entry = l->data;
1804 
1805         if (STRING_EQUALS(quality, entry->name))
1806         {
1807             priv->quality = entry;
1808             break;
1809         }
1810     }
1811 
1812     if (!priv->quality)
1813         priv->quality = (GtPlaylistEntry*) priv->stream_qualities->data;
1814 
1815     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_STREAM_QUALITY]);
1816 
1817     /* TODO: This needs to happen async */
1818     gt_player_backend_stop(priv->backend);
1819     gt_player_backend_set_uri(priv->backend, priv->quality->uri);
1820     gt_player_backend_play(priv->backend);
1821 }
1822 
1823 void
gt_player_toggle_muted(GtPlayer * self)1824 gt_player_toggle_muted(GtPlayer* self)
1825 {
1826     gboolean muted;
1827 
1828     g_object_get(self, "muted", &muted, NULL);
1829     g_object_set(self, "muted", !muted, NULL);
1830 }
1831 
1832 GtChannel*
gt_player_get_channel(GtPlayer * self)1833 gt_player_get_channel(GtPlayer* self)
1834 {
1835     g_assert(GT_IS_PLAYER(self));
1836 
1837     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1838 
1839     return priv->channel;
1840 }
1841 
1842 GList*
gt_player_get_available_stream_qualities(GtPlayer * self)1843 gt_player_get_available_stream_qualities(GtPlayer* self)
1844 {
1845     g_assert(GT_IS_PLAYER(self));
1846 
1847     GtPlayerPrivate* priv = gt_player_get_instance_private(self);
1848 
1849     return priv->stream_qualities;
1850 }
1851 
1852 GtPlayerChannelSettings*
gt_player_channel_settings_new()1853 gt_player_channel_settings_new()
1854 {
1855     GtPlayerChannelSettings* settings =  g_slice_new(GtPlayerChannelSettings);
1856 
1857     settings->dark_theme = TRUE;
1858     settings->opacity = 1.0;
1859     settings->visible = TRUE;
1860     settings->docked = TRUE;
1861     settings->width = 0.2;
1862     settings->height = 0.7;
1863     settings->x_pos = 1.0;
1864     settings->y_pos = 0.5;
1865     settings->docked_handle_pos = 0.75;
1866 
1867     return settings;
1868 }
1869 
1870 void
gt_player_channel_settings_free(GtPlayerChannelSettings * settings)1871 gt_player_channel_settings_free(GtPlayerChannelSettings* settings)
1872 {
1873     if (!settings)
1874         return;
1875 
1876     g_slice_free(GtPlayerChannelSettings, settings);
1877 }
1878