From 8cd376d3119a6b5afd5234a7d343ee9c6b66568a Mon Sep 17 00:00:00 2001 From: Trigg Date: Tue, 13 Feb 2024 23:24:48 +0000 Subject: [PATCH] - This change is very far reaching into the internals. For this reason a version bump is required. - These is a very high chance of new bugs or repeats of old bugs. Be watchful! - Removed periodic timeout for overlay ticking - Removed 60hz timeout for reading websocket - Removed 1hz timeout for text overlay - Removed 1hz timeout for notification overlay - Added a one-call timeout for each overlay which happens a configurable time after render to remove it excess data - Changed flat bool needsrender to a function call set_needs_render - Where needed, this schedules an idle callback to rerender, cutting down on multiple renders in extremely short time - Ripped out do_read from connector - Piped the websocket socket into GLib, allowing it to call back when new data is readable - Implemented reconnect logic in GLib - Shortened connect timeout as localhost should be rather quick --- discover_overlay/discord_connector.py | 124 +++++++++++++---------- discover_overlay/discover_overlay.py | 17 ---- discover_overlay/notification_overlay.py | 4 +- discover_overlay/overlay.py | 37 +++++-- discover_overlay/text_overlay.py | 24 ++--- discover_overlay/voice_overlay.py | 78 +++++++------- setup.py | 2 +- 7 files changed, 147 insertions(+), 139 deletions(-) diff --git a/discover_overlay/discord_connector.py b/discover_overlay/discord_connector.py index 6744878..f8408f7 100644 --- a/discover_overlay/discord_connector.py +++ b/discover_overlay/discord_connector.py @@ -29,6 +29,11 @@ import calendar import websocket import requests +import gi +gi.require_version("Gtk", "3.0") +# pylint: disable=wrong-import-position,wrong-import-order +from gi.repository import GLib # nopep8 + log = logging.getLogger(__name__) @@ -60,9 +65,13 @@ class DiscordConnector: self.text_altered = False self.text = [] self.authed = False + self.thread = None + self.last_rate_limit_send = 0 + + self.socket_watch = None self.rate_limited_channels = [] - self.reconnect_delay = 0 + self.reconnect_cb = None def get_access_token_stage1(self): """ @@ -313,7 +322,6 @@ class DiscordConnector: self.list_altered = True self.userlist[j["data"]["user_id"]]["speaking"] = True self.userlist[j["data"]["user_id"]]["lastspoken"] = time.time() - self.list_altered = True self.set_in_room(j["data"]["user_id"], True) elif j["evt"] == "SPEAKING_STOP": self.list_altered = True @@ -465,14 +473,10 @@ class DiscordConnector: Called when connection is closed """ log.warning("Connection closed") - self.discover.voice_overlay.set_blank() - if self.discover.text_overlay: - self.discover.text_overlay.set_blank() - if self.discover.notification_overlay: - self.discover.notification_overlay.set_blank() self.websocket = None - self.reconnect_delay = 60 * 20 # Try again in 5 seconds ish + self.update_overlays_from_data self.current_voice = "0" + self.schedule_reconnect() def req_auth(self): """ @@ -696,27 +700,14 @@ class DiscordConnector: if self.websocket: self.websocket.send(json.dumps(cmd)) - def do_read(self): - """ - Poorly named logic center. - - Checks for new data on socket, passes to on_message - - Also passes out text data to text overlay and voice data to voice overlay - - Called at 60Hz approximately but has near zero bearing on rendering - """ - # Ensure connection - if not self.websocket: - if self.reconnect_delay <= 0: - # No timeout left, connect to discord again - self.connect() - return True - else: - # Timeout requested, wait it out - self.reconnect_delay -= 1 - return True - # Recreate a list of users in current room + def update_overlays_from_data(self): + if self.websocket == None: + self.discover.voice_overlay.set_blank() + if self.discover.text_overlay: + self.discover.text_overlay.set_blank() + if self.discover.notification_overlay: + self.discover.notification_overlay.set_blank() + return newlist = [] for userid in self.in_room: newlist.append(self.userlist[userid]) @@ -731,32 +722,19 @@ class DiscordConnector: self.text_altered = False if self.authed and len(self.rate_limited_channels) > 0: - guild = self.rate_limited_channels.pop() + now = time.time() + if self.last_rate_limit_send < now - 60: + guild = self.rate_limited_channels.pop() - cmd = { - "cmd": "GET_CHANNELS", - "args": { - "guild_id": guild - }, - "nonce": guild - } - self.websocket.send(json.dumps(cmd)) - - # Poll socket for new information - recv, _w, _e = select.select((self.websocket.sock,), (), (), 0) - while recv: - try: - # Receive & send to on_message - msg = self.websocket.recv() - self.on_message(msg) - if not self.websocket: - # Connection was closed in the meantime - return True - recv, _w, _e = select.select((self.websocket.sock,), (), (), 0) - except (websocket.WebSocketConnectionClosedException, json.decoder.JSONDecodeError): - self.on_close() - return True - return True + cmd = { + "cmd": "GET_CHANNELS", + "args": { + "guild_id": guild + }, + "nonce": guild + } + self.websocket.send(json.dumps(cmd)) + self.last_rate_limit_send = now def start_listening_text(self, channel): """ @@ -782,22 +760,56 @@ class DiscordConnector: return self.rate_limited_channels.append(guild_id) + def schedule_reconnect(self): + if self.reconnect_cb == None: + log.info("Scheduled a reconnect") + self.reconnect_cb = GLib.timeout_add_seconds(60, self.connect) + else: + log.error("Reconnect already scheduled") + def connect(self): """ Attempt to connect to websocket Should not throw simply for being unable to connect, only for more serious issues """ + log.info("Connecting...") if self.websocket: + log.warn("Already connected?") return + if self.reconnect_cb: + GLib.source_remove(self.reconnect_cb) + self.reconnect_cb = None try: self.websocket = websocket.create_connection( "ws://127.0.0.1:6463/?v=1&client_id=%s" % (self.oauth_token), - origin="http://localhost:3000" + origin="http://localhost:3000", + timeout=0.1 ) self.warn_connection=True # Warn on next disconnect + if self.socket_watch: + GLib.source_remove(self.socket_watch) + self.socket_watch = GLib.io_add_watch(self.websocket.sock, GLib.IOCondition.HUP | GLib.IOCondition.IN | GLib.IOCondition.ERR, self.socket_glib) except ConnectionError as error: if self.warn_connection: log.error(error) self.warn_connection=False - self.reconnect_delay = 60 * 30 # Try again in a minute + self.schedule_reconnect() + + def socket_glib(self, a=None, b=None): + if self.websocket: + recv, _w, _e = select.select((self.websocket.sock,), (), (), 0) + while recv: + try: + # Receive & send to on_message + msg = self.websocket.recv() + self.on_message(msg) + if not self.websocket: + # Connection was closed in the meantime + break + recv, _w, _e = select.select((self.websocket.sock,), (), (), 0) + except (websocket.WebSocketConnectionClosedException, json.decoder.JSONDecodeError): + self.on_close() + break + self.update_overlays_from_data() + return True diff --git a/discover_overlay/discover_overlay.py b/discover_overlay/discover_overlay.py index 2006b91..0057701 100755 --- a/discover_overlay/discover_overlay.py +++ b/discover_overlay/discover_overlay.py @@ -78,8 +78,6 @@ class Discover: self.connection = DiscordConnector(self) self.connection.connect() - GLib.timeout_add((1000 / 60), self.connection.do_read) - GLib.timeout_add((1000 / 20), self.periodic_run) rpc_file = Gio.File.new_for_path(rpc_file) monitor = rpc_file.monitor_file(0, None) @@ -93,21 +91,6 @@ class Discover: Gtk.main() - def periodic_run(self, data=None): - if self.voice_overlay.needsredraw: - self.voice_overlay.redraw() - - if self.text_overlay: - self.text_overlay.tick() - if self.text_overlay.needsredraw: - self.text_overlay.redraw() - - if self.notification_overlay: - self.notification_overlay.tick() - if self.notification_overlay.enabled and self.notification_overlay.needsredraw: - self.notification_overlay.redraw() - return True - def do_args(self, data, normal_close): """ Read in arg list from command or RPC and act accordingly diff --git a/discover_overlay/notification_overlay.py b/discover_overlay/notification_overlay.py index f51fd7d..94e0729 100644 --- a/discover_overlay/notification_overlay.py +++ b/discover_overlay/notification_overlay.py @@ -68,7 +68,7 @@ class NotificationOverlayWindow(OverlayWindow): def set_blank(self): self.content = [] - self.needsredraw = True + self.set_needs_redraw() def tick(self): # This doesn't really belong in overlay or settings @@ -145,6 +145,7 @@ class NotificationOverlayWindow(OverlayWindow): Set the duration that a message will be visible for. """ self.text_time = timer + self.timer_after_draw = timer def set_limit_width(self, limit): """ @@ -285,6 +286,7 @@ class NotificationOverlayWindow(OverlayWindow): context.set_operator(cairo.OPERATOR_SOURCE) context.paint() + self.tick() context.save() if self.is_wayland or self.piggyback_parent or self.discover.steamos: # Special case! diff --git a/discover_overlay/overlay.py b/discover_overlay/overlay.py index 1f1e1b3..27d9be6 100644 --- a/discover_overlay/overlay.py +++ b/discover_overlay/overlay.py @@ -23,7 +23,7 @@ from Xlib.display import Display from Xlib import X, Xatom gi.require_version("Gtk", "3.0") # pylint: disable=wrong-import-position,wrong-import-order -from gi.repository import Gtk, Gdk # nopep8 +from gi.repository import Gtk, Gdk, GLib # nopep8 try: gi.require_version('GtkLayerShell', '0.1') from gi.repository import GtkLayerShell @@ -62,7 +62,6 @@ class OverlayWindow(Gtk.Window): self.pos_y = None self.width = None self.height = None - self.needsredraw = True self.hidden = False self.enabled = False self.set_size_request(50, 50) @@ -99,6 +98,10 @@ class OverlayWindow(Gtk.Window): self.force_xshape = False self.context = None self.autohide = False + + self.redraw_id = None + + self.timer_after_draw = None if piggyback: self.set_piggyback(piggyback) @@ -173,7 +176,7 @@ class OverlayWindow(Gtk.Window): Set the font used by the overlay """ self.text_font = font - self.needsredraw = True + self.set_needs_redraw() def set_floating(self, floating, pos_x, pos_y, width, height): """ @@ -222,7 +225,7 @@ class OverlayWindow(Gtk.Window): width = geometry.width height = geometry.height self.resize(width, height) - self.needsredraw = True + self.set_needs_redraw() return if not self.is_wayland: self.set_decorated(False) @@ -252,7 +255,21 @@ class OverlayWindow(Gtk.Window): (width, height) = self.get_size() self.width = width self.height = height - self.needsredraw = True + self.set_needs_redraw() + + def set_needs_redraw(self): + if not self.hidden and self.enabled: + if self.piggyback_parent: + self.piggyback_parent.set_need_redraw() + + if self.redraw_id == None: + self.redraw_id = GLib.idle_add(self.redraw) + else: + log.debug("Already awaiting paint") + + # If this overlay has data that expires after draw, plan for that here + if self.timer_after_draw != None: + GLib.timeout_add_seconds(self.timer_after_draw, self.redraw) def redraw(self): """ @@ -260,7 +277,7 @@ class OverlayWindow(Gtk.Window): If we're using XShape (optionally or forcibly) then render the image into the shape so that we only cut out clear sections """ - self.needsredraw = False + self.redraw_id = None gdkwin = self.get_window() if self.piggyback_parent: self.piggyback_parent.redraw() @@ -282,6 +299,8 @@ class OverlayWindow(Gtk.Window): else: gdkwin.shape_combine_region(None, 0, 0) self.queue_draw() + self.redraw_id = None + return False def set_hidden(self, hidden): self.hidden = hidden @@ -308,7 +327,7 @@ class OverlayWindow(Gtk.Window): log.error("No get_monitor in display") self.set_untouchable() self.force_location() - self.needsredraw = True + self.set_needs_redraw() def set_align_x(self, align_right): """ @@ -316,7 +335,7 @@ class OverlayWindow(Gtk.Window): """ self.align_right = align_right self.force_location() - self.needsredraw = True + self.set_needs_redraw() def set_align_y(self, align_vert): """ @@ -324,7 +343,7 @@ class OverlayWindow(Gtk.Window): """ self.align_vert = align_vert self.force_location() - self.needsredraw = True + self.set_needs_redraw() def col(self, col, alpha=1.0): """ diff --git a/discover_overlay/text_overlay.py b/discover_overlay/text_overlay.py index fdad57e..fffec6d 100644 --- a/discover_overlay/text_overlay.py +++ b/discover_overlay/text_overlay.py @@ -54,16 +54,10 @@ class TextOverlayWindow(OverlayWindow): self.warned_filetypes = [] self.set_title("Discover Text") self.redraw() - GLib.timeout_add_seconds(1, self.set_need_redraw) - - def set_need_redraw(self): - if self.popup_style: - self.needsredraw = True - return True def set_blank(self): self.content = [] - self.needsredraw = True + self.set_needs_redraw() def tick(self): if len(self.attachment) > self.line_limit: @@ -82,7 +76,8 @@ class TextOverlayWindow(OverlayWindow): Set the duration that a message will be visible for. """ self.text_time = timer - self.needsredraw = True + self.timer_after_draw = timer + self.set_needs_redraw() def set_text_list(self, tlist, altered): """ @@ -90,28 +85,28 @@ class TextOverlayWindow(OverlayWindow): """ self.content = tlist[-self.line_limit:] if altered: - self.needsredraw = True + self.set_needs_redraw() def set_fg(self, fg_col): """ Set default text colour """ self.fg_col = fg_col - self.needsredraw = True + self.set_needs_redraw() def set_bg(self, bg_col): """ Set background colour """ self.bg_col = bg_col - self.needsredraw = True + self.set_needs_redraw() def set_show_attach(self, attachment): """ Set if attachments should be shown inline """ self.show_attach = attachment - self.needsredraw = True + self.set_needs_redraw() def set_popup_style(self, boolean): """ @@ -129,7 +124,7 @@ class TextOverlayWindow(OverlayWindow): font = Pango.FontDescription(self.text_font) self.pango_rect.width = font.get_size() * Pango.SCALE self.pango_rect.height = font.get_size() * Pango.SCALE - self.needsredraw = True + self.set_needs_redraw() def set_line_limit(self, limit): """ @@ -192,7 +187,7 @@ class TextOverlayWindow(OverlayWindow): Called when an image has been downloaded by image_getter """ self.attachment[identifier] = pix - self.needsredraw = True + self.set_needs_redraw() def has_content(self): if not self.enabled: @@ -216,6 +211,7 @@ class TextOverlayWindow(OverlayWindow): context.set_source_rgba(0.0, 0.0, 0.0, 0.0) context.set_operator(cairo.OPERATOR_SOURCE) context.paint() + self.tick() context.save() if self.is_wayland or self.piggyback_parent or self.discover.steamos: # Special case! diff --git a/discover_overlay/voice_overlay.py b/discover_overlay/voice_overlay.py index 0809325..1ddd9c2 100644 --- a/discover_overlay/voice_overlay.py +++ b/discover_overlay/voice_overlay.py @@ -118,12 +118,6 @@ class VoiceOverlayWindow(OverlayWindow): 'def', self.avatar_size, self.icon_transparency) self.set_title("Discover Voice") self.redraw() - GLib.timeout_add_seconds(1, self.set_need_redraw) - - def set_need_redraw(self): - if self.only_speaking: - self.needsredraw = True - return True def set_icon_transparency(self, trans): if self.icon_transparency == trans: @@ -139,178 +133,179 @@ class VoiceOverlayWindow(OverlayWindow): self.channel_icon = None self.channel_mask = None - self.needsredraw = True + self.set_needs_redraw() def set_blank(self): self.userlist = [] self.channel_icon = None self.channel_icon_url = None self.channel_title = None - self.needsredraw = True + self.connection_status = "DISCONNECTED" + self.set_needs_redraw() def set_title_font(self, font): self.title_font = font - self.needsredraw = True + self.set_needs_redraw() def set_show_connection(self, show_connection): self.show_connection = show_connection - self.needsredraw = True + self.set_needs_redraw() def set_show_avatar(self, show_avatar): self.show_avatar = show_avatar - self.needsredraw = True + self.set_needs_redraw() def set_show_title(self, show_title): self.show_title = show_title - self.needsredraw = True + self.set_needs_redraw() def set_show_disconnected(self, show_disconnected): self.show_disconnected = show_disconnected - self.needsredraw = True + self.set_needs_redraw() def set_show_dummy(self, show_dummy): """ Toggle use of dummy userdata to help choose settings """ self.use_dummy = show_dummy - self.needsredraw = True + self.set_needs_redraw() def set_dummy_count(self, dummy_count): self.dummy_count = dummy_count - self.needsredraw = True + self.set_needs_redraw() def set_overflow(self, overflow): """ How should excessive numbers of users be dealt with? """ self.overflow = overflow - self.needsredraw = True + self.set_needs_redraw() def set_bg(self, background_colour): """ Set the background colour """ self.norm_col = background_colour - self.needsredraw = True + self.set_needs_redraw() def set_fg(self, foreground_colour): """ Set the text colour """ self.text_col = foreground_colour - self.needsredraw = True + self.set_needs_redraw() def set_tk(self, talking_colour): """ Set the border colour for users who are talking """ self.talk_col = talking_colour - self.needsredraw = True + self.set_needs_redraw() def set_mt(self, mute_colour): """ Set the colour of mute and deafen logos """ self.mute_col = mute_colour - self.needsredraw = True + self.set_needs_redraw() def set_mute_bg(self, mute_bg_col): """ Set the background colour for mute/deafen icon """ self.mute_bg_col = mute_bg_col - self.needsredraw = True + self.set_needs_redraw() def set_avatar_bg_col(self, avatar_bg_col): """ Set Avatar background colour """ self.avatar_bg_col = avatar_bg_col - self.needsredraw = True + self.set_needs_redraw() def set_hi(self, highlight_colour): """ Set the colour of background for speaking users """ self.hili_col = highlight_colour - self.needsredraw = True + self.set_needs_redraw() def set_fg_hi(self, highlight_colour): """ Set the colour of background for speaking users """ self.text_hili_col = highlight_colour - self.needsredraw = True + self.set_needs_redraw() def set_bo(self, border_colour): """ Set the colour for idle border """ self.border_col = border_colour - self.needsredraw = True + self.set_needs_redraw() def set_avatar_size(self, size): """ Set the size of the avatar icons """ self.avatar_size = size - self.needsredraw = True + self.set_needs_redraw() def set_nick_length(self, size): """ Set the length of nickname """ self.nick_length = size - self.needsredraw = True + self.set_needs_redraw() def set_icon_spacing(self, i): """ Set the spacing between avatar icons """ self.icon_spacing = i - self.needsredraw = True + self.set_needs_redraw() def set_text_padding(self, i): """ Set padding between text and border """ self.text_pad = i - self.needsredraw = True + self.set_needs_redraw() def set_text_baseline_adj(self, i): """ Set padding between text and border """ self.text_baseline_adj = i - self.needsredraw = True + self.set_needs_redraw() def set_vert_edge_padding(self, i): """ Set padding between top/bottom of screen and overlay contents """ self.vert_edge_padding = i - self.needsredraw = True + self.set_needs_redraw() def set_horz_edge_padding(self, i): """ Set padding between left/right of screen and overlay contents """ self.horz_edge_padding = i - self.needsredraw = True + self.set_needs_redraw() def set_square_avatar(self, i): """ Set if the overlay should crop avatars to a circle or show full square image """ self.round_avatar = not i - self.needsredraw = True + self.set_needs_redraw() def set_fancy_border(self, border): """ Sets if border should wrap around non-square avatar images """ self.fancy_border = border - self.needsredraw = True + self.set_needs_redraw() def set_only_speaking(self, only_speaking): """ @@ -323,6 +318,7 @@ class VoiceOverlayWindow(OverlayWindow): Set grace period before hiding people who are not talking """ self.only_speaking_grace_period = grace_period + self.timer_after_draw = grace_period def set_highlight_self(self, highlight_self): """ @@ -336,22 +332,22 @@ class VoiceOverlayWindow(OverlayWindow): """ self.order = i self.sort_list(self.userlist) - self.needsredraw = True + self.set_needs_redraw() def set_icon_only(self, i): """ Set if the overlay should draw only the icon """ self.icon_only = i - self.needsredraw = True + self.set_needs_redraw() def set_border_width(self, width): self.border_width = width - self.needsredraw = True + self.set_needs_redraw() def set_horizontal(self, horizontal=False): self.horizontal = horizontal - self.needsredraw = True + self.set_needs_redraw() def set_guild_ids(self, guild_ids=tuple()): if self.discover.connection: @@ -414,7 +410,7 @@ class VoiceOverlayWindow(OverlayWindow): user["friendlyname"] = user["username"] self.sort_list(self.userlist) if alt: - self.needsredraw = True + self.set_needs_redraw() def set_connection_status(self, connection): """ @@ -422,7 +418,7 @@ class VoiceOverlayWindow(OverlayWindow): """ if self.connection_status != connection['state']: self.connection_status = connection['state'] - self.needsredraw = True + self.set_needs_redraw() def sort_list(self, in_list): if self.order == 1: # ID Sort @@ -678,7 +674,7 @@ class VoiceOverlayWindow(OverlayWindow): else: self.avatars[identifier] = pix self.avatar_masks[identifier] = mask - self.needsredraw = True + self.set_needs_redraw() def delete_avatar(self, identifier): """ diff --git a/setup.py b/setup.py index 6bf205e..ea2ed05 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='discover-overlay', author='trigg', author_email='', - version='0.6.10', + version='0.7.0', description='Voice chat overlay', long_description=readme(), long_description_content_type='text/markdown',