Hide keyboard shortcuts

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. 

7 

8import os 

9from math import sqrt 

10from time import sleep 

11 

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 

22 

23log = Logger("encoding") 

24proxylog = Logger("proxy") 

25statslog = Logger("stats") 

26 

27MIN_PIXEL_RECALCULATE = envint("XPRA_MIN_PIXEL_RECALCULATE", 2000) 

28 

29 

30""" 

31Store information about the client's support for encodings. 

32Runs the encode thread. 

33""" 

34class EncodingsMixin(StubSourceMixin): 

35 

36 @classmethod 

37 def is_needed(cls, caps : typedict) -> bool: 

38 return bool(caps.strtupleget("encodings")) 

39 

40 

41 def init_state(self): 

42 self.wants_encodings = False 

43 self.wants_features = False 

44 

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 

48 

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 

61 

62 self.zlib = True 

63 self.lz4 = use("lz4") 

64 self.lzo = use("lzo") 

65 

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 

71 

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() 

76 

77 

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 

87 

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() 

96 

97 

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()) 

102 

103 

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 

113 

114 

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) 

189 

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) 

209 

210 def cancel_recalculate_timer(self): 

211 ct = self.calculate_timer 

212 if ct: 

213 self.calculate_timer = 0 

214 self.source_remove(ct) 

215 

216 

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 

244 

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) 

252 

253 self.vrefresh = c.intget("vrefresh", -1) 

254 

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) 

270 

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) 

290 

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) 

328 

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) 

338 

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) 

358 

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!") 

374 

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) 

404 

405 

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) 

418 

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 

453 

454 

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 

480 

481 

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) 

485 

486 def set_quality(self, quality : int): 

487 for ws in tuple(self.all_window_sources()): 

488 ws.set_quality(quality) 

489 

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) 

493 

494 def set_speed(self, speed : int): 

495 for ws in tuple(self.all_window_sources()): 

496 ws.set_speed(speed) 

497 

498 

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) 

511 

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