Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/source/windows_mixin.py : 53%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8import os
9from io import BytesIO
11from xpra.server.source.stub_source_mixin import StubSourceMixin
12from xpra.server.window.metadata import make_window_metadata
13from xpra.net.compression import Compressed
14from xpra.os_util import monotonic_time, strtobytes, bytestostr
15from xpra.util import typedict, envint, envbool, DEFAULT_METADATA_SUPPORTED, XPRA_BANDWIDTH_NOTIFICATION_ID
16from xpra.log import Logger
18log = Logger("server")
19focuslog = Logger("focus")
20cursorlog = Logger("cursor")
21metalog = Logger("metadata")
22bandwidthlog = Logger("bandwidth")
23eventslog = Logger("events")
24filterslog = Logger("filters")
26CONGESTION_WARNING_EVENT_COUNT = envint("XPRA_CONGESTION_WARNING_EVENT_COUNT", 10)
27CONGESTION_REPEAT_DELAY = envint("XPRA_CONGESTION_REPEAT_DELAY", 60)
28SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False)
29MIN_BANDWIDTH = envint("XPRA_MIN_BANDWIDTH", 5*1024*1024)
31PROPERTIES_DEBUG = [x.strip() for x in os.environ.get("XPRA_WINDOW_PROPERTIES_DEBUG", "").split(",")]
34"""
35Handle window forwarding:
36- damage
37- geometry
38- events
39etc
40"""
41class WindowsMixin(StubSourceMixin):
43 @classmethod
44 def is_needed(cls, caps : typedict) -> bool:
45 return caps.boolget("windows")
48 def __init__(self):
49 self.get_transient_for = None
50 self.get_focus = None
51 self.get_cursor_data_cb = None
52 self.get_window_id = None
53 self.window_filters = []
54 self.readonly = False
55 #duplicated from encodings:
56 self.global_batch_config = None
57 #duplicated from clientconnection:
58 self.statistics = None
60 def init_from(self, _protocol, server):
61 self.get_transient_for = server.get_transient_for
62 self.get_focus = server.get_focus
63 self.get_cursor_data_cb = server.get_cursor_data
64 self.get_window_id = server.get_window_id
65 self.window_filters = server.window_filters
66 self.readonly = server.readonly
68 def init_state(self):
69 #WindowSource for each Window ID
70 self.window_sources = {}
72 self.window_frame_sizes = {}
73 self.suspended = False
74 self.send_cursors = False
75 self.cursor_encodings = ()
76 self.send_bell = False
77 self.send_windows = True
78 self.pointer_grabs = False
79 self.window_min_size = 0, 0
80 self.window_max_size = 0, 0
81 self.window_restack = False
82 self.system_tray = False
83 self.metadata_supported = ()
85 self.cursor_timer = None
86 self.last_cursor_sent = None
88 def cleanup(self):
89 for window_source in self.all_window_sources():
90 window_source.cleanup()
91 self.window_sources = {}
92 self.cancel_cursor_timer()
94 def all_window_sources(self):
95 return tuple(self.window_sources.values())
98 def suspend(self, ui, wd):
99 eventslog("suspend(%s, %s) suspended=%s", ui, wd, self.suspended)
100 if ui:
101 self.suspended = True
102 for wid in wd.keys():
103 ws = self.window_sources.get(wid)
104 if ws:
105 ws.suspend()
107 def resume(self, ui, wd):
108 eventslog("resume(%s, %s) suspended=%s", ui, wd, self.suspended)
109 if ui:
110 self.suspended = False
111 for wid in wd.keys():
112 ws = self.window_sources.get(wid)
113 if ws:
114 ws.resume()
115 self.send_cursor()
118 def go_idle(self):
119 #usually fires from the server's idle_grace_timeout_cb
120 if self.idle:
121 return
122 self.idle = True
123 for window_source in self.all_window_sources():
124 window_source.go_idle()
126 def no_idle(self):
127 #on user event, we stop being idle
128 if not self.idle:
129 return
130 self.idle = False
131 for window_source in self.all_window_sources():
132 window_source.no_idle()
135 def parse_client_caps(self, c):
136 self.send_windows = c.boolget("ui_client", True) and c.boolget("windows", True)
137 self.pointer_grabs = c.boolget("pointer.grabs")
138 self.send_cursors = self.send_windows and c.boolget("cursors")
139 self.cursor_encodings = c.strtupleget("encodings.cursor")
140 self.send_bell = c.boolget("bell")
141 self.system_tray = c.boolget("system_tray")
142 self.metadata_supported = c.strtupleget("metadata.supported", DEFAULT_METADATA_SUPPORTED)
143 self.window_frame_sizes = typedict(c.dictget("window.frame_sizes", {}))
144 self.window_min_size = c.inttupleget("window.min-size", (0, 0))
145 self.window_max_size = c.inttupleget("window.max-size", (0, 0))
146 self.window_restack = c.boolget("window.restack", False)
147 log("cursors=%s (encodings=%s), bell=%s",
148 self.send_cursors, self.cursor_encodings, self.send_bell)
149 #window filters:
150 try:
151 for object_name, property_name, operator, value in c.tupleget("window-filters"):
152 self.add_window_filter(object_name, property_name, operator, value)
153 except Exception as e:
154 filterslog.error("Error parsing window-filters: %s", e)
157 def get_caps(self) -> dict:
158 return {}
161 ######################################################################
162 # info:
163 def get_info(self) -> dict:
164 info = {
165 "windows" : self.send_windows,
166 "cursors" : self.send_cursors,
167 "bell" : self.send_bell,
168 "system-tray" : self.system_tray,
169 "suspended" : self.suspended,
170 }
171 wsize = info.setdefault("window-size", {})
172 wsize.update({
173 "min" : self.window_min_size,
174 "max" : self.window_max_size,
175 })
176 if self.window_frame_sizes:
177 wsize.update({"frame-sizes" : self.window_frame_sizes})
178 info.update(self.get_window_info())
179 return info
181 def get_window_info(self) -> dict:
182 """
183 Adds encoding and window specific information
184 """
185 from xpra.simple_stats import get_list_stats
186 pqpixels = [x[2] for x in tuple(self.packet_queue)]
187 pqpi = get_list_stats(pqpixels)
188 if pqpixels:
189 pqpi["current"] = pqpixels[-1]
190 info = {"damage" : {
191 "compression_queue" : {"size" : {"current" : self.encode_queue_size()}},
192 "packet_queue" : {"size" : {"current" : len(self.packet_queue)}},
193 "packet_queue_pixels" : pqpi,
194 },
195 }
196 gbc = self.global_batch_config
197 if gbc:
198 info["batch"] = self.global_batch_config.get_info()
199 s = self.statistics
200 if s:
201 info.update(s.get_info())
202 if self.window_sources:
203 total_pixels = 0
204 total_time = 0.0
205 in_latencies, out_latencies = [], []
206 winfo = {}
207 for wid, ws in list(self.window_sources.items()):
208 #per-window source stats:
209 winfo[wid] = ws.get_info()
210 #collect stats for global averages:
211 for _, _, pixels, _, _, encoding_time in tuple(ws.statistics.encoding_stats):
212 total_pixels += pixels
213 total_time += encoding_time
214 in_latencies += [x*1000 for _, _, _, x in tuple(ws.statistics.damage_in_latency)]
215 out_latencies += [x*1000 for _, _, _, x in tuple(ws.statistics.damage_out_latency)]
216 info["window"] = winfo
217 v = 0
218 if total_time>0:
219 v = int(total_pixels / total_time)
220 info.setdefault("encoding", {})["pixels_encoded_per_second"] = v
221 dinfo = info.setdefault("damage", {})
222 dinfo["in_latency"] = get_list_stats(in_latencies, show_percentile=[9])
223 dinfo["out_latency"] = get_list_stats(out_latencies, show_percentile=[9])
224 return info
227 ######################################################################
228 # grabs:
229 def pointer_grab(self, wid):
230 if self.pointer_grabs and self.hello_sent:
231 self.send("pointer-grab", wid)
233 def pointer_ungrab(self, wid):
234 if self.pointer_grabs and self.hello_sent:
235 self.send("pointer-ungrab", wid)
238 ######################################################################
239 # cursors:
240 def send_cursor(self):
241 if not self.send_cursors or self.suspended or not self.hello_sent:
242 return
243 #if not pending already, schedule it:
244 gbc = self.global_batch_config
245 if not self.cursor_timer and gbc:
246 delay = max(10, int(gbc.delay/4))
247 self.cursor_timer = self.timeout_add(delay, self.do_send_cursor, delay)
249 def cancel_cursor_timer(self):
250 ct = self.cursor_timer
251 if ct:
252 self.cursor_timer = None
253 self.source_remove(ct)
255 def do_send_cursor(self, delay):
256 self.cursor_timer = None
257 cd = self.get_cursor_data_cb()
258 if not cd or not cd[0]:
259 self.send_empty_cursor()
260 return
261 cursor_data = list(cd[0])
262 cursor_sizes = cd[1]
263 #skip first two fields (if present) as those are coordinates:
264 if self.last_cursor_sent and self.last_cursor_sent[2:9]==cursor_data[2:9]:
265 cursorlog("do_send_cursor(..) cursor identical to the last one we sent, nothing to do")
266 return
267 self.last_cursor_sent = cursor_data[:9]
268 w, h, _xhot, _yhot, serial, pixels, name = cursor_data[2:9]
269 #compress pixels if needed:
270 encoding = "raw"
271 if pixels is not None:
272 #convert bytearray to string:
273 cpixels = strtobytes(pixels)
274 if "png" in self.cursor_encodings:
275 from PIL import Image
276 cursorlog("do_send_cursor() loading %i bytes of cursor pixel data for %ix%i cursor named '%s'",
277 len(cpixels), w, h, bytestostr(name))
278 img = Image.frombytes("RGBA", (w, h), cpixels, "raw", "BGRA", w*4, 1)
279 buf = BytesIO()
280 img.save(buf, "PNG")
281 pngdata = buf.getvalue()
282 buf.close()
283 cpixels = Compressed("png cursor", pngdata, can_inline=True)
284 encoding = "png"
285 if SAVE_CURSORS:
286 filename = "raw-cursor-%#x.png" % serial
287 with open(filename, "wb") as f:
288 f.write(pngdata)
289 cursorlog("cursor saved to %s", filename)
290 elif len(cpixels)>=256 and ("raw" in self.cursor_encodings or not self.cursor_encodings):
291 cpixels = self.compressed_wrapper("cursor", pixels)
292 cursorlog("do_send_cursor(..) pixels=%s ", cpixels)
293 encoding = "raw"
294 cursor_data[7] = cpixels
295 cursorlog("do_send_cursor(..) %sx%s %s cursor name='%s', serial=%#x with delay=%s (cursor_encodings=%s)",
296 w, h, (encoding or "empty"), bytestostr(name), serial, delay, self.cursor_encodings)
297 args = [encoding] + list(cursor_data[:9]) + [cursor_sizes[0]] + list(cursor_sizes[1])
298 self.send_more("cursor", *args)
300 def send_empty_cursor(self):
301 cursorlog("send_empty_cursor(..)")
302 self.last_cursor_sent = None
303 self.send_more("cursor", "")
306 def bell(self, wid, device, percent, pitch, duration, bell_class, bell_id, bell_name):
307 if not self.send_bell or self.suspended or not self.hello_sent:
308 return
309 self.send_async("bell", wid, device, percent, pitch, duration, bell_class, bell_id, bell_name)
312 ######################################################################
313 # window filters:
314 def reset_window_filters(self):
315 self.window_filters = [(uuid, f) for uuid, f in self.window_filters if uuid!=self.uuid]
317 def get_all_window_filters(self):
318 return [f for uuid, f in self.window_filters if uuid==self.uuid]
320 def add_window_filter(self, object_name, property_name, operator, value):
321 from xpra.server.window.filters import get_window_filter
322 window_filter = get_window_filter(object_name, property_name, operator, value)
323 assert window_filter
324 self.do_add_window_filter(window_filter)
326 def do_add_window_filter(self, window_filter):
327 #(reminder: filters are shared between all sources)
328 self.window_filters.append((self.uuid, window_filter))
330 def can_send_window(self, window):
331 if not self.hello_sent or not (self.send_windows or self.system_tray):
332 return False
333 #we could also allow filtering for system tray windows?
334 if self.window_filters and self.send_windows and not window.is_tray():
335 for uuid, window_filter in self.window_filters:
336 filterslog("can_send_window(%s) checking %s for uuid=%s (client uuid=%s)",
337 window, window_filter, uuid, self.uuid)
338 if window_filter.matches(window):
339 v = uuid=="*" or uuid==self.uuid
340 filterslog("can_send_window(%s)=%s", window, v)
341 return v
342 if self.send_windows and self.system_tray:
343 #common case shortcut
344 v = True
345 elif window.is_tray():
346 v = self.system_tray
347 else:
348 v = self.send_windows
349 filterslog("can_send_window(%s)=%s", window, v)
350 return v
353 ######################################################################
354 # windows:
355 def initiate_moveresize(self, wid, window, x_root, y_root, direction, button, source_indication):
356 if not self.can_send_window(window):
357 return
358 log("initiate_moveresize sending to %s", self)
359 self.send("initiate-moveresize", wid, x_root, y_root, direction, button, source_indication)
361 def or_window_geometry(self, wid, window, x, y, w, h):
362 if not self.can_send_window(window):
363 return
364 self.send("configure-override-redirect", wid, x, y, w, h)
366 def window_metadata(self, wid, window, prop):
367 if not self.can_send_window(window):
368 return
369 if prop=="icons":
370 self.send_window_icon(wid, window)
371 else:
372 metadata = self._make_metadata(window, prop)
373 if prop in PROPERTIES_DEBUG:
374 metalog.info("make_metadata(%s, %s, %s)=%s", wid, window, prop, metadata)
375 else:
376 metalog("make_metadata(%s, %s, %s)=%s", wid, window, prop, metadata)
377 if metadata:
378 self.send("window-metadata", wid, metadata)
381 # Takes the name of a WindowModel property, and returns a dictionary of
382 # xpra window metadata values that depend on that property
383 def _make_metadata(self, window, propname, skip_defaults=False):
384 if propname not in self.metadata_supported:
385 metalog("make_metadata: client does not support '%s'", propname)
386 return {}
387 metadata = make_window_metadata(window, propname,
388 get_transient_for=self.get_transient_for,
389 get_window_id=self.get_window_id,
390 skip_defaults=skip_defaults)
391 if self.readonly:
392 metalog("overriding size-constraints for readonly mode")
393 size = window.get_dimensions()
394 metadata["size-constraints"] = {
395 "maximum-size" : size,
396 "minimum-size" : size,
397 "base-size" : size,
398 }
399 return metadata
401 def new_tray(self, wid, window, w, h):
402 assert window.is_tray()
403 if not self.can_send_window(window):
404 return
405 metadata = {}
406 for propname in list(window.get_property_names()):
407 metadata.update(self._make_metadata(window, propname, skip_defaults=True))
408 self.send_async("new-tray", wid, w, h, metadata)
410 def new_window(self, ptype, wid, window, x, y, w, h, client_properties):
411 if not self.can_send_window(window):
412 return
413 send_props = list(window.get_property_names())
414 send_raw_icon = "icons" in send_props
415 if send_raw_icon:
416 send_props.remove("icons")
417 metadata = {}
418 for prop in send_props:
419 v = self._make_metadata(window, prop, skip_defaults=True)
420 if prop in PROPERTIES_DEBUG:
421 metalog.info("make_metadata(%s, %s, %s)=%s", wid, window, prop, v)
422 else:
423 metalog("make_metadata(%s, %s, %s)=%s", wid, window, prop, v)
424 metadata.update(v)
425 log("new_window(%s, %s, %s, %s, %s, %s, %s, %s) metadata(%s)=%s",
426 ptype, window, wid, x, y, w, h, client_properties, send_props, metadata)
427 self.send_async(ptype, wid, x, y, w, h, metadata, client_properties or {})
428 if send_raw_icon:
429 self.send_window_icon(wid, window)
431 def send_window_icon(self, wid, window):
432 if not self.can_send_window(window):
433 return
434 #we may need to make a new source at this point:
435 ws = self.make_window_source(wid, window)
436 if ws:
437 ws.send_window_icon()
440 def lost_window(self, wid, _window):
441 self.send("lost-window", wid)
443 def move_resize_window(self, wid, window, x, y, ww, wh, resize_counter=0):
444 """
445 The server detected that the application window has been moved and/or resized,
446 we forward it if the client supports this type of event.
447 """
448 if not self.can_send_window(window):
449 return
450 self.send("window-move-resize", wid, x, y, ww, wh, resize_counter)
452 def resize_window(self, wid, window, ww, wh, resize_counter=0):
453 if not self.can_send_window(window):
454 return
455 self.send("window-resized", wid, ww, wh, resize_counter)
458 def cancel_damage(self, wid):
459 """
460 Use this method to cancel all currently pending and ongoing
461 damage requests for a window.
462 """
463 ws = self.window_sources.get(wid)
464 if ws:
465 ws.cancel_damage()
468 def map_window(self, wid, window, coords=None):
469 ws = self.make_window_source(wid, window)
470 ws.map(coords)
472 def unmap_window(self, wid, _window):
473 ws = self.window_sources.get(wid)
474 if ws:
475 ws.unmap()
477 def restack_window(self, wid, window, detail, sibling):
478 focuslog("restack_window%s", (wid, window, detail, sibling))
479 if not self.can_send_window(window):
480 return
481 if not self.window_restack:
482 #older clients can only handle "raise-window"
483 if detail!=0:
484 return
485 self.send_async("raise-window", wid)
486 return
487 sibling_wid = 0
488 if sibling:
489 sibling_wid = self.get_window_id(sibling)
490 self.send_async("restack-window", wid, detail, sibling_wid)
492 def raise_window(self, wid, window):
493 if not self.can_send_window(window):
494 return
495 self.send_async("raise-window", wid)
497 def remove_window(self, wid, window):
498 """ The given window is gone, ensure we free all the related resources """
499 if not self.can_send_window(window):
500 return
501 ws = self.window_sources.pop(wid, None)
502 if ws:
503 ws.cleanup()
504 self.calculate_window_pixels.pop(wid, None)
507 def refresh(self, wid, window, opts):
508 if not self.can_send_window(window):
509 return
510 self.cancel_damage(wid)
511 w, h = window.get_dimensions()
512 self.damage(wid, window, 0, 0, w, h, opts)
514 def update_batch(self, wid, window, batch_props):
515 ws = self.window_sources.get(wid)
516 if ws:
517 if "reset" in batch_props:
518 ws.batch_config = self.make_batch_config(wid, window)
519 for x in ("always", "locked"):
520 if x in batch_props:
521 setattr(ws.batch_config, x, batch_props.boolget(x))
522 for x in ("min_delay", "max_delay", "timeout_delay", "delay"):
523 if x in batch_props:
524 setattr(ws.batch_config, x, batch_props.intget(x))
525 log("batch config updated for window %s: %s", wid, ws.batch_config)
527 def set_client_properties(self, wid, window, new_client_properties):
528 assert self.send_windows
529 ws = self.make_window_source(wid, window)
530 ws.set_client_properties(new_client_properties)
533 def get_window_source(self, wid):
534 return self.window_sources.get(wid)
536 def make_window_source(self, wid, window):
537 ws = self.window_sources.get(wid)
538 if ws is None:
539 batch_config = self.make_batch_config(wid, window)
540 ww, wh = window.get_dimensions()
541 bandwidth_limit = self.bandwidth_limit
542 mmap = getattr(self, "mmap", None)
543 mmap_size = getattr(self, "mmap_size", 0)
544 av_sync = getattr(self, "av_sync", False)
545 av_sync_delay = getattr(self, "av_sync_delay", 0)
546 if mmap_size>0:
547 bandwidth_limit = 0
548 from xpra.server.window.window_video_source import WindowVideoSource
549 ws = WindowVideoSource(
550 self.idle_add, self.timeout_add, self.source_remove,
551 ww, wh,
552 self.record_congestion_event, self.encode_queue_size,
553 self.call_in_encode_thread, self.queue_packet,
554 self.statistics,
555 wid, window, batch_config, self.auto_refresh_delay,
556 av_sync, av_sync_delay,
557 self.video_helper,
558 self.server_core_encodings, self.server_encodings,
559 self.encoding, self.encodings, self.core_encodings,
560 self.window_icon_encodings, self.encoding_options, self.icons_encoding_options,
561 self.rgb_formats,
562 self.default_encoding_options,
563 mmap, mmap_size, bandwidth_limit, self.jitter)
564 self.window_sources[wid] = ws
565 if len(self.window_sources)>1:
566 #re-distribute bandwidth:
567 self.update_bandwidth_limits()
568 return ws
571 def damage(self, wid, window, x, y, w, h, options=None):
572 """
573 Main entry point from the window manager,
574 we dispatch to the WindowSource for this window id
575 (creating a new one if needed)
576 """
577 if not self.can_send_window(window):
578 return
579 assert window is not None
580 if options:
581 damage_options = options.copy()
582 else:
583 damage_options = {}
584 s = self.statistics
585 if s:
586 s.damage_last_events.append((wid, monotonic_time(), w*h))
587 ws = self.make_window_source(wid, window)
588 ws.damage(x, y, w, h, damage_options)
590 def client_ack_damage(self, damage_packet_sequence, wid, width, height, decode_time, message):
591 """
592 The client is acknowledging a damage packet,
593 we record the 'client decode time' (which is provided by the client)
594 and WindowSource will calculate and record the "client latency".
595 (since it knows when the "draw" packet was sent)
596 """
597 if not self.send_windows:
598 log.error("client_ack_damage when we don't send any window data!?")
599 return
600 if decode_time>0:
601 self.statistics.client_decode_time.append((wid, monotonic_time(), width*height, decode_time))
602 ws = self.window_sources.get(wid)
603 if ws:
604 ws.damage_packet_acked(damage_packet_sequence, width, height, decode_time, message)
605 self.may_recalculate(wid, width*height)
607#
608# Methods used by WindowSource:
609#
610 def record_congestion_event(self, source, late_pct=0, send_speed=0):
611 if not self.bandwidth_detection:
612 return
613 gs = self.statistics
614 if not gs:
615 #window cleaned up?
616 return
617 now = monotonic_time()
618 elapsed = now-self.bandwidth_warning_time
619 bandwidthlog("record_congestion_event(%s, %i, %i) bandwidth_warnings=%s, elapsed time=%i",
620 source, late_pct, send_speed, self.bandwidth_warnings, elapsed)
621 gs.last_congestion_time = now
622 gs.congestion_send_speed.append((now, late_pct, send_speed))
623 if self.bandwidth_warnings and elapsed>CONGESTION_REPEAT_DELAY:
624 #enough congestion events?
625 T = 10
626 min_time = now-T
627 count = len(tuple(True for x in gs.congestion_send_speed if x[0]>min_time))
628 bandwidthlog("record_congestion_event: %i events in the last %i seconds (warnings after %i)",
629 count, T, CONGESTION_WARNING_EVENT_COUNT)
630 if count>CONGESTION_WARNING_EVENT_COUNT:
631 self.bandwidth_warning_time = now
632 nid = XPRA_BANDWIDTH_NOTIFICATION_ID
633 summary = "Network Performance Issue"
634 body = "Your network connection is struggling to keep up,\n" + \
635 "consider lowering the bandwidth limit,\n" + \
636 "or turning off automatic network congestion management.\n" + \
637 "Choosing 'ignore' will silence all further warnings."
638 actions = []
639 if self.bandwidth_limit==0 or self.bandwidth_limit>MIN_BANDWIDTH:
640 actions += ["lower-bandwidth", "Lower bandwidth limit"]
641 actions += ["bandwidth-off", "Turn off"]
642 #if self.default_min_quality>10:
643 # actions += ["lower-quality", "Lower quality"]
644 actions += ["ignore", "Ignore"]
645 hints = {}
646 self.may_notify(nid, summary, body, actions, hints,
647 icon_name="connect", user_callback=self.congestion_notification_callback)
649 def congestion_notification_callback(self, nid, action_id):
650 bandwidthlog("congestion_notification_callback(%i, %s)", nid, action_id)
651 if action_id=="lower-bandwidth":
652 bandwidth_limit = 50*1024*1024
653 if self.bandwidth_limit>256*1024:
654 bandwidth_limit = self.bandwidth_limit//2
655 css = 50*1024*1024
656 if self.statistics.avg_congestion_send_speed>256*1024:
657 #round up:
658 css = int(self.statistics.avg_congestion_send_speed//16/1024)*16*1024
659 self.bandwidth_limit = max(MIN_BANDWIDTH, min(bandwidth_limit, css))
660 self.setting_changed("bandwidth-limit", self.bandwidth_limit)
661 #elif action_id=="lower-quality":
662 # self.default_min_quality = max(1, self.default_min_quality-15)
663 # self.set_min_quality(self.default_min_quality)
664 # self.setting_changed("min-quality", self.default_min_quality)
665 elif action_id=="bandwidth-off":
666 self.bandwidth_detection = False
667 elif action_id=="ignore":
668 self.bandwidth_warnings = False