Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/window_backing_base.py : 38%
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) 2008 Nathaniel Smith <njs@pobox.com>
3# Copyright (C) 2012-2020 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 hashlib
8from threading import Lock
10from xpra.net.mmap_pipe import mmap_read
11from xpra.net import compression
12from xpra.util import typedict, csv, envint, envbool, first_time
13from xpra.codecs.loader import get_codec
14from xpra.codecs.video_helper import getVideoHelper
15from xpra.os_util import bytestostr
16from xpra.common import (
17 NorthWestGravity,
18 NorthGravity,
19 NorthEastGravity,
20 WestGravity,
21 CenterGravity,
22 EastGravity,
23 SouthWestGravity,
24 SouthGravity,
25 SouthEastGravity,
26 StaticGravity,
27 GRAVITY_STR,
28 )
29from xpra.log import Logger
31log = Logger("paint")
32videolog = Logger("video", "paint")
34INTEGRITY_HASH = envbool("XPRA_INTEGRITY_HASH", False)
35PAINT_BOX = envint("XPRA_PAINT_BOX", 0) or envint("XPRA_OPENGL_PAINT_BOX", 0)
36WEBP_PILLOW = envbool("XPRA_WEBP_PILLOW", False)
37SCROLL_ENCODING = envbool("XPRA_SCROLL_ENCODING", True)
38REPAINT_ALL = envbool("XPRA_REPAINT_ALL", False)
41#ie:
42#CSC_OPTIONS = { "YUV420P" : {"RGBX" : [swscale.spec], "BGRX" : ...} }
43CSC_OPTIONS = None
44def load_csc_options():
45 global CSC_OPTIONS
46 if CSC_OPTIONS is None:
47 CSC_OPTIONS = {}
48 vh = getVideoHelper()
49 for csc_in in vh.get_csc_inputs():
50 CSC_OPTIONS[csc_in] = vh.get_csc_specs(csc_in)
51 return CSC_OPTIONS
53#get the list of video encodings (and the module for each one):
54VIDEO_DECODERS = None
55def load_video_decoders():
56 global VIDEO_DECODERS
57 if VIDEO_DECODERS is None:
58 VIDEO_DECODERS = {}
59 vh = getVideoHelper()
60 for encoding in vh.get_decodings():
61 specs = vh.get_decoder_specs(encoding)
62 for colorspace, decoders in specs.items():
63 log("%-5s decoders for %7s: %s", encoding, colorspace, csv([d.get_type() for _,d in decoders]))
64 assert decoders
65 #use the first one:
66 _, decoder_module = decoders[0]
67 VIDEO_DECODERS[encoding] = decoder_module
68 log("video decoders: %s", dict((e,d.get_type()) for e,d in VIDEO_DECODERS.items()))
69 return VIDEO_DECODERS
72def fire_paint_callbacks(callbacks, success=True, message=""):
73 for x in callbacks:
74 try:
75 x(success, message)
76 except Exception:
77 log.error("error calling %s(%s)", x, success, exc_info=True)
80def verify_checksum(img_data, options):
81 l = options.intget("z.len")
82 if l:
83 assert l==len(img_data), "compressed pixel data failed length integrity check: expected %i bytes but got %i" % (l, len(img_data))
84 try:
85 chksum = options.get("z.md5")
86 if chksum:
87 h = hashlib.md5(img_data)
88 except ValueError:
89 chksum = options.get("z.sha1")
90 if chksum:
91 h = hashlib.sha1(img_data)
92 if h:
93 hd = h.hexdigest()
94 assert chksum==hd, "pixel data failed compressed chksum integrity check: expected %s but got %s" % (chksum, hd)
97"""
98Generic superclass for all Backing code,
99see CairoBackingBase and GTK2WindowBacking subclasses for actual implementations
100"""
101class WindowBackingBase:
102 RGB_MODES = ()
104 def __init__(self, wid : int, window_alpha : bool):
105 load_csc_options()
106 load_video_decoders()
107 self.wid = wid
108 self.size = 0, 0
109 self.render_size = 0, 0
110 #padding between the window contents and where we actually draw the backing
111 #(ie: if the window is bigger than the backing,
112 # we may be rendering the backing in the center of the window)
113 self.offsets = 0, 0, 0, 0 #top,left,bottom,right
114 self.gravity = 0
115 self._alpha_enabled = window_alpha
116 self._backing = None
117 self._video_decoder = None
118 self._csc_decoder = None
119 self._decoder_lock = Lock()
120 self._PIL_encodings = []
121 self.default_paint_box_line_width = PAINT_BOX or 1
122 self.paint_box_line_width = PAINT_BOX
123 self.pointer_overlay = None
124 self.cursor_data = None
125 self.default_cursor_data = None
126 self.jpeg_decoder = None
127 self.webp_decoder = None
128 self.pil_decoder = get_codec("dec_pillow")
129 if self.pil_decoder:
130 self._PIL_encodings = self.pil_decoder.get_encodings()
131 self.jpeg_decoder = get_codec("dec_jpeg")
132 self.webp_decoder = get_codec("dec_webp")
133 self.draw_needs_refresh = True
134 self.repaint_all = REPAINT_ALL
135 self.mmap = None
136 self.mmap_enabled = False
138 def idle_add(self, *_args, **_kwargs):
139 raise NotImplementedError()
141 def get_info(self):
142 info = {
143 "rgb-formats" : self.RGB_MODES,
144 "transparency" : self._alpha_enabled,
145 "mmap" : bool(self.mmap_enabled),
146 "size" : self.size,
147 "render-size" : self.render_size,
148 "offsets" : self.offsets,
149 }
150 vd = self._video_decoder
151 if vd:
152 info["video-decoder"] = self._video_decoder.get_info()
153 csc = self._csc_decoder
154 if csc:
155 info["csc"] = self._csc_decoder
156 return info
159 def enable_mmap(self, mmap_area):
160 self.mmap = mmap_area
161 self.mmap_enabled = True
163 def gravity_copy_coords(self, oldw, oldh, bw, bh):
164 sx = sy = dx = dy = 0
165 def center_y():
166 if bh>=oldh:
167 #take the whole source, paste it in the middle
168 return 0, (bh-oldh)//2
169 #skip the edges of the source, paste all of it
170 return (oldh-bh)//2, 0
171 def center_x():
172 if bw>=oldw:
173 return 0, (bw-oldw)//2
174 return (oldw-bw)//2, 0
175 def east_x():
176 if bw>=oldw:
177 return 0, bw-oldw
178 return oldw-bw, 0
179 def west_x():
180 return 0, 0
181 def north_y():
182 return 0, 0
183 def south_y():
184 if bh>=oldh:
185 return 0, bh-oldh
186 return oldh-bh, 0
187 g = self.gravity
188 if not g or g==NorthWestGravity:
189 #undefined (or 0), use NW
190 sx, dx = west_x()
191 sy, dy = north_y()
192 elif g==NorthGravity:
193 sx, dx = center_x()
194 sy, dy = north_y()
195 elif g==NorthEastGravity:
196 sx, dx = east_x()
197 sy, dy = north_y()
198 elif g==WestGravity:
199 sx, dx = west_x()
200 sy, dy = center_y()
201 elif g==CenterGravity:
202 sx, dx = center_x()
203 sy, dy = center_y()
204 elif g==EastGravity:
205 sx, dx = east_x()
206 sy, dy = center_y()
207 elif g==SouthWestGravity:
208 sx, dx = west_x()
209 sy, dy = south_y()
210 elif g==SouthGravity:
211 sx, dx = center_x()
212 sy, dy = south_y()
213 elif g==SouthEastGravity:
214 sx, dx = east_x()
215 sy, dy = south_y()
216 elif g==StaticGravity:
217 if first_time("StaticGravity-%i" % self.wid):
218 log.warn("Warning: static gravity is not handled")
219 w = min(bw, oldw)
220 h = min(bh, oldh)
221 return sx, sy, dx, dy, w, h
223 def gravity_adjust(self, x, y, options):
224 #if the window size has changed,
225 #adjust the coordinates honouring the window gravity:
226 window_size = options.inttupleget("window-size", None)
227 g = self.gravity
228 log("gravity_adjust%s window_size=%s, size=%s, gravity=%s",
229 (x, y, options), window_size, self.size, GRAVITY_STR.get(g, "unknown"))
230 if not window_size:
231 return x, y
232 window_size = tuple(window_size)
233 if window_size==self.size:
234 return x, y
235 if g==0 or self.gravity==NorthWestGravity:
236 return x, y
237 oldw, oldh = window_size
238 bw, bh = self.size
239 def center_y():
240 if bh>=oldh:
241 return y + (bh-oldh)//2
242 return y - (oldh-bh)//2
243 def center_x():
244 if bw>=oldw:
245 return x + (bw-oldw)//2
246 return x - (oldw-bw)//2
247 def east_x():
248 if bw>=oldw:
249 return x + (bw-oldw)
250 return x - (oldw-bw)
251 def west_x():
252 return x
253 def north_y():
254 return y
255 def south_y():
256 if bh>=oldh:
257 return y + (bh-oldh)
258 return y - (oldh-bh)
259 if g==NorthGravity:
260 return center_x(), north_y()
261 if g==NorthEastGravity:
262 return east_x(), north_y()
263 if g==WestGravity:
264 return west_x(), center_y()
265 if g==CenterGravity:
266 return center_x(), center_y()
267 if g==EastGravity:
268 return east_x(), center_y()
269 if g==SouthWestGravity:
270 return west_x(), south_y()
271 if g==SouthGravity:
272 return center_x(), south_y()
273 if g==SouthEastGravity:
274 return east_x(), south_y()
275 #if self.gravity==StaticGravity:
276 # pass
277 return x, y
280 def close(self):
281 self._backing = None
282 log("%s.close() video_decoder=%s", self, self._video_decoder)
283 #try without blocking, if that fails then
284 #the lock is held by the decoding thread,
285 #and it will run the cleanup after releasing the lock
286 #(it checks for self._backing None)
287 self.close_decoder(False)
289 def close_decoder(self, blocking=False):
290 videolog("close_decoder(%s)", blocking)
291 dl = self._decoder_lock
292 if dl is None or not dl.acquire(blocking):
293 videolog("close_decoder(%s) lock %s not acquired", blocking, dl)
294 return False
295 try:
296 self.do_clean_video_decoder()
297 self.do_clean_csc_decoder()
298 return True
299 finally:
300 dl.release()
302 def do_clean_video_decoder(self):
303 if self._video_decoder:
304 self._video_decoder.clean()
305 self._video_decoder = None
307 def do_clean_csc_decoder(self):
308 if self._csc_decoder:
309 self._csc_decoder.clean()
310 self._csc_decoder = None
313 def get_encoding_properties(self):
314 return {
315 "encodings.rgb_formats" : self.RGB_MODES,
316 "encoding.transparency" : self._alpha_enabled,
317 "encoding.full_csc_modes" : self._get_full_csc_modes(self.RGB_MODES),
318 "encoding.send-window-size" : True,
319 "encoding.render-size" : self.render_size,
320 }
322 def _get_full_csc_modes(self, rgb_modes):
323 #calculate the server CSC modes the server is allowed to use
324 #based on the client CSC modes we can convert to in the backing class we use
325 #and trim the transparency if we cannot handle it
326 target_rgb_modes = tuple(rgb_modes)
327 if not self._alpha_enabled:
328 target_rgb_modes = tuple(x for x in target_rgb_modes if x.find("A")<0)
329 full_csc_modes = getVideoHelper().get_server_full_csc_modes_for_rgb(*target_rgb_modes)
330 full_csc_modes["webp"] = [x for x in rgb_modes if x in ("BGRX", "BGRA", "RGBX", "RGBA")]
331 videolog("_get_full_csc_modes(%s) with target_rgb_modes=%s", rgb_modes, target_rgb_modes)
332 for e in sorted(full_csc_modes.keys()):
333 modes = full_csc_modes.get(e)
334 videolog(" * %s : %s", e, modes)
335 return full_csc_modes
338 def unpremultiply(self, img_data):
339 from xpra.codecs.argb.argb import unpremultiply_argb, unpremultiply_argb_in_place #@UnresolvedImport
340 if not isinstance(img_data, str):
341 try:
342 unpremultiply_argb_in_place(img_data)
343 return img_data
344 except Exception:
345 log.warn("failed to unpremultiply %s (len=%s)" % (type(img_data), len(img_data)))
346 return unpremultiply_argb(img_data)
349 def set_cursor_data(self, cursor_data):
350 self.cursor_data = cursor_data
353 def paint_jpeg(self, img_data, x, y, width, height, options, callbacks):
354 img = self.jpeg_decoder.decompress_to_rgb("RGBX", img_data)
355 rgb_format = img.get_pixel_format()
356 img_data = img.get_pixels()
357 rowstride = img.get_rowstride()
358 w = img.get_width()
359 h = img.get_height()
360 self.idle_add(self.do_paint_rgb, rgb_format, img_data,
361 x, y, w, h, width, height, rowstride, options, callbacks)
364 def paint_image(self, coding, img_data, x, y, width, height, options, callbacks):
365 # can be called from any thread
366 rgb_format, img_data, iwidth, iheight, rowstride = self.pil_decoder.decompress(coding, img_data, options)
367 self.idle_add(self.do_paint_rgb, rgb_format, img_data,
368 x, y, iwidth, iheight, width, height, rowstride, options, callbacks)
370 def paint_webp(self, img_data, x, y, width, height, options, callbacks):
371 if not self.webp_decoder or WEBP_PILLOW:
372 #if webp is enabled, then Pillow should be able to take care of it:
373 self.paint_image("webp", img_data, x, y, width, height, options, callbacks)
374 return
375 rgb_format = options.strget("rgb_format")
376 has_alpha = options.boolget("has_alpha", False)
377 (
378 buffer_wrapper,
379 iwidth, iheight, stride, has_alpha,
380 rgb_format,
381 ) = self.webp_decoder.decompress(img_data, has_alpha, rgb_format, self.RGB_MODES)
382 def free_buffer(*_args):
383 buffer_wrapper.free()
384 callbacks.append(free_buffer)
385 data = buffer_wrapper.get_pixels()
386 #if the backing can't handle this format,
387 #ie: tray only supports RGBA
388 if rgb_format not in self.RGB_MODES:
389 from xpra.codecs.rgb_transform import rgb_reformat
390 from xpra.codecs.image_wrapper import ImageWrapper
391 img = ImageWrapper(x, y, iwidth, iheight, data, rgb_format,
392 len(rgb_format)*8, stride, len(rgb_format), ImageWrapper.PACKED, True, None)
393 rgb_reformat(img, self.RGB_MODES, has_alpha and self._alpha_enabled)
394 rgb_format = img.get_pixel_format()
395 data = img.get_pixels()
396 stride = img.get_rowstride()
397 #replace with the actual rgb format we get from the decoder:
398 options[b"rgb_format"] = rgb_format
399 self.idle_add(self.do_paint_rgb, rgb_format, data,
400 x, y, iwidth, iheight, width, height, stride, options, callbacks)
402 def paint_rgb(self, rgb_format, raw_data, x, y, width, height, rowstride, options, callbacks):
403 """ can be called from a non-UI thread """
404 iwidth, iheight = options.intpair("scaled-size", (width, height))
405 #was a compressor used?
406 comp = tuple(x for x in compression.ALL_COMPRESSORS if options.intget(x, 0))
407 if comp:
408 assert len(comp)==1, "more than one compressor specified: %s" % str(comp)
409 rgb_data = compression.decompress_by_name(raw_data, algo=comp[0])
410 else:
411 rgb_data = raw_data
412 self.idle_add(self.do_paint_rgb, rgb_format, rgb_data,
413 x, y, iwidth, iheight, width, height, rowstride, options, callbacks)
415 def do_paint_rgb(self, rgb_format, img_data,
416 x, y, width, height, render_width, render_height, rowstride, options, callbacks):
417 """ must be called from the UI thread
418 this method is only here to ensure that we always fire the callbacks,
419 the actual paint code is in _do_paint_rgb[24|32]
420 """
421 x, y = self.gravity_adjust(x, y, options)
422 try:
423 if not options.boolget("paint", True):
424 fire_paint_callbacks(callbacks)
425 return
426 if self._backing is None:
427 fire_paint_callbacks(callbacks, -1, "no backing")
428 return
429 if rgb_format=="r210":
430 bpp = 30
431 elif rgb_format=="BGR565":
432 bpp = 16
433 else:
434 bpp = len(rgb_format)*8 #ie: "BGRA" -> 32
435 if bpp==16:
436 paint_fn = self._do_paint_rgb16
437 elif bpp==24:
438 paint_fn = self._do_paint_rgb24
439 elif bpp==30:
440 paint_fn = self._do_paint_rgb30
441 elif bpp==32:
442 paint_fn = self._do_paint_rgb32
443 else:
444 raise Exception("invalid rgb format '%s'" % rgb_format)
445 options[b"rgb_format"] = rgb_format
446 success = paint_fn(img_data, x, y, width, height, render_width, render_height, rowstride, options)
447 fire_paint_callbacks(callbacks, success)
448 except Exception as e:
449 if not self._backing:
450 fire_paint_callbacks(callbacks, -1, "paint error on closed backing ignored")
451 else:
452 log.error("Error painting rgb%s", bpp, exc_info=True)
453 message = "paint rgb%s error: %s" % (bpp, e)
454 fire_paint_callbacks(callbacks, False, message)
456 def _do_paint_rgb16(self, img_data, x, y, width, height, render_width, render_height, rowstride, options):
457 raise Exception("override me!")
459 def _do_paint_rgb24(self, img_data, x, y, width, height, render_width, render_height, rowstride, options):
460 raise Exception("override me!")
462 def _do_paint_rgb30(self, img_data, x, y, width, height, render_width, render_height, rowstride, options):
463 raise Exception("override me!")
465 def _do_paint_rgb32(self, img_data, x, y, width, height, render_width, render_height, rowstride, options):
466 raise Exception("override me!")
469 def eos(self):
470 dl = self._decoder_lock
471 with dl:
472 self.do_clean_csc_decoder()
473 self.do_clean_video_decoder()
476 def make_csc(self, src_width, src_height, src_format,
477 dst_width, dst_height, dst_format_options, speed):
478 global CSC_OPTIONS
479 in_options = CSC_OPTIONS.get(src_format, {})
480 if not in_options:
481 log.error("Error: no csc options for '%s' input, only found:", src_format)
482 for k,v in CSC_OPTIONS.items():
483 log.error(" * %-8s : %s", k, csv(v))
484 raise Exception("no csc options for '%s' input in %s" % (src_format, csv(CSC_OPTIONS.keys())))
485 videolog("make_csc%s",
486 (src_width, src_height, src_format, dst_width, dst_height, dst_format_options, speed))
487 for dst_format in dst_format_options:
488 specs = in_options.get(dst_format)
489 videolog("make_csc specs(%s)=%s", dst_format, specs)
490 if not specs:
491 continue
492 for spec in specs:
493 v = self.validate_csc_size(spec, src_width, src_height, dst_width, dst_height)
494 if v:
495 continue
496 try:
497 csc = spec.make_instance()
498 csc.init_context(src_width, src_height, src_format,
499 dst_width, dst_height, dst_format, speed)
500 return csc
501 except Exception as e:
502 videolog("make_csc%s",
503 (src_width, src_height, src_format, dst_width, dst_height, dst_format_options, speed),
504 exc_info=True)
505 videolog.error("Error: failed to create csc instance %s", spec.codec_class)
506 videolog.error(" for %s to %s: %s", src_format, dst_format, e)
507 videolog.error("Error: no matching CSC module found")
508 videolog.error(" for %ix%i %s source format,", src_width, src_height, src_format)
509 videolog.error(" to %ix%i %s", dst_width, dst_height, " or ".join(dst_format_options))
510 videolog.error(" with options=%s, speed=%i", dst_format_options, speed)
511 videolog.error(" tested:")
512 for dst_format in dst_format_options:
513 specs = in_options.get(dst_format)
514 if not specs:
515 continue
516 videolog.error(" * %s:", dst_format)
517 for spec in specs:
518 videolog.error(" - %s:", spec)
519 v = self.validate_csc_size(spec, src_width, src_height, dst_width, dst_height)
520 if v:
521 videolog.error(" "+v[0], *v[1:])
522 raise Exception("no csc module found for wid %i %s(%sx%s) to %s(%sx%s) in %s" %
523 (self.wid, src_format, src_width, src_height, " or ".join(dst_format_options),
524 dst_width, dst_height, CSC_OPTIONS))
526 def validate_csc_size(self, spec, src_width, src_height, dst_width, dst_height):
527 if not spec.can_scale and (src_width!=dst_width or src_height!=dst_height):
528 return "scaling not suported"
529 if src_width<spec.min_w:
530 return "source width %i is out of range: minimum is %i", src_width, spec.min_w
531 if src_height<spec.min_h:
532 return "source height %i is out of range: minimum is %i", src_height, spec.min_h
533 if dst_width<spec.min_w:
534 return "target width %i is out of range: minimum is %i", dst_width, spec.min_w
535 if dst_height<spec.min_h:
536 return "target height %i is out of range: minimum is %i", dst_height, spec.min_h
537 if src_width>spec.max_w:
538 return "source width %i is out of range: maximum is %i", src_width, spec.max_w
539 if src_height>spec.max_h:
540 return "source height %i is out of range: maximum is %i", src_height, spec.max_h
541 if dst_width>spec.max_w:
542 return "target width %i is out of range: maximum is %i", dst_width, spec.max_w
543 if dst_height>spec.max_h:
544 return "target height %i is out of range: maximum is %i", dst_height, spec.max_h
545 return None
547 def paint_with_video_decoder(self, decoder_module, coding, img_data, x, y, width, height, options, callbacks):
548 assert decoder_module, "decoder module not found for %s" % coding
549 dl = self._decoder_lock
550 if dl is None:
551 fire_paint_callbacks(callbacks, False, "no lock - retry")
552 return
553 with dl:
554 if self._backing is None:
555 message = "window %s is already gone!" % self.wid
556 log(message)
557 fire_paint_callbacks(callbacks, -1, message)
558 return
559 enc_width, enc_height = options.intpair("scaled_size", (width, height))
560 input_colorspace = options.strget("csc")
561 if not input_colorspace:
562 message = "csc mode is missing from the video options!"
563 log.error(message)
564 fire_paint_callbacks(callbacks, False, message)
565 return
566 #do we need a prep step for decoders that cannot handle the input_colorspace directly?
567 decoder_colorspaces = decoder_module.get_input_colorspaces(coding)
568 assert input_colorspace in decoder_colorspaces, "decoder %s does not support %s for %s" % (
569 decoder_module.get_type(), input_colorspace, coding)
571 vd = self._video_decoder
572 if vd:
573 if options.intget("frame", -1)==0:
574 videolog("paint_with_video_decoder: first frame of new stream")
575 self.do_clean_video_decoder()
576 elif vd.get_encoding()!=coding:
577 videolog("paint_with_video_decoder: encoding changed from %s to %s", vd.get_encoding(), coding)
578 self.do_clean_video_decoder()
579 elif vd.get_width()!=enc_width or vd.get_height()!=enc_height:
580 videolog("paint_with_video_decoder: video dimensions have changed from %s to %s",
581 (vd.get_width(), vd.get_height()), (enc_width, enc_height))
582 self.do_clean_video_decoder()
583 elif vd.get_colorspace()!=input_colorspace:
584 #this should only happen on encoder restart, which means this should be the first frame:
585 videolog.warn("Warning: colorspace unexpectedly changed from %s to %s",
586 vd.get_colorspace(), input_colorspace)
587 self.do_clean_video_decoder()
588 if self._video_decoder is None:
589 videolog("paint_with_video_decoder: new %s(%s,%s,%s)",
590 decoder_module.Decoder, width, height, input_colorspace)
591 vd = decoder_module.Decoder()
592 vd.init_context(coding, enc_width, enc_height, input_colorspace)
593 self._video_decoder = vd
594 videolog("paint_with_video_decoder: info=%s", vd.get_info())
596 img = vd.decompress_image(img_data, options)
597 if not img:
598 if options.intget("delayed", 0)>0:
599 #there are further frames queued up,
600 #and this frame references those, so assume all is well:
601 fire_paint_callbacks(callbacks)
602 else:
603 fire_paint_callbacks(callbacks, False,
604 "video decoder %s failed to decode %i bytes of %s data" % (
605 vd.get_type(), len(img_data), coding))
606 videolog.error("Error: decode failed on %s bytes of %s data", len(img_data), coding)
607 videolog.error(" %sx%s pixels using %s", width, height, vd.get_type())
608 videolog.error(" frame options:")
609 for k,v in options.items():
610 if isinstance(v, bytes):
611 v = bytestostr(v)
612 videolog.error(" %s=%s", bytestostr(k), v)
613 return
615 x, y = self.gravity_adjust(x, y, options)
616 self.do_video_paint(img, x, y, enc_width, enc_height, width, height, options, callbacks)
617 if self._backing is None:
618 self.close_decoder(True)
620 def do_video_paint(self, img, x, y, enc_width, enc_height, width, height, options, callbacks):
621 target_rgb_formats = self.RGB_MODES
622 #as some video formats like vpx can forward transparency
623 #also we could skip the csc step in some cases:
624 pixel_format = img.get_pixel_format()
625 cd = self._csc_decoder
626 if cd is not None:
627 if cd.get_src_format()!=pixel_format:
628 videolog("do_video_paint csc: switching src format from %s to %s", cd.get_src_format(), pixel_format)
629 self.do_clean_csc_decoder()
630 elif cd.get_dst_format() not in target_rgb_formats:
631 videolog("do_video_paint csc: switching dst format from %s to %s", cd.get_dst_format(), target_rgb_formats)
632 self.do_clean_csc_decoder()
633 elif cd.get_src_width()!=enc_width or cd.get_src_height()!=enc_height:
634 videolog("do_video_paint csc: switching src size from %sx%s to %sx%s",
635 enc_width, enc_height, cd.get_src_width(), cd.get_src_height())
636 self.do_clean_csc_decoder()
637 elif cd.get_dst_width()!=width or cd.get_dst_height()!=height:
638 videolog("do_video_paint csc: switching src size from %sx%s to %sx%s",
639 width, height, cd.get_dst_width(), cd.get_dst_height())
640 self.do_clean_csc_decoder()
641 if self._csc_decoder is None:
642 #use higher quality csc to compensate for lower quality source
643 #(which generally means that we downscaled via YUV422P or lower)
644 #or when upscaling the video:
645 q = options.intget("quality", 50)
646 csc_speed = int(min(100, 100-q, 100.0 * (enc_width*enc_height) / (width*height)))
647 cd = self.make_csc(enc_width, enc_height, pixel_format,
648 width, height, target_rgb_formats, csc_speed)
649 videolog("do_video_paint new csc decoder: %s", cd)
650 self._csc_decoder = cd
651 rgb_format = cd.get_dst_format()
652 rgb = cd.convert_image(img)
653 videolog("do_video_paint rgb using %s.convert_image(%s)=%s", cd, img, rgb)
654 img.free()
655 assert rgb.get_planes()==0, "invalid number of planes for %s: %s" % (rgb_format, rgb.get_planes())
656 #make a new options dict and set the rgb format:
657 paint_options = typedict(options)
658 #this will also take care of firing callbacks (from the UI thread):
659 def paint():
660 data = rgb.get_pixels()
661 rowstride = rgb.get_rowstride()
662 try:
663 self.do_paint_rgb(rgb_format, data,
664 x, y, width, height, width, height, rowstride, paint_options, callbacks)
665 finally:
666 rgb.free()
667 self.idle_add(paint)
669 def paint_mmap(self, img_data, x, y, width, height, rowstride, options, callbacks):
670 """ must be called from UI thread
671 see _mmap_send() in server.py for details """
672 assert self.mmap_enabled
673 data = mmap_read(self.mmap, *img_data)
674 rgb_format = options.strget(b"rgb_format", b"RGB")
675 #Note: BGR(A) is only handled by gl_window_backing
676 x, y = self.gravity_adjust(x, y, options)
677 self.do_paint_rgb(rgb_format, data, x, y, width, height, width, height, rowstride, options, callbacks)
679 def paint_scroll(self, img_data, options, callbacks):
680 log("paint_scroll%s", (img_data, options, callbacks))
681 raise NotImplementedError("no paint scroll on %s" % type(self))
684 def draw_region(self, x, y, width, height, coding, img_data, rowstride, options, callbacks):
685 """ dispatches the paint to one of the paint_XXXX methods """
686 try:
687 assert self._backing is not None
688 log("draw_region(%s, %s, %s, %s, %s, %s bytes, %s, %s, %s)",
689 x, y, width, height, coding, len(img_data), rowstride, options, callbacks)
690 coding = bytestostr(coding)
691 options["encoding"] = coding #used for choosing the color of the paint box
692 if INTEGRITY_HASH:
693 verify_checksum(img_data, options)
694 if coding == "mmap":
695 self.idle_add(self.paint_mmap, img_data, x, y, width, height, rowstride, options, callbacks)
696 elif coding in ("rgb24", "rgb32"):
697 #avoid confusion over how many bytes-per-pixel we may have:
698 rgb_format = options.strget(b"rgb_format")
699 if not rgb_format:
700 rgb_format = {
701 "rgb24" : "RGB",
702 "rgb32" : "RGBX",
703 }.get(coding)
704 if rowstride==0:
705 rowstride = width * len(rgb_format)
706 self.paint_rgb(rgb_format, img_data, x, y, width, height, rowstride, options, callbacks)
707 elif coding in VIDEO_DECODERS:
708 self.paint_with_video_decoder(VIDEO_DECODERS.get(coding),
709 coding,
710 img_data, x, y, width, height, options, callbacks)
711 elif self.jpeg_decoder and coding=="jpeg":
712 self.paint_jpeg(img_data, x, y, width, height, options, callbacks)
713 elif coding == "webp":
714 self.paint_webp(img_data, x, y, width, height, options, callbacks)
715 elif coding in self._PIL_encodings:
716 self.paint_image(coding, img_data, x, y, width, height, options, callbacks)
717 elif coding == "scroll":
718 self.paint_scroll(img_data, options, callbacks)
719 else:
720 self.do_draw_region(x, y, width, height, coding, img_data, rowstride, options, callbacks)
721 except Exception:
722 if self._backing is None:
723 fire_paint_callbacks(callbacks, -1, "this backing is closed - retry?")
724 else:
725 raise
727 def do_draw_region(self, _x, _y, _width, _height, coding, _img_data, _rowstride, _options, callbacks):
728 msg = "invalid encoding: '%s'" % coding
729 log.error("Error: %s", msg)
730 fire_paint_callbacks(callbacks, False, msg)