optional quality and resolution

This commit is contained in:
Devaev Maxim 2020-07-13 04:10:26 +03:00
parent 07fb731b21
commit 77f3dab55c
8 changed files with 122 additions and 29 deletions

View File

@ -56,6 +56,7 @@ from ..validators.basic import valid_number
from ..validators.basic import valid_int_f1
from ..validators.basic import valid_float_f0
from ..validators.basic import valid_float_f01
from ..validators.basic import valid_string_list
from ..validators.auth import valid_user
from ..validators.auth import valid_users_list
@ -74,6 +75,7 @@ from ..validators.net import valid_ssl_ciphers
from ..validators.kvm import valid_stream_quality
from ..validators.kvm import valid_stream_fps
from ..validators.kvm import valid_stream_resolution
from ..validators.kvm import valid_hid_key
from ..validators.kvm import valid_hid_mouse_move
@ -259,9 +261,11 @@ def _get_config_scheme() -> Dict:
"shutdown_delay": Option(10.0, type=valid_float_f01),
"state_poll": Option(1.0, type=valid_float_f01),
"quality": Option(80, type=valid_stream_quality),
"quality": Option(80, type=(lambda arg: (valid_stream_quality(arg) if arg else 0))), # 0 for disabled feature
"desired_fps": Option(30, type=valid_stream_fps),
"max_fps": Option(60, type=valid_stream_fps),
"resolution": Option("", type=(lambda arg: (valid_stream_resolution(arg) if arg else ""))),
"available_resolutions": Option([], type=(lambda arg: valid_string_list(arg, subval=valid_stream_resolution))),
"host": Option("localhost", type=valid_ip_or_host),
"port": Option(0, type=valid_port),

View File

@ -56,6 +56,7 @@ from ...validators.basic import valid_bool
from ...validators.kvm import valid_stream_quality
from ...validators.kvm import valid_stream_fps
from ...validators.kvm import valid_stream_resolution
from ... import aiotools
from ... import aioproc
@ -193,6 +194,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
for (name, validator) in [
("quality", valid_stream_quality),
("desired_fps", valid_stream_fps),
("resolution", valid_stream_resolution),
]:
if (value := request.query.get(name)):
value = validator(value)

View File

@ -54,6 +54,69 @@ class StreamerSnapshot:
data: bytes
class _StreamerParams:
__DESIRED_FPS = "desired_fps"
__MAX_FPS = "max_fps"
__QUALITY = "quality"
__RESOLUTION = "resolution"
__AVAILABLE_RESOLUTIONS = "available_resolutions"
def __init__(
self,
desired_fps: int,
max_fps: int,
quality: int,
resolution: str,
available_resolutions: List[str],
) -> None:
self.__has_quality = bool(quality)
self.__has_resolution = bool(resolution)
self.__params: Dict = {
self.__DESIRED_FPS: desired_fps,
**({self.__QUALITY: quality} if self.__has_quality else {}),
**({self.__RESOLUTION: resolution} if self.__has_resolution else {}),
}
self.__limits: Dict = {
self.__MAX_FPS: max_fps,
**({self.__AVAILABLE_RESOLUTIONS: available_resolutions} if self.__has_resolution else {}),
}
def get_features(self) -> Dict:
return {
self.__QUALITY: self.__has_quality,
self.__RESOLUTION: self.__has_resolution,
}
def get_limits(self) -> Dict:
limits = dict(self.__limits)
if self.__has_resolution:
limits[self.__AVAILABLE_RESOLUTIONS] = list(limits[self.__AVAILABLE_RESOLUTIONS])
return limits
def get_params(self) -> Dict:
return dict(self.__params)
def set_params(self, params: Dict) -> None:
new_params = dict(self.__params)
if self.__DESIRED_FPS in params:
new_params[self.__DESIRED_FPS] = min(max(params[self.__DESIRED_FPS], 0), self.__limits[self.__MAX_FPS])
if self.__QUALITY in params and self.__has_quality:
new_params[self.__QUALITY] = min(max(params[self.__QUALITY], 1), 100)
if self.__RESOLUTION in params and self.__has_resolution:
if params[self.__RESOLUTION] in self.__limits[self.__AVAILABLE_RESOLUTIONS]:
new_params[self.__RESOLUTION] = params[self.__RESOLUTION]
self.__params = new_params
class Streamer: # pylint: disable=too-many-instance-attributes
def __init__( # pylint: disable=too-many-arguments,too-many-locals
self,
@ -66,10 +129,6 @@ class Streamer: # pylint: disable=too-many-instance-attributes
shutdown_delay: float,
state_poll: float,
quality: int,
desired_fps: int,
max_fps: int,
host: str,
port: int,
unix_path: str,
@ -78,6 +137,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes
process_name_prefix: str,
cmd: List[str],
**params_kwargs: Any,
) -> None:
self.__cap_pin = (gpio.set_output(cap_pin) if cap_pin >= 0 else -1)
@ -89,12 +150,6 @@ class Streamer: # pylint: disable=too-many-instance-attributes
self.__shutdown_delay = shutdown_delay
self.__state_poll = state_poll
self.__params = {
"quality": quality,
"desired_fps": desired_fps,
}
self.__max_fps = max_fps
assert port or unix_path
self.__host = host
self.__port = port
@ -105,6 +160,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes
self.__cmd = cmd
self.__params = _StreamerParams(**params_kwargs)
self.__stop_task: Optional[asyncio.Task] = None
self.__stop_wip = False
@ -183,16 +240,10 @@ class Streamer: # pylint: disable=too-many-instance-attributes
def set_params(self, params: Dict) -> None:
assert not self.__streamer_task
self.__params = {
key: min(max(params.get(key, self.__params[key]), a), b)
for (key, a, b) in [
("quality", 0, 100),
("desired_fps", 0, self.__max_fps),
]
}
return self.__params.set_params(params)
def get_params(self) -> Dict:
return dict(self.__params)
return self.__params.get_params()
# =====
@ -216,10 +267,11 @@ class Streamer: # pylint: disable=too-many-instance-attributes
del snapshot["data"]
return {
"limits": {"max_fps": self.__max_fps},
"params": self.__params,
"limits": self.__params.get_limits(),
"params": self.__params.get_params(),
"snapshot": {"saved": snapshot},
"state": state,
"features": self.__params.get_features(),
}
async def poll_state(self) -> AsyncGenerator[Dict, None]:
@ -370,11 +422,11 @@ class Streamer: # pylint: disable=too-many-instance-attributes
except asyncio.CancelledError:
break
except Exception as err:
except Exception:
if self.__streamer_proc:
logger.exception("Unexpected streamer error: pid=%d", self.__streamer_proc.pid)
else:
logger.exception("Can't start streamer: %s", err)
logger.exception("Can't start streamer")
await self.__kill_streamer_proc()
await asyncio.sleep(1)
@ -386,7 +438,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
port=self.__port,
unix=self.__unix_path,
process_name_prefix=self.__process_name_prefix,
**self.__params,
**self.__params.get_params(),
)
for part in self.__cmd
]

