(Super) Timeline, Power Viewer v10
parent
17f535b9ec
commit
b4566ca886
|
@ -7752,6 +7752,12 @@ module.exports = function(s,config,lang){
|
|||
<span class="badge bg-light text-dark rounded-pill align-text-bottom cameraCount"><i class="fa fa-spinner fa-pulse"></i></span>`,
|
||||
pageOpen: 'monitorsList',
|
||||
},
|
||||
{
|
||||
icon: 'barcode',
|
||||
label: `${lang['Timeline']}`,
|
||||
pageOpen: 'timeline',
|
||||
addUl: true,
|
||||
},
|
||||
{
|
||||
icon: 'film',
|
||||
label: `${lang['Videos']}`,
|
||||
|
@ -9106,5 +9112,84 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
}
|
||||
},
|
||||
"Timeline": {
|
||||
"section": "Timeline",
|
||||
"blocks": {
|
||||
"Search Settings": {
|
||||
"name": lang["Timeline"],
|
||||
"color": "blue",
|
||||
"noHeader": true,
|
||||
"noDefaultSectionClasses": true,
|
||||
"box-wrapper-class": "flex-direction-column",
|
||||
"info": [
|
||||
{
|
||||
"fieldType": "div",
|
||||
"class": "row m-0",
|
||||
"id": "timeline-video-canvas",
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"class": "row p-1 m-0",
|
||||
"id": "timeline-info",
|
||||
"divContent": `
|
||||
<div class="text-center">
|
||||
<span class="current-time font-monospace ${textWhiteOnBgDark}"></span>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"class": "p-2",
|
||||
"id": "timeline-controls",
|
||||
"divContent": `
|
||||
<div class="text-center">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default" timeline-action="jumpLeft" title="${lang.jumpFiveSeconds}"><i class="fa fa-arrow-circle-left"></i></a>
|
||||
<a class="btn btn-sm btn-primary" timeline-action="playpause" title="${lang.Play}/${lang.Pause}"><i class="fa fa-play-circle-o"></i></a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="jumpRight" title="${lang.jumpFiveSeconds}"><i class="fa fa-arrow-circle-right"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default btn-success" timeline-action="speed" speed="1" title="${lang.Speed} x1">x1</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="2" title="${lang.Speed} x2">x2</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="5" title="${lang.Speed} x5">x5</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="7" title="${lang.Speed} x7">x7</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="10" title="${lang.Speed} x10">x10</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default" timeline-action="gridSize" size="md-12">1</a>
|
||||
<a class="btn btn-sm btn-default btn-success" timeline-action="gridSize" size="md-6">2</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="gridSize" size="md-4">3</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<input class="form-control form-control-sm" id="timeline-video-object-search" placeholder="${lang['Search Object Tags']}">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-primary" timeline-action="autoGridSizer" title="${lang.autoResizeGrid}"><i class="fa fa-expand"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-success" timeline-action="downloadAll" title="${lang.Download}"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default" class_toggle="show-only-playing" data-target="#timeline-video-canvas" icon-toggle="fa-eye fa-eye-slash" icon-child="i" title="${lang['Show Only Playing']}"><i class="fa fa-eye"></i></a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="refresh" title="${lang.Refresh}"><i class="fa fa-refresh"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-primary" id="timeline-date-selector" title="${lang.Date}"><i class="fa fa-calendar"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"id": "timeline-bottom-strip",
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"id": "timeline-pre-buffers",
|
||||
"class": "hidden",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"Accuracy Mode": "Accuracy Mode",
|
||||
"Client ID": "Client ID",
|
||||
"Tags": "Tags",
|
||||
"addTagText": "Add Tags to your monitors to get more choices here.",
|
||||
"tagsCannotAddText": "Cannot add Tag",
|
||||
"tagsTriggerCannotAddText": "Trigger tags must match an existing tag that was added to a Monitor previously or is pending to be added to this monitor that is currently being edited.",
|
||||
"tagsFieldText": "Automatically group Monitors based on a common identifier.",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"Tile Size": "Tile Size",
|
||||
"fieldTextTileSize": "When in Accuracy Mode this is the size of each tile in pixels squared. A lower number will have higher accuracy but more resource use.",
|
||||
"Turn Speed": "Turn Speed",
|
||||
"Speed": "Speed",
|
||||
"Session Key": "Session Key",
|
||||
"Active Monitors": "Active Monitors",
|
||||
"Storage Use": "Storage Use",
|
||||
|
@ -121,8 +123,10 @@
|
|||
"Slice": "Slice",
|
||||
"Stitch": "Stitch",
|
||||
"Studio": "Studio",
|
||||
"Show Only Playing": "Show Only Playing",
|
||||
"Power Video Viewer": "Power Video Viewer",
|
||||
"Time-lapse": "Time-lapse",
|
||||
"Timeline": "Timeline",
|
||||
"Montage": "Montage",
|
||||
"Registered": "Registered",
|
||||
"Viewing Server Stats": "Viewing Server Stats",
|
||||
|
@ -1013,6 +1017,10 @@
|
|||
"File Not Exist": "File Not Exist",
|
||||
"No Videos Found": "No Videos Found",
|
||||
"activateRequiredLiveStream": "Live Stream is only available here with an Activated installation.",
|
||||
"activationRequired": "Activation Required",
|
||||
"autoResizeGrid": "Auto Resize Grid",
|
||||
"jumpFiveSeconds": "Jump 5 Seconds",
|
||||
"featureRequiresActivationText": "The feature you are trying to use requires that your installation be activated. Please see the Help tab for more information.",
|
||||
"FileNotExistText": "Cannot save non existant file. Something went wrong.",
|
||||
"CameraNotRecordingText": "Settings may be incompatible. Check encoders. Restarting...",
|
||||
"Camera is not running": "Camera is not running",
|
||||
|
|
|
@ -130,17 +130,29 @@ module.exports = function(s,config,lang){
|
|||
})
|
||||
device = activeMonitor.onvifConnection
|
||||
}
|
||||
controlOptions.ProfileToken = device.current_profile.token
|
||||
const actionResponse = await s.runOnvifMethod({
|
||||
auth: {
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
action: 'continuousMove',
|
||||
service: 'ptz',
|
||||
},
|
||||
options: controlOptions,
|
||||
});
|
||||
return actionResponse;
|
||||
function returnResponse(){
|
||||
return new Promise((resolve,reject) => {
|
||||
if(
|
||||
!device ||
|
||||
!device.current_profile ||
|
||||
!device.current_profile.token
|
||||
){
|
||||
resolve({ok: false, msg: lang.ONVIFEventsNotAvailableText1})
|
||||
return
|
||||
}
|
||||
controlOptions.ProfileToken = device.current_profile.token
|
||||
s.runOnvifMethod({
|
||||
auth: {
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
action: 'continuousMove',
|
||||
service: 'ptz',
|
||||
},
|
||||
options: controlOptions,
|
||||
},resolve)
|
||||
})
|
||||
}
|
||||
return await returnResponse();
|
||||
}
|
||||
function stopMoveOnvif(options){
|
||||
return new Promise((resolve,reject) => {
|
||||
|
|
|
@ -351,7 +351,7 @@ module.exports = (s,config,lang) => {
|
|||
function bindTagLegendForMonitors(groupKey){
|
||||
const newTagLegend = {}
|
||||
const theGroup = s.group[groupKey]
|
||||
const monitorIds = Object.keys(theGroup.rawMonitorConfigurations)
|
||||
const monitorIds = Object.keys(theGroup.rawMonitorConfigurations || {})
|
||||
monitorIds.forEach((monitorId) => {
|
||||
const monitorConfig = theGroup.rawMonitorConfigurations[monitorId]
|
||||
const theTags = (monitorConfig.tags || '').split(',')
|
||||
|
|
|
@ -124,7 +124,7 @@ module.exports = function(s,config,lang){
|
|||
k.details = k.details && k.details instanceof Object ? k.details : {}
|
||||
var listOEvents = activeMonitor.detector_motion_count || []
|
||||
var listOTags = listOEvents.filter(row => row.details.reason === 'object').map(row => row.details.matrices.map(matrix => matrix.tag).join(',')).join(',').split(',')
|
||||
if(listOTags && !k.objects)k.objects = [...new Set(listOTags)].join(',');
|
||||
if(listOTags && !k.objects)k.objects = [...new Set(listOTags)].filter(item => !!item).join(',');
|
||||
k.filename = k.filename || k.file
|
||||
k.ext = k.ext || e.ext || k.filename.split('.')[1]
|
||||
k.stat = fs.statSync(k.dir+k.file)
|
||||
|
|
|
@ -83,6 +83,7 @@ module.exports = function(s,config,lang,io){
|
|||
'home/videosTable',
|
||||
'home/studio',
|
||||
'home/monitorMap',
|
||||
'home/timeline',
|
||||
'confirm',
|
||||
'home/help',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
const fetch = require('node-fetch');
|
||||
const dgram = require('dgram');
|
||||
const parseString = require('xml2js').parseString;
|
||||
const base64 = require('base-64');
|
||||
|
||||
const USERNAME = process.argv[2]
|
||||
const PASSWORD = process.argv[3]
|
||||
|
||||
if(!USERNAME || !PASSWORD){
|
||||
console.log(`Missing Username and/or Password!`)
|
||||
console.log(`Example : node ./onvifGetStreamUri.js "USERNAME" "PASSWORD"`)
|
||||
console.log(`Put the quotations seen in the example!`)
|
||||
return
|
||||
}
|
||||
|
||||
function cleanKeys(obj) {
|
||||
const cleanKey = (key) => key.includes(':') ? key.split(':').pop() : key;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(cleanKeys);
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((newObj, key) => {
|
||||
newObj[cleanKey(key)] = cleanKeys(obj[key]);
|
||||
return newObj;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
async function getStreamUri(deviceXAddr, username, password) {
|
||||
let envelope = `
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<GetProfiles xmlns="http://www.onvif.org/ver10/media/wsdl" />
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`;
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/soap+xml',
|
||||
'Authorization': 'Basic ' + base64.encode(username + ':' + password),
|
||||
},
|
||||
body: envelope,
|
||||
};
|
||||
|
||||
let response = await fetch(deviceXAddr, options);
|
||||
let body = await response.text();
|
||||
|
||||
parseString(body, async (err, result) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
const soapBody = cleanKeys(result).Envelope.Body[0]
|
||||
if (soapBody.Fault) {
|
||||
console.log(deviceXAddr,'Not Authorized');
|
||||
return;
|
||||
}
|
||||
try{
|
||||
var profiles = soapBody.GetProfilesResponse[0].Profiles;
|
||||
}catch(err){
|
||||
console.log(err.stack)
|
||||
console.error(deviceXAddr,`getStreamUri soapBody on ERROR`,JSON.stringify(soapBody,null,3))
|
||||
return
|
||||
}
|
||||
if (!profiles || !profiles.length) {
|
||||
console.log(deviceXAddr,'No profiles found');
|
||||
return;
|
||||
}
|
||||
|
||||
const firstProfileToken = profiles[0]['$']['token'];
|
||||
|
||||
envelope = `
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<GetStreamUri xmlns="http://www.onvif.org/ver10/media/wsdl">
|
||||
<StreamSetup>
|
||||
<Stream xmlns="http://www.onvif.org/ver10/schema">RTP-Unicast</Stream>
|
||||
<Transport xmlns="http://www.onvif.org/ver10/schema">
|
||||
<Protocol>RTSP</Protocol>
|
||||
</Transport>
|
||||
</StreamSetup>
|
||||
<ProfileToken>${firstProfileToken}</ProfileToken>
|
||||
</GetStreamUri>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`;
|
||||
|
||||
options.body = envelope;
|
||||
response = await fetch(deviceXAddr, options);
|
||||
body = await response.text();
|
||||
|
||||
parseString(body, (err, result) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = result['SOAP-ENV:Envelope']['SOAP-ENV:Body'][0]['trt:GetStreamUriResponse'][0]['trt:MediaUri'][0]['tt:Uri'][0];
|
||||
console.log('Stream URI:', uri);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const DISCOVER_MSG = Buffer.from(`
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
||||
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
|
||||
xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>
|
||||
<a:MessageID>uuid:1000-3000-5000-70000000000000</a:MessageID>
|
||||
<a:ReplyTo>
|
||||
<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
|
||||
</a:ReplyTo>
|
||||
<a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<d:Probe>
|
||||
<d:Types>dn:NetworkVideoTransmitter</d:Types>
|
||||
</d:Probe>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`);
|
||||
|
||||
const socket = dgram.createSocket('udp4');
|
||||
socket.bind(() => {
|
||||
socket.setBroadcast(true);
|
||||
socket.setMulticastTTL(128);
|
||||
socket.addMembership('239.255.255.250');
|
||||
socket.send(DISCOVER_MSG, 0, DISCOVER_MSG.length, 3702, '239.255.255.250');
|
||||
});
|
||||
|
||||
socket.on('message', function (message, rinfo) {
|
||||
parseString(message, (err, result) => {
|
||||
if (err) {
|
||||
console.error('Failed to parse XML', err);
|
||||
return;
|
||||
}
|
||||
const cleanJson = cleanKeys(result)
|
||||
if (!cleanJson.Envelope || !cleanJson.Envelope.Body) {
|
||||
console.error('Unexpected message format', result);
|
||||
console.error('cleanJson', cleanJson);
|
||||
return;
|
||||
}
|
||||
|
||||
const soapBody = cleanJson.Envelope.Body[0];
|
||||
const probeMatches = soapBody.ProbeMatches;
|
||||
if (!probeMatches) {
|
||||
console.error('No probe matches in message', soapBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const probeMatch = probeMatches[0].ProbeMatch[0];
|
||||
if (!probeMatch) {
|
||||
console.error('No probe match in probe matches', probeMatches);
|
||||
return;
|
||||
}
|
||||
let xAddrs = probeMatch.XAddrs[0];
|
||||
console.log('Found ONVIF device', xAddrs);
|
||||
getStreamUri(xAddrs, USERNAME, PASSWORD)
|
||||
.catch(err => console.error('Failed to get stream URI', err));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
#tab-timeline .loading-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #1f80f9;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
#tab-timeline .loading-mask i {
|
||||
|
||||
}
|
||||
|
||||
#timeline-video-canvas {
|
||||
background: rgba(0,0,0,0.9);
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
#timeline-video-canvas video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.col-md-4 {
|
||||
height:30vh;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.col-md-6 {
|
||||
height: 40vh;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.col-md-12 {
|
||||
height: 80vh;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video:not(.no-video){
|
||||
background-color: #000!important;
|
||||
}
|
||||
#timeline-video-canvas.show-only-playing .timeline-video.no-video{
|
||||
display: none;
|
||||
}
|
||||
#timeline-info {
|
||||
background: #004e8e;
|
||||
}
|
||||
#timeline-bottom-strip {
|
||||
background: rgba(0,0,0,0.6);
|
||||
height: 93px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#timeline-bottom-strip * {
|
||||
transition: none;
|
||||
}
|
||||
#timeline-bottom-strip .vis-timeline {
|
||||
visibility: visible!important;
|
||||
border: 1px solid #2a4cb5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#timeline-bottom-strip .vis-item {
|
||||
opacity:0.4;
|
||||
}
|
||||
#timeline-bottom-strip .vis-time-axis .vis-grid.vis-minor {
|
||||
border-color: rgb(31 128 249 / 40%);
|
||||
}
|
||||
|
||||
#timeline-bottom-strip .vis-panel.vis-bottom,
|
||||
#timeline-bottom-strip .vis-panel.vis-center,
|
||||
#timeline-bottom-strip .vis-panel.vis-left,
|
||||
#timeline-bottom-strip .vis-panel.vis-right,
|
||||
#timeline-bottom-strip .vis-panel.vis-top {
|
||||
border-color: rgb(31 128 249 / 40%)!important;
|
||||
}
|
|
@ -440,3 +440,10 @@ ul.squeeze {
|
|||
.fixed-table-body {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
|
||||
.flex-direction-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
|
|
@ -18,9 +18,11 @@ var chartColors = {
|
|||
purple: 'rgb(153, 102, 255)',
|
||||
grey: 'rgb(201, 203, 207)'
|
||||
};
|
||||
var isAppleDevice = navigator.userAgent.match(/(iPod|iPhone|iPad)/)||(navigator.userAgent.match(/(Safari)/)&&!navigator.userAgent.match('Chrome'));
|
||||
var userAgent = navigator.userAgent;
|
||||
var isAppleDevice = userAgent.match(/(iPod|iPhone|iPad)/)||(navigator.userAgent.match(/(Safari)/)&&!navigator.userAgent.match('Chrome'));
|
||||
var isMobile = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
||||
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))
|
||||
var isChromiumBased = userAgent.includes('Chrome') && !userAgent.includes('Edg') && !userAgent.includes('OPR') || userAgent.includes('Brave');
|
||||
var keyShortcuts = {}
|
||||
function base64ArrayBuffer(arrayBuffer) {
|
||||
var base64 = ''
|
||||
|
@ -73,6 +75,55 @@ function base64ArrayBuffer(arrayBuffer) {
|
|||
|
||||
return base64
|
||||
}
|
||||
function timeAgo(date) {
|
||||
const now = new Date();
|
||||
const secondsPast = (now.getTime() - date.getTime()) / 1000;
|
||||
if(secondsPast < 60) {
|
||||
return parseInt(secondsPast) + ' seconds ago';
|
||||
}
|
||||
if(secondsPast < 3600) {
|
||||
return parseInt(secondsPast / 60) + ' minutes ago';
|
||||
}
|
||||
if(secondsPast < 86400) {
|
||||
return parseInt(secondsPast / 3600) + ' hours ago';
|
||||
}
|
||||
return parseInt(secondsPast / 86400) + ' days ago';
|
||||
}
|
||||
function getDayOfWeek(date) {
|
||||
const days = [lang.Sunday, lang.Monday, lang.Tuesday, lang.Wednesday, lang.Thursday, lang.Friday, lang.Saturday];
|
||||
return days[date.getDay()];
|
||||
}
|
||||
function stringToColor(str) {
|
||||
let blueColors = [
|
||||
'#0000FF', // Blue
|
||||
'#00008B', // Dark Blue
|
||||
'#0000CD', // Medium Blue
|
||||
'#000080', // Navy
|
||||
'#1E90FF', // Dodger Blue
|
||||
'#4169E1', // Royal Blue
|
||||
'#4682B4', // Steel Blue
|
||||
'#5F9EA0', // Cadet Blue
|
||||
'#6495ED', // Cornflower Blue
|
||||
'#6A5ACD', // Slate Blue
|
||||
'#7B68EE', // Medium Slate Blue
|
||||
'#87CEEB', // Sky Blue
|
||||
'#87CEFA', // Light Sky Blue
|
||||
'#8A2BE2', // Blue Violet
|
||||
'#ADD8E6' // Light Blue
|
||||
];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return blueColors[Math.abs(hash) % blueColors.length];
|
||||
}
|
||||
function getTimeBetween(start, end, percent) {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const difference = endDate - startDate;
|
||||
const time = new Date(startDate.getTime() + difference * percent / 100);
|
||||
return time;
|
||||
}
|
||||
function stringContains(find,string,toLowerCase){
|
||||
var newString = string + ''
|
||||
if(toLowerCase)newString = newString.toLowerCase()
|
||||
|
@ -1021,6 +1072,20 @@ function setSubmitButton(editorForm,text,icon,toggle){
|
|||
var submitButtons = editorForm.find('[type="submit"]').prop('disabled',toggle)
|
||||
submitButtons.html(`<i class="fa fa-${icon}"></i> ${text}`)
|
||||
}
|
||||
function featureIsActivated(showNotice){
|
||||
if(userHasSubscribed){
|
||||
return true
|
||||
}else{
|
||||
if(showNotice){
|
||||
new PNotify({
|
||||
title: lang.activationRequired,
|
||||
text: lang.featureRequiresActivationText,
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
$(document).ready(function(){
|
||||
onInitWebsocket(function(){
|
||||
loadMonitorsIntoMemory(function(data){
|
||||
|
|
|
@ -0,0 +1,768 @@
|
|||
$(document).ready(function(){
|
||||
var theWindow = $('#tab-timeline')
|
||||
var timeStripVideoCanvas = $('#timeline-video-canvas');
|
||||
var timeStripEl = $('#timeline-bottom-strip');
|
||||
var timeStripControls = $('#timeline-controls');
|
||||
var timeStripInfo = $('#timeline-info');
|
||||
var timeStripPreBuffers = $('#timeline-pre-buffers');
|
||||
var timeStripObjectSearchInput = $('#timeline-video-object-search');
|
||||
var dateSelector = $('#timeline-date-selector');
|
||||
var sideMenuList = $(`#side-menu-link-timeline ul`)
|
||||
var playToggles = timeStripControls.find('[timeline-action="playpause"]')
|
||||
var speedButtons = timeStripControls.find('[timeline-action="speed"]')
|
||||
var gridSizeButtons = timeStripControls.find('[timeline-action="gridSize"]')
|
||||
var autoGridSizerButtons = timeStripControls.find('[timeline-action="autoGridSizer"]')
|
||||
var currentTimeLabel = timeStripInfo.find('.current-time')
|
||||
var timelineActionButtons = timeStripControls.find('[timeline-action]')
|
||||
var timelineSpeed = 1;
|
||||
var timelineGridSizing = `md-6`;
|
||||
var timeStripVis = null;
|
||||
var timeStripVisTick = null;
|
||||
var timeStripVisItems = null;
|
||||
var timeStripCurrentStart = null;
|
||||
var timeStripCurrentEnd = null;
|
||||
var timeStripVisTickMovementInterval = null;
|
||||
var timeStripVisTickMovementIntervalSecond = null;
|
||||
var timeStripHollowClickQueue = {}
|
||||
var timeStripTickPosition = new Date()
|
||||
var timeStripPreBuffersEls = {}
|
||||
var timeStripItemColors = {}
|
||||
var timeStripAutoGridSizer = false
|
||||
var timeStripListOfQueries = []
|
||||
var timeStripSelectedMonitors = []
|
||||
var timeStripAutoScrollTimeout = null;
|
||||
var timeStripAutoScrollPositionStart = null;
|
||||
var timeStripAutoScrollPositionEnd = null;
|
||||
var timeStripAutoScrollAmount = null;
|
||||
var loadedVideosOnTimeStrip = []
|
||||
var loadedVideosOnCanvas = {}
|
||||
var loadedVideoElsOnCanvas = {}
|
||||
var loadedVideoElsOnCanvasNextVideoTimeout = {}
|
||||
var loadedVideoEndingTimeouts = {}
|
||||
var isPlaying = false
|
||||
var earliestStart = null
|
||||
var latestEnd = null
|
||||
var timeChanging = false
|
||||
var dateRangeChanging = false
|
||||
function setLoadingMask(turnOn){
|
||||
if(turnOn){
|
||||
if(theWindow.find('.loading-mask').length === 0){
|
||||
var html = `<div class="loading-mask"><i class="fa fa-spinner fa-pulse fa-5x"></i></div>`
|
||||
theWindow.prepend(html)
|
||||
}
|
||||
}else{
|
||||
theWindow.find('.loading-mask').remove()
|
||||
}
|
||||
|
||||
}
|
||||
function addVideoBeforeAndAfter(videos) {
|
||||
videos.sort((a, b) => {
|
||||
if (a.mid === b.mid) {
|
||||
return new Date(a.time) - new Date(b.time);
|
||||
}
|
||||
return a.mid.localeCompare(b.mid);
|
||||
});
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
if (i > 0 && videos[i].mid === videos[i - 1].mid) {
|
||||
videos[i].videoBefore = videos[i - 1];
|
||||
} else {
|
||||
videos[i].videoBefore = null;
|
||||
}
|
||||
if (i < videos.length - 1 && videos[i].mid === videos[i + 1].mid) {
|
||||
videos[i].videoAfter = videos[i + 1];
|
||||
} else {
|
||||
videos[i].videoAfter = null;
|
||||
}
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
function findGapsInSearchRanges(timeRanges, range) {
|
||||
timeRanges.sort((a, b) => a[0] - b[0]);
|
||||
let gaps = [];
|
||||
let currentEnd = new Date(range[0]);
|
||||
for (let i = 0; i < timeRanges.length; i++) {
|
||||
let [start, end] = timeRanges[i];
|
||||
if (start > currentEnd) {
|
||||
gaps.push([currentEnd, start]);
|
||||
}
|
||||
if (end > currentEnd) {
|
||||
currentEnd = end;
|
||||
}
|
||||
}
|
||||
if (currentEnd < range[1]) {
|
||||
gaps.push([currentEnd, range[1]]);
|
||||
}
|
||||
return gaps;
|
||||
}
|
||||
async function getVideosInGaps(gaps,monitorIds){
|
||||
var searchQuery = timeStripObjectSearchInput.val()
|
||||
var videos = []
|
||||
async function loopOnGaps(monitorId){
|
||||
for (let i = 0; i < gaps.length; i++) {
|
||||
var range = gaps[i]
|
||||
videos.push(...(await getVideos({
|
||||
monitorId,
|
||||
startDate: range[0],
|
||||
endDate: range[1],
|
||||
searchQuery,
|
||||
// archived: false,
|
||||
// customVideoSet: wantCloudVideo ? 'cloudVideos' : null,
|
||||
},null,true)).videos);
|
||||
}
|
||||
}
|
||||
if(monitorIds && monitorIds.length > 0){
|
||||
for (let ii = 0; ii < monitorIds.length; ii++) {
|
||||
var monitorId = monitorIds[ii]
|
||||
await loopOnGaps(monitorId)
|
||||
}
|
||||
}else{
|
||||
await loopOnGaps('')
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
async function getVideosByTimeStripRange(addOrOverWrite){
|
||||
// timeStripSelectedMonitors = selected monitors
|
||||
var currentVideosLength = parseInt(loadedVideosOnTimeStrip.length)
|
||||
var stripDate = getTimestripDate()
|
||||
var startDate = stripDate.start
|
||||
var endDate = stripDate.end
|
||||
var gaps = findGapsInSearchRanges(timeStripListOfQueries, [startDate,endDate])
|
||||
// console.error([startDate,endDate])
|
||||
// console.log('range : ',JSON.stringify([startDate,endDate]))
|
||||
// console.log('timeRanges : ',JSON.stringify(timeStripListOfQueries))
|
||||
// console.log('gaps : ',JSON.stringify(gaps))
|
||||
if(gaps.length > 0){
|
||||
setLoadingMask(true)
|
||||
timeStripListOfQueries.push(...gaps)
|
||||
var videos = await getVideosInGaps(gaps,timeStripSelectedMonitors)
|
||||
videos = addVideoBeforeAndAfter(videos)
|
||||
loadedVideosOnTimeStrip.push(...videos)
|
||||
if(currentVideosLength !== loadedVideosOnTimeStrip.length)resetTimelineItems(loadedVideosOnTimeStrip);
|
||||
setLoadingMask(false)
|
||||
}
|
||||
return loadedVideosOnTimeStrip
|
||||
}
|
||||
function selectVideosForCanvas(time, videos){
|
||||
var selectedVideosByMonitorId = {}
|
||||
$.each(loadedMonitors,function(n,monitor){
|
||||
selectedVideosByMonitorId[monitor.mid] = null
|
||||
})
|
||||
var filteredVideos = videos.filter(video => {
|
||||
var startTime = new Date(video.time);
|
||||
var endTime = new Date(video.end);
|
||||
return time >= startTime && time <= endTime;
|
||||
});
|
||||
$.each(filteredVideos,function(n,video){
|
||||
selectedVideosByMonitorId[video.mid] = video;
|
||||
})
|
||||
return selectedVideosByMonitorId;
|
||||
}
|
||||
function drawVideosToCanvas(selectedVideosByMonitorId){
|
||||
var html = ''
|
||||
var preBufferHtml = ''
|
||||
$.each(loadedMonitors,function(monitorId,monitor){
|
||||
var itemColor = stringToColor(monitorId)
|
||||
timeStripItemColors[monitorId] = itemColor
|
||||
html += `<div class="timeline-video col-${timelineGridSizing} p-0 m-0 no-video" data-mid="${monitorId}" data-ke="${monitor.ke}" style="background-color:${itemColor}"></div>`
|
||||
preBufferHtml += `<div class="timeline-video-buffer" data-mid="${monitorId}" data-ke="${monitor.ke}"></div>`
|
||||
})
|
||||
timeStripVideoCanvas.html(html)
|
||||
timeStripPreBuffers.html(preBufferHtml)
|
||||
$.each(selectedVideosByMonitorId,function(monitorId,video){
|
||||
if(!video)return;
|
||||
setVideoInCanvas(video)
|
||||
})
|
||||
}
|
||||
function destroyTimeline(){
|
||||
try{
|
||||
timeStripVis.destroy()
|
||||
}catch(err){
|
||||
// console.log(err)
|
||||
}
|
||||
}
|
||||
function formatVideosForTimeline(videos){
|
||||
var i = 0;
|
||||
var formattedVideos = (videos || []).map((item) => {
|
||||
var blockColor = timeStripItemColors[item.mid];
|
||||
++i;
|
||||
return {
|
||||
id: i,
|
||||
content: ``,
|
||||
style: `background-color: ${blockColor};border-color: ${blockColor}`,
|
||||
start: item.time,
|
||||
end: item.end,
|
||||
group: 1
|
||||
}
|
||||
});
|
||||
return formattedVideos
|
||||
}
|
||||
function createTimelineItems(){
|
||||
var items = new vis.DataSet([]);
|
||||
var groups = new vis.DataSet([
|
||||
{id: 1, content: ''}
|
||||
]);
|
||||
timeStripVisItems = items
|
||||
return {
|
||||
items,
|
||||
groups,
|
||||
}
|
||||
}
|
||||
function resetTimelineItems(videos){
|
||||
var newVideos = formatVideosForTimeline(videos)
|
||||
timeStripVisItems.clear();
|
||||
timeStripVisItems.add(newVideos);
|
||||
}
|
||||
async function resetTimeline(clickTime){
|
||||
await getAndDrawVideosToTimeline(clickTime,true)
|
||||
setTickDate(clickTime)
|
||||
setTimeLabel(clickTime)
|
||||
setTimeOfCanvasVideos(clickTime)
|
||||
setHollowClickQueue()
|
||||
}
|
||||
function createTimeline(){
|
||||
var timeChangingTimeout = null
|
||||
var dateNow = new Date()
|
||||
var hour = 1000 * 60 * 60
|
||||
var startTimeForLoad = new Date(dateNow.getTime() - (hour * 24 + hour))
|
||||
destroyTimeline()
|
||||
var {
|
||||
items,
|
||||
groups,
|
||||
} = createTimelineItems()
|
||||
// make chart
|
||||
timeStripVis = new vis.Timeline(timeStripEl[0], items, groups, {
|
||||
showCurrentTime: false,
|
||||
stack: false,
|
||||
start: timeStripCurrentStart || startTimeForLoad,
|
||||
end: timeStripCurrentEnd || dateNow,
|
||||
});
|
||||
// make tick
|
||||
timeStripVisTick = timeStripVis.addCustomTime(dateNow, `${lang.Time}`);
|
||||
timeStripVis.on('click', async function(properties) {
|
||||
var currentlyPlaying = !!isPlaying;
|
||||
timeStripPlay(true)
|
||||
if(!timeChanging){
|
||||
var clickTime = properties.time;
|
||||
await resetTimeline(clickTime)
|
||||
}
|
||||
if(currentlyPlaying){
|
||||
setTimeout(() => {
|
||||
timeStripPlay()
|
||||
},500)
|
||||
}
|
||||
});
|
||||
timeStripVis.on('rangechange', function(properties){
|
||||
timeChanging = true
|
||||
})
|
||||
timeStripVis.on('rangechanged', function(properties){
|
||||
clearTimeout(timeChangingTimeout)
|
||||
timeStripCurrentStart = properties.start;
|
||||
timeStripCurrentEnd = properties.end;
|
||||
timeStripAutoScrollPositionStart = getTimeBetween(timeStripCurrentStart,timeStripCurrentEnd,10);
|
||||
timeStripAutoScrollPositionEnd = getTimeBetween(timeStripCurrentStart,timeStripCurrentEnd,90);
|
||||
timeStripAutoScrollAmount = getTimelineScrollAmount(timeStripCurrentStart,timeStripCurrentEnd);
|
||||
if(dateRangeChanging)return;
|
||||
timeChangingTimeout = setTimeout(function(){
|
||||
var clickTime = properties.time;
|
||||
resetDateRangePicker()
|
||||
setTimeout(() => {
|
||||
timeChanging = false
|
||||
getAndDrawVideosToTimeline(clickTime)
|
||||
},500)
|
||||
},300)
|
||||
})
|
||||
setTimeout(function(){
|
||||
timeStripEl.find('.vis-timeline').resize()
|
||||
},2000)
|
||||
}
|
||||
function getTimelineScrollAmount(start,end){
|
||||
var startTime = start.getTime()
|
||||
var endTime = end.getTime()
|
||||
var difference = (endTime - startTime) / 1000;
|
||||
var minute = 60
|
||||
var hour = 60 * 60
|
||||
var day = 60 * 60 * 24
|
||||
// returns hours
|
||||
if(difference <= 60){
|
||||
return 0.1
|
||||
}else if(difference > minute && difference <= hour){
|
||||
return 0.3
|
||||
}else if(difference > hour && difference < day){
|
||||
return 0.6
|
||||
}else if(difference >= day){
|
||||
return 10
|
||||
}
|
||||
}
|
||||
function scrollTimeline(addHours){
|
||||
if(timeStripAutoScrollTimeout)return;
|
||||
timeStripAutoScrollTimeout = setTimeout(() => {
|
||||
delete(timeStripAutoScrollTimeout)
|
||||
timeStripAutoScrollTimeout = null
|
||||
},2000)
|
||||
var stripTime = getTimestripDate()
|
||||
var timeToAdd = addHours * 1000 * 60 * 60
|
||||
var start = new Date(stripTime.start.getTime() + timeToAdd)
|
||||
var end = new Date(stripTime.end.getTime() + timeToAdd)
|
||||
setTimestripDate(start,end)
|
||||
}
|
||||
function setTickDate(newDate){
|
||||
if(isPlaying){
|
||||
if(newDate >= timeStripAutoScrollPositionEnd){
|
||||
scrollTimeline(timeStripAutoScrollAmount)
|
||||
}else if(newDate >= new Date()){
|
||||
timeStripPlay(true)
|
||||
}
|
||||
}
|
||||
timeStripTickPosition = new Date(newDate)
|
||||
return timeStripVis.setCustomTime(newDate, timeStripVisTick);
|
||||
}
|
||||
function setTimeLabel(newDate){
|
||||
return currentTimeLabel.text(`${timeAgo(newDate)}, ${getDayOfWeek(newDate)}, ${formattedTime(newDate)}`)
|
||||
}
|
||||
function getTickDate() {
|
||||
return timeStripTickPosition;
|
||||
}
|
||||
function getTimestripDate() {
|
||||
var visibleWindow = timeStripVis.getWindow();
|
||||
var start = visibleWindow.start;
|
||||
var end = visibleWindow.end;
|
||||
return {
|
||||
start,
|
||||
end
|
||||
};
|
||||
}
|
||||
function setTimestripDate(newStart, newEnd) {
|
||||
return timeStripVis.setWindow(newStart, newEnd);
|
||||
}
|
||||
function selectAndDrawVideosToCanvas(theTime,redrawVideos){
|
||||
var selectedVideosForTime = selectVideosForCanvas(theTime,loadedVideosOnTimeStrip)
|
||||
loadedVideosOnCanvas = selectedVideosForTime;
|
||||
if(redrawVideos){
|
||||
drawVideosToCanvas(selectedVideosForTime)
|
||||
}
|
||||
}
|
||||
async function getAndDrawVideosToTimeline(theTime,redrawVideos){
|
||||
await getVideosByTimeStripRange()
|
||||
selectAndDrawVideosToCanvas(theTime,redrawVideos)
|
||||
}
|
||||
function getVideoContainerInCanvas(video){
|
||||
return timeStripVideoCanvas.find(`[data-mid="${video.mid}"][data-ke="${video.ke}"]`)
|
||||
}
|
||||
function getVideoElInCanvas(video){
|
||||
return getVideoContainerInCanvas(video).find('video')[0]
|
||||
}
|
||||
function getVideoContainerPreBufferEl(video){
|
||||
return timeStripPreBuffers.find(`[data-mid="${video.mid}"][data-ke="${video.ke}"]`)
|
||||
}
|
||||
function getWaitTimeUntilNextVideo(endTimeOfFirstVideo,startTimeOfNextVideo){
|
||||
return (new Date(startTimeOfNextVideo).getTime() - new Date(endTimeOfFirstVideo).getTime()) / timelineSpeed
|
||||
}
|
||||
function clearVideoInCanvas(oldVideo){
|
||||
var monitorId = oldVideo.mid
|
||||
loadedVideosOnCanvas[monitorId] = null
|
||||
loadedVideoElsOnCanvas[monitorId] = null
|
||||
clearTimeout(loadedVideoEndingTimeouts[monitorId])
|
||||
var container = getVideoContainerInCanvas(oldVideo).addClass('no-video')
|
||||
var videoEl = container.find('video')
|
||||
videoEl.attr('src','')
|
||||
try{
|
||||
videoEl[0].pause()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
container.empty()
|
||||
timeStripAutoGridResize()
|
||||
}
|
||||
function setVideoInCanvas(newVideo){
|
||||
var monitorId = newVideo.mid
|
||||
getVideoContainerInCanvas(newVideo).removeClass('no-video').html(`<video muted src="${newVideo.href}"></video>`)
|
||||
var vidEl = getVideoElInCanvas(newVideo)
|
||||
vidEl.playbackRate = timelineSpeed
|
||||
if(isPlaying)playVideo(vidEl)
|
||||
loadedVideoElsOnCanvas[monitorId] = vidEl
|
||||
loadedVideosOnCanvas[monitorId] = newVideo
|
||||
timeStripPreBuffersEls[monitorId] = getVideoContainerPreBufferEl(newVideo)
|
||||
queueNextVideo(newVideo)
|
||||
timeStripAutoGridResize()
|
||||
}
|
||||
function setTimeOfCanvasVideos(newTime){
|
||||
$.each(loadedVideosOnCanvas,function(n,video){
|
||||
if(!video)return;
|
||||
var monitorId = video.mid
|
||||
var timeAfterStart = (newTime - new Date(video.time)) / 1000;
|
||||
var videoEl = loadedVideoElsOnCanvas[monitorId]
|
||||
videoEl.currentTime = timeAfterStart
|
||||
// playVideo(videoEl)
|
||||
// pauseVideo(videoEl)
|
||||
})
|
||||
}
|
||||
function queueNextVideo(video){
|
||||
if(!video)return;
|
||||
var monitorId = video.mid
|
||||
var videoEl = loadedVideoElsOnCanvas[monitorId]
|
||||
var videoAfter = video.videoAfter
|
||||
var endingTimeout = null;
|
||||
var alreadyDone = false;
|
||||
function currentVideoIsOver(){
|
||||
if(alreadyDone)return;
|
||||
alreadyDone = true;
|
||||
clearVideoInCanvas(video)
|
||||
if(videoAfter){
|
||||
var waitTimeTimeTillNext = getWaitTimeUntilNextVideo(video.end,videoAfter.time)
|
||||
// console.log('End of Video',video)
|
||||
// console.log('Video After',videoAfter)
|
||||
// console.log('Starting in ',waitTimeTimeTillNext / 1000, 'seconds')
|
||||
loadedVideoElsOnCanvasNextVideoTimeout[monitorId] = setTimeout(() => {
|
||||
setVideoInCanvas(videoAfter)
|
||||
},waitTimeTimeTillNext)
|
||||
// }else{
|
||||
// console.log('End of Timeline for Monitor',loadedMonitors[monitorId].name)
|
||||
}
|
||||
}
|
||||
videoEl.onerror = function(err){
|
||||
err.preventDefault()
|
||||
console.error(`video error`)
|
||||
console.error(err)
|
||||
}
|
||||
videoEl.ontimeupdate = function(){
|
||||
if(videoEl.currentTime >= videoEl.duration){
|
||||
clearTimeout(loadedVideoEndingTimeouts[monitorId])
|
||||
currentVideoIsOver()
|
||||
}else if(isPlaying){
|
||||
var theTime = getTickDate()
|
||||
var waitTimeTimeTillNext = getWaitTimeUntilNextVideo(theTime,video.end)
|
||||
clearTimeout(loadedVideoEndingTimeouts[monitorId])
|
||||
loadedVideoEndingTimeouts[monitorId] = setTimeout(() => {
|
||||
currentVideoIsOver()
|
||||
},waitTimeTimeTillNext)
|
||||
}
|
||||
}
|
||||
|
||||
// pre-buffer it
|
||||
timeStripPreBuffersEls[monitorId].html(videoAfter ? `<video preload="auto" muted src="${videoAfter.href}"></video>` : '')
|
||||
}
|
||||
function findVideoAfterTime(time, monitorId) {
|
||||
let inputTime = new Date(time);
|
||||
let matchingVideos = loadedVideosOnTimeStrip.filter(video => {
|
||||
let videoTime = new Date(video.time);
|
||||
return video.mid === monitorId && videoTime > inputTime;
|
||||
});
|
||||
matchingVideos.sort((a, b) => new Date(a.time) - new Date(b.time));
|
||||
return matchingVideos.length > 0 ? matchingVideos[0] : null;
|
||||
}
|
||||
function setHollowClickQueue(){
|
||||
$.each(loadedVideosOnCanvas,function(monitorId,video){
|
||||
if(!video){
|
||||
// console.log(`Add Hollow Action`, loadedMonitors[monitorId].name)
|
||||
var tickTime = getTickDate()
|
||||
var foundVideo = findVideoAfterTime(tickTime,monitorId)
|
||||
clearTimeout(loadedVideoElsOnCanvasNextVideoTimeout[monitorId])
|
||||
if(foundVideo){
|
||||
var waitTimeTimeTillNext = getWaitTimeUntilNextVideo(tickTime,foundVideo.time)
|
||||
// console.log('Found Video',foundVideo)
|
||||
// console.log('Video Starts in ',waitTimeTimeTillNext / 1000, 'seconds after Play')
|
||||
timeStripHollowClickQueue[monitorId] = () => {
|
||||
// console.log('Hollow Start Point for',loadedMonitors[monitorId].name)
|
||||
loadedVideoElsOnCanvasNextVideoTimeout[monitorId] = setTimeout(() => {
|
||||
// console.log('Hollow Replace')
|
||||
setVideoInCanvas(foundVideo)
|
||||
},waitTimeTimeTillNext)
|
||||
}
|
||||
}else{
|
||||
// console.log('End of Timeline for Monitor',loadedMonitors[monitorId].name)
|
||||
timeStripHollowClickQueue[monitorId] = () => {}
|
||||
}
|
||||
}else{
|
||||
timeStripHollowClickQueue[monitorId] = () => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
function runHollowClickQueues(){
|
||||
$.each(timeStripHollowClickQueue,function(monitorId,theAction){
|
||||
theAction()
|
||||
})
|
||||
}
|
||||
function getAllActiveVideosInSlots(){
|
||||
return timeStripVideoCanvas.find('video')
|
||||
}
|
||||
function playVideo(videoEl){
|
||||
try{
|
||||
videoEl.playbackRate = timelineSpeed
|
||||
videoEl.play()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
function pauseVideo(videoEl){
|
||||
try{
|
||||
videoEl.pause()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
function playAllVideos(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
playVideo(video)
|
||||
})
|
||||
}
|
||||
function pauseAllVideos(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
pauseVideo(video)
|
||||
})
|
||||
}
|
||||
function setPlayToggleUI(icon){
|
||||
playToggles.html(`<i class="fa fa-${icon}"></i>`)
|
||||
}
|
||||
function timeStripPlay(forcePause){
|
||||
if(!forcePause && !isPlaying){
|
||||
isPlaying = true
|
||||
var currentDate = getTickDate().getTime();
|
||||
var msSpeed = 50
|
||||
var addition = 0
|
||||
var newTime
|
||||
runHollowClickQueues()
|
||||
playAllVideos()
|
||||
timeStripVisTickMovementInterval = setInterval(function() {
|
||||
addition += (msSpeed * timelineSpeed);
|
||||
newTime = new Date(currentDate + addition)
|
||||
setTickDate(newTime);
|
||||
// setTimeOfCanvasVideos(newTime)
|
||||
}, msSpeed)
|
||||
timeStripVisTickMovementIntervalSecond = setInterval(function() {
|
||||
setTimeLabel(newTime);
|
||||
}, 1000)
|
||||
setPlayToggleUI(`pause-circle-o`)
|
||||
}else{
|
||||
isPlaying = false
|
||||
pauseAllVideos()
|
||||
clearInterval(timeStripVisTickMovementInterval)
|
||||
clearInterval(timeStripVisTickMovementIntervalSecond)
|
||||
$.each(loadedVideoElsOnCanvasNextVideoTimeout,function(n,timeout){
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
$.each(loadedVideoEndingTimeouts,function(n,timeout){
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
setPlayToggleUI(`play-circle-o`)
|
||||
}
|
||||
}
|
||||
// function downloadPlayingVideo(video){
|
||||
// if(video.currentSrc){
|
||||
// var filename = getFilenameFromUrl(video.currentSrc)
|
||||
// downloadFile(video.currentSrc,filename)
|
||||
// }
|
||||
// }
|
||||
function getLoadedVideosOnCanvas(){
|
||||
return Object.values(loadedVideosOnCanvas).filter(item => !!item)
|
||||
}
|
||||
function downloadAllPlayingVideos(){
|
||||
zipVideosAndDownloadWithConfirm(getLoadedVideosOnCanvas())
|
||||
}
|
||||
async function jumpTimeline(amountInMs,direction){
|
||||
timeStripPlay(true)
|
||||
var tickTime = getTickDate().getTime()
|
||||
var newTime = 0;
|
||||
if(direction === 'right'){
|
||||
newTime = tickTime + amountInMs
|
||||
}else{
|
||||
newTime = tickTime - amountInMs
|
||||
}
|
||||
newTime = new Date(newTime)
|
||||
await resetTimeline(newTime)
|
||||
if(tickTime <= timeStripAutoScrollPositionStart){
|
||||
scrollTimeline(-timeStripAutoScrollAmount)
|
||||
}else if(tickTime >= timeStripAutoScrollPositionEnd){
|
||||
scrollTimeline(timeStripAutoScrollAmount)
|
||||
}
|
||||
}
|
||||
function adjustTimelineSpeed(newSpeed){
|
||||
var currentlyPlaying = !!isPlaying;
|
||||
if(currentlyPlaying)timeStripPlay(true);
|
||||
timelineSpeed = newSpeed + 0
|
||||
setHollowClickQueue()
|
||||
if(currentlyPlaying)timeStripPlay();
|
||||
}
|
||||
function adjustTimelineGridSize(newCol){
|
||||
timelineGridSizing = `${newCol}`
|
||||
var containerEls = timeStripVideoCanvas.find('.timeline-video')
|
||||
containerEls.removeClass (function (index, className) {
|
||||
return (className.match (/(^|\s)col-\S+/g) || []).join(' ');
|
||||
}).addClass(`col-${newCol}`)
|
||||
gridSizeButtons.removeClass('btn-success')
|
||||
timeStripControls.find(`[timeline-action="gridSize"][size="${newCol}"]`).addClass('btn-success')
|
||||
}
|
||||
async function refreshTimeline(){
|
||||
var currentlyPlaying = !!isPlaying;
|
||||
timeStripPlay(true)
|
||||
timeStripListOfQueries = []
|
||||
loadedVideosOnTimeStrip = []
|
||||
createTimeline()
|
||||
await resetTimeline(getTickDate())
|
||||
if(currentlyPlaying)timeStripPlay();
|
||||
}
|
||||
function timeStripAutoGridSizerToggle(){
|
||||
if(timeStripAutoGridSizer){
|
||||
timeStripAutoGridSizer = false
|
||||
autoGridSizerButtons.removeClass('btn-success')
|
||||
dashboardOptions('timeStripAutoGridSizer','2')
|
||||
}else{
|
||||
timeStripAutoGridSizer = true
|
||||
autoGridSizerButtons.addClass('btn-success')
|
||||
timeStripAutoGridResize()
|
||||
dashboardOptions('timeStripAutoGridSizer','1')
|
||||
}
|
||||
}
|
||||
function timeStripAutoGridResize(){
|
||||
if(!timeStripAutoGridSizer)return;
|
||||
var numberOfBlocks = getLoadedVideosOnCanvas().length
|
||||
if(numberOfBlocks <= 1){
|
||||
adjustTimelineGridSize(`md-12`)
|
||||
}else if(numberOfBlocks >= 2 && numberOfBlocks < 5){
|
||||
adjustTimelineGridSize(`md-6`)
|
||||
}else if(numberOfBlocks >= 5){
|
||||
adjustTimelineGridSize(`md-4`)
|
||||
}
|
||||
}
|
||||
function resetDateRangePicker(){
|
||||
var stripDate = getTimestripDate()
|
||||
var startDate = stripDate.start
|
||||
var endDate = stripDate.end
|
||||
var picker = dateSelector.data('daterangepicker')
|
||||
picker.setStartDate(startDate);
|
||||
picker.setEndDate(endDate);
|
||||
}
|
||||
function drawFoundCamerasSubMenu(){
|
||||
var tags = getListOfTagsFromMonitors()
|
||||
var allFound = [
|
||||
{
|
||||
attributes: `timeline-menu-action="selectMonitorGroup" tag=""`,
|
||||
class: `cursor-pointer`,
|
||||
color: 'green',
|
||||
label: lang['All Monitors'],
|
||||
}
|
||||
]
|
||||
$.each(tags,function(tag,monitors){
|
||||
allFound.push({
|
||||
attributes: `timeline-menu-action="selectMonitorGroup" tag="${tag}"`,
|
||||
class: `cursor-pointer`,
|
||||
color: 'blue',
|
||||
label: tag,
|
||||
})
|
||||
})
|
||||
if(allFound.length === 1){
|
||||
allFound.push({
|
||||
attributes: ``,
|
||||
class: ``,
|
||||
color: ' d-none',
|
||||
label: `<small class="mt-1">${lang.addTagText}</small>`,
|
||||
})
|
||||
}
|
||||
var html = buildSubMenuItems(allFound)
|
||||
sideMenuList.html(html)
|
||||
}
|
||||
sideMenuList.on('click','[timeline-menu-action]',function(){
|
||||
var el = $(this)
|
||||
var type = el.attr('timeline-menu-action')
|
||||
switch(type){
|
||||
case'selectMonitorGroup':
|
||||
var tag = el.attr('tag')
|
||||
if(!tag){
|
||||
timeStripSelectedMonitors = []
|
||||
}else{
|
||||
var tags = getListOfTagsFromMonitors()
|
||||
var monitorIds = tags[tag]
|
||||
timeStripSelectedMonitors = [...monitorIds];
|
||||
}
|
||||
refreshTimeline()
|
||||
break;
|
||||
}
|
||||
})
|
||||
timelineActionButtons.click(function(){
|
||||
var el = $(this)
|
||||
var type = el.attr('timeline-action')
|
||||
switch(type){
|
||||
case'playpause':
|
||||
timeStripPlay()
|
||||
break;
|
||||
case'downloadAll':
|
||||
if(featureIsActivated(true)){
|
||||
downloadAllPlayingVideos()
|
||||
}
|
||||
break;
|
||||
case'jumpLeft':
|
||||
jumpTimeline(5000,'left')
|
||||
break;
|
||||
case'jumpRight':
|
||||
jumpTimeline(5000,'right')
|
||||
break;
|
||||
case'speed':
|
||||
var speed = parseInt(el.attr('speed'))
|
||||
if(featureIsActivated(true)){
|
||||
adjustTimelineSpeed(speed)
|
||||
speedButtons.removeClass('btn-success')
|
||||
el.addClass('btn-success')
|
||||
}
|
||||
break;
|
||||
case'gridSize':
|
||||
var size = el.attr('size')
|
||||
adjustTimelineGridSize(size)
|
||||
break;
|
||||
case'refresh':
|
||||
refreshTimeline()
|
||||
break;
|
||||
case'autoGridSizer':
|
||||
timeStripAutoGridSizerToggle()
|
||||
break;
|
||||
}
|
||||
})
|
||||
timeStripObjectSearchInput.change(function(){
|
||||
refreshTimeline()
|
||||
})
|
||||
timeStripVideoCanvas.on('dblclick','.timeline-video',function(){
|
||||
var monitorId = $(this).attr('data-mid')
|
||||
openVideosTableView(monitorId)
|
||||
})
|
||||
addOnTabOpen('timeline', async function () {
|
||||
createTimeline()
|
||||
await resetTimeline(getTickDate())
|
||||
drawFoundCamerasSubMenu()
|
||||
})
|
||||
addOnTabReopen('timeline', function () {
|
||||
drawFoundCamerasSubMenu()
|
||||
})
|
||||
addOnTabAway('timeline', function () {
|
||||
timeStripPlay(true)
|
||||
})
|
||||
loadDateRangePicker(dateSelector,{
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
timePickerSeconds: true,
|
||||
timePickerIncrement: 30,
|
||||
autoApply: true,
|
||||
buttonClasses: 'hidden',
|
||||
drops: 'up',
|
||||
maxDate: new Date(),
|
||||
onChange: function(start, end, label) {
|
||||
if(!timeChanging){
|
||||
setLoadingMask(true)
|
||||
dateRangeChanging = true
|
||||
setTimestripDate(start, end)
|
||||
var newTickPosition = getTimeBetween(start,end,50);
|
||||
setTickDate(newTickPosition)
|
||||
setTimeout(() => {
|
||||
dateRangeChanging = false
|
||||
refreshTimeline()
|
||||
},2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
var currentOptions = dashboardOptions()
|
||||
if(isChromiumBased){
|
||||
[ 7, 10 ].forEach((speed) => {
|
||||
timeStripControls.find(`[timeline-action="speed"][speed="${speed}"]`).remove()
|
||||
});
|
||||
}
|
||||
if(!currentOptions.timeStripAutoGridSizer || currentOptions.timeStripAutoGridSizer === '1'){
|
||||
timeStripAutoGridSizerToggle()
|
||||
}
|
||||
})
|
|
@ -408,7 +408,7 @@ function loadEventsData(videoEvents){
|
|||
loadedEventsInMemory[`${anEvent.mid}${anEvent.time}`] = anEvent
|
||||
})
|
||||
}
|
||||
function getVideos(options,callback){
|
||||
function getVideos(options,callback,noEvents){
|
||||
return new Promise((resolve,reject) => {
|
||||
options = options ? options : {}
|
||||
var searchQuery = options.searchQuery
|
||||
|
@ -444,7 +444,7 @@ function getVideos(options,callback){
|
|||
})
|
||||
})
|
||||
$.getJSON(`${getApiPrefix(`timelapse`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(timelapseFrames){
|
||||
$.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${eventLimit}`]).join('&')}`,function(eventData){
|
||||
function completeRequest(eventData){
|
||||
var theEvents = eventData.events || eventData;
|
||||
var newVideos = applyDataListToVideos(videos,theEvents)
|
||||
newVideos = applyTimelapseFramesListToVideos(newVideos,timelapseFrames.frames || timelapseFrames,'timelapseFrames',true).map((video) => {
|
||||
|
@ -455,7 +455,14 @@ function getVideos(options,callback){
|
|||
loadVideosData(newVideos)
|
||||
if(callback)callback({videos: newVideos, frames: timelapseFrames});
|
||||
resolve({videos: newVideos, frames: timelapseFrames})
|
||||
})
|
||||
}
|
||||
if(noEvents){
|
||||
completeRequest([])
|
||||
}else{
|
||||
$.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${eventLimit}`]).join('&')}`,function(eventData){
|
||||
completeRequest(eventData)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -37,8 +37,11 @@ $(document).ready(function(e){
|
|||
imgBlock.find('img').attr('src',href)
|
||||
})
|
||||
}
|
||||
function openVideosTableView(monitorId,startDate,endDate){
|
||||
drawVideosTableViewElements(monitorId,startDate,endDate)
|
||||
window.openVideosTableView = function(monitorId){
|
||||
openTab(`videosTableView`,{})
|
||||
drawMonitorListToSelector(monitorsList,null,null,true)
|
||||
monitorsList.val(monitorId)
|
||||
drawVideosTableViewElements()
|
||||
}
|
||||
loadDateRangePicker(dateSelector,{
|
||||
onChange: function(start, end, label) {
|
||||
|
@ -242,11 +245,7 @@ $(document).ready(function(e){
|
|||
.on('click','.open-videosTable',function(e){
|
||||
e.preventDefault()
|
||||
var monitorId = getRowsMonitorId(this)
|
||||
openTab(`videosTableView`,{},null,null,null,() => {
|
||||
drawMonitorListToSelector(monitorsList,null,null,true)
|
||||
monitorsList.val(monitorId)
|
||||
drawVideosTableViewElements()
|
||||
})
|
||||
openVideosTableView()
|
||||
return false;
|
||||
});
|
||||
sideLinkListBox
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<main class="page-tab" id="tab-timeline" style="position:relative;margin-right:-1.5rem;margin-left:-1.5rem;">
|
||||
<div class="dark">
|
||||
<%
|
||||
var drawBlock
|
||||
var buildOptions
|
||||
%>
|
||||
<%
|
||||
include fieldBuilders.ejs
|
||||
%>
|
||||
<%
|
||||
var pageName = 'Timeline';
|
||||
Object.keys(define[pageName].blocks).forEach(function(blockKey){
|
||||
var pageLayout = define[pageName].blocks[blockKey]
|
||||
drawBlock(pageLayout)
|
||||
})
|
||||
%>
|
||||
|
||||
</div>
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.timeline.css" />
|
||||
<script src="<%-window.libURL%>assets/js/bs5.timeline.js"></script>
|
||||
</main>
|
Loading…
Reference in New Issue