From a10e13738cb0ed47b9bdb8230802f2e9a70fa6c6 Mon Sep 17 00:00:00 2001 From: totaam Date: Tue, 30 May 2023 23:10:01 +0700 Subject: [PATCH] #3872 add 'stream' encoding option --- xpra/client/gtk_base/gtk_tray_menu_base.py | 2 + xpra/client/mixins/encodings.py | 30 +++++---- xpra/codecs/codec_constants.py | 8 +++ xpra/codecs/loader.py | 29 ++++---- xpra/scripts/main.py | 11 ++- xpra/server/mixins/encoding.py | 59 ++++++++-------- xpra/server/source/encodings.py | 74 ++++++++++---------- xpra/server/source/shell_mixin.py | 78 ---------------------- xpra/server/window/window_source.py | 51 +++++++------- xpra/server/window/window_video_source.py | 41 +++++++----- 10 files changed, 164 insertions(+), 219 deletions(-) delete mode 100644 xpra/server/source/shell_mixin.py diff --git a/xpra/client/gtk_base/gtk_tray_menu_base.py b/xpra/client/gtk_base/gtk_tray_menu_base.py index 645cd80629..e00fd35444 100644 --- a/xpra/client/gtk_base/gtk_tray_menu_base.py +++ b/xpra/client/gtk_base/gtk_tray_menu_base.py @@ -651,6 +651,8 @@ def get_encoding_options(self): #auto at the very top: client_encodings.insert(0, "auto") server_encodings.insert(0, "auto") + client_encodings.insert(1, "stream") + server_encodings.insert(1, "stream") return client_encodings, server_encodings def make_encodingssubmenu(self): diff --git a/xpra/client/mixins/encodings.py b/xpra/client/mixins/encodings.py index 6dd68992a5..e796ce1a65 100644 --- a/xpra/client/mixins/encodings.py +++ b/xpra/client/mixins/encodings.py @@ -5,9 +5,9 @@ # later version. See the file COPYING for details. import os -from typing import Dict, Any +from typing import Dict, Any, Tuple -from xpra.codecs.codec_constants import preforder +from xpra.codecs.codec_constants import preforder, STREAM_ENCODINGS from xpra.codecs.loader import load_codec, codec_versions, has_codec, get_codec from xpra.codecs.video_helper import getVideoHelper from xpra.scripts.config import parse_bool_or_int @@ -77,17 +77,17 @@ def __init__(self): self.video_scaling = None self.video_max_size = VIDEO_MAX_SIZE - self.server_encodings = [] - self.server_core_encodings = [] - self.server_encodings_with_speed = () - self.server_encodings_with_quality = () - self.server_encodings_with_lossless_mode = () + self.server_encodings : Tuple[str,...] = () + self.server_core_encodings : Tuple[str,...] = () + self.server_encodings_with_speed : Tuple[str,...] = () + self.server_encodings_with_quality : Tuple[str,...] = () + self.server_encodings_with_lossless_mode : Tuple[str,...] = () #what we told the server about our encoding defaults: self.encoding_defaults = {} - def init(self, opts): + def init(self, opts) -> None: self.allowed_encodings = opts.encodings self.encoding = opts.encoding if opts.video_scaling.lower() in ("auto", "on"): @@ -118,23 +118,23 @@ def init(self, opts): vh.init() - def cleanup(self): + def cleanup(self) -> None: try: getVideoHelper().cleanup() except Exception: # pragma: no cover log.error("error on video cleanup", exc_info=True) - def init_authenticated_packet_handlers(self): + def init_authenticated_packet_handlers(self) -> None: self.add_packet_handler("encodings", self._process_encodings, False) - def _process_encodings(self, packet): + def _process_encodings(self, packet) -> None: caps = typedict(packet[1]) self._parse_server_capabilities(caps) - def get_info(self): + def get_info(self) -> Dict[str,Any]: return { "encodings" : { "core" : self.get_core_encodings(), @@ -167,7 +167,7 @@ def parse_server_capabilities(self, c : typedict) -> bool: self._parse_server_capabilities(c) return True - def _parse_server_capabilities(self, c): + def _parse_server_capabilities(self, c) -> None: self.server_encodings = c.strtupleget("encodings", DEFAULT_ENCODINGS) self.server_core_encodings = c.strtupleget("encodings.core", self.server_encodings) #old servers only supported x264: @@ -270,7 +270,7 @@ def get_encodings_caps(self) -> Dict[str,Any]: log("encoding capabilities: %s", caps) return caps - def get_encodings(self): + def get_encodings(self) -> Tuple[str,...]: """ Unlike get_core_encodings(), this method returns "rgb" for both "rgb24" and "rgb32". That's because although we may support both, the encoding chosen is plain "rgb", @@ -280,6 +280,8 @@ def get_encodings(self): cenc = [{"rgb32" : "rgb", "rgb24" : "rgb"}.get(x, x) for x in self.get_core_encodings()] if "grayscale" not in cenc and "png/L" in cenc: cenc.append("grayscale") + if any(x in cenc for x in STREAM_ENCODINGS): + cenc.append("stream") return preforder(cenc) def get_cursor_encodings(self): diff --git a/xpra/codecs/codec_constants.py b/xpra/codecs/codec_constants.py index bb0768914c..f6122dda33 100644 --- a/xpra/codecs/codec_constants.py +++ b/xpra/codecs/codec_constants.py @@ -24,7 +24,14 @@ "h265", "av1", "scroll", "grayscale", + "stream", ) +STREAM_ENCODINGS : Tuple[str,...] = ( + "h264", "vp9", "vp8", "mpeg4", + "mpeg4+mp4", "h264+mp4", "vp8+webm", "vp9+webm", + "h265", "av1", + ) + #encoding order for edges (usually one pixel high or wide): EDGE_ENCODING_ORDER : Tuple[str, ...] = ( "rgb24", "rgb32", @@ -34,6 +41,7 @@ HELP_ORDER : Tuple[str, ...] = ( "auto", + "stream", "grayscale", "h264", "h265", "av1", "vp8", "vp9", "mpeg4", "png", "png/P", "png/L", "webp", "avif", diff --git a/xpra/codecs/loader.py b/xpra/codecs/loader.py index 7bd59fca3c..e9972d2608 100755 --- a/xpra/codecs/loader.py +++ b/xpra/codecs/loader.py @@ -54,7 +54,7 @@ def filt(*values) -> Tuple[str,...]: codec_errors : Dict[str,str] = {} codecs = {} -def codec_import_check(name, description, top_module, class_module, classnames): +def codec_import_check(name:str, description:str, top_module, class_module, classnames): log(f"{name}:") log(" codec_import_check%s", (name, description, top_module, class_module, classnames)) if any(name.find(s)>=0 for s in SKIP_LIST): @@ -142,7 +142,7 @@ def codec_import_check(name, description, top_module, class_module, classnames): name, description, exc_info=True) return None codec_versions : Dict[str,Tuple[Any, ...]]= {} -def add_codec_version(name, top_module, version="get_version()", alt_version="__version__"): +def add_codec_version(name:str, top_module, version:str="get_version()", alt_version:str="__version__"): try: fieldnames = [x for x in (version, alt_version) if x is not None] for fieldname in fieldnames: @@ -174,7 +174,7 @@ def add_codec_version(name, top_module, version="get_version()", alt_version="__ log.warn("", exc_info=True) return None -def xpra_codec_import(name, description, top_module, class_module, classname): +def xpra_codec_import(name:str, description:str, top_module, class_module, classname:str): xpra_top_module = f"xpra.codecs.{top_module}" xpra_class_module = f"{xpra_top_module}.{class_module}" if codec_import_check(name, description, xpra_top_module, xpra_class_module, classname): @@ -185,7 +185,7 @@ def xpra_codec_import(name, description, top_module, class_module, classname): platformname = sys.platform.rstrip("0123456789") -CODEC_OPTIONS = { +CODEC_OPTIONS : Dict[str,Tuple[str,str,str,str]] = { #encoders: "enc_rgb" : ("RGB encoder", "argb", "encoder", "encode"), "enc_pillow" : ("Pillow encoder", "pillow", "encoder", "encode"), @@ -226,7 +226,7 @@ def xpra_codec_import(name, description, top_module, class_module, classname): "nvfbc" : ("NVIDIA Capture SDK","nvidia.nvfbc", f"fbc_capture_{platformname}", "NvFBC_SysCapture"), } -NOLOAD = [] +NOLOAD : List[str] = [] if OSX: #none of the nvidia codecs are available on MacOS, #so don't bother trying: @@ -253,10 +253,9 @@ def load_codec(name): return get_codec(name) -def load_codecs(encoders=True, decoders=True, csc=True, video=True, sources=False): +def load_codecs(encoders=True, decoders=True, csc=True, video=True, sources=False) -> Tuple[str,...]: log("loading codecs") - - loaded = [] + loaded : List[str] = [] def load(*names): for name in names: if has_codec(name): @@ -281,9 +280,9 @@ def load(*names): if sources: load(*SOURCES) log("done loading codecs: %s", loaded) - return loaded + return tuple(loaded) -def show_codecs(show:Tuple[str,...]=()): +def show_codecs(show:Tuple[str,...]=()) -> None: #print("codec_status=%s" % codecs) for name in sorted(show or ALL_CODECS): log(f"* {name.ljust(20)} : {str(name in codecs).ljust(10)} {codecs.get(name, '')}") @@ -318,9 +317,10 @@ def get_rgb_compression_options() -> List[str]: RGB_COMP_OPTIONS += ["/".join(compressors)] return RGB_COMP_OPTIONS -def get_encoding_name(encoding): - ENCODINGS_TO_NAME = { +def get_encoding_name(encoding:str) -> str: + ENCODINGS_TO_NAME : Dict[str,str] = { "auto" : "automatic", + "stream" : "video stream", "h264" : "H.264", "h265" : "H.265", "mpeg4" : "MPEG4", @@ -338,7 +338,7 @@ def get_encoding_name(encoding): } return ENCODINGS_TO_NAME.get(encoding, encoding) -def get_encoding_help(encoding): +def get_encoding_help(encoding:str) -> str: # pylint: disable=import-outside-toplevel from xpra.net import compression compressors = [x for x in compression.get_enabled_compressors() @@ -348,6 +348,7 @@ def get_encoding_help(encoding): compressors_str = ", may be compressed using "+(" or ".join(compressors))+" " return { "auto" : "automatic mode (recommended)", + "stream" : "video stream", "grayscale" : "same as 'auto' but in grayscale mode", "h264" : "H.264 video codec", "h265" : "H.265 (HEVC) video codec (not recommended)", @@ -368,7 +369,7 @@ def get_encoding_help(encoding): }.get(encoding) -def encodings_help(encodings): +def encodings_help(encodings) -> List[str]: h = [] for e in HELP_ORDER: if e in encodings: diff --git a/xpra/scripts/main.py b/xpra/scripts/main.py index 389a3b5262..58ad37f15e 100755 --- a/xpra/scripts/main.py +++ b/xpra/scripts/main.py @@ -1514,14 +1514,13 @@ def get_client_gui_app(error_cb, opts, request_mode, extra_args, mode:str): if opts.encoding=="auto": opts.encoding = "" if opts.encoding: - encodings = app.get_encodings() - err = opts.encoding and (opts.encoding not in encodings) - einfo = "" - if err and opts.encoding!="help": + encodings = list(app.get_encodings())+["auto", "stream"] + err = opts.encoding not in encodings + ehelp = opts.encoding=="help" + if err and not ehelp: einfo = f"invalid encoding: {opts.encoding}\n" - if opts.encoding=="help" or err: + if err or ehelp: from xpra.codecs.loader import encodings_help - encodings = ["auto"] + list(encodings) raise InitInfo(einfo+"%s xpra client supports the following encodings:\n * %s" % (app.client_toolkit(), "\n * ".join(encodings_help(encodings)))) def handshake_complete(*_args): diff --git a/xpra/server/mixins/encoding.py b/xpra/server/mixins/encoding.py index fc75384ea6..61a8744f45 100644 --- a/xpra/server/mixins/encoding.py +++ b/xpra/server/mixins/encoding.py @@ -5,13 +5,13 @@ # later version. See the file COPYING for details. #pylint: disable-msg=E1101 -from typing import Dict, Any +from typing import Dict, Any, Tuple from xpra.scripts.config import parse_bool_or_int, csvstrl from xpra.util import envint from xpra.os_util import bytestostr, OSX from xpra.version_util import vtrim -from xpra.codecs.codec_constants import preforder +from xpra.codecs.codec_constants import preforder, STREAM_ENCODINGS from xpra.codecs.loader import get_codec, has_codec, codec_versions, load_codec from xpra.codecs.video_helper import getVideoHelper from xpra.server.mixins.stub_server_mixin import StubServerMixin @@ -34,17 +34,17 @@ def __init__(self): self.default_min_quality = 0 self.default_speed = -1 self.default_min_speed = 0 - self.allowed_encodings = None - self.core_encodings = () - self.encodings = () - self.lossless_encodings = () - self.lossless_mode_encodings = () - self.default_encoding = None + self.allowed_encodings : Tuple[str,...] = () + self.core_encodings : Tuple[str,...] = () + self.encodings : Tuple[str,...] = () + self.lossless_encodings : Tuple[str,...] = () + self.lossless_mode_encodings : Tuple[str,...] = () + self.default_encoding : str = "" self.scaling_control = None self.video_encoders = () self.csc_modules = () - def init(self, opts): + def init(self, opts) -> None: self.encoding = opts.encoding self.allowed_encodings = opts.encodings self.default_quality = opts.quality @@ -56,7 +56,7 @@ def init(self, opts): self.video_encoders = csvstrl(opts.video_encoders).split(",") self.csc_modules = csvstrl(opts.csc_modules).split(",") - def setup(self): + def setup(self) -> None: #essential codecs, load them early: load_codec("enc_rgb") load_codec("enc_pillow") @@ -75,7 +75,7 @@ def setup(self): self.init_encodings() self.add_init_thread_callback(self.reinit_encodings) - def reinit_encodings(self): + def reinit_encodings(self) -> None: self.init_encodings() #any window mapped before the threaded init completed #may need to re-initialize its list of encodings: @@ -84,7 +84,7 @@ def reinit_encodings(self): if isinstance(ss, WindowsMixin): ss.reinit_encodings(self) - def threaded_setup(self): + def threaded_setup(self) -> None: if INIT_DELAY>0: from time import sleep sleep(INIT_DELAY) @@ -96,11 +96,11 @@ def threaded_setup(self): getVideoHelper().init() self.init_encodings() - def cleanup(self): + def cleanup(self) -> None: getVideoHelper().cleanup() - def get_server_features(self, _source=None): + def get_server_features(self, _source=None) -> Dict[str,Any]: return { "auto-video-encoding" : True, #from v4.0, clients assume this is available } @@ -133,7 +133,7 @@ def get_encoding_info(self) -> Dict[str,Any]: "with_lossless_mode" : self.lossless_mode_encodings, } - def init_encodings(self): + def init_encodings(self) -> None: encs, core_encs = [], [] log("init_encodings() allowed_encodings=%s", self.allowed_encodings) def add_encoding(encoding): @@ -199,28 +199,25 @@ def add_encodings(*encodings): break #now update the variables: encs.append("grayscale") - self.encodings = encs - self.core_encodings = tuple(core_encs) - self.lossless_mode_encodings = tuple(lossless) - self.lossless_encodings = tuple(x for x in self.core_encodings + if any(x in encs for x in STREAM_ENCODINGS): + encs.append("stream") + self.encodings = preforder(encs) + self.core_encodings = preforder(core_encs) + self.lossless_mode_encodings = preforder(lossless) + self.lossless_encodings = preforder(x for x in self.core_encodings if (x.startswith("png") or x.startswith("rgb") or x=="webp")) log("allowed encodings=%s, encodings=%s, core encodings=%s, lossless encodings=%s", self.allowed_encodings, encs, core_encs, self.lossless_encodings) - pref = preforder(self.encodings) - if pref: - self.default_encoding = pref[0] - else: - self.default_encoding = None + self.default_encoding = self.encodings[0] #default encoding: if not self.encoding or str(self.encoding).lower() in ("auto", "none"): - self.default_encoding = None + self.default_encoding = "" elif self.encoding in self.encodings: - self.default_encoding = self.encoding - else: log.warn("ignored invalid default encoding option: %s", self.encoding) + self.default_encoding = self.encoding - def _process_encoding(self, proto, packet): + def _process_encoding(self, proto, packet) -> None: encoding = bytestostr(packet[1]) ss = self.get_server_source(proto) if ss is None: @@ -242,7 +239,7 @@ def _process_encoding(self, proto, packet): ss.set_encoding(encoding, wids) self._refresh_windows(proto, wid_windows, {}) - def _modify_sq(self, proto, packet): + def _modify_sq(self, proto, packet) -> None: """ modify speed or quality """ ss = self.get_server_source(proto) if not ss: @@ -258,14 +255,14 @@ def _modify_sq(self, proto, packet): fn(value) self.call_idle_refresh_all_windows(proto) - def call_idle_refresh_all_windows(self, proto): + def call_idle_refresh_all_windows(self, proto) -> None: #we can't assume that the window server mixin is loaded: refresh = getattr(self, "_idle_refresh_all_windows", None) if refresh: refresh(proto) # pylint: disable=not-callable - def init_packet_handlers(self): + def init_packet_handlers(self) -> None: self.add_packet_handler("encoding", self._process_encoding) for ptype in ( "quality", "min-quality", "max-quality", diff --git a/xpra/server/source/encodings.py b/xpra/server/source/encodings.py index 7992c4762e..4712397913 100644 --- a/xpra/server/source/encodings.py +++ b/xpra/server/source/encodings.py @@ -7,7 +7,7 @@ import os from math import sqrt -from typing import Dict, Any +from typing import Dict, Any, Tuple, Set from time import sleep, monotonic from xpra.server.source.stub_source_mixin import StubSourceMixin @@ -38,28 +38,28 @@ class EncodingsMixin(StubSourceMixin): def is_needed(cls, caps : typedict) -> bool: return bool(caps.strtupleget("encodings")) or caps.boolget("windows") - def init_state(self): + def init_state(self) -> None: #contains default values, some of which may be supplied by the client: self.default_batch_config = batch_config.DamageBatchConfig() self.global_batch_config = self.default_batch_config.clone() #global batch config - self.encoding = None #the default encoding for all windows - self.encodings = () #all the encodings supported by the client - self.core_encodings = () + self.encoding = "" #the default encoding for all windows + self.encodings : Tuple[str,...] = () #all the encodings supported by the client + self.core_encodings : Tuple[str,...] = () self.encodings_packet = False #supports delayed encodings initialization? - self.window_icon_encodings = [] - self.rgb_formats = ("RGB",) + self.window_icon_encodings : Tuple[str,...] = () + self.rgb_formats : Tuple[str,...] = ("RGB",) self.encoding_options = typedict() self.icons_encoding_options = typedict() self.default_encoding_options = typedict() - self.auto_refresh_delay = 0 + self.auto_refresh_delay : int = 0 self.zlib = True self.lz4 = use("lz4") #for managing the recalculate_delays work: self.calculate_window_pixels = {} - self.calculate_window_ids = set() + self.calculate_window_ids : Set[int] = set() self.calculate_timer = 0 self.calculate_last_time = 0 @@ -70,7 +70,7 @@ def init_state(self): self.cuda_device_context = None - def init_from(self, _protocol, server): + def init_from(self, _protocol, server) -> None: self.server_core_encodings = server.core_encodings self.server_encodings = server.encodings self.default_encoding = server.default_encoding @@ -80,11 +80,11 @@ def init_from(self, _protocol, server): self.default_speed = server.default_speed self.default_min_speed = server.default_min_speed - def reinit_encodings(self, server): + def reinit_encodings(self, server) -> None: self.server_core_encodings = server.core_encodings self.server_encodings = server.encodings - def cleanup(self): + def cleanup(self) -> None: self.cancel_recalculate_timer() if self.cuda_device_context: self.queue_encode((False, self.free_cuda_device_context)) @@ -95,13 +95,13 @@ def cleanup(self): #this should be a noop since we inherit an initialized helper: self.video_helper.cleanup() - def free_cuda_device_context(self): + def free_cuda_device_context(self) -> None: cdd = self.cuda_device_context if cdd: self.cuda_device_context = None cdd.free() - def all_window_sources(self): + def all_window_sources(self) -> Tuple: #we can't assume that the window mixin is loaded: window_sources = getattr(self, "window_sources", {}) return tuple(window_sources.values()) @@ -118,7 +118,7 @@ def get_caps(self) -> Dict[str,Any]: return caps - def recalculate_delays(self): + def recalculate_delays(self) -> None: """ calls update_averages() on ServerSource.statistics (GlobalStatistics) and WindowSource.statistics (WindowPerformanceStatistics) for each window id in calculate_window_ids, this runs in the worker thread. @@ -193,7 +193,7 @@ def recalculate_delays(self): log("delay_per_megapixel=%i, delay=%i, for wdelay=%i, avg_size=%i, ratio=%.2f", normalized_delay, delay, wdelay, avg_size, ratio) - def may_recalculate(self, wid, pixel_count): + def may_recalculate(self, wid:int, pixel_count:int) -> None: if wid in self.calculate_window_ids: return #already scheduled v = self.calculate_window_pixels.get(wid, 0)+pixel_count @@ -213,14 +213,14 @@ def may_recalculate(self, wid, pixel_count): delay = int(1000*(RECALCULATE_DELAY-delta)) self.calculate_timer = self.timeout_add(delay, add_work_item, self.recalculate_delays) - def cancel_recalculate_timer(self): + def cancel_recalculate_timer(self) -> None: ct = self.calculate_timer if ct: self.calculate_timer = 0 self.source_remove(ct) - def parse_client_caps(self, c : typedict): + def parse_client_caps(self, c : typedict) -> None: #batch options: def batch_value(prop, default, minv=None, maxv=None): assert default is not None @@ -287,7 +287,7 @@ def parse_batch_int(value, varname): else: self.parse_encoding_caps(c) - def parse_encoding_caps(self, c): + def parse_encoding_caps(self, c:typedict) -> None: self.set_encoding(c.strget("encoding", None), None) #encoding options (filter): #1: these properties are special cased here because we @@ -375,23 +375,25 @@ def parse_encoding_caps(self, c): cudalog.estr(e) cudalog.error(" NVJPEG and NVENC will not be available") - def print_encoding_info(self): + def print_encoding_info(self) -> None: log("print_encoding_info() core-encodings=%s, server-core-encodings=%s", self.core_encodings, self.server_core_encodings) others = tuple(x for x in self.core_encodings if x in self.server_core_encodings and x!=self.encoding) if self.encoding=="auto": s = "automatic picture encoding enabled" + elif self.encoding=="stream": + s = "streaming mode enabled" else: - s = f"using {self.encoding} as primary encoding" + s = f"using {self.encoding!r} as primary encoding" if others: - log.info(" %s, also available:", s) - log.info(" %s", csv(others)) + log.info(f" {s}, also available:") + log.info(" "+ csv(others)) else: - log.warn(" %s", s) + log.warn(f" {s}") log.warn(" no other encodings are available!") - def parse_proxy_video(self): + def parse_proxy_video(self) -> None: self.wait_for_threaded_init() from xpra.codecs.proxy.encoder import Encoder #pylint: disable=import-outside-toplevel proxy_video_encodings = self.encoding_options.get("proxy.video.encodings") @@ -426,7 +428,7 @@ def parse_proxy_video(self): # Functions used by the server to request something # (window events, stats, user requests, etc) # - def set_auto_refresh_delay(self, delay : int, window_ids): + def set_auto_refresh_delay(self, delay : int, window_ids) -> None: if window_ids is not None: wss = (self.window_sources.get(wid) for wid in window_ids) else: @@ -435,20 +437,20 @@ def set_auto_refresh_delay(self, delay : int, window_ids): if ws is not None: ws.set_auto_refresh_delay(delay) - def set_encoding(self, encoding : str, window_ids, strict=False): + def set_encoding(self, encoding : str, window_ids, strict=False) -> None: """ Changes the encoder for the given 'window_ids', or for all windows if 'window_ids' is None. """ log("set_encoding(%s, %s, %s)", encoding, window_ids, strict) - if encoding and encoding!="auto": + if encoding and encoding not in ("auto", "stream"): #old clients (v0.9.x and earlier) only supported 'rgb24' as 'rgb' mode: if encoding=="rgb24": encoding = "rgb" if encoding not in self.encodings: - log.warn("Warning: client specified '%s' encoding,", encoding) + log.warn(f"Warning: client specified {encoding!r} encoding,") log.warn(" but it only supports: " + csv(self.encodings)) if encoding not in self.server_encodings: - log.error("Error: encoding %s is not supported by this server", encoding) + log.error(f"Error: encoding {encoding!r} is not supported by this server") log.error(" server encodings: " + csv(self.server_encodings)) encoding = None if not encoding: @@ -497,27 +499,27 @@ def get_info(self) -> Dict[str,Any]: return info - def set_min_quality(self, min_quality : int): + def set_min_quality(self, min_quality : int) -> None: for ws in tuple(self.all_window_sources()): ws.set_min_quality(min_quality) - def set_max_quality(self, max_quality : int): + def set_max_quality(self, max_quality : int) -> None: for ws in tuple(self.all_window_sources()): ws.set_max_quality(max_quality) - def set_quality(self, quality : int): + def set_quality(self, quality : int) -> None: for ws in tuple(self.all_window_sources()): ws.set_quality(quality) - def set_min_speed(self, min_speed : int): + def set_min_speed(self, min_speed : int) -> None: for ws in tuple(self.all_window_sources()): ws.set_min_speed(min_speed) - def set_max_speed(self, max_speed : int): + def set_max_speed(self, max_speed : int) -> None: for ws in tuple(self.all_window_sources()): ws.set_max_speed(max_speed) - def set_speed(self, speed : int): + def set_speed(self, speed : int) -> None: for ws in tuple(self.all_window_sources()): ws.set_speed(speed) diff --git a/xpra/server/source/shell_mixin.py b/xpra/server/source/shell_mixin.py deleted file mode 100644 index 41c1bca109..0000000000 --- a/xpra/server/source/shell_mixin.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of Xpra. -# Copyright (C) 2020-2023 Antoine Martin -# Xpra is released under the terms of the GNU GPL v2, or, at your option, any -# later version. See the file COPYING for details. - -import io -from typing import Dict, Any -from contextlib import redirect_stdout, redirect_stderr - -from xpra.util import typedict -from xpra.scripts.config import TRUE_OPTIONS -from xpra.server.source.stub_source_mixin import StubSourceMixin -from xpra.log import Logger - -log = Logger("exec") - - -class ShellMixin(StubSourceMixin): - - @classmethod - def is_needed(cls, caps : typedict) -> bool: - return caps.boolget("shell", False) - - def __init__(self, *_args): - self._server = None - self.shell_enabled = False - self.saved_logging_handler = None - self.log_records = [] - self.log_thread = None - - def init_from(self, protocol, server): - self._server = server - try: - options = protocol._conn.options - shell = options.get("shell", "") - self.shell_enabled = shell.lower() in TRUE_OPTIONS - except AttributeError: - options = {} - self.shell_enabled = False - log("init_from(%s, %s) shell_enabled(%s)=%s", protocol, server, options, self.shell_enabled) - - def get_caps(self) -> Dict[str,Any]: - return {"shell" : self.shell_enabled} - - def get_info(self) -> Dict[str,Any]: - return {"shell" : self.shell_enabled} - - def shell_exec(self, code): - stdout, stderr = self.do_shell_exec(code) - log("shell_exec(%s) stdout=%r", code, stdout) - log("shell_exec(%s) stderr=%r", code, stderr) - if stdout is not None: - self.send("shell-reply", 1, stdout) - if stderr: - self.send("shell-reply", 2, stderr) - return stdout, stderr - - def do_shell_exec(self, code): - log("shell_exec(%r)", code) - try: - assert self.shell_enabled, "shell support is not available with this connection" - _globals = { - "connection" : self, - "server" : self._server, - "log" : log, - } - stdout = io.StringIO() - stderr = io.StringIO() - with redirect_stdout(stdout): - with redirect_stderr(stderr): - exec(code, _globals, {}) #pylint: disable=exec-used - return stdout.getvalue(), stderr.getvalue() - except Exception as e: - log("shell_exec(..)", exc_info=True) - log.error("Error running %r:", code) - log.estr(e) - return None, str(e) diff --git a/xpra/server/window/window_source.py b/xpra/server/window/window_source.py index 6222cd2fcb..59873780b2 100644 --- a/xpra/server/window/window_source.py +++ b/xpra/server/window/window_source.py @@ -165,14 +165,14 @@ def __init__(self, ww:int, wh:int, record_congestion_event:Callable, queue_size:Callable, call_in_encode_thread:Callable, queue_packet:Callable, statistics, - wid:int, window, batch_config, auto_refresh_delay, - av_sync, av_sync_delay, + wid:int, window, batch_config, auto_refresh_delay:int, + av_sync:bool, av_sync_delay:int, video_helper, cuda_device_context, - server_core_encodings, server_encodings, - encoding:str, encodings, core_encodings, window_icon_encodings, - encoding_options, icons_encoding_options, - rgb_formats, + server_core_encodings:Tuple[str,...], server_encodings:Tuple[str,...], + encoding:str, encodings:Tuple[str,...], core_encodings:Tuple[str,...], window_icon_encodings:Tuple[str,...], + encoding_options:typedict, icons_encoding_options:typedict, + rgb_formats:Tuple[str,...], default_encoding_options, mmap, mmap_size:int, bandwidth_limit:int, jitter:int): super().__init__(window_icon_encodings, icons_encoding_options) @@ -277,22 +277,23 @@ def children_updated(*_args): self.jitter = jitter self.pixel_format = None #ie: BGRX - self.image_depth = window.get_property("depth") + self.image_depth : int = window.get_property("depth") # general encoding tunables (mostly used by video encoders): #keep track of the target encoding_quality: (event time, info, encoding speed): - self._encoding_quality : deque = deque(maxlen=100) - self._encoding_quality_info = {} + self._encoding_quality : deque[Tuple[float,int]] = deque(maxlen=100) + self._encoding_quality_info : Dict[str,Any] = {} #keep track of the target encoding_speed: (event time, info, encoding speed): - self._encoding_speed : deque = deque(maxlen=100) - self._encoding_speed_info = {} + self._encoding_speed : deque[Tuple[float,int]] = deque(maxlen=100) + self._encoding_speed_info : Dict[str,Any] = {} # they may have fixed values: - self._fixed_quality = default_encoding_options.get("quality", -1) - self._fixed_min_quality = capr(default_encoding_options.get("min-quality", 0)) - self._fixed_max_quality = capr(default_encoding_options.get("max-quality", MAX_QUALITY)) - self._fixed_speed = default_encoding_options.get("speed", -1) - self._fixed_min_speed = capr(default_encoding_options.get("min-speed", 0)) - self._fixed_max_speed = capr(default_encoding_options.get("max-speed", MAX_SPEED)) + deo = typedict(default_encoding_options) + self._fixed_quality = deo.intget("quality", -1) + self._fixed_min_quality = capr(deo.intget("min-quality", 0)) + self._fixed_max_quality = capr(deo.intget("max-quality", MAX_QUALITY)) + self._fixed_speed = deo.intget("speed", -1) + self._fixed_min_speed = capr(deo.intget("min-speed", 0)) + self._fixed_max_speed = capr(deo.intget("max-speed", MAX_SPEED)) self._encoding_hint : str = "" self._quality_hint = self.window.get("quality", -1) dyn_props = window.get_dynamic_property_names() @@ -337,10 +338,10 @@ def children_updated(*_args): self._current_speed = capr(self._fixed_speed) else: self._current_speed = capr(encoding_options.intget("initial_speed", INITIAL_SPEED*(1+int(nobwl)))) - self._want_alpha = False - self._lossless_threshold_base = 85 - self._lossless_threshold_pixel_boost = 20 - self._rgb_auto_threshold = MAX_PIXELS_PREFER_RGB + self._want_alpha : bool = False + self._lossless_threshold_base : int = 85 + self._lossless_threshold_pixel_boost : int = 20 + self._rgb_auto_threshold : int = MAX_PIXELS_PREFER_RGB self.init_encoders() log("initial encoding for %s: %s", self.wid, self.encoding) @@ -877,7 +878,7 @@ def update_encoding_selection(self, encoding=None, exclude=(), init:bool=False) raise ValueError("no common encodings found (server: %s vs client: %s, excluding: %s)" % ( csv(self._encoders.keys()), csv(self.core_encodings), csv(exclude))) #ensure the encoding chosen is supported by this source: - if (encoding in self.common_encodings or encoding in ("auto", "grayscale")) and len(self.common_encodings)>1: + if (encoding in self.common_encodings or encoding in ("stream", "auto", "grayscale")) and len(self.common_encodings)>1: self.encoding = encoding else: self.encoding = self.common_encodings[0] @@ -986,7 +987,7 @@ def get_best_encoding_impl(self) -> Callable: return self.encoding_is_pngL assert "png/P" in self.common_encodings return self.encoding_is_pngP - if self.strict and self.encoding!="auto": + if self.strict and self.encoding not in ("auto", "stream"): #honour strict flag if self.encoding=="rgb": #choose between rgb32 and rgb24 already @@ -1018,7 +1019,7 @@ def get_best_encoding_impl(self) -> Callable: def get_best_encoding_impl_default(self) -> Callable: #stick to what is specified or use rgb for small regions: - if self.encoding=="auto": + if self.encoding in ("auto", "stream"): return self.get_auto_encoding if self.encoding=="grayscale": return self.encoding_is_grayscale @@ -1940,7 +1941,7 @@ def send_full_window_update(cause): self.process_damage_region(damage_time, 0, 0, ww, wh, actual_encoding, options) if exclude_region is None: - if self.full_frames_only: + if self.full_frames_only or self.encoding=="stream": send_full_window_update("full-frames-only set") return diff --git a/xpra/server/window/window_video_source.py b/xpra/server/window/window_video_source.py index 1b962b5ec1..046add45a9 100644 --- a/xpra/server/window/window_video_source.py +++ b/xpra/server/window/window_video_source.py @@ -192,6 +192,7 @@ def add(enc, encode_fn): #video_encode() is used for more than just video encoders: #(always enable it and let it fall through) add("auto", self.video_encode) + add("stream", self.video_encode) #these are used for non-video areas, ensure "jpeg" is used if available #as we may be dealing with large areas still, and we want speed: enc_options = set(self.server_core_encodings) & set(self._encoders.keys()) @@ -461,6 +462,10 @@ def do_set_client_properties(self, properties : typedict) -> None: def get_best_encoding_impl_default(self) -> Callable: log("get_best_encoding_impl_default() window_type=%s, encoding=%s", self.window_type, self.encoding) + if self.is_tray: + return super().get_best_encoding_impl_default() + if self.encoding=="stream": + return self.get_best_encoding_video if self.window_type.intersection(LOSSLESS_WINDOW_TYPES): return super().get_best_encoding_impl_default() if self.encoding!="grayscale" or has_codec("csc_libyuv"): @@ -469,7 +474,7 @@ def get_best_encoding_impl_default(self) -> Callable: return super().get_best_encoding_impl_default() - def get_best_encoding_video(self, ww : int, wh : int, options, current_encoding : str) -> str: + def get_best_encoding_video(self, w : int, h : int, options, current_encoding : str) -> str: """ decide whether we send a full window update using the video encoder, or if a separate small region(s) is a better choice @@ -479,7 +484,7 @@ def nonvideo(qdiff=None, info=""): quality = options.get("quality", self._current_quality) + qdiff options["quality"] = max(self._fixed_min_quality, min(self._fixed_max_quality, quality)) videolog("nonvideo(%s, %s)", qdiff, info) - return WindowSource.get_auto_encoding(self, ww, wh, options) + return WindowSource.get_auto_encoding(self, w, h, options) #log("get_best_encoding_video%s non_video_encodings=%s, common_video_encodings=%s, supports_scrolling=%s", # (pixel_count, ww, wh, speed, quality, current_encoding), @@ -487,7 +492,7 @@ def nonvideo(qdiff=None, info=""): if not self.non_video_encodings: return current_encoding if not self.common_video_encodings and not self.supports_scrolling: - return nonvideo(info="no common video encodings") + return nonvideo(info="no common video encodings or scrolling") if self.is_tray: return nonvideo(100, "system tray") text_hint = self.content_type.find("text")>=0 @@ -495,8 +500,14 @@ def nonvideo(qdiff=None, info=""): return nonvideo(100, info="text content-type") #ensure the dimensions we use for decision making are the ones actually used: - cww = ww & self.width_mask - cwh = wh & self.height_mask + cww = w & self.width_mask + cwh = h & self.height_mask + if cww<64 or cwh<64: + return nonvideo(info="area is too small") + + if self.encoding=="stream": + return current_encoding + video_hint = int(self.content_type.find("video")>=0) if self.pixel_format: #if we have a hardware video encoder, use video more: @@ -520,16 +531,13 @@ def nonvideo(qdiff=None, info=""): else: videomin = min(640*480, cww*cwh) // (1+video_hint*2) #log(f"ww={ww}, wh={wh}, rgbmax={rgbmax}, videohint={video_hint} videomin={videomin}, sr={sr}, pixel_count={pixel_count}") - pixel_count = ww*wh - if pixel_count<=rgbmax or cww<8 or cwh<8: + pixel_count = w*h + if pixel_count<=rgbmax: return nonvideo(info=f"low pixel count {pixel_count}") if current_encoding not in ("auto", "grayscale") and current_encoding not in self.common_video_encodings: return nonvideo(info=f"{current_encoding} not a supported video encoding") - if cww*cwh<=MAX_NONVIDEO_PIXELS or cww<16 or cwh<16: - return nonvideo(info="window is too small") - if cwwself.max_w or cwhself.max_h: return nonvideo(info="size out of range for video encoder") @@ -568,7 +576,7 @@ def nonvideo(qdiff=None, info=""): max_nvp = int(reduce(operator.mul, factors, MAX_NONVIDEO_PIXELS)) if pixel_count<=max_nvp: #below threshold - return nonvideo(info="not enough pixels") + return nonvideo(info=f"not enough pixels: {pixel_count}<{max_nvp}") return current_encoding def get_best_nonvideo_encoding(self, ww : int, wh : int, options : Dict, @@ -773,7 +781,7 @@ def send_nonvideo(regions=regions, encoding=coding, exclude_region=None, send_nonvideo(encoding=None) return - if coding not in ("auto", "grayscale") and coding not in self.video_encodings: + if coding not in ("auto", "stream", "grayscale") and coding not in self.video_encodings: sublog("not a video encoding: %s", coding) #keep current encoding selection function send_nonvideo(get_best_encoding=self.get_best_encoding) @@ -923,7 +931,7 @@ def process_damage_region(self, damage_time, x : int, y : int, w : int, h : int, # * the video encoder needs a thread safe image # (the xshm backing may change from underneath us if we don't freeze it) av_delay = options.get("av-delay", 0) - video_mode = coding in self.common_video_encodings or (coding=="auto" and self.common_video_encodings) + video_mode = coding in self.common_video_encodings or (coding in ("auto", "stream") and self.common_video_encodings) must_freeze = av_delay>0 or (video_mode and not image.is_thread_safe()) log("process_damage_region: av_delay=%s, must_freeze=%s, size=%s, encoding=%s", av_delay, must_freeze, (w, h), coding) @@ -1099,6 +1107,9 @@ def update_encoding_video_subregion(self) -> None: vs = self.video_subregion if not vs: return + if self.encoding=="stream": + vs.reset() + return if (self.encoding not in ("auto", "grayscale") and self.encoding not in self.common_video_encodings) or \ self.full_frames_only or STRICT_MODE or not self.non_video_encodings or not self.common_video_encodings or \ (self.content_type.find("text")>=0 and TEXT_USE_VIDEO) or \ @@ -1202,7 +1213,7 @@ def checknovideo(*info): scorelog(*info) self.cleanup_codecs() #which video encodings to evaluate: - if self.encoding in ("auto", "grayscale"): + if self.encoding in ("auto", "stream", "grayscale"): if not self.common_video_encodings: return checknovideo("no common video encodings") eval_encodings = self.common_video_encodings @@ -1641,7 +1652,7 @@ def check_pipeline(self, encoding : str, width : int, height : int, src_format : Runs in the 'encode' thread. """ - if encoding in ("auto", "grayscale"): + if encoding in ("auto", "stream", "grayscale"): encodings = self.common_video_encodings else: encodings = (encoding, )