View File

@ -24,8 +24,10 @@ from typing import Any
from ..keyboard.mappings import KEYMAP
from . import raise_error
from . import check_string_in_list
from .basic import valid_stripped_string_not_empty
from .basic import valid_number
from .os import valid_printable_filename
@ -56,6 +58,17 @@ def valid_stream_fps(arg: Any) -> int:
return int(valid_number(arg, min=0, max=120, name="stream FPS"))
def valid_stream_resolution(arg: Any) -> str:
name = "stream resolution"
arg = valid_stripped_string_not_empty(arg, name)
parts = arg.split("x")
if len(parts) != 2:
raise_error(arg, name)
width = int(valid_number(parts[0], min=1, name=f"{name} (width)"))
height = int(valid_number(parts[1], min=1, name=f"{name} (height)"))
return f"{width}x{height}"
# =====
def valid_hid_key(arg: Any) -> str:
return check_string_in_list(arg, "HID key", KEYMAP, lower=False)

View File

@ -142,6 +142,7 @@ def test_fail__valid_float_f01(arg: Any) -> None:
# =====
@pytest.mark.parametrize("arg, retval", [
("a, b, c", ["a", "b", "c"]),
("a, b,, c", ["a", "b", "c"]),
("a b c", ["a", "b", "c"]),
(["a", "b", "c"], ["a", "b", "c"]),
("", []),

View File

@ -32,6 +32,7 @@ from kvmd.validators.kvm import valid_atx_button
from kvmd.validators.kvm import valid_log_seek
from kvmd.validators.kvm import valid_stream_quality
from kvmd.validators.kvm import valid_stream_fps
from kvmd.validators.kvm import valid_stream_resolution
from kvmd.validators.kvm import valid_hid_key
from kvmd.validators.kvm import valid_hid_mouse_move
from kvmd.validators.kvm import valid_hid_mouse_button
@ -104,6 +105,20 @@ def test_fail__valid_stream_fps(arg: Any) -> None:
print(valid_stream_fps(arg))
# =====
@pytest.mark.parametrize("arg", ["1280x720 ", "1x1"])
def test_ok__valid_stream_resolution(arg: Any) -> None:
value = valid_stream_resolution(arg)
assert type(value) == str # pylint: disable=unidiomatic-typecheck
assert value == str(arg).strip()
@pytest.mark.parametrize("arg", ["x", None, "0x0", "0x1", "1x0", "1280", "1280x", "1280x720x"])
def test_fail__valid_stream_resolution(arg: Any) -> None:
with pytest.raises(ValidatorError):
print(valid_stream_resolution(arg))
# =====
def test_ok__valid_hid_key() -> None:
for key in KEYMAP:

View File

@ -142,11 +142,13 @@
<button data-force-hide-menu id="show-keyboard-button">&bull; Show keyboard</button>
<button data-force-hide-menu id="show-about-button">&bull; Show about</button>
</div>
<hr>
<div class="menu-item-content-text">
Stream quality: <span id="stream-quality-value">80%</span>
<div class="stream-slider-box">
<input disabled type="range" id="stream-quality-slider" class="slider" />
<div id="stream-quality" class="feature-disabled">
<hr>
<div class="menu-item-content-text">
Stream quality: <span id="stream-quality-value">80%</span>
<div class="stream-slider-box">
<input disabled type="range" id="stream-quality-slider" class="slider" />
</div>
</div>
</div>
<hr>

View File

@ -67,6 +67,10 @@ export function Streamer() {
/************************************************************************/
self.setState = function(state) {
if (state) {
tools.setFeatureEnabled($("stream-quality"), state.features.quality && (state.state === null || state.state.encoder.quality > 0));
}
if (state && state.state) {
let max_fps = state.limits.max_fps;
state = state.state;