web_kbn: basic support of cams hls streaming
This commit is contained in:
parent
4215537047
commit
f14bdc6752
@ -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__':
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
21
web/kbn_templates/cams.j2
Normal file
21
web/kbn_templates/cams.j2
Normal file
@ -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 %}
|
Loading…
x
Reference in New Issue
Block a user