This commit is contained in:
Devaev Maxim
2018-09-26 02:11:23 +03:00
parent 6e9a3222ce
commit 940989b6e9
24 changed files with 265 additions and 173 deletions

View File

@@ -15,6 +15,6 @@ search = version="{current_version}"
replace = version="{new_version}"
[bumpversion:file:PKGBUILD]
search = pkgver="{current_version}"
replace = pkgver="{new_version}"
search = pkgver={current_version}
replace = pkgver={new_version}

View File

@@ -20,7 +20,8 @@ all:
run:
docker build --rm --tag $(TESTENV_IMAGE) -f testenv/Dockerfile .
sudo modprobe loop
docker build $(TESTENV_OPTS) --rm --tag $(TESTENV_IMAGE) -f testenv/Dockerfile .
- docker run --rm \
--volume `pwd`/kvmd:/kvmd:ro \
--volume `pwd`/web:/usr/share/kvmd/web:ro \

View File

@@ -2,26 +2,26 @@
# Author: Maxim Devaev <mdevaev@gmail.com>
pkgname="kvmd"
pkgver="0.66"
pkgrel="1"
pkgname=kvmd
pkgver=0.66
pkgrel=1
pkgdesc="The main Pi-KVM daemon"
arch=("any")
url="https://github.com/pi-kvm/pi-kvm"
license=("GPL")
license=(GPL)
arch=(any)
depends=(
"python"
"python-yaml"
"python-aiohttp"
"python-aiofiles"
"python-pyudev"
"python-raspberry-gpio"
"python-pyserial"
"python-setproctitle"
python
python-yaml
python-aiohttp
python-aiofiles
python-pyudev
python-raspberry-gpio
python-pyserial
python-setproctitle
)
makedepends=("python-setuptools")
makedepends=(python-setuptools)
source=("$url/archive/v$pkgver.tar.gz")
md5sums=("SKIP")
md5sums=(SKIP)
build() {
@@ -34,9 +34,9 @@ build() {
package() {
cd $srcdir/$pkgname-build
python setup.py install --root=$pkgdir
install -Dm644 configs/kvmd.service "$pkgdir"/usr/lib/systemd/system/kvmd.service
mkdir -p "$pkgdir"/usr/share/kvmd
cp -r web "$pkgdir"/usr/share/kvmd
cp -r configs "$pkgdir"/usr/share/kvmd
python setup.py install --root="$pkgdir"
install -Dm644 configs/kvmd.service "$pkgdir/usr/lib/systemd/system/kvmd.service"
mkdir -p "$pkgdir/usr/share/kvmd"
cp -r web "$pkgdir/usr/share/kvmd"
cp -r configs "$pkgdir/usr/share/kvmd"
}

View File

@@ -36,15 +36,21 @@ kvmd:
init_restart_after: 1.0
shutdown_delay: 10.0
resolutions:
- 800x600 - 720x576
quality: 80
cmd:
- "/usr/bin/mjpg_streamer"
- "-i"
- "input_uvc.so -d /dev/kvmd-streamer -e 2 -t pal -y -n -r {resolution}"
- "-o"
- "output_http.so -l localhost -p 8082"
- "/usr/bin/ustreamer"
- "--device=/dev/kvmd-streamer"
- "--tv-standard=pal"
- "--format=yuyv"
- "--encoder=omx"
- "--jpeg-quality={quality}"
- "--width=720"
- "--height=576"
- "--fake-width=800"
- "--fake-height=600"
- "--host=localhost"
- "--port=8082"
logging:
version: 1

72
kvmd/configs/kvmd/v2.yaml Normal file
View File

@@ -0,0 +1,72 @@
kvmd:
server:
host: localhost
port: 8081
heartbeat: 3.0
hid:
device: /dev/ttyAMA0
speed: 115200
atx:
pinout:
power_led: 16
hdd_led: 12
power_switch: 26
reset_switch: 20
click_delay: 0.1
long_click_delay: 5.5
state_poll: 0.1
msd:
device: "/dev/kvmd-msd"
init_delay: 2.0
write_meta: true
chunk_size: 65536
streamer:
pinout:
cap: -1
conv: -1
sync_delay: 0.0
init_delay: 1.0
init_restart_after: 0.0
shutdown_delay: 10.0
quality: 80
cmd:
- "/usr/bin/ustreamer"
- "--device=/dev/kvmd-streamer"
- "--format=uyvy"
- "--encoder=omx"
- "--jpeg-quality={quality}"
- "--dv-timings"
- "--host=localhost"
- "--port=8082"
logging:
version: 1
disable_existing_loggers: false
formatters:
console:
(): logging.Formatter
style: "{"
datefmt: "%H:%M:%S"
format: "[{asctime}] {name:20.20} {levelname:>7} --- {message}"
handlers:
console:
level: DEBUG
class: logging.StreamHandler
stream: ext://sys.stdout
formatter: console
root:
level: INFO
handlers:
- console

View File

@@ -31,7 +31,7 @@ http {
server localhost:8081 fail_timeout=0s max_fails=0;
}
upstream mjpg_streamer {
upstream ustreamer {
server localhost:8082 fail_timeout=0s max_fails=0;
}
@@ -112,9 +112,9 @@ http {
include /etc/nginx/proxy-params.conf;
}
location ~ ^/streamer/(snapshot|stream)(?:/(.*))?$ {
rewrite /streamer/?(.*)(?:/(.*))?$ /?action=$1 break;
proxy_pass http://mjpg_streamer;
location /streamer {
rewrite /streamer/?(.*) /$1 break;
proxy_pass http://ustreamer;
include /etc/nginx/proxy-params.conf;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;

View File

@@ -49,7 +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"]),
resolutions=config["streamer"]["resolutions"],
quality=int(config["streamer"]["quality"]),
cmd=list(map(str, config["streamer"]["cmd"])),
loop=loop,
)

View File

@@ -128,7 +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()
self.__streamer_quality = streamer.get_current_quality()
def run(self, host: str, port: int) -> None:
self.__hid.start()
@@ -166,7 +166,7 @@ class Server: # pylint: disable=too-many-instance-attributes
# ===== INFO
async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
async def __info_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json(_get_system_info())
# ===== WEBSOCKET
@@ -305,12 +305,15 @@ class Server: # pylint: disable=too-many-instance-attributes
@_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():
self.__streamer_resolution = resolution
else:
raise BadRequest("Unknown resolution %r" % (resolution))
quality = request.query.get("quality")
if quality:
try:
quality_int = int(quality)
if not (1 <= quality_int <= 100):
raise ValueError()
except Exception:
raise BadRequest("Invalid quality %r" % (quality))
self.__streamer_quality = quality_int
return _json()
async def __streamer_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
@@ -356,7 +359,7 @@ 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(self.__streamer_resolution)
await self.__streamer.start(self.__streamer_quality)
await self.__broadcast_event("streamer_state", **self.__streamer.get_state())
elif prev > 0 and cur == 0:
shutdown_at = time.time() + self.__streamer_shutdown_delay
@@ -365,10 +368,10 @@ class Server: # pylint: disable=too-many-instance-attributes
await self.__streamer.stop()
await self.__broadcast_event("streamer_state", **self.__streamer.get_state())
if self.__reset_streamer or self.__streamer_resolution != self.__streamer.get_current_resolution():
if self.__reset_streamer or self.__streamer_quality != self.__streamer.get_current_quality():
if self.__streamer.is_running():
await self.__streamer.stop()
await self.__streamer.start(self.__streamer_resolution, no_init_restart=True)
await self.__streamer.start(self.__streamer_quality, no_init_restart=True)
await self.__broadcast_event("streamer_state", **self.__streamer.get_state())
self.__reset_streamer = False

View File

@@ -1,8 +1,6 @@
import asyncio
import asyncio.subprocess
from collections import OrderedDict as odict
from typing import List
from typing import Dict
from typing import Optional
@@ -21,10 +19,8 @@ class Streamer: # pylint: disable=too-many-instance-attributes
sync_delay: float,
init_delay: float,
init_restart_after: float,
resolutions: List[str],
quality: int,
cmd: List[str],
loop: asyncio.AbstractEventLoop,
) -> None:
@@ -33,26 +29,18 @@ class Streamer: # pylint: disable=too-many-instance-attributes
self.__sync_delay = sync_delay
self.__init_delay = init_delay
self.__init_restart_after = init_restart_after
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.__quality = quality
self.__cmd = cmd
self.__loop = loop
self.__proc_task: Optional[asyncio.Task] = None
async def start(self, resolution: str, no_init_restart: bool=False) -> None:
async def start(self, quality: int, no_init_restart: bool=False) -> None:
logger = get_logger()
logger.info("Starting streamer ...")
assert resolution in self.__resolutions, (resolution, self.__resolutions)
self.__resolution = resolution
assert 1 <= quality <= 100
self.__quality = quality
await self.__inner_start()
if self.__init_restart_after > 0.0 and not no_init_restart:
logger.info("Stopping streamer to restart ...")
@@ -67,22 +55,13 @@ 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_current_quality(self) -> int:
return self.__quality
def get_state(self) -> Dict:
(width, height) = tuple(map(int, self.__resolution.split("x")))
return {
"is_running": self.is_running(),
"size": {
"width": width,
"height": height,
},
"resolution": self.__resolution,
"resolutions": list(self.__resolutions),
"quality": self.__quality,
}
async def cleanup(self) -> None:
@@ -118,7 +97,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(resolution=self.__resolutions[self.__resolution]) for part in self.__cmd]
cmd = [part.format(quality=self.__quality) for part in self.__cmd]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,

View File

@@ -33,7 +33,7 @@ RUN pacman -Syy \
python-pip \
nginx-mainline \
nginx-mainline-mod-lua \
mjpg-streamer \
ustreamer \
socat \
&& pacman -Sc --noconfirm

View File

@@ -36,17 +36,16 @@ kvmd:
init_restart_after: 1.0
shutdown_delay: 10.0
resolutions:
- 640x480
- 800x600
- 1024x768
quality: 80
cmd:
- "/usr/bin/mjpg_streamer"
- "-i"
- "input_uvc.so -d /dev/kvmd-streamer -e 2 -y -n -r {resolution}"
- "-o"
- "output_http.so -l 0.0.0.0 -p 8082"
- "/usr/bin/ustreamer"
- "--device=/dev/kvmd-streamer"
- "--jpeg-quality={quality}"
- "--width=800"
- "--height=600"
- "--host=0.0.0.0"
- "--port=8082"
logging:
version: 1

View File

@@ -35,7 +35,7 @@ div.stream-box-mouse-enabled {
cursor: url("../svg/stream-mouse-cursor.svg"), pointer;
}
select#stream-resolution-select {
select#stream-quality-select {
margin: 8px 0 8px 0;
}

View File

@@ -75,9 +75,9 @@
</div>
<hr>
<div data-dont-hide-menu class="ctl-dropdown-content-text">
Resolution:
<select disabled id="stream-resolution-select">
<option>640x480</option>
Quality:
<select disabled id="stream-quality-select">
<option>80%</option>
</select>
</div>
<hr>

View File

@@ -5,8 +5,7 @@ function Stream() {
var __prev_state = false;
var __resolution = "640x480";
var __resolutions = ["640x480"];
var __quality = 80;
var __normal_size = {width: 640, height: 480};
var __size_factor = 1;
@@ -14,8 +13,13 @@ function Stream() {
var __init__ = function() {
$("stream-led").title = "Stream inactive";
var quality = 10;
for (; quality <= 100; quality += 10) {
$("stream-quality-select").innerHTML += "<option value=\"" + quality + "\">" + quality + "%</option>";
}
tools.setOnClick($("stream-reset-button"), __clickResetButton);
$("stream-resolution-select").onchange = __changeResolution;
$("stream-quality-select").onchange = __changeQuality;
$("stream-size-slider").oninput = __resize;
$("stream-size-slider").onchange = __resize;
@@ -27,12 +31,10 @@ function Stream() {
// 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/snapshot", function() {
if (http.readyState === 2 || http.readyState === 4) {
var status = http.status;
http.onreadystatechange = null;
http.abort();
if (status !== 200) {
var http = tools.makeRequest("GET", "/streamer/ping", function() {
if (http.readyState === 4) {
var response = (http.status === 200 ? JSON.parse(http.responseText) : null);
if (http.status !== 200 || !response.stream.online) {
tools.info("Refreshing stream ...");
__prev_state = false;
$("stream-image").className = "stream-image-inactive";
@@ -40,8 +42,9 @@ function Stream() {
$("stream-led").className = "led-off";
$("stream-led").title = "Stream inactive";
$("stream-reset-button").disabled = true;
$("stream-resolution-select").disabled = true;
} else if (!__prev_state) {
$("stream-quality-select").disabled = true;
} else if (http.status === 200 && !__prev_state) {
__normal_size = response.stream.resolution;
__refreshImage();
__prev_state = true;
$("stream-image").className = "stream-image-active";
@@ -49,6 +52,7 @@ function Stream() {
$("stream-led").className = "led-on";
$("stream-led").title = "Stream is active";
$("stream-reset-button").disabled = false;
$("stream-quality-select").disabled = false;
}
}
});
@@ -66,11 +70,11 @@ function Stream() {
});
};
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() {
var __changeQuality = function() {
var quality = parseInt($("stream-quality-select").value);
if (__quality != quality) {
$("stream-quality-select").disabled = true;
var http = tools.makeRequest("POST", "/kvmd/streamer/set_params?quality=" + quality, function() {
if (http.readyState === 4) {
if (http.status !== 200) {
ui.error("Can't configure stream:<br>", http.responseText);
@@ -99,25 +103,14 @@ function Stream() {
if (http.readyState === 4 && http.status === 200) {
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 (__quality != result.quality) {
tools.info("Quality changed:", result.quality);
document.querySelector("#stream-quality-select [value=\"" + result.quality + "\"]").selected = true;
__quality = result.quality;
}
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/stream/" + new Date().getTime();
$("stream-image").src = "/streamer/stream?t=" + new Date().getTime();
}
});
};