Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/mixins/encodings.py : 77%
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# This file is part of Xpra.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2019 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7import os
9from xpra.codecs.codec_constants import PREFERRED_ENCODING_ORDER, PROBLEMATIC_ENCODINGS
10from xpra.codecs.loader import load_codec, codec_versions, has_codec, get_codec
11from xpra.codecs.video_helper import getVideoHelper, NO_GFX_CSC_OPTIONS
12from xpra.scripts.config import parse_bool_or_int
13from xpra.net import compression
14from xpra.util import envint, envbool, updict, csv, typedict
15from xpra.client.mixins.stub_client_mixin import StubClientMixin
16from xpra.log import Logger
18log = Logger("client", "encoding")
20B_FRAMES = envbool("XPRA_B_FRAMES", True)
21PAINT_FLUSH = envbool("XPRA_PAINT_FLUSH", True)
22MAX_SOFT_EXPIRED = envint("XPRA_MAX_SOFT_EXPIRED", 5)
23SEND_TIMESTAMPS = envbool("XPRA_SEND_TIMESTAMPS", False)
24VIDEO_MAX_SIZE = tuple(int(x) for x in os.environ.get("XPRA_VIDEO_MAX_SIZE", "4096,4096").replace("x", ",").split(","))
25SCROLL_ENCODING = envbool("XPRA_SCROLL_ENCODING", True)
27#we assume that any server will support at least those:
28DEFAULT_ENCODINGS = os.environ.get("XPRA_DEFAULT_ENCODINGS", "rgb32,rgb24,jpeg,png").split(",")
31"""
32Mixin for adding encodings to a client
33"""
34class Encodings(StubClientMixin):
36 def __init__(self):
37 StubClientMixin.__init__(self)
38 self.allowed_encodings = []
39 self.encoding = None
40 self.quality = -1
41 self.min_quality = 0
42 self.speed = 0
43 self.min_speed = -1
44 self.video_scaling = None
45 self.video_max_size = VIDEO_MAX_SIZE
47 self.server_encodings = []
48 self.server_core_encodings = []
49 self.server_encodings_problematic = PROBLEMATIC_ENCODINGS
50 self.server_encodings_with_speed = ()
51 self.server_encodings_with_quality = ()
52 self.server_encodings_with_lossless_mode = ()
54 #what we told the server about our encoding defaults:
55 self.encoding_defaults = {}
58 def init(self, opts):
59 self.allowed_encodings = opts.encodings
60 self.encoding = opts.encoding
61 if opts.video_scaling.lower() in ("auto", "on"):
62 self.video_scaling = None
63 else:
64 self.video_scaling = parse_bool_or_int("video-scaling", opts.video_scaling)
65 self.quality = opts.quality
66 self.min_quality = opts.min_quality
67 self.speed = opts.speed
68 self.min_speed = opts.min_speed
69 load_codec("dec_pillow")
70 ae = self.allowed_encodings
71 if "jpeg" in ae:
72 #try to load the fast jpeg encoder:
73 load_codec("dec_jpeg")
74 if "webp" in ae:
75 #try to load the fast webp encoder:
76 load_codec("dec_webp")
77 vh = getVideoHelper()
78 vh.set_modules(video_decoders=opts.video_decoders, csc_modules=opts.csc_modules or NO_GFX_CSC_OPTIONS)
79 vh.init()
82 def cleanup(self):
83 try:
84 getVideoHelper().cleanup()
85 except Exception: # pragma: no cover
86 log.error("error on video cleanup", exc_info=True)
89 def init_authenticated_packet_handlers(self):
90 self.add_packet_handler("encodings", self._process_encodings, False)
93 def _process_encodings(self, packet):
94 caps = typedict(packet[1])
95 self._parse_server_capabilities(caps)
98 def get_info(self):
99 return {
100 "encodings" : {
101 "core" : self.get_core_encodings(),
102 "window-icon" : self.get_window_icon_encodings(),
103 "cursor" : self.get_cursor_encodings(),
104 "quality" : self.quality,
105 "min-quality" : self.min_quality,
106 "speed" : self.speed,
107 "min-speed" : self.min_speed,
108 "encoding" : self.encoding or "auto",
109 "video-scaling" : self.video_scaling if self.video_scaling is not None else "auto",
110 },
111 "server-encodings" : self.server_core_encodings,
112 }
115 def get_caps(self) -> dict:
116 caps = {
117 "encodings" : self.get_encodings(),
118 "encodings.core" : self.get_core_encodings(),
119 "encodings.window-icon" : self.get_window_icon_encodings(),
120 "encodings.cursor" : self.get_cursor_encodings(),
121 "encodings.packet" : True,
122 }
123 updict(caps, "batch", self.get_batch_caps())
124 updict(caps, "encoding", self.get_encodings_caps())
125 return caps
127 def parse_server_capabilities(self, caps : typedict) -> bool:
128 self._parse_server_capabilities(caps)
129 return True
131 def _parse_server_capabilities(self, c):
132 self.server_encodings = c.strtupleget("encodings", DEFAULT_ENCODINGS)
133 self.server_core_encodings = c.strtupleget("encodings.core", self.server_encodings)
134 #server is telling us to try to avoid those:
135 self.server_encodings_problematic = c.strtupleget("encodings.problematic", PROBLEMATIC_ENCODINGS)
136 #old servers only supported x264:
137 self.server_encodings_with_speed = c.strtupleget("encodings.with_speed", ("h264",))
138 self.server_encodings_with_quality = c.strtupleget("encodings.with_quality", ("jpeg", "webp", "h264"))
139 self.server_encodings_with_lossless_mode = c.strtupleget("encodings.with_lossless_mode", ())
140 e = c.strget("encoding")
141 if e and not c.boolget("encodings.delayed"):
142 if self.encoding and e!=self.encoding:
143 if self.encoding not in self.server_core_encodings:
144 log.warn("server does not support %s encoding and has switched to %s", self.encoding, e)
145 else:
146 log.info("server is using %s encoding instead of %s", e, self.encoding)
147 self.encoding = e
150 def get_batch_caps(self) -> dict:
151 #batch options:
152 caps = {}
153 for bprop in ("always", "min_delay", "max_delay", "delay", "max_events", "max_pixels", "time_unit"):
154 evalue = os.environ.get("XPRA_BATCH_%s" % bprop.upper())
155 if evalue:
156 try:
157 caps["batch.%s" % bprop] = int(evalue)
158 except ValueError:
159 log.error("Error: invalid environment value for %s: %s", bprop, evalue)
160 log("get_batch_caps()=%s", caps)
161 return caps
163 def get_encodings_caps(self) -> dict:
164 if B_FRAMES:
165 video_b_frames = ("h264", ) #only tested with dec_avcodec2
166 else:
167 video_b_frames = ()
168 caps = {
169 "flush" : PAINT_FLUSH, #v4 servers assume this is available
170 "video_scaling" : True, #v4 servers assume this is available
171 "video_b_frames" : video_b_frames,
172 "video_max_size" : self.video_max_size,
173 "max-soft-expired" : MAX_SOFT_EXPIRED,
174 "send-timestamps" : SEND_TIMESTAMPS,
175 }
176 if self.video_scaling is not None:
177 caps["scaling.control"] = self.video_scaling
178 if self.encoding:
179 caps[""] = self.encoding
180 for k,v in codec_versions.items():
181 caps["%s.version" % k] = v
182 if self.quality>0:
183 caps["quality"] = self.quality
184 if self.min_quality>0:
185 caps["min-quality"] = self.min_quality
186 if self.speed>=0:
187 caps["speed"] = self.speed
188 if self.min_speed>=0:
189 caps["min-speed"] = self.min_speed
191 #generic rgb compression flags:
192 for x in compression.ALL_COMPRESSORS:
193 caps["rgb_%s" % x] = x in compression.get_enabled_compressors()
194 #these are the defaults - when we instantiate a window,
195 #we can send different values as part of the map event
196 #these are the RGB modes we want (the ones we are expected to be able to paint with):
197 rgb_formats = ["RGB", "RGBX", "RGBA"]
198 caps["rgb_formats"] = rgb_formats
199 #figure out which CSC modes (usually YUV) can give us those RGB modes:
200 full_csc_modes = getVideoHelper().get_server_full_csc_modes_for_rgb(*rgb_formats)
201 if has_codec("dec_webp"):
202 if self.opengl_enabled:
203 full_csc_modes["webp"] = ("BGRX", "BGRA", "RGBX", "RGBA")
204 else:
205 full_csc_modes["webp"] = ("BGRX", "BGRA", )
206 log("supported full csc_modes=%s", full_csc_modes)
207 caps["full_csc_modes"] = full_csc_modes
209 if "h264" in self.get_core_encodings():
210 # some profile options: "baseline", "main", "high", "high10", ...
211 # set the default to "high10" for YUV420P
212 # as the python client always supports all the profiles
213 # whereas on the server side, the default is baseline to accomodate less capable clients.
214 # YUV422P requires high422, and
215 # YUV444P requires high444,
216 # so we don't bother specifying anything for those two.
217 h264_caps = {}
218 for csc_name, default_profile in (
219 ("YUV420P", "high10"),
220 ("YUV422P", ""),
221 ("YUV444P", "")):
222 profile = os.environ.get("XPRA_H264_%s_PROFILE" % (csc_name), default_profile)
223 if profile:
224 h264_caps["%s.profile" % (csc_name)] = profile
225 h264_caps["fast-decode"] = envbool("XPRA_X264_FAST_DECODE", False)
226 log("x264 encoding options: %s", h264_caps)
227 updict(caps, "h264", h264_caps)
228 log("encoding capabilities: %s", caps)
229 return caps
231 def get_encodings(self):
232 """
233 Unlike get_core_encodings(), this method returns "rgb" for both "rgb24" and "rgb32".
234 That's because although we may support both, the encoding chosen is plain "rgb",
235 and the actual encoding used ("rgb24" or "rgb32") depends on the window's bit depth.
236 ("rgb32" if there is an alpha channel, and if the client supports it)
237 """
238 cenc = self.get_core_encodings()
239 if ("rgb24" in cenc or "rgb32" in cenc) and "rgb" not in cenc:
240 cenc.append("rgb")
241 if "grayscale" not in cenc:
242 cenc.append("grayscale")
243 return [x for x in PREFERRED_ENCODING_ORDER if x in cenc and x not in ("rgb32", "rgb24")]
245 def get_cursor_encodings(self):
246 e = ["raw"]
247 if "png" in self.get_core_encodings():
248 e.append("png")
249 return e
251 def get_window_icon_encodings(self):
252 e = ["premult_argb32", "BGRA", "default"]
253 if "png" in self.get_core_encodings():
254 e.append("png")
255 return e
257 def get_core_encodings(self):
258 """
259 This method returns the actual encodings supported.
260 ie: ["rgb24", "vp8", "webp", "png", "png/L", "png/P", "jpeg", "h264", "vpx"]
261 It is often overriden in the actual client class implementations,
262 where extra encodings can be added (generally just 'rgb32' for transparency),
263 or removed if the toolkit implementation class is more limited.
264 """
265 #we always support rgb:
266 core_encodings = ["rgb24", "rgb32"]
267 for codec in ("dec_pillow", "dec_webp", "dec_jpeg"):
268 if has_codec(codec):
269 c = get_codec(codec)
270 encs = c.get_encodings()
271 log("%s.get_encodings()=%s", codec, encs)
272 for e in encs:
273 if e not in core_encodings:
274 core_encodings.append(e)
275 if SCROLL_ENCODING:
276 core_encodings.append("scroll")
277 #we enable all the video decoders we know about,
278 #what will actually get used by the server will still depend on the csc modes supported
279 video_decodings = getVideoHelper().get_decodings()
280 log("video_decodings=%s", video_decodings)
281 for encoding in video_decodings:
282 if encoding not in core_encodings:
283 core_encodings.append(encoding)
284 #remove duplicates and use prefered encoding order:
285 core_encodings = [x for x in PREFERRED_ENCODING_ORDER
286 if x in set(core_encodings) and x in self.allowed_encodings]
287 return core_encodings
289 def set_encoding(self, encoding):
290 log("set_encoding(%s)", encoding)
291 if encoding=="auto":
292 self.encoding = ""
293 else:
294 assert encoding in self.get_encodings(), "encoding %s is not supported!" % encoding
295 if encoding not in self.server_encodings:
296 log.error("Error: encoding %s is not supported by the server", encoding)
297 log.error(" the only encodings allowed are:")
298 log.error(" %s", csv(self.server_encodings))
299 return
300 self.encoding = encoding
301 self.send("encoding", self.encoding)
303 def send_quality(self):
304 q = self.quality
305 log("send_quality() quality=%s", q)
306 assert q==-1 or 0<=q<=100, "invalid quality: %s" % q
307 self.send("quality", q)
309 def send_min_quality(self):
310 q = self.min_quality
311 log("send_min_quality() min-quality=%s", q)
312 assert q==-1 or 0<=q<=100, "invalid min-quality: %s" % q
313 self.send("min-quality", q)
315 def send_speed(self):
316 s = self.speed
317 log("send_speed() min-speed=%s", s)
318 assert s==-1 or 0<=s<=100, "invalid speed: %s" % s
319 self.send("speed", s)
321 def send_min_speed(self):
322 s = self.min_speed
323 log("send_min_speed() min-speed=%s", s)
324 assert s==-1 or 0<=s<=100, "invalid min-speed: %s" % s
325 self.send("min-speed", s)