configurable stream resolution

This commit is contained in:
Devaev Maxim 2018-08-24 07:13:40 +03:00
parent 0468100bba
commit 52d2b8a315
12 changed files with 383 additions and 41 deletions

View File

@ -36,14 +36,13 @@ kvmd:
init_restart_after: 1.0
shutdown_delay: 10.0
size:
width: 800
height: 600
resolutions:
- 800x600 - 720x576
cmd:
- "/usr/bin/mjpg_streamer"
- "-i"
- "input_uvc.so -d /dev/kvmd-streamer -e 2 -t pal -y -n -r 720x576"
- "input_uvc.so -d /dev/kvmd-streamer -e 2 -t pal -y -n -r {resolution}"
- "-o"
- "output_http.so -l localhost -p 8082"

View File

@ -49,8 +49,7 @@ def main() -> None:
sync_delay=float(config["streamer"]["sync_delay"]),
init_delay=float(config["streamer"]["init_delay"]),
init_restart_after=float(config["streamer"]["init_restart_after"]),
width=int(config["streamer"]["size"]["width"]),
height=int(config["streamer"]["size"]["height"]),
resolutions=config["streamer"]["resolutions"],
cmd=list(map(str, config["streamer"]["cmd"])),
loop=loop,
)

View File

@ -128,6 +128,7 @@ class Server: # pylint: disable=too-many-instance-attributes
self.__system_tasks: List[asyncio.Task] = []
self.__reset_streamer = False
self.__streamer_resolution = streamer.get_current_resolution()
def run(self, host: str, port: int) -> None:
self.__hid.start()
@ -148,6 +149,7 @@ class Server: # pylint: disable=too-many-instance-attributes
app.router.add_post("/msd/write", self.__msd_write_handler)
app.router.add_get("/streamer", self.__streamer_state_handler)
app.router.add_post("/streamer/set_params", self.__streamer_set_params_handler)
app.router.add_post("/streamer/reset", self.__streamer_reset_handler)
app.on_shutdown.append(self.__on_shutdown)
@ -301,6 +303,18 @@ class Server: # pylint: disable=too-many-instance-attributes
async def __streamer_state_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json(self.__streamer.get_state())
@_wrap_exceptions_for_web("Can't set stream params")
async def __streamer_set_params_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
resolution = request.query.get("resolution")
if resolution:
if resolution in self.__streamer.get_available_resolutions():
if resolution != self.__streamer_resolution:
self.__streamer_resolution = resolution
self.__reset_streamer = True
else:
raise BadRequest("Unknown resolution %r" % (resolution))
return _json()
async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
self.__reset_streamer = True
return _json()
@ -344,17 +358,20 @@ class Server: # pylint: disable=too-many-instance-attributes
cur = len(self.__sockets)
if prev == 0 and cur > 0:
if not self.__streamer.is_running():
await self.__streamer.start()
await self.__streamer.start(self.__streamer_resolution)
await self.__broadcast_event("streamer_state", **self.__streamer.get_state())
elif prev > 0 and cur == 0:
shutdown_at = time.time() + self.__streamer_shutdown_delay
elif prev == 0 and cur == 0 and time.time() > shutdown_at:
if self.__streamer.is_running():
await self.__streamer.stop()
await self.__broadcast_event("streamer_state", **self.__streamer.get_state())
if self.__reset_streamer:
if self.__streamer.is_running():
await self.__streamer.stop()
await self.__streamer.start(no_init_restart=True)
await self.__streamer.start(self.__streamer_resolution, no_init_restart=True)
await self.__broadcast_event("streamer_state", **self.__streamer.get_state())
self.__reset_streamer = False
prev = cur

View File

