lws: support h265 camera streams

This commit is contained in:
Evgeny Zinoviev 2023-05-29 23:40:14 +03:00
parent bd8d5040eb
commit 850083225c
5 changed files with 216 additions and 45 deletions

View File

@ -49,8 +49,8 @@ return [
],
'static' => [
'app.css' => 10,
'app.js' => 5,
'app.css' => 12,
'app.js' => 7,
'polyfills.js' => 1,
'modem.js' => 2,
'inverter.js' => 2,

View File

@ -67,41 +67,71 @@ class MiscHandler extends RequestHandler
$tab = $high ? 'high' : 'low';
$hls_opts = [
'startPosition' => -1,
// h264
$js_hls_config = [
'opts' => [
'startPosition' => -1,
// // https://github.com/video-dev/hls.js/issues/3884#issuecomment-842380784
'liveSyncDuration' => 2,
'liveMaxLatencyDuration' => 3,
'maxLiveSyncPlaybackRate' => 2,
'liveDurationInfinity' => true,
// // https://github.com/video-dev/hls.js/issues/3884#issuecomment-842380784
'liveSyncDuration' => 2,
'liveMaxLatencyDuration' => 3,
'maxLiveSyncPlaybackRate' => 2,
'liveDurationInfinity' => true,
],
'debugVideoEvents' => !!$video_events,
];
if ($hls_debug)
$js_hls_config['debug'] = true;
// h265
$js_h265webjs_config = [
// https://github.com/numberwolf/h265web.js/blob/master/README_EN.MD#freetoken
'token' => 'base64:QXV0aG9yOmNoYW5neWFubG9uZ3xudW1iZXJ3b2xmLEdpdGh1YjpodHRwczovL2dpdGh1Yi5jb20vbnVtYmVyd29sZixFbWFpbDpwb3JzY2hlZ3QyM0Bmb3htYWlsLmNvbSxRUTo1MzEzNjU4NzIsSG9tZVBhZ2U6aHR0cDovL3h2aWRlby52aWRlbyxEaXNjb3JkOm51bWJlcndvbGYjODY5NCx3ZWNoYXI6bnVtYmVyd29sZjExLEJlaWppbmcsV29ya0luOkJhaWR1',
];
if ($hls_debug)
$hls_opts['debug'] = true;
$js_config = [
'isLow' => $tab == 'low',
'proto' => config::get('cam_hls_proto'),
'host' => config::get('cam_hls_host'),
'camIds' => $camera_ids,
'camLabels' => array_map(fn($id) => $config['cam_list']['labels'][$id], $camera_ids)
];
$this->tpl->add_static('hls.js');
// $this->tpl->add_external_static('js', 'https://cdn.jsdelivr.net/npm/hls.js@latest');
$cams_by_type = [];
$include_h264 = false;
$include_h265 = false;
foreach ($camera_ids as $camera_id) {
$var_name = 'include_'.$config['cam_list']['full'][$camera_id]['type'];
$cams_by_type[$camera_id] = $config['cam_list']['full'][$camera_id]['type'];
$$var_name = true;
}
if ($include_h264) {
$js_config['hlsConfig'] = $js_hls_config;
$this->tpl->add_static('hls.js');
}
if ($include_h265) {
$js_config['h265webjsConfig'] = $js_h265webjs_config;
$this->tpl->add_static('h265webjs-dist/missile.js');
$this->tpl->add_static('h265webjs-dist/h265webjs-v20221106.js');
}
$hls_host = config::get('cam_hls_host');
$hls_proto = config::get('cam_hls_proto');
$js_config['camsByType'] = $cams_by_type;
$hls_key = config::get('cam_hls_access_key');
if ($hls_key)
setcookie_safe('hls_key', $hls_key);
$cam_filter = function($id) use ($config, $camera_ids) {
return in_array($id, $camera_ids);
};
// $cam_filter = function($id) use ($config, $camera_ids) {
// return in_array($id, $camera_ids);
// };
$this->tpl->set([
'hls_host' => $hls_host,
'hls_proto' => $hls_proto,
'hls_opts' => $hls_opts,
'hls_access_key' => $config['cam_hls_access_key'],
'js_config' => $js_config,
// 'hls_access_key' => $config['cam_hls_access_key'],
'camera_param' => $camera_param,
'cams' => array_values(array_filter($config['cam_list'][$tab], $cam_filter)),
// 'cams' => array_values(array_filter($config['cam_list'][$tab], $cam_filter)),
'tab' => $tab,
'video_events' => $video_events
]);

View File

@ -159,15 +159,17 @@
flex-direction: row;
}
.camfeeds:not(.is_mobile) > video {
.camfeeds:not(.is_mobile) > video,
.camfeeds:not(.is_mobile) > .video-container {
display: flex;
flex-basis: calc(50% - 20px);
justify-content: center;
flex-direction: column;
width: calc(50% - 10px);
width: calc(50% - 10px) !important;
margin: 5px;
}
.camfeeds.is_mobile > video {
.camfeeds.is_mobile > video,
.camfeeds.is_mobile > .video-container {
max-width: 100%;
}

View File

@ -90,12 +90,17 @@ window.addClass = function(el, name) {
window.Cameras = {
hlsOptions: null,
hlsHost: null,
hlsProto: null,
debugVideoEvents: false,
h265webjsOptions: null,
host: null,
proto: null,
hlsDebugVideoEvents: false,
getUrl: function(name) {
return this.proto + '://' + this.host + '/ipcam/' + name + '/live.m3u8';
},
setupHls: function(video, name, useHls) {
var src = this.hlsProto + '://' + this.hlsHost + '/ipcam/' + name + '/live.m3u8';
var src = this.getUrl(name);
// hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled.
@ -128,7 +133,7 @@ window.Cameras = {
video.src = src;
var events = ['canplay'];
if (this.debugVideoEvents)
if (this.hlsDebugVideoEvents)
events.push('canplay', 'canplaythrough', 'durationchange', 'ended', 'loadeddata', 'loadedmetadata', 'pause', 'play', 'playing', 'progress', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'waiting');
for (var i = 0; i < events.length; i++) {
@ -146,26 +151,160 @@ window.Cameras = {
}
},
init: function(cams, options, proto, host, debugVideoEvents) {
// this.cams = cams;
this.hlsOptions = options;
this.hlsProto = proto;
this.hlsHost = host;
this.debugVideoEvents = debugVideoEvents
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;
let useHls = Hls.isSupported();
if (!useHls && !this.hasFallbackSupport()) {
if (!useHls && !this.hasFallbackSupport() && opts.hlsConfig !== undefined) {
alert('Neither HLS nor vnd.apple.mpegurl is not supported by your browser.');
return;
}
for (var i = 0; i < cams.length; i++) {
var name = cams[i];
var video = document.createElement('video');
video.setAttribute('id', 'video-'+name);
document.getElementById('videos').appendChild(video);
for (var camId in opts.camsByType) {
var name = camId + '';
if (opts.isLow)
name += '-low';
var type = opts.camsByType[camId];
this.setupHls(video, name, useHls);
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;
}
}
},

View File

@ -17,5 +17,5 @@
if (isTouchDevice()) {
addClass(ge('videos'), 'is_mobile');
}
Cameras.init({{ cams|json_encode|raw }}, {{ hls_opts|json_encode|raw }}, '{{ hls_proto }}', '{{ hls_host }}', {{ video_events ? 'true' : 'false' }});
Cameras.init({{ js_config|json_encode|raw }});
{% endjs %}