- 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
This commit is contained in:
Trigg 2024-02-13 23:24:48 +00:00
parent 667ad02a4c
commit 8cd376d311
7 changed files with 147 additions and 139 deletions

View file

@ -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

View file

@ -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

View file

@ -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!

View file

@ -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):
"""

View file

@ -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!

View file

@ -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):
"""

View file

@ -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',