@ -1,6 +1,8 @@
import asyncio
import asyncio.subprocess
from collections import OrderedDict as odict
from typing import List
from typing import Dict
from typing import Optional
@ -20,8 +22,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
init_delay: float,
init_restart_after: float,
width: int,
height: int,
resolutions: List[str],
cmd: List[str],
loop: asyncio.AbstractEventLoop,
@ -33,17 +34,25 @@ class Streamer: # pylint: disable=too-many-instance-attributes
self.__init_delay = init_delay
self.__init_restart_after = init_restart_after
self.__width = width
self.__height = height
self.__resolutions = odict([
(display, (real or display))
for (display, real) in [
(tuple(map(str.lower, map(str.strip, resolution.split("-", maxsplit=1)))) + ("",))[:2]
for resolution in resolutions
]
])
self.__resolution = list(self.__resolutions)[0]
self.__cmd = cmd
self.__loop = loop
self.__proc_task: Optional[asyncio.Task] = None
async def start(self, no_init_restart: bool=False) -> None:
async def start(self, resolution: str, no_init_restart: bool=False) -> None:
logger = get_logger()
logger.info("Starting streamer ...")
assert resolution in self.__resolutions, (resolution, self.__resolutions)
self.__resolution = resolution
await self.__inner_start()
if self.__init_restart_after > 0.0 and not no_init_restart:
logger.info("Stopping streamer to restart ...")
@ -58,13 +67,22 @@ class Streamer: # pylint: disable=too-many-instance-attributes
def is_running(self) -> bool:
return bool(self.__proc_task)
def get_current_resolution(self) -> str:
return self.__resolution
def get_available_resolutions(self) -> List[str]:
return list(self.__resolutions)
def get_state(self) -> Dict:
(width, height) = tuple(map(int, self.__resolution.split("x")))
return {
"is_running": self.is_running(),
"size": {
"width": self.__width,
"height": self.__height,
"width": width,
"height": height,
},
"resolution": self.__resolution,
"resolutions": list(self.__resolutions),
}
async def cleanup(self) -> None:
@ -100,7 +118,7 @@ class Streamer: # pylint: disable=too-many-instance-attributes
while True: # pylint: disable=too-many-nested-blocks
proc: Optional[asyncio.subprocess.Process] = None # pylint: disable=no-member
try:
cmd = [part.format(width=self.__width, height=self.__height) for part in self.__cmd]
cmd = [part.format(resolution=self.__resolutions[self.__resolution]) for part in self.__cmd]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,

View File

@ -36,14 +36,15 @@ kvmd:
init_restart_after: 1.0
shutdown_delay: 10.0
size:
width: 800
height: 600
resolutions:
- 640x480
- 800x600
- 1024x768
cmd:
- "/usr/bin/mjpg_streamer"
- "-i"
- "input_uvc.so -d /dev/kvmd-streamer -e 2 -y -n -r {width}x{height}"
- "input_uvc.so -d /dev/kvmd-streamer -e 2 -y -n -r {resolution}"
- "-o"
- "output_http.so -l 0.0.0.0 -p 8082"

View File

