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