Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/source/encodings_mixin.py : 76%
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) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
4# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
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 math import sqrt
10from time import sleep
12from xpra.server.source.stub_source_mixin import StubSourceMixin
13from xpra.server.window.batch_config import DamageBatchConfig
14from xpra.server.server_core import ClientException
15from xpra.codecs.video_helper import getVideoHelper
16from xpra.codecs.codec_constants import video_spec
17from xpra.net.compression import use
18from xpra.os_util import monotonic_time, strtobytes
19from xpra.server.background_worker import add_work_item
20from xpra.util import csv, typedict, envint
21from xpra.log import Logger
23log = Logger("encoding")
24proxylog = Logger("proxy")
25statslog = Logger("stats")
27MIN_PIXEL_RECALCULATE = envint("XPRA_MIN_PIXEL_RECALCULATE", 2000)
30"""
31Store information about the client's support for encodings.
32Runs the encode thread.
33"""
34class EncodingsMixin(StubSourceMixin):
36 @classmethod
37 def is_needed(cls, caps : typedict) -> bool:
38 return bool(caps.strtupleget("encodings"))
41 def init_state(self):
42 self.wants_encodings = False
43 self.wants_features = False
45 #contains default values, some of which may be supplied by the client:
46 self.default_batch_config = DamageBatchConfig()
47 self.global_batch_config = self.default_batch_config.clone() #global batch config
49 self.vrefresh = -1
50 self.supports_transparency = False
51 self.encoding = None #the default encoding for all windows
52 self.encodings = () #all the encodings supported by the client
53 self.core_encodings = ()
54 self.encodings_packet = False #supports delayed encodings initialization?
55 self.window_icon_encodings = ["premult_argb32"]
56 self.rgb_formats = ("RGB",)
57 self.encoding_options = typedict()
58 self.icons_encoding_options = typedict()
59 self.default_encoding_options = typedict()
60 self.auto_refresh_delay = 0
62 self.zlib = True
63 self.lz4 = use("lz4")
64 self.lzo = use("lzo")
66 #for managing the recalculate_delays work:
67 self.calculate_window_pixels = {}
68 self.calculate_window_ids = set()
69 self.calculate_timer = 0
70 self.calculate_last_time = 0
72 #if we "proxy video", we will modify the video helper to add
73 #new encoders, so we must make a deep copy to preserve the original
74 #which may be used by other clients (other ServerSource instances)
75 self.video_helper = getVideoHelper().clone()
78 def init_from(self, _protocol, server):
79 self.server_core_encodings = server.core_encodings
80 self.server_encodings = server.encodings
81 self.default_encoding = server.default_encoding
82 self.scaling_control = server.scaling_control
83 self.default_quality = server.default_quality
84 self.default_min_quality = server.default_min_quality
85 self.default_speed = server.default_speed
86 self.default_min_speed = server.default_min_speed
88 def cleanup(self):
89 self.cancel_recalculate_timer()
90 #Warning: this mixin must come AFTER the window mixin!
91 #to make sure that it is safe to add the end of queue marker:
92 #(all window sources will have stopped queuing data)
93 self.queue_encode(None)
94 #this should be a noop since we inherit an initialized helper:
95 self.video_helper.cleanup()
98 def all_window_sources(self):
99 #we can't assume that the window mixin is loaded:
100 window_sources = getattr(self, "window_sources", {})
101 return tuple(window_sources.values())
104 def get_caps(self) -> dict:
105 caps = {}
106 if self.wants_encodings and self.encoding:
107 caps["encoding"] = self.encoding
108 if self.wants_features:
109 caps.update({
110 "auto_refresh_delay" : self.auto_refresh_delay,
111 })
112 return caps
115 def recalculate_delays(self):
116 """ calls update_averages() on ServerSource.statistics (GlobalStatistics)
117 and WindowSource.statistics (WindowPerformanceStatistics) for each window id in calculate_window_ids,
118 this runs in the worker thread.
119 """
120 self.calculate_timer = 0
121 if self.is_closed():
122 return
123 now = monotonic_time()
124 self.calculate_last_time = now
125 p = self.protocol
126 if not p:
127 return
128 conn = p._conn
129 if not conn:
130 return
131 #we can't assume that 'self' is a full ClientConnection object:
132 stats = getattr(self, "statistics", None)
133 if stats:
134 stats.bytes_sent.append((now, conn.output_bytecount))
135 stats.update_averages()
136 self.update_bandwidth_limits()
137 wids = tuple(self.calculate_window_ids) #make a copy so we don't clobber new wids
138 focus = self.get_focus()
139 sources = self.window_sources.items()
140 maximized_wids = tuple(wid for wid, source in sources if source is not None and source.maximized)
141 fullscreen_wids = tuple(wid for wid, source in sources if source is not None and source.fullscreen)
142 log("recalculate_delays() wids=%s, focus=%s, maximized=%s, fullscreen=%s",
143 wids, focus, maximized_wids, fullscreen_wids)
144 for wid in wids:
145 #this is safe because we only add to this set from other threads:
146 self.calculate_window_ids.remove(wid)
147 self.calculate_window_pixels.pop(wid, None)
148 ws = self.window_sources.get(wid)
149 if ws is None:
150 continue
151 try:
152 ws.statistics.update_averages()
153 ws.calculate_batch_delay(wid==focus,
154 len(fullscreen_wids)>0 and wid not in fullscreen_wids,
155 len(maximized_wids)>0 and wid not in maximized_wids)
156 ws.reconfigure()
157 except Exception:
158 log.error("error on window %s", wid, exc_info=True)
159 if self.is_closed():
160 return
161 #allow other threads to run
162 #(ideally this would be a low priority thread)
163 sleep(0)
164 #calculate weighted average as new global default delay:
165 wdimsum, wdelay, tsize, tcount = 0, 0, 0, 0
166 for ws in tuple(self.window_sources.values()):
167 if ws.batch_config.last_updated<=0:
168 continue
169 w, h = ws.window_dimensions
170 tsize += w*h
171 tcount += 1
172 time_w = 2.0+(now-ws.batch_config.last_updated) #add 2 seconds to even things out
173 weight = int(w*h*time_w)
174 wdelay += ws.batch_config.delay*weight
175 wdimsum += weight
176 if wdimsum>0 and tcount>0:
177 #weighted delay:
178 delay = wdelay // wdimsum
179 self.global_batch_config.last_delays.append((now, delay))
180 self.global_batch_config.delay = delay
181 #store the delay as a normalized value per megapixel
182 #so we can adapt it to different window sizes:
183 avg_size = tsize // tcount
184 ratio = sqrt(1000000.0 / avg_size)
185 normalized_delay = int(delay * ratio)
186 self.global_batch_config.delay_per_megapixel = normalized_delay
187 log("delay_per_megapixel=%i, delay=%i, for wdelay=%i, avg_size=%i, ratio=%.2f",
188 normalized_delay, delay, wdelay, avg_size, ratio)
190 def may_recalculate(self, wid, pixel_count):
191 if wid in self.calculate_window_ids:
192 return #already scheduled
193 v = self.calculate_window_pixels.get(wid, 0)+pixel_count
194 self.calculate_window_pixels[wid] = v
195 if v<MIN_PIXEL_RECALCULATE:
196 return #not enough pixel updates
197 statslog("may_recalculate(%i, %i) total %i pixels, scheduling recalculate work item", wid, pixel_count, v)
198 self.calculate_window_ids.add(wid)
199 if self.calculate_timer:
200 #already due
201 return
202 delta = monotonic_time() - self.calculate_last_time
203 RECALCULATE_DELAY = 1.0 #1s
204 if delta>RECALCULATE_DELAY:
205 add_work_item(self.recalculate_delays)
206 else:
207 delay = int(1000*(RECALCULATE_DELAY-delta))
208 self.calculate_timer = self.timeout_add(delay, add_work_item, self.recalculate_delays)
210 def cancel_recalculate_timer(self):
211 ct = self.calculate_timer
212 if ct:
213 self.calculate_timer = 0
214 self.source_remove(ct)
217 def parse_client_caps(self, c : typedict):
218 #batch options:
219 def batch_value(prop, default, minv=None, maxv=None):
220 assert default is not None
221 def parse_batch_int(value, varname):
222 if value is not None:
223 try:
224 return int(value)
225 except (TypeError, ValueError):
226 log.error("Error: invalid value '%s' for batch option %s", value, varname)
227 return None
228 #from client caps first:
229 cpname = "batch.%s" % prop
230 v = parse_batch_int(c.get(cpname), cpname)
231 #try env:
232 if v is None:
233 evname = "XPRA_BATCH_%s" % prop.upper()
234 v = parse_batch_int(os.environ.get(evname), evname)
235 #fallback to default:
236 if v is None:
237 v = default
238 if minv is not None:
239 v = max(minv, v)
240 if maxv is not None:
241 v = min(maxv, v)
242 assert v is not None
243 return v
245 #general features:
246 self.zlib = c.boolget("zlib", True)
247 self.lz4 = c.boolget("lz4", False) and use("lz4")
248 self.lzo = c.boolget("lzo", False) and use("lzo")
249 self.brotli = c.boolget("brotli", False) and use("brotli")
250 log("compressors: zlib=%s, lz4=%s, lzo=%s, brotli=%s",
251 self.zlib, self.lz4, self.lzo, self.brotli)
253 self.vrefresh = c.intget("vrefresh", -1)
255 #assume 50Hz:
256 ms_per_frame = 1000//50
257 if 30<=self.vrefresh<=500:
258 #looks like a valid vrefresh value, use it:
259 ms_per_frame = 1000//self.vrefresh
260 default_min_delay = max(DamageBatchConfig.MIN_DELAY, ms_per_frame)
261 dbc = self.default_batch_config
262 dbc.always = bool(batch_value("always", DamageBatchConfig.ALWAYS))
263 dbc.min_delay = batch_value("min_delay", default_min_delay, 0, 1000)
264 dbc.max_delay = batch_value("max_delay", DamageBatchConfig.MAX_DELAY, 1, 15000)
265 dbc.max_events = batch_value("max_events", DamageBatchConfig.MAX_EVENTS)
266 dbc.max_pixels = batch_value("max_pixels", DamageBatchConfig.MAX_PIXELS)
267 dbc.time_unit = batch_value("time_unit", DamageBatchConfig.TIME_UNIT, 1)
268 dbc.delay = batch_value("delay", DamageBatchConfig.START_DELAY, 0)
269 log("default batch config: %s", dbc)
271 #encodings:
272 self.encodings_packet = c.boolget("encodings.packet", False)
273 self.encodings = c.strtupleget("encodings")
274 self.core_encodings = c.strtupleget("encodings.core", self.encodings)
275 log("encodings=%s, core_encodings=%s", self.encodings, self.core_encodings)
276 #we can't assume that the window mixin is loaded,
277 #or that the ui_client flag exists:
278 send_ui = getattr(self, "ui_client", True) and getattr(self, "send_windows", True)
279 if send_ui and not self.core_encodings:
280 raise ClientException("client failed to specify any supported encodings")
281 self.window_icon_encodings = c.strtupleget("encodings.window-icon", ("premult_argb32",))
282 #try both spellings for older versions:
283 for x in ("encodings", "encoding",):
284 self.rgb_formats = c.strtupleget(x+".rgb_formats", self.rgb_formats)
285 #skip all other encoding related settings if we don't send pixels:
286 if not send_ui:
287 log("windows/pixels forwarding is disabled for this client")
288 else:
289 self.parse_encoding_caps(c)
291 def parse_encoding_caps(self, c):
292 self.set_encoding(c.strget("encoding", None), None)
293 #encoding options (filter):
294 #1: these properties are special cased here because we
295 #defined their name before the "encoding." prefix convention,
296 #or because we want to pass default values (zlib/lz4):
297 for k,ek in {"initial_quality" : "initial_quality",
298 "quality" : "quality",
299 }.items():
300 if k in c:
301 self.encoding_options[ek] = c.intget(k)
302 for k,ek in {"zlib" : "rgb_zlib",
303 "lz4" : "rgb_lz4",
304 }.items():
305 if k in c:
306 self.encoding_options[ek] = c.boolget(k)
307 #2: standardized encoding options:
308 for k in c.keys():
309 #yaml gives us str..
310 k = strtobytes(k)
311 if k.startswith(b"theme.") or k.startswith(b"encoding.icons."):
312 self.icons_encoding_options[k.replace(b"encoding.icons.", b"").replace(b"theme.", b"")] = c.get(k)
313 elif k.startswith(b"encoding."):
314 stripped_k = k[len(b"encoding."):]
315 if stripped_k in (b"transparency",
316 b"rgb_zlib", b"rgb_lz4", b"rgb_lzo",
317 ):
318 v = c.boolget(k)
319 elif stripped_k in (b"initial_quality", b"initial_speed",
320 b"min-quality", b"quality",
321 b"min-speed", b"speed"):
322 v = c.intget(k)
323 else:
324 v = c.get(k)
325 self.encoding_options[stripped_k] = v
326 log("encoding options: %s", self.encoding_options)
327 log("icons encoding options: %s", self.icons_encoding_options)
329 #handle proxy video: add proxy codec to video helper:
330 pv = self.encoding_options.boolget("proxy.video")
331 proxylog("proxy.video=%s", pv)
332 if pv:
333 #enabling video proxy:
334 try:
335 self.parse_proxy_video()
336 except Exception:
337 proxylog.error("failed to parse proxy video", exc_info=True)
339 sc = self.encoding_options.get("scaling.control", self.scaling_control)
340 if sc is not None:
341 #"encoding_options" are exposed via "xpra info",
342 #so we can't have None values in there (bencoder would choke)
343 self.default_encoding_options["scaling.control"] = sc
344 q = self.encoding_options.intget("quality", self.default_quality) #0.7 onwards:
345 if q>0:
346 self.default_encoding_options["quality"] = q
347 mq = self.encoding_options.intget("min-quality", self.default_min_quality)
348 if mq>0 and (q<=0 or q>mq):
349 self.default_encoding_options["min-quality"] = mq
350 s = self.encoding_options.intget("speed", self.default_speed)
351 if s>0:
352 self.default_encoding_options["speed"] = s
353 ms = self.encoding_options.intget("min-speed", self.default_min_speed)
354 if ms>0 and (s<=0 or s>ms):
355 self.default_encoding_options["min-speed"] = ms
356 log("default encoding options: %s", self.default_encoding_options)
357 self.auto_refresh_delay = c.intget("auto_refresh_delay", 0)
359 def print_encoding_info(self):
360 log("print_encoding_info() core-encodings=%s, server-core-encodings=%s",
361 self.core_encodings, self.server_core_encodings)
362 others = tuple(x for x in self.core_encodings
363 if x in self.server_core_encodings and x!=self.encoding)
364 if self.encoding=="auto":
365 s = "automatic picture encoding enabled"
366 else:
367 s = "using %s as primary encoding" % self.encoding
368 if others:
369 log.info(" %s, also available:", s)
370 log.info(" %s", csv(others))
371 else:
372 log.warn(" %s", s)
373 log.warn(" no other encodings are available!")
375 def parse_proxy_video(self):
376 self.wait_for_threaded_init()
377 from xpra.codecs.enc_proxy.encoder import Encoder
378 proxy_video_encodings = self.encoding_options.get("proxy.video.encodings")
379 proxylog("parse_proxy_video() proxy.video.encodings=%s", proxy_video_encodings)
380 for encoding, colorspace_specs in proxy_video_encodings.items():
381 for colorspace, spec_props in colorspace_specs.items():
382 for spec_prop in spec_props:
383 #make a new spec based on spec_props:
384 spec_prop = typedict(spec_prop)
385 input_colorspace = spec_prop.strget("input_colorspace")
386 output_colorspaces = spec_prop.strtupleget("output_colorspaces")
387 if not input_colorspace or not output_colorspaces:
388 log.warn("Warning: invalid proxy video encoding '%s':", encoding)
389 log.warn(" missing colorspace attributes")
390 continue
391 spec = video_spec(codec_class=Encoder,
392 has_lossless_mode=spec_prop.boolget("has_lossless_mode", False),
393 input_colorspace=input_colorspace,
394 output_colorspaces=output_colorspaces,
395 codec_type="proxy", encoding=encoding,
396 )
397 for k,v in spec_prop.items():
398 if k.startswith("_") or not hasattr(spec, k):
399 log.warn("Warning: invalid proxy codec attribute '%s'", k)
400 continue
401 setattr(spec, k, v)
402 proxylog("parse_proxy_video() adding: %s / %s / %s", encoding, colorspace, spec)
403 self.video_helper.add_encoder_spec(encoding, colorspace, spec)
406 ######################################################################
407 # Functions used by the server to request something
408 # (window events, stats, user requests, etc)
409 #
410 def set_auto_refresh_delay(self, delay : int, window_ids):
411 if window_ids is not None:
412 wss = (self.window_sources.get(wid) for wid in window_ids)
413 else:
414 wss = self.all_window_sources()
415 for ws in wss:
416 if ws is not None:
417 ws.set_auto_refresh_delay(delay)
419 def set_encoding(self, encoding : str, window_ids, strict=False):
420 """ Changes the encoder for the given 'window_ids',
421 or for all windows if 'window_ids' is None.
422 """
423 log("set_encoding(%s, %s, %s)", encoding, window_ids, strict)
424 if encoding and encoding!="auto":
425 #old clients (v0.9.x and earlier) only supported 'rgb24' as 'rgb' mode:
426 if encoding=="rgb24":
427 encoding = "rgb"
428 if encoding not in self.encodings:
429 log.warn("Warning: client specified '%s' encoding,", encoding)
430 log.warn(" but it only supports: %s" % csv(self.encodings))
431 if encoding not in self.server_encodings:
432 log.error("Error: encoding %s is not supported by this server", encoding)
433 encoding = None
434 if not encoding:
435 encoding = "auto"
436 if window_ids is not None:
437 wss = [self.window_sources.get(wid) for wid in window_ids]
438 else:
439 wss = self.all_window_sources()
440 #if we're updating all the windows, reset global stats too:
441 if set(wss).issuperset(self.all_window_sources()):
442 log("resetting global stats")
443 #we can't assume that 'self' is a full ClientConnection object:
444 stats = getattr(self, "statistics", None)
445 if stats:
446 stats.reset()
447 self.global_batch_config = self.default_batch_config.clone()
448 for ws in wss:
449 if ws is not None:
450 ws.set_new_encoding(encoding, strict)
451 if not window_ids:
452 self.encoding = encoding
455 def get_info(self) -> dict:
456 info = {
457 "auto_refresh" : self.auto_refresh_delay,
458 "lz4" : self.lz4,
459 "lzo" : self.lzo,
460 "vertical-refresh" : self.vrefresh,
461 }
462 ieo = dict(self.icons_encoding_options)
463 ieo.pop("default.icons", None)
464 #encoding:
465 info.update({
466 "encodings" : {
467 "" : self.encodings,
468 "core" : self.core_encodings,
469 "window-icon" : self.window_icon_encodings,
470 },
471 "icons" : ieo,
472 })
473 einfo = {
474 "default" : self.default_encoding or "",
475 "defaults" : self.default_encoding_options,
476 "client-defaults" : self.encoding_options,
477 }
478 info.setdefault("encoding", {}).update(einfo)
479 return info
482 def set_min_quality(self, min_quality : int):
483 for ws in tuple(self.all_window_sources()):
484 ws.set_min_quality(min_quality)
486 def set_quality(self, quality : int):
487 for ws in tuple(self.all_window_sources()):
488 ws.set_quality(quality)
490 def set_min_speed(self, min_speed : int):
491 for ws in tuple(self.all_window_sources()):
492 ws.set_min_speed(min_speed)
494 def set_speed(self, speed : int):
495 for ws in tuple(self.all_window_sources()):
496 ws.set_speed(speed)
499 def update_batch(self, wid : int, window, batch_props):
500 ws = self.window_sources.get(wid)
501 if ws:
502 if "reset" in batch_props:
503 ws.batch_config = self.make_batch_config(wid, window)
504 for x in ("always", "locked"):
505 if x in batch_props:
506 setattr(ws.batch_config, x, batch_props.boolget(x))
507 for x in ("min_delay", "max_delay", "timeout_delay", "delay"):
508 if x in batch_props:
509 setattr(ws.batch_config, x, batch_props.intget(x))
510 log("batch config updated for window %s: %s", wid, ws.batch_config)
512 def make_batch_config(self, wid : int, window):
513 batch_config = self.default_batch_config.clone()
514 batch_config.wid = wid
515 #scale initial delay based on window size
516 #(the global value is normalized to 1MPixel)
517 #but use sqrt to smooth things and prevent excesses
518 #(ie: a 4MPixel window, will start at 2 times the global delay)
519 #(ie: a 0.5MPixel window will start at 0.7 times the global delay)
520 dpm = self.global_batch_config.delay_per_megapixel
521 w, h = window.get_dimensions()
522 if dpm>=0:
523 ratio = sqrt(1000000.0 / (w*h))
524 batch_config.delay = max(batch_config.min_delay, min(batch_config.max_delay, int(dpm * sqrt(ratio))))
525 log("make_batch_config(%i, %s) global delay per megapixel=%i, new window delay for %ix%i=%s",
526 wid, window, dpm, w, h, batch_config.delay)
527 return batch_config