@ -91,7 +91,7 @@ div.ctl-dropdown-content div.buttons-row {
padding: 0;
font-size: 0;
}
div.ctl-dropdown-content button {
div.ctl-dropdown-content button, select {
box-shadow: none;
border: none;
color: var(--fg-color-normal);
@ -105,26 +105,48 @@ div.ctl-dropdown-content button {
outline: none;
cursor: pointer;
}
div.ctl-dropdown-content button:enabled:hover {
div.ctl-dropdown-content button:enabled:hover, select:enabled:hover {
color: var(--fg-color-intensive);
background-color: var(--bg-color-dark) !important;
}
div.ctl-dropdown-content button:disabled {
div.ctl-dropdown-content button:disabled, select:disabled {
color: var(--fg-color-inactive);
cursor: default;
}
div.ctl-dropdown-content button:active {
div.ctl-dropdown-content button:active, select:active {
color: var(--fg-color-selected) !important;
}
div.ctl-dropdown-content button.row50 {
div.ctl-dropdown-content select {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
background-image: url("../svg/select-arrow-normal.svg");
background-position: center right;
background-repeat: no-repeat;
}
div.ctl-dropdown-content select:enabled:hover {
background-image: url("../svg/select-arrow-intensive.svg") !important;
}
div.ctl-dropdown-content select:disabled {
background-image: url("../svg/select-arrow-inactive.svg") !important;
}
div.ctl-dropdown-content select:active {
color: var(--fg-color-intensive) !important;
background-color: var(--bg-color-dark) !important;
background-image: url("../svg/select-arrow-intensive.svg") !important;
}
div.ctl-dropdown-content .row50 {
display: inline-block;
width: 50%;
}
div.ctl-dropdown-content button.row25 {
div.ctl-dropdown-content .row25 {
display: inline-block;
width: 25%;
}
div.ctl-dropdown-content button.row50:not(:first-child), button.row25:not(:first-child) {
div.ctl-dropdown-content .row50:not(:first-child), .row25:not(:first-child) {
border-left: var(--dark-border);
}
div.ctl-dropdown-content hr {

View File

@ -35,21 +35,26 @@ div.stream-box-mouse-enabled {
cursor: url("../svg/stream-mouse-cursor.svg"), pointer;
}
div#stream-size {
div.stream-params {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 12px;
margin: 5px 15px 5px 15px;
}
div#stream-size span#stream-size-counter {
div.stream-params select#stream-resolution-select {
margin: 8px 0 8px 0;
}
div#stream-size div#stream-size-slider-box {
div.stream-params span#stream-size-value {
}
div.stream-params div#stream-size-slider-box {
margin-top: 5px;
display: flex;
}
@supports (-webkit-appearance:none) {
div#stream-size div#stream-size-slider-box input[type=range] {
div.stream-params div#stream-size-slider-box input[type=range] {
cursor: pointer;
outline: none;
width: 100%;
@ -60,7 +65,7 @@ div#stream-size div#stream-size-slider-box {
}
}
@supports not (-webkit-appearance:none) {
div#stream-size div#stream-size-slider-box input[type=range] {
div.stream-params div#stream-size-slider-box input[type=range] {
cursor: pointer;
outline: none;
width: 100%;
@ -69,12 +74,12 @@ div#stream-size div#stream-size-slider-box {
margin-right: 0;
}
}
div#stream-size div#stream-size-slider-box input[type=range]::-webkit-slider-runnable-track {
div.stream-params div#stream-size-slider-box input[type=range]::-webkit-slider-runnable-track {
height: 5px;
background: var(--bg-color-light);
border-radius: 3px;
}
div#stream-size div#stream-size-slider-box input[type=range]::-webkit-slider-thumb {
div.stream-params div#stream-size-slider-box input[type=range]::-webkit-slider-thumb {
border: var(--intensive-border);
height: 18px;
width: 18px;
@ -83,12 +88,12 @@ div#stream-size div#stream-size-slider-box input[type=range]::-webkit-slider-thu
-webkit-appearance: none;
margin-top: -7px;
}
div#stream-size div#stream-size-slider-box input[type=range]::-moz-range-track {
div.stream-params div#stream-size-slider-box input[type=range]::-moz-range-track {
height: 5px;
background: var(--bg-color-light);
border-radius: 3px;
}
div#stream-size div#stream-size-slider-box input[type=range]::-moz-range-thumb {
div.stream-params div#stream-size-slider-box input[type=range]::-moz-range-thumb {
border: var(--intensive-border);
height: 18px;
width: 18px;

View File

@ -70,8 +70,15 @@
<button id="show-stream-button">&bull; Show stream</button>
<button disabled id="stream-reset-button">&bull; Reset stream</button>
<hr>
<div data-dont-hide-menu id="stream-size">
Stream size: <span id="stream-size-counter">100%</span>
<div data-dont-hide-menu class="stream-params">
Resolution:
<select disabled data-dont-hide-menu id="stream-resolution-select">
<option>640x480</option>
</select>
</div>
<hr>
<div data-dont-hide-menu class="stream-params">
Stream size: <span id="stream-size-value">100%</span>
<div id="stream-size-slider-box">
<input id="stream-size-slider" type="range" min="50" max="150" value="100" step="10" />
</div>

View File

@ -4,6 +4,10 @@ function Stream(ui) {
/********************************************************************************/
var __prev_state = false;
var __resolution = "640x480";
var __resolutions = ["640x480"];
var __normal_size = {width: 640, height: 480};
var __size_factor = 1;
@ -11,6 +15,7 @@ function Stream(ui) {
$("stream-led").title = "Stream inactive";
$("stream-reset-button").onclick = __clickResetButton;
$("stream-resolution-select").onchange = __changeResolution;
$("stream-size-slider").oninput = __resize;
$("stream-size-slider").onchange = __resize;
@ -19,6 +24,8 @@ function Stream(ui) {
/********************************************************************************/
// XXX: In current implementation we don't need this event because Stream() has own state poller
var __startPoller = function() {
var http = tools.makeRequest("GET", "/streamer/?action=snapshot", function() {
if (http.readyState === 2 || http.readyState === 4) {
@ -33,6 +40,7 @@ function Stream(ui) {
$("stream-led").className = "led-off";
$("stream-led").title = "Stream inactive";
$("stream-reset-button").disabled = true;
$("stream-resolution-select").disabled = true;
} else if (!__prev_state) {
__refreshImage();
__prev_state = true;
@ -44,7 +52,7 @@ function Stream(ui) {
}
}
});
setTimeout(__startPoller, 2000);
setTimeout(__startPoller, 1500);
};
var __clickResetButton = function() {
@ -58,9 +66,23 @@ function Stream(ui) {
});
};
var __changeResolution = function() {
var resolution = $("stream-resolution-select").value;
if (__resolution != resolution) {
$("stream-resolution-select").disabled = true;
var http = tools.makeRequest("POST", "/kvmd/streamer/set_params?resolution=" + resolution, function() {
if (http.readyState === 4) {
if (http.status !== 200) {
alert("Can't change stream:", http.responseText);
}
}
});
}
};
var __resize = function() {
var percent = $("stream-size-slider").value;
$("stream-size-counter").innerHTML = percent + "%";
$("stream-size-value").innerHTML = percent + "%";
__size_factor = percent / 100;
__applySizeFactor();
};
@ -75,7 +97,25 @@ function Stream(ui) {
var __refreshImage = function() {
var http = tools.makeRequest("GET", "/kvmd/streamer", function() {
if (http.readyState === 4 && http.status === 200) {
__normal_size = JSON.parse(http.responseText).result.size;
var result = JSON.parse(http.responseText).result;
if (__resolutions != result.resolutions) {
tools.info("Resolutions list changed:", result.resolutions);
$("stream-resolution-select").innerHTML = "";
result.resolutions.forEach(function(resolution) {
$("stream-resolution-select").innerHTML += "<option value=\"" + resolution + "\">" + resolution + "</option>";
});
$("stream-resolution-select").disabled = (result.resolutions.length == 1);
__resolutions = result.resolutions;
}
if (__resolution != result.resolution) {
tools.info("Resolution changed:", result.resolution);
document.querySelector("#stream-resolution-select [value=\"" + result.resolution + "\"]").selected = true;
__resolution = result.resolution;
}
__normal_size = result.size;
__applySizeFactor();
$("stream-image").src = "/streamer/?action=stream&time=" + new Date().getTime();
}

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="31.999998"
viewBox="0 0 6.3500001 8.4666662"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="select-arrow-inactive.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="27.151934"
inkscape:cy="16.615415"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-8.8745959,-36.821965)">
<path
sodipodi:type="star"
style="opacity:1;fill:#6c7481;fill-opacity:1;stroke:none;stroke-width:2.64583325;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:75.59055328;stroke-opacity:1;paint-order:normal"
id="path4749"
sodipodi:sides="3"
sodipodi:cx="12.049596"
sodipodi:cy="40.702518"
sodipodi:r1="1.411111"
sodipodi:r2="0.70555568"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.049596,42.113629 -0.611029,-1.058333 -0.611029,-1.058333 1.222058,0 1.222058,0 -0.611029,1.058333 z"
inkscape:transform-center-y="0.3527758" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="31.999998"
viewBox="0 0 6.3500001 8.4666662"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="select-arrow-intensive.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="27.151934"
inkscape:cy="16.615415"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-8.8745959,-36.821965)">
<path
sodipodi:type="star"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.64583325;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:75.59055328;stroke-opacity:1;paint-order:normal"
id="path4749"
sodipodi:sides="3"
sodipodi:cx="12.049596"
sodipodi:cy="40.702518"
sodipodi:r1="1.411111"
sodipodi:r2="0.70555568"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.049596,42.113629 -0.611029,-1.058333 -0.611029,-1.058333 1.222058,0 1.222058,0 -0.611029,1.058333 z"
inkscape:transform-center-y="0.3527758" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="31.999998"
viewBox="0 0 6.3500001 8.4666662"
version="1.1"
id="svg8"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="select-arrow-normal.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.2"
inkscape:cx="27.151934"
inkscape:cy="16.615415"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-8.8745959,-36.821965)">
<path
sodipodi:type="star"
style="opacity:1;fill:#c3c3c3;fill-opacity:1;stroke:none;stroke-width:2.64583325;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:75.59055328;stroke-opacity:1;paint-order:normal"
id="path4749"
sodipodi:sides="3"
sodipodi:cx="12.049596"
sodipodi:cy="40.702518"
sodipodi:r1="1.411111"
sodipodi:r2="0.70555568"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.049596,42.113629 -0.611029,-1.058333 -0.611029,-1.058333 1.222058,0 1.222058,0 -0.611029,1.058333 z"
inkscape:transform-center-y="0.3527758" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB