Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* guess "content-type" based on window "role" and "class-instance" attributes
* pass "content-type" to webp encoder
* use hint to choose preset and image hint
* when "content-type" is set to "video", make it more likely that we'll use a video encoder, and turn off "scrolling" detection

git-svn-id: https://xpra.org/svn/Xpra/trunk@17665 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Dec 16, 2017
1 parent 7e6c7ca commit 7de4473
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 17 deletions.
28 changes: 20 additions & 8 deletions src/xpra/codecs/webp/encode.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ PRESET_NAME_TO_CONSTANT = {}
for k,v in PRESETS.items():
PRESET_NAME_TO_CONSTANT[v] = k

CONTENT_TYPE_PRESET = {
"picture" : WEBP_PRESET_PICTURE,
"text" : WEBP_PRESET_TEXT,
"browser" : WEBP_PRESET_TEXT,
}

IMAGE_HINT = {
WEBP_HINT_DEFAULT : "default",
WEBP_HINT_PICTURE : "picture",
Expand All @@ -315,6 +321,10 @@ HINT_NAME_TO_CONSTANT = {}
for k,v in IMAGE_HINT.items():
HINT_NAME_TO_CONSTANT[v] = k

CONTENT_TYPE_HINT = {
"picture" : WEBP_HINT_PICTURE,
}

cdef WebPImageHint DEFAULT_IMAGE_HINT = HINT_NAME_TO_CONSTANT.get(os.environ.get("XPRA_WEBP_IMAGE_HINT", "graph").lower(), WEBP_HINT_GRAPH)
cdef WebPPreset DEFAULT_PRESET = PRESET_NAME_TO_CONSTANT.get(os.environ.get("XPRA_WEBP_PRESET", "text").lower(), WEBP_PRESET_TEXT)
cdef WebPPreset PRESET_SMALL = PRESET_NAME_TO_CONSTANT.get(os.environ.get("XPRA_WEBP_PRESET_SMALL", "icon").lower(), WEBP_PRESET_ICON)
Expand Down Expand Up @@ -380,14 +390,16 @@ cdef get_config_info(WebPConfig *config):
"low_memory" : config.low_memory,
}

def compress(image, int quality=50, int speed=50, supports_alpha=False):
def compress(image, int quality=50, int speed=50, supports_alpha=False, content_type=""):
pixel_format = image.get_pixel_format()
if pixel_format not in ("RGBX", "RGBA", "BGRX", "BGRA"):
raise Exception("unsupported pixel format %s" % pixel_format)
cdef unsigned int width = image.get_width()
cdef unsigned int height = image.get_height()
cdef unsigned int stride = image.get_rowstride()
cdef unsigned int Bpp = len(pixel_format)
if width>WEBP_MAX_DIMENSION or height>WEBP_MAX_DIMENSION:
raise Exception("this image is too big for webp: %ix%i" % (width, height))
pixels = image.get_pixels()

cdef uint8_t *pic_buf
Expand All @@ -396,12 +408,12 @@ def compress(image, int quality=50, int speed=50, supports_alpha=False):
cdef WebPPreset preset = DEFAULT_PRESET
if width*height<8192:
preset = PRESET_SMALL
if width>WEBP_MAX_DIMENSION or height>WEBP_MAX_DIMENSION:
raise Exception("this image is too big for webp: %ix%i" % (width, height))

preset = CONTENT_TYPE_PRESET.get(content_type, preset)
cdef WebPImageHint image_hint = CONTENT_TYPE_HINT.get(content_type, DEFAULT_IMAGE_HINT)
cdef int ret = object_as_buffer(pixels, <const void**> &pic_buf, &pic_buf_len)
assert ret==0, "failed to get buffer from pixel object: %s (returned %s)" % (type(pixels), ret)
log("webp.compress(%s, %i, %i, %s) buf=%#x", image, width, height, supports_alpha, <uintptr_t> pic_buf)
log("webp.compress(%s, %i, %i, %s, %s) buf=%#x", image, width, height, supports_alpha, content_type, <uintptr_t> pic_buf)
cdef int size = stride * height
assert pic_buf_len>=size, "pixel buffer is too small: expected at least %s bytes but got %s" % (size, pic_buf_len)

