def __init__(self, core_encodings, encodings, default_encoding, scaling_control, default_quality, default_min_quality, default_speed, default_min_speed): log("ServerSource%s", (core_encodings, encodings, default_encoding, scaling_control, default_quality, default_min_quality, default_speed, default_min_speed)) self.server_core_encodings = core_encodings self.server_encodings = encodings self.default_encoding = default_encoding self.scaling_control = scaling_control self.default_quality = default_quality #default encoding quality for lossy encodings self.default_min_quality = default_min_quality #default minimum encoding quality self.default_speed = default_speed #encoding speed (only used by x264) self.default_min_speed = default_min_speed #default minimum encoding speed self.default_batch_config = DamageBatchConfig( ) #contains default values, some of which may be supplied by the client self.global_batch_config = self.default_batch_config.clone( ) #global batch config self.vrefresh = -1 self.supports_transparency = False self.encoding = None #the default encoding for all windows self.encodings = () #all the encodings supported by the client self.core_encodings = () self.window_icon_encodings = ["premult_argb32"] self.rgb_formats = ("RGB", ) self.encoding_options = typedict() self.icons_encoding_options = typedict() self.default_encoding_options = typedict() self.auto_refresh_delay = 0 self.zlib = True self.lz4 = use_lz4 self.lzo = use_lzo #for managing the recalculate_delays work: self.calculate_window_pixels = {} self.calculate_window_ids = set() self.calculate_timer = 0 self.calculate_last_time = 0 #if we "proxy video", we will modify the video helper to add #new encoders, so we must make a deep copy to preserve the original #which may be used by other clients (other ServerSource instances) self.video_helper = getVideoHelper().clone() # the queues of damage requests we work through: self.encode_work_queue = Queue( ) #holds functions to call to compress data (pixels, clipboard) #items placed in this queue are picked off by the "encode" thread, #the functions should add the packets they generate to the 'packet_queue' self.packet_queue = deque( ) #holds actual packets ready for sending (already encoded) #these packets are picked off by the "protocol" via 'next_packet()' #format: packet, wid, pixels, start_send_cb, end_send_cb #(only packet is required - the rest can be 0/None for clipboard packets) self.encode_thread = start_thread(self.encode_loop, "encode")
def __init__(self): self.server_core_encodings = [] self.server_encodings = [] self.default_encoding = None self.scaling_control = None self.default_quality = 40 #default encoding quality for lossy encodings self.default_min_quality = 10 #default minimum encoding quality self.default_speed = 40 #encoding speed (only used by x264) self.default_min_speed = 10 #default minimum encoding speed self.default_batch_config = DamageBatchConfig( ) #contains default values, some of which may be supplied by the client self.global_batch_config = self.default_batch_config.clone( ) #global batch config self.vrefresh = -1 self.supports_transparency = False self.encoding = None #the default encoding for all windows self.encodings = () #all the encodings supported by the client self.core_encodings = () self.window_icon_encodings = ["premult_argb32"] self.rgb_formats = ("RGB", ) self.encoding_options = typedict() self.icons_encoding_options = typedict() self.default_encoding_options = typedict() self.auto_refresh_delay = 0 self.zlib = True self.lz4 = use_lz4 self.lzo = use_lzo #for managing the recalculate_delays work: self.calculate_window_pixels = {} self.calculate_window_ids = set() self.calculate_timer = 0 self.calculate_last_time = 0 #if we "proxy video", we will modify the video helper to add #new encoders, so we must make a deep copy to preserve the original #which may be used by other clients (other ServerSource instances) self.video_helper = getVideoHelper().clone()
def init_state(self): self.wants_encodings = False self.wants_features = False #contains default values, some of which may be supplied by the client: self.default_batch_config = DamageBatchConfig() self.global_batch_config = self.default_batch_config.clone( ) #global batch config self.vrefresh = -1 self.supports_transparency = False self.encoding = None #the default encoding for all windows self.encodings = () #all the encodings supported by the client self.core_encodings = () self.encodings_packet = False #supports delayed encodings initialization? self.window_icon_encodings = ["premult_argb32"] self.rgb_formats = ("RGB", ) self.encoding_options = typedict() self.icons_encoding_options = typedict() self.default_encoding_options = typedict() self.auto_refresh_delay = 0 self.zlib = True self.lz4 = use("lz4") self.lzo = use("lzo") #for managing the recalculate_delays work: self.calculate_window_pixels = {} self.calculate_window_ids = set() self.calculate_timer = 0 self.calculate_last_time = 0 #if we "proxy video", we will modify the video helper to add #new encoders, so we must make a deep copy to preserve the original #which may be used by other clients (other ServerSource instances) self.video_helper = getVideoHelper().clone() self.cuda_device_context = None
class EncodingsMixin(StubSourceMixin): def __init__(self): self.server_core_encodings = [] self.server_encodings = [] self.default_encoding = None self.scaling_control = None self.default_quality = 40 #default encoding quality for lossy encodings self.default_min_quality = 10 #default minimum encoding quality self.default_speed = 40 #encoding speed (only used by x264) self.default_min_speed = 10 #default minimum encoding speed self.default_batch_config = DamageBatchConfig( ) #contains default values, some of which may be supplied by the client self.global_batch_config = self.default_batch_config.clone( ) #global batch config self.vrefresh = -1 self.supports_transparency = False self.encoding = None #the default encoding for all windows self.encodings = () #all the encodings supported by the client self.core_encodings = () self.window_icon_encodings = ["premult_argb32"] self.rgb_formats = ("RGB", ) self.encoding_options = typedict() self.icons_encoding_options = typedict() self.default_encoding_options = typedict() self.auto_refresh_delay = 0 self.zlib = True self.lz4 = use_lz4 self.lzo = use_lzo #for managing the recalculate_delays work: self.calculate_window_pixels = {} self.calculate_window_ids = set() self.calculate_timer = 0 self.calculate_last_time = 0 #if we "proxy video", we will modify the video helper to add #new encoders, so we must make a deep copy to preserve the original #which may be used by other clients (other ServerSource instances) self.video_helper = getVideoHelper().clone() def init_from(self, _protocol, server): self.server_core_encodings = server.core_encodings self.server_encodings = server.encodings self.default_encoding = server.default_encoding self.scaling_control = server.scaling_control self.default_quality = server.default_quality self.default_min_quality = server.default_min_quality self.default_speed = server.default_speed self.default_min_speed = server.default_min_speed def cleanup(self): self.cancel_recalculate_timer() #Warning: this mixin must come AFTER the window mixin! #to make sure that it is safe to add the end of queue marker: #(all window sources will have stopped queuing data) self.queue_encode(None) #this should be a noop since we inherit an initialized helper: self.video_helper.cleanup() def get_caps(self): caps = {} if self.wants_encodings and self.encoding: caps["encoding"] = self.encoding if self.wants_features: caps.update({ "auto_refresh_delay": self.auto_refresh_delay, }) return caps def compressed_wrapper(self, datatype, data, min_saving=128): if self.zlib or self.lz4 or self.lzo: cw = compressed_wrapper(datatype, data, zlib=self.zlib, lz4=self.lz4, lzo=self.lzo, can_inline=False) if len(cw) + min_saving <= len(data): #the compressed version is smaller, use it: return cw #skip compressed version: fall through #we can't compress, so at least avoid warnings in the protocol layer: return Compressed(datatype, data, can_inline=True) def recalculate_delays(self): """ calls update_averages() on ServerSource.statistics (GlobalStatistics) and WindowSource.statistics (WindowPerformanceStatistics) for each window id in calculate_window_ids, this runs in the worker thread. """ self.calculate_timer = 0 if self.is_closed(): return now = monotonic_time() self.calculate_last_time = now p = self.protocol if not p: return conn = p._conn if not conn: return self.statistics.bytes_sent.append((now, conn.output_bytecount)) self.statistics.update_averages() self.update_bandwidth_limits() wids = tuple(self.calculate_window_ids ) #make a copy so we don't clobber new wids focus = self.get_focus() sources = self.window_sources.items() maximized_wids = [ wid for wid, source in sources if source is not None and source.maximized ] fullscreen_wids = [ wid for wid, source in sources if source is not None and source.fullscreen ] log( "recalculate_delays() wids=%s, focus=%s, maximized=%s, fullscreen=%s", wids, focus, maximized_wids, fullscreen_wids) for wid in wids: #this is safe because we only add to this set from other threads: self.calculate_window_ids.remove(wid) try: del self.calculate_window_pixels[wid] except: pass ws = self.window_sources.get(wid) if ws is None: continue try: ws.statistics.update_averages() ws.calculate_batch_delay( wid == focus, len(fullscreen_wids) > 0 and wid not in fullscreen_wids, len(maximized_wids) > 0 and wid not in maximized_wids) ws.reconfigure() except: log.error("error on window %s", wid, exc_info=True) if self.is_closed(): return #allow other threads to run #(ideally this would be a low priority thread) sleep(0) #calculate weighted average as new global default delay: wdimsum, wdelay, tsize, tcount = 0, 0, 0, 0 for ws in tuple(self.window_sources.values()): if ws.batch_config.last_updated <= 0: continue w, h = ws.window_dimensions tsize += w * h tcount += 1 time_w = 2.0 + (now - ws.batch_config.last_updated ) #add 2 seconds to even things out weight = w * h * time_w wdelay += ws.batch_config.delay * weight wdimsum += weight if wdimsum > 0 and tcount > 0: #weighted delay: avg_size = tsize / tcount wdelay = wdelay / wdimsum #store the delay as a normalized value per megapixel: delay = wdelay * 1000000 / avg_size self.global_batch_config.last_delays.append((now, delay)) self.global_batch_config.delay = delay def may_recalculate(self, wid, pixel_count): if wid in self.calculate_window_ids: return #already scheduled v = self.calculate_window_pixels.get(wid, 0) + pixel_count self.calculate_window_pixels[wid] = v if v < MIN_PIXEL_RECALCULATE: return #not enough pixel updates statslog( "may_recalculate(%i, %i) total %i pixels, scheduling recalculate work item", wid, pixel_count, v) self.calculate_window_ids.add(wid) if self.calculate_timer: #already due return delta = monotonic_time() - self.calculate_last_time RECALCULATE_DELAY = 1.0 #1s if delta > RECALCULATE_DELAY: add_work_item(self.recalculate_delays) else: self.calculate_timer = self.timeout_add( int(1000 * (RECALCULATE_DELAY - delta)), add_work_item, self.recalculate_delays) def cancel_recalculate_timer(self): ct = self.calculate_timer if ct: self.calculate_timer = 0 self.source_remove(ct) def parse_client_caps(self, c): #batch options: def batch_value(prop, default, minv=None, maxv=None): assert default is not None def parse_batch_int(value, varname): if value is not None: try: return int(value) except: log.error("invalid value for batch option %s: %s", varname, value) return None #from client caps first: cpname = "batch.%s" % prop v = parse_batch_int(c.get(cpname), cpname) #try env: if v is None: evname = "XPRA_BATCH_%s" % prop.upper() v = parse_batch_int(os.environ.get(evname), evname) #fallback to default: if v is None: v = default if minv is not None: v = max(minv, v) if maxv is not None: v = min(maxv, v) assert v is not None return v #general features: self.zlib = c.boolget("zlib", True) self.lz4 = c.boolget("lz4", False) and use_lz4 self.lzo = c.boolget("lzo", False) and use_lzo self.vrefresh = c.intget("vrefresh", -1) default_min_delay = max(DamageBatchConfig.MIN_DELAY, 1000 // (self.vrefresh or 60)) dbc = self.default_batch_config dbc.always = bool(batch_value("always", DamageBatchConfig.ALWAYS)) dbc.min_delay = batch_value("min_delay", default_min_delay, 0, 1000) dbc.max_delay = batch_value("max_delay", DamageBatchConfig.MAX_DELAY, 1, 15000) dbc.max_events = batch_value("max_events", DamageBatchConfig.MAX_EVENTS) dbc.max_pixels = batch_value("max_pixels", DamageBatchConfig.MAX_PIXELS) dbc.time_unit = batch_value("time_unit", DamageBatchConfig.TIME_UNIT, 1) dbc.delay = batch_value("delay", DamageBatchConfig.START_DELAY, 0) log("default batch config: %s", dbc) #encodings: self.encodings = c.strlistget("encodings") self.core_encodings = c.strlistget("encodings.core", self.encodings) if self.send_windows and not self.core_encodings: raise Exception("client failed to specify any supported encodings") if "png" in self.core_encodings: self.window_icon_encodings.append("png") self.window_icon_encodings = c.strlistget("encodings.window-icon", ["premult_argb32"]) self.rgb_formats = c.strlistget("encodings.rgb_formats", ["RGB"]) #skip all other encoding related settings if we don't send pixels: if not self.send_windows: log("windows/pixels forwarding is disabled for this client") else: self.parse_encoding_caps(c) def parse_encoding_caps(self, c): self.set_encoding(c.strget("encoding", None), None) #encoding options (filter): #1: these properties are special cased here because we #defined their name before the "encoding." prefix convention, #or because we want to pass default values (zlib/lz4): for k, ek in { "initial_quality": "initial_quality", "quality": "quality", }.items(): if k in c: self.encoding_options[ek] = c.intget(k) for k, ek in { "zlib": "rgb_zlib", "lz4": "rgb_lz4", }.items(): if k in c: self.encoding_options[ek] = c.boolget(k) #2: standardized encoding options: for k in c.keys(): if k.startswith(b"theme.") or k.startswith(b"encoding.icons."): self.icons_encoding_options[k.replace( b"encoding.icons.", b"").replace(b"theme.", b"")] = c[k] elif k.startswith(b"encoding."): stripped_k = k[len(b"encoding."):] if stripped_k in (b"transparency", b"rgb_zlib", b"rgb_lz4", b"rgb_lzo", b"video_scaling"): v = c.boolget(k) elif stripped_k in (b"initial_quality", b"initial_speed", b"min-quality", b"quality", b"min-speed", b"speed"): v = c.intget(k) else: v = c.get(k) self.encoding_options[stripped_k] = v log("encoding options: %s", self.encoding_options) log("icons encoding options: %s", self.icons_encoding_options) #handle proxy video: add proxy codec to video helper: pv = self.encoding_options.boolget("proxy.video") proxylog("proxy.video=%s", pv) if pv: #enabling video proxy: try: self.parse_proxy_video() except: proxylog.error("failed to parse proxy video", exc_info=True) sc = self.encoding_options.get("scaling.control", self.scaling_control) if sc is not None: #"encoding_options" are exposed via "xpra info", #so we can't have None values in there (bencoder would choke) self.default_encoding_options["scaling.control"] = sc q = self.encoding_options.intget("quality", self.default_quality) #0.7 onwards: if q > 0: self.default_encoding_options["quality"] = q mq = self.encoding_options.intget("min-quality", self.default_min_quality) if mq > 0 and (q <= 0 or q > mq): self.default_encoding_options["min-quality"] = mq s = self.encoding_options.intget("speed", self.default_speed) if s > 0: self.default_encoding_options["speed"] = s ms = self.encoding_options.intget("min-speed", self.default_min_speed) if ms > 0 and (s <= 0 or s > ms): self.default_encoding_options["min-speed"] = ms log("default encoding options: %s", self.default_encoding_options) self.auto_refresh_delay = c.intget("auto_refresh_delay", 0) #check for mmap: if getattr(self, "mmap_size", 0) == 0: others = [ x for x in self.core_encodings if x in self.server_core_encodings and x != self.encoding ] if self.encoding == "auto": s = "automatic picture encoding enabled" else: s = "using %s as primary encoding" % self.encoding if others: log.info(" %s, also available:", s) log.info(" %s", csv(others)) else: log.warn(" %s", s) log.warn(" no other encodings are available!") def parse_proxy_video(self): from xpra.codecs.enc_proxy.encoder import Encoder proxy_video_encodings = self.encoding_options.get( "proxy.video.encodings") proxylog("parse_proxy_video() proxy.video.encodings=%s", proxy_video_encodings) for encoding, colorspace_specs in proxy_video_encodings.items(): for colorspace, spec_props in colorspace_specs.items(): for spec_prop in spec_props: #make a new spec based on spec_props: spec = video_spec(codec_class=Encoder, codec_type="proxy", encoding=encoding) for k, v in spec_prop.items(): setattr(spec, k, v) proxylog("parse_proxy_video() adding: %s / %s / %s", encoding, colorspace, spec) self.video_helper.add_encoder_spec(encoding, colorspace, spec) ###################################################################### # Functions used by the server to request something # (window events, stats, user requests, etc) # def set_auto_refresh_delay(self, delay, window_ids): if window_ids is not None: wss = (self.window_sources.get(wid) for wid in window_ids) else: wss = self.window_sources.values() for ws in wss: if ws is not None: ws.set_auto_refresh_delay(delay) def set_encoding(self, encoding, window_ids, strict=False): """ Changes the encoder for the given 'window_ids', or for all windows if 'window_ids' is None. """ log("set_encoding(%s, %s, %s)", encoding, window_ids, strict) if not self.ui_client: return if encoding and encoding != "auto": #old clients (v0.9.x and earlier) only supported 'rgb24' as 'rgb' mode: if encoding == "rgb24": encoding = "rgb" if encoding not in self.encodings: log.warn("Warning: client specified '%s' encoding,", encoding) log.warn(" but it only supports: %s" % csv(self.encodings)) if encoding not in self.server_encodings: log.error("Error: encoding %s is not supported by this server", encoding) encoding = None if not encoding: encoding = "auto" if window_ids is not None: wss = [self.window_sources.get(wid) for wid in window_ids] else: wss = self.window_sources.values() #if we're updating all the windows, reset global stats too: if set(wss).issuperset(self.window_sources.values()): log("resetting global stats") self.statistics.reset() self.global_batch_config = self.default_batch_config.clone() for ws in wss: if ws is not None: ws.set_new_encoding(encoding, strict) if not window_ids: self.encoding = encoding def get_info(self): info = { "auto_refresh": self.auto_refresh_delay, "lz4": self.lz4, "lzo": self.lzo, "vertical-refresh": self.vrefresh, } ieo = dict(self.icons_encoding_options) try: del ieo["default.icons"] except: pass #encoding: info.update({ "encodings": { "": self.encodings, "core": self.core_encodings, "window-icon": self.window_icon_encodings, }, "icons": ieo, }) einfo = { "default": self.default_encoding or "", "defaults": self.default_encoding_options, "client-defaults": self.encoding_options, } info.setdefault("encoding", {}).update(einfo) return info def set_min_quality(self, min_quality): for ws in tuple(self.window_sources.values()): ws.set_min_quality(min_quality) def set_quality(self, quality): for ws in tuple(self.window_sources.values()): ws.set_quality(quality) def set_min_speed(self, min_speed): for ws in tuple(self.window_sources.values()): ws.set_min_speed(min_speed) def set_speed(self, speed): for ws in tuple(self.window_sources.values()): ws.set_speed(speed) def update_batch(self, wid, window, batch_props): ws = self.window_sources.get(wid) if ws: if "reset" in batch_props: ws.batch_config = self.make_batch_config(wid, window) for x in ("always", "locked"): if x in batch_props: setattr(ws.batch_config, x, batch_props.boolget(x)) for x in ("min_delay", "max_delay", "timeout_delay", "delay"): if x in batch_props: setattr(ws.batch_config, x, batch_props.intget(x)) log("batch config updated for window %s: %s", wid, ws.batch_config) def make_batch_config(self, wid, window): batch_config = self.default_batch_config.clone() batch_config.wid = wid #scale initial delay based on window size #(the global value is normalized to 1MPixel) #but use sqrt to smooth things and prevent excesses #(ie: a 4MPixel window, will start at 2 times the global delay) #(ie: a 0.5MPixel window will start at 0.7 times the global delay) w, h = window.get_dimensions() ratio = float(w * h) / 1000000 batch_config.delay = self.global_batch_config.delay * sqrt(ratio) return batch_config
def test_batch_config(self): now = monotonic_time() bc = DamageBatchConfig() bc.delay_per_megapixel = 100 bc.last_event = now for i in range(10): bc.last_delays.append((now - 10 + i, 10 + i)) bc.last_actual_delays.append((now - 10 + i, 5 + i)) bc.factors = (("name", {}, 1, 1), ) assert bc.get_info() assert repr(bc) bc.cleanup() clone = bc.clone() assert repr(clone) == repr(bc) i = bc.get_info() ci = clone.get_info() for k, v in ci.items(): assert v == i.get( k), "expected %s=%s, clone has %s=%s" % (k, i.get(k), k, v) bc.locked = True assert bc.get_info().get("delay") == bc.delay
class EncodingsMixin(StubSourceMixin): @classmethod def is_needed(cls, caps: typedict) -> bool: return bool(caps.strtupleget("encodings")) def __init__(self, *_args): pass def init_state(self): self.wants_encodings = False self.wants_features = False #contains default values, some of which may be supplied by the client: self.default_batch_config = DamageBatchConfig() self.global_batch_config = self.default_batch_config.clone( ) #global batch config self.vrefresh = -1 self.supports_transparency = False self.encoding = None #the default encoding for all windows self.encodings = () #all the encodings supported by the client self.core_encodings = () self.encodings_packet = False #supports delayed encodings initialization? self.window_icon_encodings = ["premult_argb32"] self.rgb_formats = ("RGB", ) self.encoding_options = typedict() self.icons_encoding_options = typedict() self.default_encoding_options = typedict() self.auto_refresh_delay = 0 self.zlib = True self.lz4 = use("lz4") self.lzo = use("lzo") #for managing the recalculate_delays work: self.calculate_window_pixels = {} self.calculate_window_ids = set() self.calculate_timer = 0 self.calculate_last_time = 0 #if we "proxy video", we will modify the video helper to add #new encoders, so we must make a deep copy to preserve the original #which may be used by other clients (other ServerSource instances) self.video_helper = getVideoHelper().clone() self.cuda_device_context = None def init_from(self, _protocol, server): self.server_core_encodings = server.core_encodings self.server_encodings = server.encodings self.default_encoding = server.default_encoding self.scaling_control = server.scaling_control self.default_quality = server.default_quality self.default_min_quality = server.default_min_quality self.default_speed = server.default_speed self.default_min_speed = server.default_min_speed def cleanup(self): self.cancel_recalculate_timer() #Warning: this mixin must come AFTER the window mixin! #to make sure that it is safe to add the end of queue marker: #(all window sources will have stopped queuing data) self.queue_encode(None) #this should be a noop since we inherit an initialized helper: self.video_helper.cleanup() cdd = self.cuda_device_context if cdd: self.cuda_device_context = None cdd.free() def all_window_sources(self): #we can't assume that the window mixin is loaded: window_sources = getattr(self, "window_sources", {}) return tuple(window_sources.values()) def get_caps(self) -> dict: caps = {} if self.wants_encodings and self.encoding: caps["encoding"] = self.encoding if self.wants_features: caps.update({ "auto_refresh_delay": self.auto_refresh_delay, }) return caps def recalculate_delays(self): """ calls update_averages() on ServerSource.statistics (GlobalStatistics) and WindowSource.statistics (WindowPerformanceStatistics) for each window id in calculate_window_ids, this runs in the worker thread. """ self.calculate_timer = 0 if self.is_closed(): return now = monotonic_time() self.calculate_last_time = now p = self.protocol if not p: return conn = p._conn if not conn: return #we can't assume that 'self' is a full ClientConnection object: stats = getattr(self, "statistics", None) if stats: stats.bytes_sent.append((now, conn.output_bytecount)) stats.update_averages() self.update_bandwidth_limits() wids = tuple(self.calculate_window_ids ) #make a copy so we don't clobber new wids focus = self.get_focus() sources = self.window_sources.items() maximized_wids = tuple(wid for wid, source in sources if source is not None and source.maximized) fullscreen_wids = tuple(wid for wid, source in sources if source is not None and source.fullscreen) log( "recalculate_delays() wids=%s, focus=%s, maximized=%s, fullscreen=%s", wids, focus, maximized_wids, fullscreen_wids) for wid in wids: #this is safe because we only add to this set from other threads: self.calculate_window_ids.remove(wid) self.calculate_window_pixels.pop(wid, None) ws = self.window_sources.get(wid) if ws is None: continue try: ws.statistics.update_averages() ws.calculate_batch_delay( wid == focus, len(fullscreen_wids) > 0 and wid not in fullscreen_wids, len(maximized_wids) > 0 and wid not in maximized_wids) ws.reconfigure() except Exception: log.error("error on window %s", wid, exc_info=True) if self.is_closed(): return #allow other threads to run #(ideally this would be a low priority thread) sleep(0) #calculate weighted average as new global default delay: wdimsum, wdelay, tsize, tcount = 0, 0, 0, 0 for ws in tuple(self.window_sources.values()): if ws.batch_config.last_updated <= 0: continue w, h = ws.window_dimensions tsize += w * h tcount += 1 time_w = 2.0 + (now - ws.batch_config.last_updated ) #add 2 seconds to even things out weight = int(w * h * time_w) wdelay += ws.batch_config.delay * weight wdimsum += weight if wdimsum > 0 and tcount > 0: #weighted delay: delay = wdelay // wdimsum self.global_batch_config.last_delays.append((now, delay)) self.global_batch_config.delay = delay #store the delay as a normalized value per megapixel #so we can adapt it to different window sizes: avg_size = tsize // tcount ratio = sqrt(1000000.0 / avg_size) normalized_delay = int(delay * ratio) self.global_batch_config.delay_per_megapixel = normalized_delay log( "delay_per_megapixel=%i, delay=%i, for wdelay=%i, avg_size=%i, ratio=%.2f", normalized_delay, delay, wdelay, avg_size, ratio) def may_recalculate(self, wid, pixel_count): if wid in self.calculate_window_ids: return #already scheduled v = self.calculate_window_pixels.get(wid, 0) + pixel_count self.calculate_window_pixels[wid] = v if v < MIN_PIXEL_RECALCULATE: return #not enough pixel updates statslog( "may_recalculate(%i, %i) total %i pixels, scheduling recalculate work item", wid, pixel_count, v) self.calculate_window_ids.add(wid) if self.calculate_timer: #already due return delta = monotonic_time() - self.calculate_last_time RECALCULATE_DELAY = 1.0 #1s if delta > RECALCULATE_DELAY: add_work_item(self.recalculate_delays) else: delay = int(1000 * (RECALCULATE_DELAY - delta)) self.calculate_timer = self.timeout_add(delay, add_work_item, self.recalculate_delays) def cancel_recalculate_timer(self): ct = self.calculate_timer if ct: self.calculate_timer = 0 self.source_remove(ct) def parse_client_caps(self, c: typedict): #batch options: def batch_value(prop, default, minv=None, maxv=None): assert default is not None def parse_batch_int(value, varname): if value is not None: try: return int(value) except (TypeError, ValueError): log.error( "Error: invalid value '%s' for batch option %s", value, varname) return None #from client caps first: cpname = "batch.%s" % prop v = parse_batch_int(c.get(cpname), cpname) #try env: if v is None: evname = "XPRA_BATCH_%s" % prop.upper() v = parse_batch_int(os.environ.get(evname), evname) #fallback to default: if v is None: v = default if minv is not None: v = max(minv, v) if maxv is not None: v = min(maxv, v) assert v is not None return v #general features: self.zlib = c.boolget("zlib", True) self.lz4 = c.boolget("lz4", False) and use("lz4") self.lzo = c.boolget("lzo", False) and use("lzo") self.brotli = c.boolget("brotli", False) and use("brotli") log("compressors: zlib=%s, lz4=%s, lzo=%s, brotli=%s", self.zlib, self.lz4, self.lzo, self.brotli) self.vrefresh = c.intget("vrefresh", -1) delay = DamageBatchConfig.START_DELAY vrefresh = DEFAULT_VREFRESH if MIN_VREFRESH <= self.vrefresh <= MAX_VREFRESH: #looks like a valid vrefresh value, use it: vrefresh = self.vrefresh delay = 1000 // vrefresh ms_per_frame = max(5, 1000 // vrefresh - 5) default_min_delay = max(DamageBatchConfig.MIN_DELAY, ms_per_frame) dbc = self.default_batch_config dbc.always = bool(batch_value("always", DamageBatchConfig.ALWAYS)) dbc.min_delay = batch_value("min_delay", default_min_delay, 0, 1000) dbc.max_delay = batch_value("max_delay", DamageBatchConfig.MAX_DELAY, 1, 15000) dbc.max_events = batch_value("max_events", DamageBatchConfig.MAX_EVENTS) dbc.max_pixels = batch_value("max_pixels", DamageBatchConfig.MAX_PIXELS) dbc.time_unit = batch_value("time_unit", DamageBatchConfig.TIME_UNIT, 1) dbc.delay = batch_value("delay", delay, 0) log("default batch config: %s", dbc) #encodings: self.encodings_packet = c.boolget("encodings.packet", False) self.encodings = c.strtupleget("encodings") self.core_encodings = c.strtupleget("encodings.core", self.encodings) log("encodings=%s, core_encodings=%s", self.encodings, self.core_encodings) #we can't assume that the window mixin is loaded, #or that the ui_client flag exists: send_ui = getattr(self, "ui_client", True) and getattr( self, "send_windows", True) if send_ui and not self.core_encodings: raise ClientException( "client failed to specify any supported encodings") self.window_icon_encodings = c.strtupleget("encodings.window-icon", ("premult_argb32", )) #try both spellings for older versions: for x in ( "encodings", "encoding", ): self.rgb_formats = c.strtupleget(x + ".rgb_formats", self.rgb_formats) #skip all other encoding related settings if we don't send pixels: if not send_ui: log("windows/pixels forwarding is disabled for this client") else: self.parse_encoding_caps(c) def parse_encoding_caps(self, c): self.set_encoding(c.strget("encoding", None), None) #encoding options (filter): #1: these properties are special cased here because we #defined their name before the "encoding." prefix convention, #or because we want to pass default values (zlib/lz4): for k, ek in { "initial_quality": "initial_quality", "quality": "quality", }.items(): if k in c: self.encoding_options[ek] = c.intget(k) for k, ek in { "zlib": "rgb_zlib", "lz4": "rgb_lz4", }.items(): if k in c: self.encoding_options[ek] = c.boolget(k) #2: standardized encoding options: for k in c.keys(): #yaml gives us str.. k = strtobytes(k) if k.startswith(b"theme.") or k.startswith(b"encoding.icons."): self.icons_encoding_options[k.replace(b"encoding.icons.", b"").replace( b"theme.", b"")] = c.get(k) elif k.startswith(b"encoding."): stripped_k = k[len(b"encoding."):] if stripped_k in ( b"transparency", b"rgb_zlib", b"rgb_lz4", b"rgb_lzo", ): v = c.boolget(k) elif stripped_k in (b"initial_quality", b"initial_speed", b"min-quality", b"quality", b"min-speed", b"speed"): v = c.intget(k) else: v = c.get(k) self.encoding_options[stripped_k] = v log("encoding options: %s", self.encoding_options) log("icons encoding options: %s", self.icons_encoding_options) #handle proxy video: add proxy codec to video helper: pv = self.encoding_options.boolget("proxy.video") proxylog("proxy.video=%s", pv) if pv: #enabling video proxy: try: self.parse_proxy_video() except Exception: proxylog.error("failed to parse proxy video", exc_info=True) sc = self.encoding_options.get("scaling.control", self.scaling_control) if sc is not None: #"encoding_options" are exposed via "xpra info", #so we can't have None values in there (bencoder would choke) self.default_encoding_options["scaling.control"] = sc q = self.encoding_options.intget("quality", self.default_quality) #0.7 onwards: if q > 0: self.default_encoding_options["quality"] = q mq = self.encoding_options.intget("min-quality", self.default_min_quality) if mq > 0 and (q <= 0 or q > mq): self.default_encoding_options["min-quality"] = mq s = self.encoding_options.intget("speed", self.default_speed) if s > 0: self.default_encoding_options["speed"] = s ms = self.encoding_options.intget("min-speed", self.default_min_speed) if ms > 0 and (s <= 0 or s > ms): self.default_encoding_options["min-speed"] = ms log("default encoding options: %s", self.default_encoding_options) self.auto_refresh_delay = c.intget("auto_refresh_delay", 0) #are we going to need a cuda context? common_encodings = tuple(x for x in self.encodings if x in self.server_encodings) from xpra.codecs.loader import has_codec if "jpeg" in common_encodings and has_codec("enc_nvjpeg"): from xpra.codecs.cuda_common.cuda_context import get_device_context self.cuda_device_context = get_device_context( self.encoding_options) log.warn("cuda_device_context=%s", self.cuda_device_context) def print_encoding_info(self): log( "print_encoding_info() core-encodings=%s, server-core-encodings=%s", self.core_encodings, self.server_core_encodings) others = tuple( x for x in self.core_encodings if x in self.server_core_encodings and x != self.encoding) if self.encoding == "auto": s = "automatic picture encoding enabled" else: s = "using %s as primary encoding" % self.encoding if others: log.info(" %s, also available:", s) log.info(" %s", csv(others)) else: log.warn(" %s", s) log.warn(" no other encodings are available!") def parse_proxy_video(self): self.wait_for_threaded_init() from xpra.codecs.enc_proxy.encoder import Encoder proxy_video_encodings = self.encoding_options.get( "proxy.video.encodings") proxylog("parse_proxy_video() proxy.video.encodings=%s", proxy_video_encodings) for encoding, colorspace_specs in proxy_video_encodings.items(): for colorspace, spec_props in colorspace_specs.items(): for spec_prop in spec_props: #make a new spec based on spec_props: spec_prop = typedict(spec_prop) input_colorspace = spec_prop.strget("input_colorspace") output_colorspaces = spec_prop.strtupleget( "output_colorspaces") if not input_colorspace or not output_colorspaces: log.warn("Warning: invalid proxy video encoding '%s':", encoding) log.warn(" missing colorspace attributes") continue spec = video_spec( codec_class=Encoder, has_lossless_mode=spec_prop.boolget( "has_lossless_mode", False), input_colorspace=input_colorspace, output_colorspaces=output_colorspaces, codec_type="proxy", encoding=encoding, ) for k, v in spec_prop.items(): if k.startswith("_") or not hasattr(spec, k): log.warn( "Warning: invalid proxy codec attribute '%s'", k) continue setattr(spec, k, v) proxylog("parse_proxy_video() adding: %s / %s / %s", encoding, colorspace, spec) self.video_helper.add_encoder_spec(encoding, colorspace, spec) ###################################################################### # Functions used by the server to request something # (window events, stats, user requests, etc) # def set_auto_refresh_delay(self, delay: int, window_ids): if window_ids is not None: wss = (self.window_sources.get(wid) for wid in window_ids) else: wss = self.all_window_sources() for ws in wss: if ws is not None: ws.set_auto_refresh_delay(delay) def set_encoding(self, encoding: str, window_ids, strict=False): """ Changes the encoder for the given 'window_ids', or for all windows if 'window_ids' is None. """ log("set_encoding(%s, %s, %s)", encoding, window_ids, strict) if encoding and encoding != "auto": #old clients (v0.9.x and earlier) only supported 'rgb24' as 'rgb' mode: if encoding == "rgb24": encoding = "rgb" if encoding not in self.encodings: log.warn("Warning: client specified '%s' encoding,", encoding) log.warn(" but it only supports: %s" % csv(self.encodings)) if encoding not in self.server_encodings: log.error("Error: encoding %s is not supported by this server", encoding) encoding = None if not encoding: encoding = "auto" if window_ids is not None: wss = [self.window_sources.get(wid) for wid in window_ids] else: wss = self.all_window_sources() #if we're updating all the windows, reset global stats too: if set(wss).issuperset(self.all_window_sources()): log("resetting global stats") #we can't assume that 'self' is a full ClientConnection object: stats = getattr(self, "statistics", None) if stats: stats.reset() self.global_batch_config = self.default_batch_config.clone() for ws in wss: if ws is not None: ws.set_new_encoding(encoding, strict) if not window_ids: self.encoding = encoding def get_info(self) -> dict: info = { "auto_refresh": self.auto_refresh_delay, "lz4": self.lz4, "lzo": self.lzo, "vertical-refresh": self.vrefresh, } ieo = dict(self.icons_encoding_options) ieo.pop("default.icons", None) #encoding: info.update({ "encodings": { "": self.encodings, "core": self.core_encodings, "window-icon": self.window_icon_encodings, }, "icons": ieo, }) einfo = { "default": self.default_encoding or "", "defaults": self.default_encoding_options, "client-defaults": self.encoding_options, } info.setdefault("encoding", {}).update(einfo) return info def set_min_quality(self, min_quality: int): for ws in tuple(self.all_window_sources()): ws.set_min_quality(min_quality) def set_quality(self, quality: int): for ws in tuple(self.all_window_sources()): ws.set_quality(quality) def set_min_speed(self, min_speed: int): for ws in tuple(self.all_window_sources()): ws.set_min_speed(min_speed) def set_speed(self, speed: int): for ws in tuple(self.all_window_sources()): ws.set_speed(speed) def update_batch(self, wid: int, window, batch_props): ws = self.window_sources.get(wid) if ws: if "reset" in batch_props: ws.batch_config = self.make_batch_config(wid, window) for x in ("always", "locked"): if x in batch_props: setattr(ws.batch_config, x, batch_props.boolget(x)) for x in ("min_delay", "max_delay", "timeout_delay", "delay"): if x in batch_props: setattr(ws.batch_config, x, batch_props.intget(x)) log("batch config updated for window %s: %s", wid, ws.batch_config) def make_batch_config(self, wid: int, window): batch_config = self.default_batch_config.clone() batch_config.wid = wid #scale initial delay based on window size #(the global value is normalized to 1MPixel) #but use sqrt to smooth things and prevent excesses #(ie: a 4MPixel window, will start at 2 times the global delay) #(ie: a 0.5MPixel window will start at 0.7 times the global delay) dpm = self.global_batch_config.delay_per_megapixel w, h = window.get_dimensions() if dpm >= 0: ratio = sqrt(1000000.0 / (w * h)) batch_config.delay = max( batch_config.min_delay, min(batch_config.max_delay, int(dpm * sqrt(ratio)))) log( "make_batch_config(%i, %s) global delay per megapixel=%i, new window delay for %ix%i=%s", wid, window, dpm, w, h, batch_config.delay) return batch_config