From f14bdc6752fc4a0ab36567d0f1e51e472a2200b6 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 18 Feb 2024 02:19:27 +0300 Subject: web_kbn: basic support of cams hls streaming --- bin/web_kbn.py | 62 +++++++++++++-- include/py/homekit/camera/config.py | 3 + include/py/homekit/util.py | 2 + web/kbn_assets/app.js | 152 ++---------------------------------- web/kbn_templates/cams.j2 | 21 +++++ 5 files changed, 89 insertions(+), 151 deletions(-) create mode 100644 web/kbn_templates/cams.j2 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'' -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': 'Камеры'}]) }} + +{##} + +
+ +{% endblock %} + +{% block js %} +if (isTouchDevice()) { + addClass(ge('videos'), 'is_mobile'); +} +Cameras.init({{ js_config|tojson }}); +{% endblock %} -- cgit v1.2.3