Expand Down Expand Up @@ -449,13 +461,13 @@ def compress(image, int quality=50, int speed=50, supports_alpha=False):
config.autofilter = 0
config._pass = MAX(1, MIN(10, (100-speed)//10))
config.preprocessing = int(speed<50)
config.image_hint = DEFAULT_IMAGE_HINT
config.image_hint = image_hint
config.thread_level = WEBP_THREADING
config.partitions = 3
config.partition_limit = MAX(0, MIN(100, 100-quality))

log("webp.compress config: lossless=%s, quality=%s, method=%s, alpha=%s,%s,%s", config.lossless, config.quality, config.method,
config.alpha_compression, config.alpha_filtering, config.alpha_quality)
log("webp.compress config: lossless=%-5s, quality=%3i, method=%i, alpha=%3i,%3i,%3i, preset=%-8s, image hint=%s", config.lossless, config.quality, config.method,
config.alpha_compression, config.alpha_filtering, config.alpha_quality, PRESETS.get(preset, preset), IMAGE_HINT.get(image_hint, image_hint))
ret = WebPValidateConfig(&config)
if not ret:
info = get_config_info(&config)
Expand Down
4 changes: 2 additions & 2 deletions src/xpra/server/picture_encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def warn_encoding_once(key, message):
encoding_warnings.add(key)


def webp_encode(image, rgb_formats, supports_transparency, quality, speed):
def webp_encode(image, rgb_formats, supports_transparency, quality, speed, content_type):
pixel_format = image.get_pixel_format()
#log("webp_encode%s", (coding, image, rgb_formats, supports_transparency, quality, speed))
if pixel_format not in rgb_formats:
Expand All @@ -45,7 +45,7 @@ def webp_encode(image, rgb_formats, supports_transparency, quality, speed):
#log("WEBP_PILLOW=%s, enc_webp=%s, stride=%s, pixel_format=%s", WEBP_PILLOW, enc_webp, stride, pixel_format)
if not WEBP_PILLOW and enc_webp and stride>0 and stride%4==0 and pixel_format in ("BGRA", "BGRX", "RGBA", "RGBX"):
#prefer Cython module:
cdata, client_options = enc_webp.compress(image, quality, speed, supports_transparency)
cdata, client_options = enc_webp.compress(image, quality, speed, supports_transparency, content_type)
return "webp", compression.Compressed("webp", cdata), client_options, image.get_width(), image.get_height(), 0, 24
#fallback using Pillow:
enc_pillow = get_codec("enc_pillow")
Expand Down
48 changes: 48 additions & 0 deletions src/xpra/server/window/content_guesser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# This file is part of Xpra.
# Copyright (C) 2017 Antoine Martin <antoine@devloop.org.uk>
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.

from xpra.log import Logger
log = Logger("window", "util")

ROLE_MAP = {
"text" : ("gimp-dock", "gimp-toolbox", ),
"picture" : ("gimp-image-window", ),
"browser" : ("browser", ),
}

RES_NAME = {
"text" : ("xterm", "terminal", "Eclipse", "gedit", "Mail", ),
"video" : ("vlc", ),
"browser" : ("google-chrome", "Navigator", "VirtualBox Manager", "chromium-browser", ),
"picture" : ("gimp", "VirtualBox Machine"),
}
RES_CLASS = {
"browser" : ("Firefox", "Thunderbird", ),
}


def match_map(value, ctmap):
for content_type, values in ctmap.items():
if any(value.find(x)>=0 for x in values):
return content_type
return None

def guess_content_type(window):
role = window.get("role")
if role:
v = match_map(role, ROLE_MAP)
if v:
return v
ci = window.get("class-instance")
if not ci or len(ci)!=2:
return ""
res_name, res_class = ci
v = None
if res_name:
v = match_map(res_name, RES_NAME)
if res_class and not v:
v = match_map(res_class, RES_CLASS)
return v or ""
13 changes: 12 additions & 1 deletion src/xpra/server/window/window_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
HARDCODED_ENCODING = os.environ.get("XPRA_HARDCODED_ENCODING")

from xpra.os_util import BytesIOClass, memoryview_to_bytes
from xpra.server.window.content_guesser import guess_content_type
from xpra.server.window.window_stats import WindowPerformanceStatistics
from xpra.server.window.batch_config import DamageBatchConfig
from xpra.simple_stats import get_list_stats
Expand Down Expand Up @@ -170,7 +171,11 @@ def __init__(self,
self.scaling = None
self.maximized = False #set by the client!
self.iconic = False
self.content_type = ""
self.window_signal_handlers = []
sid = window.connect("notify::class-instance", self._class_changed)
self.window_signal_handlers.append(sid)
self._class_changed(window)
if "iconic" in window.get_dynamic_property_names():
self.iconic = window.get_property("iconic")
sid = window.connect("notify::iconic", self._iconic_changed)
Expand Down Expand Up @@ -404,6 +409,7 @@ def get_info(self):
info.update({
"dimensions" : self.window_dimensions,
"suspended" : self.suspended or False,
"content-type" : self.content_type,
"bandwidth-limit" : self.bandwidth_limit,
"av-sync" : {
"enabled" : self.av_sync,
Expand Down Expand Up @@ -667,6 +673,11 @@ def _iconic_changed(self, _window, *_args):
else:
self.no_idle()

def _class_changed(self, window, *_args):
self.content_type = guess_content_type(window)
log("class-changed(%s, %s) content-type=%s", window, _args, self.content_type)


def set_client_properties(self, properties):
#filter out stuff we don't care about
#to see if there is anything to set at all,
Expand Down Expand Up @@ -2128,7 +2139,7 @@ def make_draw_packet(self, x, y, outw, outh, coding, data, outstride, client_opt
def webp_encode(self, coding, image, options):
q = options.get("quality") or self.get_quality(coding)
s = options.get("speed") or self.get_speed(coding)
return webp_encode(image, self.rgb_formats, self.supports_transparency, q, s)
return webp_encode(image, self.rgb_formats, self.supports_transparency, q, s, self.content_type)

def rgb_encode(self, coding, image, options):
s = options.get("speed") or self._current_speed
Expand Down
18 changes: 12 additions & 6 deletions src/xpra/server/window/window_video_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,10 @@ def lossless(reason):
#ensure the dimensions we use for decision making are the ones actually used:
cww = ww & self.width_mask
cwh = wh & self.height_mask
video_hint = self.content_type=="video"

rgbmax = self._rgb_auto_threshold
videomin = min(640*480, cww*cwh)
videomin = min(640*480, cww*cwh) // (1+video_hint*2)
sr = self.video_subregion.rectangle
if sr:
videomin = min(videomin, sr.width * sr.height)
Expand Down Expand Up @@ -411,7 +412,6 @@ def lossless(reason):
lim = now-4
pixels_last_4secs = sum(w*h for when,_,_,w,h in lde if when>lim)
if pixels_last_4secs<3*videomin:
#less than 5 full frames in last 2 seconds
return nonvideo(quality+30, "not enough frames")
lim = now-1
pixels_last_sec = sum(w*h for when,_,_,w,h in lde if when>lim)
Expand All @@ -423,7 +423,8 @@ def lossless(reason):
factors = (max(1, (speed-75)/5.0), #speed multiplier
1 + int(self.is_OR or self.is_tray)*2, #OR windows tend to be static
max(1, 10-self._sequence), #gradual discount the first 9 frames, as the window may be temporary
1.0 / (int(bool(self._video_encoder)) + 1) #if we have a video encoder already, make it more likely we'll use it:
1.0 / (int(bool(self._video_encoder)) + 1), #if we have a video encoder already, make it more likely we'll use it:
(1-video_hint/2.0), #video hint lowers the threshold
)
max_nvp = int(reduce(operator.mul, factors, MAX_NONVIDEO_PIXELS))
if pixel_count<=max_nvp:
Expand Down Expand Up @@ -614,9 +615,14 @@ def subregion_is_video(self):
if not vr:
return False
events_count = self.statistics.damage_events_count - self.video_subregion.set_at
if events_count<MIN_VIDEO_EVENTS:
min_video_events = MIN_VIDEO_EVENTS
min_video_fps = MIN_VIDEO_FPS
if self.content_type=="video":
min_video_events //= 2
min_video_fps //= 2
if events_count<min_video_events:
return False
if self.video_subregion.fps<MIN_VIDEO_FPS:
if self.video_subregion.fps<min_video_fps:
return False
return True

Expand Down Expand Up @@ -1685,7 +1691,7 @@ def do_video_encode(self, encoding, image, options):

#don't download the pixels if we have a GPU buffer,
#since that means we're likely to be able to compress on the GPU too with NVENC:
if self.supports_scrolling and image.has_pixels():
if self.supports_scrolling and image.has_pixels() and self.content_type!="video":
scroll_data = self.scroll_data
if self.b_frame_flush_timer and scroll_data:
scrolllog("not testing scrolling: b_frame_flush_timer=%s", self.b_frame_flush_timer)
Expand Down

0 comments on commit 7de4473

Please sign in to comment.