aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2024-02-18 02:19:27 +0300
committerEvgeny Zinoviev <me@ch1p.io>2024-02-18 02:19:27 +0300
commitf14bdc6752fc4a0ab36567d0f1e51e472a2200b6 (patch)
tree22c6b3af78fc079ab6424751289971aa65051f97
parent42155370475b1f6619498ec2c43c1c7f328ce1a1 (diff)
web_kbn: basic support of cams hls streaming
-rw-r--r--bin/web_kbn.py62
-rw-r--r--include/py/homekit/camera/config.py3
-rw-r--r--include/py/homekit/util.py2
-rw-r--r--web/kbn_assets/app.js152
-rw-r--r--web/kbn_templates/cams.j221
5 files changed, 89 insertions, 151 deletions
diff --git a/bin/web_kbn.py b/bin/web_kbn.py
index 5b36d53..9a99ab3 100644
--- a/bin/web_kbn.py
+++ b/bin/web_kbn.py
@@ -10,7 +10,7 @@ import time
import __py_include
from io import StringIO
-from aiohttp.web import HTTPFound
+from aiohttp.web import HTTPFound, HTTPBadRequest
from typing import Optional, Union
from homekit.config import config, AppConfigUnit, is_development_mode, Translation
from homekit.camera import IpcamConfig
@@ -30,6 +30,7 @@ class WebKbnConfig(AppConfigUnit):
'listen_addr': cls._addr_schema(required=True),
'assets_public_path': {'type': 'string'},
'pump_addr': cls._addr_schema(required=True),
+ 'cam_hls_host': cls._addr_schema(required=True, only_ip=True),
'inverter_grafana_url': {'type': 'string'},
'sensors_grafana_url': {'type': 'string'},
}
@@ -60,9 +61,11 @@ def get_css_link(file, version) -> str:
return f'<link rel="stylesheet" type="text/css" href="{config.app_config["assets_public_path"]}/{file}">'
-def get_head_static() -> str:
+def get_head_static(files=None) -> str:
buf = StringIO()
- for file in STATIC_FILES:
+ if files is None:
+ files = []
+ for file in STATIC_FILES+files:
v = 2
try:
q_ind = file.index('?')
@@ -214,12 +217,13 @@ class WebSite(http.HTTPServer):
req: http.Request,
template_name: str,
title: Optional[str] = None,
- context: Optional[dict] = None):
+ context: Optional[dict] = None,
+ assets: Optional[list] = None):
if context is None:
context = {}
context = {
**context,
- 'head_static': get_head_static()
+ 'head_static': get_head_static(assets)
}
if title is not None:
context['title'] = title
@@ -363,7 +367,53 @@ class WebSite(http.HTTPServer):
context=dict(status=status))
async def cams(self, req: http.Request):
- pass
+ cc = IpcamConfig()
+
+ cam = req.query.get('id', None)
+ zone = req.query.get('zone', None)
+ debug_hls = bool(req.query.get('debug_hls', False))
+ debug_video_events = bool(req.query.get('debug_video_events', False))
+
+ if cam is not None:
+ if not cc.has_camera(int(cam)):
+ raise ValueError('invalid camera id')
+ cams = [int(cam)]
+ mode = {'type': 'single', 'cam': cam}
+
+ elif zone is not None:
+ if not cc.has_zone(zone):
+ raise ValueError('invalid zone')
+ cams = cc['zones'][zone]
+ mode = {'type': 'zone', 'zone': zone}
+
+ else:
+ raise HTTPBadRequest(text='no camera id or zone found')
+
+ js_config = {
+ 'host': config.app_config['cam_hls_host'],
+ 'proto': 'http',
+ 'cams': cams,
+ 'hlsConfig': {
+ 'opts': {
+ 'startPosition': -1,
+ # https://github.com/video-dev/hls.js/issues/3884#issuecomment-842380784
+ 'liveSyncDuration': 2,
+ 'liveMaxLatencyDuration': 3,
+ 'maxLiveSyncPlaybackRate': 2,
+ 'liveDurationInfinity': True
+ },
+ 'debugVideoEvents': debug_video_events,
+ 'debug': debug_hls
+ }
+ }
+
+ return await self.render_page(req, 'cams',
+ title='Камеры',
+ assets=['hls.js'],
+ context=dict(
+ mode=mode,
+ js_config=js_config,
+ ))
if __name__ == '__main__':
diff --git a/include/py/homekit/camera/config.py b/include/py/homekit/camera/config.py
index bcd5d07..257e5f1 100644
--- a/include/py/homekit/camera/config.py
+++ b/include/py/homekit/camera/config.py
@@ -138,6 +138,9 @@ class IpcamConfig(ConfigUnit):
def has_camera(self, camera: int) -> bool:
return camera in tuple(self['cameras'].keys())
+ def has_zone(self, zone: str) -> bool:
+ return zone in tuple(self['zones'].keys())
+
def get_camera_container(self, camera: int) -> VideoContainerType:
return self.get_camera_type(camera).get_container()
diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py
index c686f29..4410251 100644
--- a/include/py/homekit/util.py
+++ b/include/py/homekit/util.py
@@ -121,6 +121,8 @@ def json_serial(obj):
return obj.value
if isinstance(obj, KeysView):
return list(obj)
+ if isinstance(obj, Addr):
+ return str(obj)
raise TypeError("Type %s not serializable" % type(obj))
diff --git a/web/kbn_assets/app.js b/web/kbn_assets/app.js
index 6081681..82b2db1 100644
--- a/web/kbn_assets/app.js
+++ b/web/kbn_assets/app.js
@@ -122,9 +122,8 @@ function indexInit() {
}
}
-window.Cameras = {
+var Cameras = {
hlsOptions: null,
- h265webjsOptions: null,
host: null,
proto: null,
hlsDebugVideoEvents: false,
@@ -185,126 +184,10 @@ window.Cameras = {
}
},
- setupH265WebJS: function(videoContainer, name) {
- var containerHeightFixed = false;
- var config = {
- player: 'video-'+name,
- width: videoContainer.offsetWidth,
- height: parseInt(videoContainer.offsetWidth * 9 / 16, 10),
- accurateSeek: true,
- token: this.h265webjsOptions.token,
- extInfo: {
- moovStartFlag: true,
- readyShow: true,
- autoPlay: true,
- rawFps: 15,
- }
- };
-
- var mediaInfo;
- var player = window.new265webjs(this.getUrl(name), config);
-
- player.onSeekStart = (pts) => {
- console.log(name + ": onSeekStart:" + pts);
- };
-
- player.onSeekFinish = () => {
- console.log(name + ": onSeekFinish");
- };
-
- player.onPlayFinish = () => {
- console.log(name + ": onPlayFinish");
- };
-
- player.onRender = (width, height, imageBufferY, imageBufferB, imageBufferR) => {
- // console.log(name + ": onRender");
- if (!containerHeightFixed) {
- var ratio = height / width;
- videoContainer.style.width = parseInt(videoContainer.offsetWidth * ratio, 10)+'px';
- containerHeightFixed = true;
- }
- };
-
- player.onOpenFullScreen = () => {
- console.log(name + ": onOpenFullScreen");
- };
-
- player.onCloseFullScreen = () => {
- console.log(name + ": onCloseFullScreen");
- };
-
- player.onSeekFinish = () => {
- console.log(name + ": onSeekFinish");
- };
-
- player.onLoadCache = () => {
- console.log(name + ": onLoadCache");
- };
-
- player.onLoadCacheFinshed = () => {
- console.log(name + ": onLoadCacheFinshed");
- };
-
- player.onReadyShowDone = () => {
- // console.log(name + ": onReadyShowDone:【You can play now】");
- player.play()
- };
-
- player.onLoadFinish = () => {
- console.log(name + ": onLoadFinish");
-
- player.setVoice(1.0);
-
- mediaInfo = player.mediaInfo();
- console.log("onLoadFinish mediaInfo===========>", mediaInfo);
-
- var codecName = "h265";
- if (mediaInfo.meta.isHEVC === false) {
- console.log(name + ": onLoadFinish is Not HEVC/H.265");
- codecName = "h264";
- } else {
- console.log(name + ": onLoadFinish is HEVC/H.265");
- }
-
- console.log(name + ": onLoadFinish media Codec:" + codecName);
- console.log(name + ": onLoadFinish media FPS:" + mediaInfo.meta.fps);
- console.log(name + ": onLoadFinish media size:" + mediaInfo.meta.size.width + "x" + mediaInfo.meta.size.height);
-
- if (mediaInfo.meta.audioNone) {
- console.log(name + ": onLoadFinish media no Audio");
- } else {
- console.log(name + ": onLoadFinish media sampleRate:" + mediaInfo.meta.sampleRate);
- }
-
- if (mediaInfo.videoType == "vod") {
- console.log(name + ": onLoadFinish media is VOD");
- console.log(name + ": onLoadFinish media dur:" + Math.ceil(mediaInfo.meta.durationMs) / 1000.0);
- } else {
- console.log(name + ": onLoadFinish media is LIVE");
- }
- };
-
- player.onCacheProcess = (cPts) => {
- console.log(name + ": onCacheProcess:" + cPts);
- };
-
- player.onPlayTime = (videoPTS) => {
- if (mediaInfo.videoType == "vod") {
- console.log(name + ": onPlayTime:" + videoPTS);
- } else {
- // LIVE
- }
- };
-
- player.do();
- // console.log('setupH265WebJS: video: ', video.offsetWidth, video.offsetHeight)
- },
-
init: function(opts) {
this.proto = opts.proto;
this.host = opts.host;
this.hlsOptions = opts.hlsConfig;
- this.h265webjsOptions = opts.h265webjsConfig;
var useHls;
if (opts.hlsConfig !== undefined) {
@@ -315,33 +198,12 @@ window.Cameras = {
}
}
- for (var camId in opts.camsByType) {
- var name = camId + '';
- if (opts.isLow)
- name += '-low';
- var type = opts.camsByType[camId];
-
- switch (type) {
- case 'h265':
- var videoContainer = document.createElement('div');
- videoContainer.setAttribute('id', 'video-'+name);
- videoContainer.setAttribute('style', 'position: relative'); // a hack to fix an error in h265webjs lib
- videoContainer.className = 'video-container';
- document.getElementById('videos').appendChild(videoContainer);
- try {
- this.setupH265WebJS(videoContainer, name);
- } catch (e) {
- console.error('cam'+camId+': error', e)
- }
- break;
-
- case 'h264':
- var video = document.createElement('video');
- video.setAttribute('id', 'video-'+name);
- document.getElementById('videos').appendChild(video);
- this.setupHls(video, name, useHls);
- break;
- }
+ for (var i = 0; i < opts.cams.length; i++) {
+ var camId = opts.cams[i]+'-low';
+ var video = document.createElement('video');
+ video.setAttribute('id', 'video-'+camId);
+ document.getElementById('videos').appendChild(video);
+ this.setupHls(video, camId, useHls);
}
},
diff --git a/web/kbn_templates/cams.j2 b/web/kbn_templates/cams.j2
new file mode 100644
index 0000000..79cb64b
--- /dev/null
+++ b/web/kbn_templates/cams.j2
@@ -0,0 +1,21 @@
+{% extends "base.j2" %}
+{% block content %}
+{{ breadcrumbs([{'text': 'Камеры'}]) }}
+
+{#<nav>#}
+{# <div class="nav nav-tabs" id="nav-tab">#}
+{# <a href="/cams/{{ camera_param ? camera_param~"/" : "" }}" class="text-decoration-none"><button class="nav-link{% if tab == 'low' %} active{% endif %}" type="button">Low-res</button></a>#}
+{# <a href="/cams/{{ camera_param ? camera_param~"/" : "" }}?high=1" class="text-decoration-none"><button class="nav-link{% if tab == 'high' %} active{% endif %}" type="button">High-res</button></a>#}
+{# </div>#}
+{#</nav>#}
+
+<div id="videos" class="camfeeds"></div>
+
+{% endblock %}
+
+{% block js %}
+if (isTouchDevice()) {
+ addClass(ge('videos'), 'is_mobile');
+}
+Cameras.init({{ js_config|tojson }});
+{% endblock %}