Motostoke City

master^2
Moe 2025-02-06 03:41:12 +00:00
parent e197f0674f
commit fe818a73a8
88 changed files with 6987 additions and 6210 deletions

55
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,55 @@
docker-latest-build:
image: docker:latest
stage: build
variables:
BASE_IMAGE: "node:20-bullseye-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-nvidia-build:
image: docker:latest
stage: build
variables:
BASE_IMAGE: "nvidia/cuda:11.7.1-cudnn8-runtime-ubuntu22.04"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile

View File

@ -1,6 +1,8 @@
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
# OUTDATED, Newest Docker method here :
# This Docker method is only for integrated Database and simple volume mounts. For separate database and more elaborate installation please use this registry instead :
https://gitlab.com/Shinobi-Systems/ShinobiDocker
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
@ -33,27 +35,9 @@ Once complete open port `8080` of your Docker host in a web browser.
> Please remember to check out the Environment Variables table further down this README.
```
docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":'/dev/shm/streams':'rw' -v "$HOME/Shinobi/config":'/config':'rw' -v "$HOME/Shinobi/customAutoLoad":'/home/Shinobi/libs/customAutoLoad':'rw' -v "$HOME/Shinobi/database":'/var/lib/mysql':'rw' -v "$HOME/Shinobi/videos":'/home/Shinobi/videos':'rw' -v "$HOME/Shinobi/plugins":'/home/Shinobi/plugins':'rw' -v '/etc/localtime':'/etc/localtime':'ro' registry.gitlab.com/shinobi-systems/shinobi:dev
docker run -d --name='Shinobi' --memory=2g -p '8080:8080/tcp' -p '21:21/tcp' -v "$HOME/ShinobiDatabase":'/var/lib/mysql':'rw' -v "$HOME/Shinobi":'/home/Shinobi':'rw' registry.gitlab.com/shinobi-systems/shinobi:dev
```
**Installing Object Detection (TensorFlow.js)**
*Updated Image only works with Dashboard v3*
> This requires that you add the plugin key to the Shinobi container. This key is generated and displayed in the startup logs of the Object Detection docker container.
- `-p '8082:8082/tcp'` is an optional flag if you decide to run the plugin in host mode.
- `-e PLUGIN_HOST='10.1.103.113'` Set this as your Shinobi IP Address.
- `-e PLUGIN_PORT='8080'` Set this as your Shinobi Web Port number.
```
docker run -d --name='shinobi-tensorflow' -e PLUGIN_HOST='10.1.103.113' -e PLUGIN_PORT='8080' -v "$HOME/Shinobi/docker-plugins/tensorflow":'/config':'rw' registry.gitlab.com/shinobi-systems/docker-plugin-tensorflow.js:master
```
More Information about this plugin :
- CPU : https://gitlab.com/Shinobi-Systems/docker-plugin-tensorflow.js
- GPU (NVIDIA CUDA) : https://gitlab.com/Shinobi-Systems/docker-plugin-tensorflow.js/-/tree/gpu
## From Source
> Image is based on Ubuntu Bionic (20.04). Node.js 12 is used. MariaDB and FFmpeg are included.
@ -86,27 +70,18 @@ docker build -f Dockerfile.arm32v7 --tag shinobi-image:1.0 .
> This command only works on Linux because of the temporary directory used. This location must exist in RAM. `-v "/dev/shm/shinobiStreams":'/dev/shm/streams':'rw'`. The timezone is also acquired from the host by the volume declaration of `-v '/etc/localtime':'/etc/localtime':'ro'`.
```
docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":'/dev/shm/streams':'rw' -v "$HOME/Shinobi/config":'/config':'rw' -v "$HOME/Shinobi/customAutoLoad":'/home/Shinobi/libs/customAutoLoad':'rw' -v "$HOME/Shinobi/database":'/var/lib/mysql':'rw' -v "$HOME/Shinobi/videos":'/home/Shinobi/videos':'rw' -v "$HOME/Shinobi/plugins":'/home/Shinobi/plugins':'rw' -v '/etc/localtime':'/etc/localtime':'ro' shinobi-image:1.0
docker run -d --name='Shinobi' --memory=2g -p '8080:8080/tcp' -p '21:21/tcp' -v "$HOME/ShinobiDatabase":'/var/lib/mysql':'rw' -v "$HOME/Shinobi":'/home/Shinobi':'rw' shinobi-image:1.0
```
> Host mount paths have been updated in this document.
### Running without Included Database (NoDB)
For information about this please see this Merge Request done by @thtmnisamnstr. It thoroughly documents how to use the NoDB installation method.
https://gitlab.com/Shinobi-Systems/Shinobi/-/merge_requests/443
### Volumes
| Volumes | Description |
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| /dev/shm/Shinobi/streams | **IMPORTANT!** This must be mapped to somewhere in the host's RAM. When running this image on Windows you will need to select a different location. |
| /config | Put `conf.json` or `super.json` files in here to override the default. values. |
| $HOME/Shinobi/customAutoLoad | Maps to the `/home/Shinobi/libs/customAutoLoad` folder for loading your own modules into Shinobi. |
| $HOME/Shinobi/database | A map to `/var/lib/mysql` in the container. This is the database's core files. |
| $HOME/Shinobi/videos | A map to `/home/Shinobi/videos`. The storage location of your recorded videos. |
| $HOME/Shinobi/plugins | A map to `/home/Shinobi/plugins`. Mapped so that plugins can easily be modified or swapped. |
| $HOME/Shinobi | Maps to the `/home/Shinobi` folder for the `customAutoLoad`, `plugins`, and `videos` folders inside and other files. Additionally you can edit the conf.json and super.json here. |
| $HOME/ShinobiDatabase | A map to `/var/lib/mysql` in the container. This is the database's core files. |
### Environment Variables

View File

@ -66,37 +66,23 @@ fi
DATABASE_CONFIG='{"host": "'$DB_HOST'","user": "'$DB_USER'","password": "'$DB_PASSWORD'","database": "'$DB_DATABASE'","port":'$DB_PORT'}'
cronKey="$(head -c 1024 < /dev/urandom | sha256sum | awk '{print substr($1,1,29)}')"
cd /home/Shinobi
mkdir -p libs/customAutoLoad
if [ -e "/config/conf.json" ]; then
cp /config/conf.json conf.json
elif [ ! -e "./conf.json" ]; then
# Create /config/conf.json if it doesn't exist
if [ ! -e "./conf.json" ]; then
cp conf.sample.json conf.json
fi
# Create /config/conf.json if it doesn't exist
if [ ! -e "/config/conf.json" ]; then
node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" databaseType="$DB_TYPE" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG"
cp /config/conf.json conf.json
fi
sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json
node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID pluginKeys="$PLUGIN_KEYS" databaseType="$DB_TYPE" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG"
echo "============="
echo "Default Superuser : admin@shinobi.video"
echo "Default Password : admin"
echo "Log in at http://HOST_IP:SHINOBI_PORT/super"
if [ -e "/config/super.json" ]; then
cp /config/super.json super.json
elif [ ! -e "./super.json" ]; then
cp super.sample.json super.json
fi
if [ -e "/config/init.extension.sh" ]; then
echo "Running extension init file ..."
( sh /config/init.extension.sh )
if [ ! -e "./super.json" ]; then
cp super.sample.json super.json
fi
# Execute Command

View File

@ -63,9 +63,8 @@ RUN sed -i -e 's/\r//g' /home/Shinobi/Docker/init.sh
RUN apt-get update -y --fix-missing
RUN apt-get upgrade -y
VOLUME ["/home/Shinobi/videos"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"]
VOLUME ["/home/Shinobi"]
VOLUME ["/var/lib/mysql"]
EXPOSE 8080 443 21 25

View File

@ -46,7 +46,7 @@ representative at support@shinobi.systems.
As of 2022-07-12 the noted situations below are seen the same as "Commercial Use".
- 25 Active Monitor Rule : Having at least 25 Active Monitors and not a Primary School or Secondary School. Does not apply to Personal Use.
- 15 Active Monitor Rule : Having at least 15 Active Monitors and not a Primary School or Secondary School. Does not apply to Personal Use.
- 150 Active Monitor Rule : If you have more than 150 Active Monitors please contact support for an Enterprise License. The retail Shinobi Pro license will not be applicable for these installations.
- Used on a Device that was part of a commercial transaction. This can be, but not limited to, being sold or provided additionally to a sale.

View File

@ -91,10 +91,14 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => {
require('./libs/auth/logins.js')(s,config,lang,app)
//rally other Shinobi
require('./libs/rally.js')(s,config,lang,app,io)
//on-start actions, daemon(s) starter
await require('./libs/startup.js')(s,config,lang)
//rally other Shinobi
require('./libs/mountManager.js')(s,config,lang,app,io)
// management server connect
require('./libs/connectToManagementServer/index.js')(s,config,lang,app)
//p2p, commander
require('./libs/commander.js')(s,config,lang,app)
//on-start actions, daemon(s) starter
await require('./libs/startup.js')(s,config,lang)
//cron
require('./libs/cron.js')(s,config,lang)
//video browser functions

View File

@ -125,6 +125,12 @@ module.exports = function(s,config,lang){
"fieldType": "select",
"possible": s.listOfStorage
},
{
"name": "detail=ptz_id",
"field": lang["PTZ Control ID"],
"example": "1",
"description": lang["ptzControlIdFieldText"],
},
{
"name": "detail=auto_compress_videos",
"field": lang['Compress Completed Videos'],
@ -307,6 +313,10 @@ module.exports = function(s,config,lang){
"name": "RTSP",
"value": "rtsp"
},
{
"name": "SRT",
"value": "srt"
},
{
"name": "RTMP",
"value": "rtmp"
@ -4472,8 +4482,8 @@ module.exports = function(s,config,lang){
"default": "0",
"example": "",
"fieldType": "select",
"attribute": "multiple",
id: 'copy_settings_monitors',
"attribute": `copy="#monSectionLogging" style="min-height:100px" multiple`,
"form-group-class": "h_copy_settings_input h_copy_settings_1",
"possible": [
{
@ -4973,10 +4983,36 @@ module.exports = function(s,config,lang){
"name": lang['Live Grid'],
"color": "navy",
"info": [
{
"field": lang['Force Monitors Per Row'],
attribute:'localStorage="montage_use"',
selector:'st_force_mon_rows',
"description": "",
"default": "0",
"example": "",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"field": lang['Monitors per row'],
"placeholder": "3",
"form-group-class":"st_force_mon_rows_input st_force_mon_rows_1",
attribute:'localStorage="montage"',
"placeholder": "3",
},
{
"field": lang.hlsOptions,
"name": "localStorage=hlsOptions",
fieldType:"textarea",
"placeholder": "{}",
},
]
},
@ -4984,46 +5020,20 @@ module.exports = function(s,config,lang){
"name": lang.Preferences,
"color": "navy",
"info": [
{
{
"field": lang['Clock Format'],
"name": "detail=clock_date_format",
"placeholder": "$DAYNAME $DAY $MONTHNAME $YEAR",
"name": "detail=clock_date_format",
"placeholder": "$DAYNAME $DAY $MONTHNAME $YEAR",
},
{
"field": lang.CSS,
"name": "detail=css",
fieldType:"textarea",
"placeholder": "#main_header{background:#b59f00}",
"description": "",
"default": "",
"example": "",
"possible": ""
},
{
"field": lang.hlsOptions,
"name": "localStorage=hlsOptions",
"field": lang.CSS,
"name": "detail=css",
fieldType:"textarea",
"placeholder": "{}",
},
{
"field": lang['Force Monitors Per Row'],
"form-group-class":"st_force_mon_rows_input st_force_mon_rows_1",
attribute:'localStorage="montage_use"',
selector:'st_force_mon_rows',
"description": "",
"default": "0",
"example": "",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
"placeholder": "#main_header{background:#b59f00}",
"description": "",
"default": "",
"example": "",
"possible": ""
},
{
"field": lang['Browser Console Log'],
@ -6637,7 +6647,7 @@ module.exports = function(s,config,lang){
{
"name": "ip",
"field": lang['IP Address'],
"description": lang[lang["fieldTextIp"]],
"description": lang["fieldTextIp"],
"example": "10.1.100.1-10.1.100.254",
},
{
@ -8472,6 +8482,23 @@ module.exports = function(s,config,lang){
"field": lang["Search Object Tags"],
"example": "person",
},
{
id:'videosTable_tag_search_andonly',
field: lang['Search Type'],
default: '0',
"fieldType": "select",
possible:[
{
"name": lang.OR,
"value": "0",
selected: true,
},
{
"name": lang.AND,
"value": "1"
},
]
},
{
"class": "date_selector",
"field": lang.Date,
@ -8479,7 +8506,7 @@ module.exports = function(s,config,lang){
{
id:'videosTable_cloudVideos',
field: lang['Video Set'],
default:'local',
default: 'local',
"fieldType": "select",
possible:[
{
@ -9211,6 +9238,13 @@ module.exports = function(s,config,lang){
<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">
<select class="form-control form-control-sm" id="timeline-video-type">
<option value="">${lang.Local}</option>
<option value="cloud">${lang.Cloud}</option>
<option value="archive">${lang.Archive}</option>
</select>
</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>
<a class="btn btn-sm btn-primary" timeline-action="playUntilVideoEnd" title="${lang.playUntilVideoEnd}"><i class="fa fa-step-forward"></i></a>

View File

@ -20,6 +20,15 @@
"Geolocation": "Geolocation",
"Configure": "Configure",
"Scan": "Scan",
"See System Logs": "See System Logs",
"Mount Manager": "Mount Manager",
"mountManagerDescription": "Setup drive mounts here.",
"Mount Added": "Mount Added",
"mountAddedText": "Mount has been added to /etc/fstab. You may now use it as your default Videos directory.",
"mountAddedBadCredentialsText": "Mount has been added to /etc/fstab but it seems credentials are incorrect.",
"Failed to Remove Mount": "Failed to Remove Mount",
"Failed to Add Mount": "Failed to Add Mount",
"Add Mount": "Add Mount",
"Rally": "Rally",
"rallyApiKeyFieldText": "Permissions required : Get Monitors, Edit Monitors, View Streams, View Videos.",
"rallyDescription": "Here you can connect to a separate Shinobi server and add their cameras to this Shinobi server. The connection would be directly from the separate server instead of directly from the cameras.",
@ -66,10 +75,13 @@
"How to Connect": "How to Connect",
"Cycle": "Cycle",
"Cycle Interval": "Cycle Interval",
"Cycle Monitor Height": "Cycle Monitor Height",
"Number of Cycle Monitors": "Number of Cycle Monitors",
"Rows and Columns": "Rows and Columns",
"Number of Monitors": "Number of Monitors",
"Number of Rows": "Number of Rows",
"Number of Columns": "Number of Columns",
"Cycle Monitors": "Cycle Monitors",
"Cycle Monitors per row": "Cycle Monitors per row",
"General": "General",
"Login": "Login",
"Room ID": "Room ID",
"Substream": "Substream",
@ -127,6 +139,11 @@
"Google Drive": "Google Drive",
"Invert Y-Axis": "Invert Y-Axis",
"Get Code": "Get Code",
"Invalid Action": "Invalid Action",
"Button Code": "Button Code",
"PTZ Control ID": "PTZ Control ID",
"ptzControlIdFieldText": "When using a Control Stick or GamePad you can choose an available number to use.",
"ptzControlIdNotFound": "The selected PTZ Control ID was not assigned to a Monitor. Please add it to a Monitor via the Monitor Settings in the Identity section.",
"PTZ Tracking": "PTZ Tracking",
"PTZ Tracking Target": "PTZ Tracking Target",
"Event Counts": "Event Counts",
@ -156,6 +173,7 @@
"Open All Monitors": "Open All Monitors",
"Open Wall Display": "Open Wall Display",
"New Wall Display": "New Wall Display",
"Wall Display Settings": "Wall Display Settings",
"openWallViewInfo": "Open Monitors in the top right of this window.",
"Accounts": "Accounts",
"Settings": "Settings",
@ -399,6 +417,7 @@
"Action for Selected": "Action for Selected",
"Selected": "Selected",
"Search": "Search",
"Search Type": "Search Type",
"No": "No",
"Yes": "Yes",
"Start": "Start",
@ -453,6 +472,7 @@
"ONVIF Port": "ONVIF Port",
"ONVIF Scanner": "ONVIF Scanner",
"ONVIF Events": "ONVIF Events",
"ONVIFNotEnabled": "ONVIF Not Enabled in Monitor Settings",
"ONVIFEventsNotAvailable": "ONVIF Events not Available",
"ONVIFEventsNotAvailableText1": "This service may not be available for this camera or ONVIF has not initialized yet.",
"ONVIFnotCompliantProfileT": "Camera is not ONVIF Profile T Compliant",
@ -533,6 +553,9 @@
"Use Global Wasabi Hot Cloud Storage Video Storage": "Use Global Wasabi Hot Cloud Storage Video Storage",
"Use Global Backblaze B2 Video Storage": "Use Global Backblaze B2 Video Storage",
"Use Global WebDAV Video Storage": "Use Global WebDAV Video Storage",
"windowsCantUseFeature": "Windows cannot use this feature",
"Mount Point": "Mount Point",
"Mounted Drive Storage": "Mounted Drive Storage",
"S3-Based Network Storage": "S3-Based Network Storage",
"Amazon S3 Upload Error": "Amazon S3 Upload Error",
"Wasabi Hot Cloud Storage Upload Error": "Wasabi Hot Cloud Storage Upload Error",
@ -545,7 +568,17 @@
"URL": "URL",
"Operating Hours": "Operating Hours",
"Autosave": "Autosave",
"Source": "Source",
"Delete Mount": "Delete Mount",
"Mount Path": "Mount Path",
"Mount Type": "Mount Type",
"setVideosDirWarning": "Doing this while Monitors are recording may cause issues. Stop them before making this change.",
"Save Directory": "Save Directory",
"Path Inside": "Path Inside",
"Path Inside Mount": "Path Inside Mount",
"New Videos Directory Set": "New Videos Directory Set",
"Use Default Videos Directory": "Use Default Videos Directory",
"Set New Videos Directory": "Set New Videos Directory?",
"CSS": "CSS <small>Style your dashboard.</small>",
"Don't Stretch Monitors": "Don't Stretch Monitors",
"Force Monitors Per Row": "Force Monitors Per Row",
@ -1960,5 +1993,8 @@
"saveUnknownFacesFieldText": "Save Unknown faces to the Face Manager. Manual sorting may still be required.",
"Current Version": "Current Version",
"Default is Global value": "Default is Global value",
"rejectUnauth": "Ignore server certificate"
"rejectUnauth": "Ignore server certificate",
"Central Management" : "Central Management",
"centralManagementSaved" : "Settings saved. Restarting connection to Central Managment Server.",
"centralManagementNotEnabled" : "Management Server connectivity is not enabled. Activate your installation and add <code>\"enableMgmtConnect\": true</code> to your conf.json."
}

1999
languages/es.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -136,7 +136,7 @@ module.exports = (processCwd,config) => {
}
return theRequester(requestUrl,requestOptions)
}
const checkSubscription = (subscriptionId,callback) => {
const checkSubscription = (subscriptionId,callback,suppressCheckNotice = false) => {
function subscriptionFailed(){
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
console.error('This Install of Shinobi is NOT Activated')
@ -163,9 +163,11 @@ module.exports = (processCwd,config) => {
}
callback(hasSubcribed)
if(hasSubcribed){
s.systemLog('This Install of Shinobi is Activated')
if(!json.expired && json.timeExpires){
s.systemLog(`This License expires on ${json.timeExpires}`)
if(!suppressCheckNotice){
s.systemLog('This Install of Shinobi is Activated')
if(!json.expired && json.timeExpires){
s.systemLog(`This License expires on ${json.timeExpires}`)
}
}
}else{
subscriptionFailed()
@ -187,6 +189,18 @@ module.exports = (processCwd,config) => {
callback(hasSubcribed)
}
}
function checkAgainSubscription(){
let checkCount = 1
return setInterval(function(){
if(checkCount === 28){
checkSubscription(config.subscriptionId || config.peerConnectKey || config.p2pApiKey, function(hasSubcribed){
config.userHasSubscribed = hasSubcribed
}, true);
checkCount = 1;
}
++checkCount;
}, 1000 * 60 * 60 * 24);
}
function isEven(value) {
if (value%2 == 0)
return true;
@ -272,6 +286,7 @@ module.exports = (processCwd,config) => {
localToUtc: localToUtc,
formattedTime: formattedTime,
checkSubscription: checkSubscription,
checkAgainSubscription,
isEven: isEven,
fetchTimeout: fetchTimeout,
fetchDownloadAndWrite: fetchDownloadAndWrite,

View File

@ -12,7 +12,7 @@ module.exports = (s,config,lang) => {
const cameraCountChecks = [
{ kind: 'ec2', maxCameras: 2, condition: config.isEC2 },
{ kind: 'highCoreCount', maxCameras: 50, condition: config.isHighCoreCount },
{ kind: 'default', maxCameras: 30, condition: true },
{ kind: 'default', maxCameras: 15, condition: true },
];
if (!config.userHasSubscribed) {
const monitorCountOnSystem = getTotalMonitorCount();

View File

@ -131,7 +131,9 @@ module.exports = function(s,config,lang,app){
runningWorker = startWorker()
}
if(config.p2pEnabled){
beginConnection()
s.onLoadedUsersAtStartup(() => {
beginConnection()
})
}
/**
* API : Superuser : Save P2P Server choice

View File

@ -0,0 +1,63 @@
const fs = require('fs').promises
const { Worker } = require('worker_threads')
const testMode = process.argv[2] === 'test'
let passedJSON = false
let passedConfig = {}
const moduleName = 'connectToManagementServer'
module.exports = (s,config,lang,app) => {
if(!config.enableMgmtConnect){
return;
}
const { modifyConfiguration, getConfiguration } = require('../system/utils.js')(config)
require('./libs/centralConnect.js')(s,config,lang)
require('./libs/pairServer.js')(s,config)
/**
* API : Superuser : Save Management Server Settings
*/
app.post(config.webPaths.superApiPrefix+':auth/mgmt/save', function (req,res){
s.superAuth(req.params,async (resp) => {
// for saving :
// form.peerConnectKey
// form.managementServer
const response = {ok: true};
const form = s.getPostData(req,'data',true);
config = Object.assign(config,form)
const currentConfig = await getConfiguration()
const configError = await modifyConfiguration(Object.assign(currentConfig,form))
if(configError)s.systemLog(configError)
try{
s.restartCentralManagement()
}catch(err){
s.debugLog(err)
}
s.closeJsonResponse(res,response)
},res,req)
})
/**
* API : Delete Management Server Settings
*/
app.post(config.webPaths.superApiPrefix+':auth/mgmt/disconnect', async function (req,res){
s.superAuth(req.params,async (resp) => {
const response = {ok: true};
const peerConnectKey = s.getPostData(req,'peerConnectKey');
const currentConfig = await getConfiguration()
if(currentConfig.peerConnectKey === peerConnectKey){
delete(config.managementServer);
delete(currentConfig.managementServer);
const configError = await modifyConfiguration(currentConfig);
if(configError)s.systemLog(configError);
try{
s.restartCentralManagement()
}catch(err){
s.debugLog(err)
}
}else{
response.ok = false;
response.msg = 'Peer Connect Key not matching! Cannot disconnect.';
}
s.closeJsonResponse(res,response)
},res,req)
})
}

View File

@ -0,0 +1,62 @@
const { Worker } = require('worker_threads')
const fs = require('fs').promises;
module.exports = (s,config,lang) => {
const {
modifyConfiguration,
} = require('../../system/utils.js')(config)
const {
getConnectionDetails,
} = require('./connectDetails.js')(s,config)
const configPath = process.cwd() + '/conf.json'
async function startWorker(){
if(!config.userHasSubscribed){
return console.log(lang.centralManagementNotEnabled)
}
const configFromFile = JSON.parse(await fs.readFile(configPath, 'utf8'))
configFromFile.timezone = config.timezone;
console.log('Central Worker Starting...')
const worker = new Worker(`${__dirname}/centralConnect/index.js`, {
workerData: {
config: configFromFile,
}
});
worker.on('message', (data) => {
switch(data.f){
case'connectDetailsRequest':
getConnectionDetails().then((connectDetails) => {
worker.postMessage({ f: 'connectDetails', connectDetails })
}).catch((error) => {
console.error('FAILED TO GET connectDetails',error)
worker.postMessage({ f: 'connectDetails', connectDetails: {} })
})
break;
case'modifyConfiguration':
console.log('Editing Configuration...', data.data.form)
modifyConfiguration(data.data.form)
break;
case'restart':
s.systemLog('Restarting Central Connection...')
worker.terminate()
break;
}
});
worker.on('error', (err) => {
console.error('cameraPeer Error', err)
});
worker.on('exit', (code) => {
console.log('cameraPeer Exited, Restarting...', code)
startWorker()
});
s.centralManagementWorker = worker;
}
s.onLoadedUsersAtStartup(() => {
startWorker()
})
s.restartCentralManagement = () => {
if(!s.centralManagementWorker){
startWorker()
}else{
s.centralManagementWorker.terminate()
}
};
}

View File

@ -0,0 +1,366 @@
const { spawn } = require('child_process');
const { parentPort, workerData } = require('worker_threads');
process.on("uncaughtException", function(error) {
console.error(error);
});
const activeTerminalCommands = {}
let config = workerData.config
let lang = workerData.lang
let sslInfo = config.ssl || {}
const expectedConfigPath = `./conf.json`
const hostPeerServer = config.managementServer;
const peerConnectKey = config.peerConnectKey;
if(!peerConnectKey || !hostPeerServer){
console.log(`Management Server Connection Not Configured!`)
setInterval(() => {
}, 1000 * 60 * 60 * 24)
return;
}
const fs = require("fs").promises
const net = require("net")
const bson = require('bson')
const WebSocket = require('cws')
const os = require('os');
const { EventEmitter } = require('node:events');
const internalEvents = new EventEmitter();
const s = {
debugLog: () => {},
systemLog: (...args) => {
parentPort.postMessage({
f: 'systemLog',
data: args
})
},
}
console.log('hostPeerServer',hostPeerServer)
if(config.debugLog){
s.debugLog = (...args) => {
parentPort.postMessage({
f: 'debugLog',
data: args
})
}
}
parentPort.on('message',(data) => {
switch(data.f){
case'init':
initialize()
break;
case'connectDetails':
data.connectDetails.peerConnectKey = peerConnectKey;
internalEvents.emit('connectDetails', data.connectDetails);
// outboundMessage('connectDetailsForManagement', data.connectDetails, '1');
break;
case'exit':
s.debugLog('Closing Central Connection...')
process.exit(0)
break;
}
})
let outboundMessage = null
var socketCheckTimer = null
var heartbeatTimer = null
var heartBeatCheckTimout = null
var onClosedTimeout = null
let stayDisconnected = false
const requestConnections = {}
const requestConnectionsData = {}
function getServerIPAddresses() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (let interfaceName in interfaces) {
for (let i = 0; i < interfaces[interfaceName].length; i++) {
const iface = interfaces[interfaceName][i];
if (iface.family === 'IPv4' && !iface.internal) {
addresses.push(iface.address);
}
}
}
return addresses;
}
function getRequestConnection(requestId){
return requestConnections[requestId] || {
write: () => {}
}
}
function clearAllTimeouts(){
clearInterval(heartbeatTimer)
clearTimeout(heartBeatCheckTimout)
clearTimeout(onClosedTimeout)
}
function getConnectionDetails(){
return new Promise((resolve) => {
internalEvents.once('connectDetails' ,(data) => {
resolve(data)
})
parentPort.postMessage({ f: 'connectDetailsRequest' })
})
}
function requestConnectionDetails(){
}
function startConnection(){
let tunnelToP2P
stayDisconnected = false
const allMessageHandlers = []
async function startWebsocketConnection(key,callback){
s.debugLog(`startWebsocketConnection EXECUTE`,new Error())
console.log('Central : Connecting to Central Server...')
function createWebsocketConnection(){
clearAllTimeouts()
return new Promise((resolve,reject) => {
try{
stayDisconnected = true
if(tunnelToP2P)tunnelToP2P.close()
}catch(err){
console.log(err)
}
tunnelToP2P = new WebSocket(hostPeerServer);
stayDisconnected = false;
tunnelToP2P.on('open', function(){
resolve(tunnelToP2P)
})
tunnelToP2P.on('error', (err) => {
console.log(`Central tunnelToCentral Error : `,err)
console.log(`Central Restarting...`)
// disconnectedConnection()
})
tunnelToP2P.on('close', () => {
console.log(`Central Connection Closed!`)
clearAllTimeouts()
// onClosedTimeout = setTimeout(() => {
// disconnectedConnection();
// },5000)
});
tunnelToP2P.onmessage = function(event){
const data = bson.deserialize(Buffer.from(event.data))
allMessageHandlers.forEach((handler) => {
if(data.f === handler.key){
handler.callback(data.data,data.rid)
}
})
}
clearInterval(socketCheckTimer)
socketCheckTimer = setInterval(() => {
// s.debugLog('Tunnel Ready State :',tunnelToP2P.readyState)
if(tunnelToP2P.readyState !== 1){
s.debugLog('Tunnel NOT Ready! Reconnecting...')
disconnectedConnection()
}
},1000 * 20)
})
}
function disconnectedConnection(code,reason){
s.debugLog('stayDisconnected',stayDisconnected)
clearAllTimeouts()
s.debugLog('DISCONNECTED!')
if(stayDisconnected)return;
s.debugLog('RESTARTING!')
setTimeout(() => {
if(tunnelToP2P && tunnelToP2P.readyState !== 1)startWebsocketConnection()
},2000)
}
s.debugLog(hostPeerServer)
await createWebsocketConnection(hostPeerServer,allMessageHandlers)
console.log('Central : Connected! Authenticating...')
const connectDetails = await getConnectionDetails()
sendDataToTunnel({
isShinobi: !!config.passwordType,
peerConnectKey,
connectDetails,
ipAddresses: getServerIPAddresses(),
config: JSON.parse(await fs.readFile(expectedConfigPath,'utf8')),
})
clearInterval(heartbeatTimer)
heartbeatTimer = setInterval(() => {
sendDataToTunnel({
f: 'ping',
})
}, 1000 * 10)
setTimeout(() => {
if(tunnelToP2P.readyState !== 1)refreshHeartBeatCheck()
},5000)
}
function sendDataToTunnel(data){
tunnelToP2P.send(bson.serialize(data))
}
startWebsocketConnection()
function onIncomingMessage(key,callback){
allMessageHandlers.push({
key: key,
callback: callback,
})
}
outboundMessage = (key,data,requestId) => {
sendDataToTunnel({
f: key,
data: data,
rid: requestId
})
}
async function createRemoteSocket(host,port,requestId,initData){
// if(requestConnections[requestId]){
// remotesocket.off('data')
// remotesocket.off('drain')
// remotesocket.off('close')
// requestConnections[requestId].end()
// }
const responseTunnel = await getResponseTunnel(requestId)
let remotesocket = new net.Socket();
remotesocket.on('ready',() => {
remotesocket.write(initData.buffer)
})
remotesocket.on('error',(err) => {
s.debugLog('createRemoteSocket ERROR',err)
})
remotesocket.on('data', function(data) {
requestConnectionsData[requestId] = data.toString()
responseTunnel.send('data',data)
})
remotesocket.on('drain', function() {
responseTunnel.send('resume',{})
});
remotesocket.on('close', function() {
delete(requestConnectionsData[requestId])
responseTunnel.send('end',{})
setTimeout(() => {
if(
responseTunnel &&
(responseTunnel.readyState === 0 || responseTunnel.readyState === 1)
){
responseTunnel.close()
}
},5000)
});
remotesocket.connect(port, host || 'localhost');
requestConnections[requestId] = remotesocket
return remotesocket
}
function writeToServer(data,requestId){
var flushed = getRequestConnection(requestId).write(data.buffer)
if (!flushed) {
outboundMessage('pause',{},requestId)
}
}
function refreshHeartBeatCheck(){
clearTimeout(heartBeatCheckTimout)
heartBeatCheckTimout = setTimeout(() => {
startWebsocketConnection()
},1000 * 10 * 1.5)
}
onIncomingMessage('connect',async (data,requestId) => {
s.debugLog('New Request Incoming', 'localhost', config.port, requestId);
const socket = await createRemoteSocket('localhost', config.port, requestId, data.init)
})
onIncomingMessage('data',writeToServer)
onIncomingMessage('resume',function(data,requestId){
requestConnections[requestId].resume()
})
onIncomingMessage('pause',function(data,requestId){
requestConnections[requestId].pause()
})
onIncomingMessage('pong',function(data,requestId){
refreshHeartBeatCheck()
})
onIncomingMessage('init',function(data,requestId){
console.log(`Central : Authenticated!`)
})
onIncomingMessage('modifyConfiguration',function(data,requestId){
parentPort.postMessage({
f: 'modifyConfiguration',
data: data
})
})
onIncomingMessage('getConfiguration',function(data, requestId){
outboundMessage('getConfigurationResponse', Object.assign({}, config), requestId)
})
onIncomingMessage('restart',function(data,requestId){
parentPort.postMessage({ f: 'restart' })
})
onIncomingMessage('end',function(data,requestId){
try{
requestConnections[requestId].end()
}catch(err){
s.debugLog(`Reqest Failed to END ${requestId}`)
s.debugLog(`Failed Request ${requestConnectionsData[requestId]}`)
delete(requestConnectionsData[requestId])
s.debugLog(err)
// console.log('requestConnections',requestConnections)
}
})
onIncomingMessage('disconnect',function(data,requestId){
console.log(`FAILED LICENSE CHECK ON P2P`)
const retryLater = data && data.retryLater;
stayDisconnected = !retryLater
if(retryLater)console.log(`Retrying Central Later...`)
})
}
const responseTunnels = {}
async function getResponseTunnel(originalRequestId){
return responseTunnels[originalRequestId] || await createResponseTunnel(originalRequestId)
}
function createResponseTunnel(originalRequestId){
const responseTunnelMessageHandlers = []
function onMessage(key,callback){
responseTunnelMessageHandlers.push({
key: key,
callback: callback,
})
}
return new Promise((resolve,reject) => {
const responseTunnel = new WebSocket(hostPeerServer);
function sendToResponseTunnel(data){
responseTunnel.send(
bson.serialize(data)
)
}
function sendData(key,data){
sendToResponseTunnel({
f: key,
data: data,
rid: originalRequestId
})
}
responseTunnel.on('error', (err) => {
s.debugLog('responseTunnel ERROR',err)
})
responseTunnel.on('open', function(){
sendToResponseTunnel({
responseTunnel: originalRequestId,
peerConnectKey,
})
})
responseTunnel.on('close', function(){
delete(responseTunnels[originalRequestId])
})
onMessage('ready', function(){
const finalData = {
onMessage,
send: sendData,
sendRaw: sendToResponseTunnel,
close: responseTunnel.close
}
responseTunnels[originalRequestId] = finalData;
resolve(finalData)
})
responseTunnel.onmessage = function(event){
const data = bson.deserialize(Buffer.from(event.data))
responseTunnelMessageHandlers.forEach((handler) => {
if(data.f === handler.key){
handler.callback(data.data,data.rid)
}
})
}
})
}
function closeResponseTunnel(originalRequestId){
// also should be handled server side
try{
responseTunnels[originalRequestId].close()
}catch(err){
s.debugLog('closeResponseTunnel',err)
}
}
startConnection()

View File

@ -0,0 +1,109 @@
const fs = require("fs").promises
module.exports = (s,config) => {
const configPath = config.thisIsDocker ? "/config/super.json" : s.location.super;
async function generateSuperUserJson(){
const baseConfig = [
{
"mail": `${s.gid(6)}@${s.gid(6)}.${s.gid(3)}`,
"pass": s.gid(32),
"tokens": [
s.gid(30)
]
}
];
await fs.writeFile(configPath, JSON.stringify(baseConfig,null,3))
return baseConfig[0]
}
async function getFirstSuperUser(){
let superUser = JSON.parse(await fs.readFile(configPath))[0]
if(!superUser){
superUser = await generateSuperUserJson()
}
if(!superUser.tokens || !superUser.tokens[0]){
const newToken = await applySuperApiKey()
superUser.tokens = [newToken]
}
return superUser
}
async function applySuperApiKey(){
const superUserList = JSON.parse(await fs.readFile(configPath))
const newToken = s.gid(30)
superUserList[0].tokens = [newToken]
await fs.writeFile(configPath, JSON.stringify(superUserList,null,3))
return newToken;
}
async function getFirstSuperApiKey(){
const superUser = await getFirstSuperUser()
const apiKey = superUser.tokens[0];
return apiKey
}
async function getFirstAdminApiKey(){
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Users",
limit: 1,
});
const user = rows[0];
const apiKey = await getFirstApiKey(user.ke, user.uid)
return { groupKey: user.ke, apiKey }
}
async function getFirstApiKey(groupKey, userId){
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "API",
where: { ke: groupKey, uid: userId }
});
let suitableKey = null;
for(row of rows){
row.details = JSON.parse(row.details)
const detailValues = Object.values(row.details);
const theFiltered = detailValues.filter(item => item != 1);
if(theFiltered.length === 0){
suitableKey = row.code
}
};
if(!suitableKey){
suitableKey = await createApiKey(groupKey, userId)
}
return suitableKey;
}
async function createApiKey(groupKey, userId){
const newApiKey = s.gid(30);
await s.knexQueryPromise({
action: "insert",
table: "API",
insert: {
ke : groupKey,
uid : userId,
code : newApiKey,
ip : '0.0.0.0',
details : s.stringJSON({
"auth_socket": "1",
"get_monitors": "1",
"edit_monitors": "1",
"control_monitors": "1",
"get_logs": "1",
"watch_stream": "1",
"watch_snapshot": "1",
"watch_videos": "1",
"delete_videos": "1"
})
}
});
return newApiKey
}
async function getConnectionDetails(){
const superApiKey = await getFirstSuperApiKey()
const { groupKey, apiKey } = await getFirstAdminApiKey()
return {
superApiKey,
groupKey,
apiKey,
}
}
return {
getConnectionDetails
}
}

View File

@ -0,0 +1,56 @@
const http = require('http');
const express = require('express');
const app = express();
var cors = require('cors');
var bodyParser = require('body-parser');
module.exports = (s,config) => {
const { modifyConfiguration, getConfiguration } = require('../../system/utils.js')(config)
const pairPort = config.pairPort || 8091
const bindIp = config.bindip
const server = http.createServer(app);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cors());
server.listen(pairPort, bindIp, function(){
console.log('Management Pair Server Listening on '+pairPort);
});
/**
* API : Superuser : Save Management Server Settings
*/
app.post('/mgmt/connect', async function (req,res){
// form.managementServer
// Example of Shinobi and MGMT on same server
// ws://127.0.0.1:8663
const response = {ok: true};
if(!config.managementServer){
const managementServer = s.getPostData(req,'managementServer');
const peerConnectKey = s.getPostData(req,'peerConnectKey');
if(peerConnectKey){
config = Object.assign(config, { managementServer, peerConnectKey })
const currentConfig = await getConfiguration()
if(peerConnectKey){
currentConfig.peerConnectKey = peerConnectKey
}
// else if(!currentConfig.peerConnectKey){
// currentConfig.peerConnectKey = `bihan${s.gid(20)}`
// }
const configError = await modifyConfiguration(Object.assign(currentConfig, { managementServer }))
if(configError)s.systemLog(configError)
try{
s.centralManagementWorker.terminate()
}catch(err){
s.debugLog(err)
}
}else{
response.ok = false;
response.msg = 'No P2P API Key Provided';
}
}else{
response.ok = false;
response.msg = 'Already Configured';
}
s.closeJsonResponse(res,response)
})
}

View File

@ -0,0 +1,24 @@
function prettyPrint(obj){
return JSON.stringify(obj,null,3)
}
function generateId(x){
if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < x; i++ )
t += p.charAt(Math.floor(Math.random() * p.length));
return t;
}
function parseJSON(string){
var parsed
try{
parsed = JSON.parse(string)
}catch(err){
}
if(!parsed)parsed = string
return parsed
}
module.exports = {
parseJSON,
prettyPrint,
generateId,
}

View File

@ -0,0 +1,32 @@
const WebSocket = require('cws');
function createWebSocketServer(options){
const theWebSocket = new WebSocket.Server(options ? options : {
noServer: true
});
theWebSocket.broadcast = function(data) {
theWebSocket.clients.forEach((client) => {
try{
client.sendData(data)
}catch(err){
// console.log(err)
}
})
};
return theWebSocket
}
function createWebSocketClient(connectionHost,options){
const clientConnection = new WebSocket(connectionHost, options.engineOptions);
if(options.onMessage){
const onMessage = options.onMessage;
clientConnection.on('message', message => {
const data = JSON.parse(message);
onMessage(data);
});
}
return clientConnection
}
module.exports = {
createWebSocketServer,
createWebSocketClient,
}

View File

@ -137,14 +137,16 @@ module.exports = function(s,config,lang,app,io){
if(onvifOptions.mid && onvifOptions.ke){
const groupKey = onvifOptions.ke
const monitorId = onvifOptions.mid
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
theUrl = addCredentialsToUrl({
username: onvifOptions.username,
password: onvifOptions.password,
url: (await theDevice.services.media.getSnapshotUri({
ProfileToken : theDevice.current_profile.token,
})).GetSnapshotUriResponse.MediaUri.Uri
});
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({ id: e.mid, ke: e.ke })).device;
if(theDevice){
theUrl = addCredentialsToUrl({
username: theDevice.user,
password: theDevice.pass,
url: (await theDevice.services.media.getSnapshotUri({
ProfileToken : theDevice.current_profile.token,
})).data.GetSnapshotUriResponse.MediaUri.Uri
});
}
}else{
theUrl = addCredentialsToStreamLink({
username: onvifOptions.username,

View File

@ -283,11 +283,13 @@ module.exports = async (s,config,lang,app,io) => {
if(config.language === rule){
lang = Object.assign(lang,fileData)
}
if(s.loadedLanguages[rule]){
s.loadedLanguages[rule] = Object.assign(s.loadedLanguages[rule],fileData)
}else{
s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),fileData)
}
if(!s.languageModifications[rule])s.languageModifications[rule] = [];
s.languageModifications[rule].push(fileData);
// if(s.loadedLanguages[rule]){
// s.loadedLanguages[rule] = Object.assign(s.loadedLanguages[rule],fileData)
// }else{
// s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),fileData)
// }
})
})
break;
@ -345,6 +347,7 @@ module.exports = async (s,config,lang,app,io) => {
LibsCss: [],
AssetsJs: [],
AssetsCss: [],
superPageTabs: [],
superPageBlocks: [],
superLibsJs: [],
superRawJs: [],

View File

@ -251,7 +251,9 @@ module.exports = function(s,config,lang,app,io){
var firstDroppedPart = pathPieces[2]
var monitorEventDropDir = s.dir.dropInEvents + ke + '/' + mid + '/'
var deleteKey = monitorEventDropDir + firstDroppedPart
onFileOrFolderFound(monitorEventDropDir + firstDroppedPart,deleteKey,Object.assign({},s.group[ke].rawMonitorConfigurations[mid]))
fs.mkdir(pathPieces.join('/'), { recursive: true }, (err) => {
onFileOrFolderFound(monitorEventDropDir + firstDroppedPart,deleteKey,Object.assign({},s.group[ke].rawMonitorConfigurations[mid]))
})
})
resolve({root: s.dir.dropInEvents + user.ke})
}else{

View File

@ -43,12 +43,15 @@ module.exports = function (s, config, lang) {
return new Cam(options, function (error) {
if (error) {
onvifEventLog(`ONVIF Event Error`,e)
onvifEventLog(`ONVIF Event Error`,error)
return
}
this.on('event', function (event) {
handleEvent(event, monitorConfig, onvifEventLog);
})
this.on('eventsError', function (e) {
onvifEventLog(`ONVIF Event Error`,e)
})
})
}

View File

@ -583,7 +583,7 @@ module.exports = (s,config,lang) => {
monitorDetails.detector_buffer_acodec !== 'no' &&
monitorDetails.detector_buffer_acodec !== 'auto'
){
outputMap += `-map 0:1 `
outputMap += `-map 0:1? `
}
const secondsBefore = parseInt(monitorDetails.detector_buffer_seconds_before) || 5
let LiveStartIndex = parseInt(secondsBefore / 2 + 1)
@ -611,6 +611,7 @@ module.exports = (s,config,lang) => {
const secondBefore = (parseInt(monitorDetails.detector_buffer_seconds_before) || 5) + 1
s.insertCompletedVideo(monitorConfig,{
file : filename,
objects: overlappingRecordings && d.details && d.details.matrices instanceof Array ? d.details.matrices : undefined,
endTime: moment(new Date()).subtract(secondBefore,'seconds')._d,
},function(err,response){
const autoCompressionEnabled = monitorDetails.auto_compress_videos === '1';

View File

@ -75,6 +75,7 @@ module.exports = function(s,config){
///////// SYSTEM ////////
createExtension(`onProcessReady`)
createExtension(`onProcessExit`)
createExtension(`onLoadedUsersAtStartup`)
createExtension(`onBeforeDatabaseLoad`)
createExtension(`onFFmpegLoaded`)
createExtension(`beforeMonitorsLoadedOnStartup`)

View File

@ -1,5 +1,6 @@
var fs = require('fs')
module.exports = function(s,config){
s.languageModifications = {};
if(!config.language){
config.language='en_CA'
}
@ -8,6 +9,11 @@ module.exports = function(s,config){
let gotLang = {}
try{
eval(`gotLang = ${fs.readFileSync(s.location.languages+'/'+choice+'.json','utf8')}`)
if(s.languageModifications[choice]){
for(mods of s.languageModifications[choice]){
Object.assign(gotLang, mods)
}
}
}catch(er){
console.error(er)
console.log('There was an error loading your language file.')
@ -34,6 +40,11 @@ module.exports = function(s,config){
s.getLanguageFile = function(rule){
if(rule && rule !== ''){
var file = s.loadedLanguages[file]
// if(s.languageModifications[file]){
// for(mods of s.languageModifications[file]){
// Object.assign(file, mods)
// }
// }
s.debugLog(file)
if(!file){
try{

View File

@ -497,7 +497,8 @@ module.exports = function(s,config,lang){
s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke)
}
}
if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){
const onvifDevice = s.group[e.ke].activeMonitors[e.mid].onvifConnection || (await s.createOnvifDevice({ id: e.mid, ke: e.ke })).device;
if(onvifDevice){
try{
const screenShot = await s.getSnapshotFromOnvif({
ke: e.ke,
@ -733,6 +734,13 @@ module.exports = function(s,config,lang){
break;
case'start':case'record':
await monitorStart(e)
s.tx({
f: 'monitor_watch_on',
id: monitorId,
ke: groupKey,
subStreamChannel: s.group[groupKey].activeMonitors[monitorId].subStreamChannel,
warnings: s.group[groupKey].activeMonitors[monitorId].warnings || []
}, `MON_${groupKey}${monitorId}`)
break;
default:
console.log('No s.camera execute : ',selectedMode)

View File

@ -220,7 +220,7 @@ module.exports = (s,config,lang) => {
}
const temporaryImageFile = streamDir + s.gid(5) + '.jpg'
const ffmpegCmd = splitForFFMPEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f mjpeg -an -frames:v 1 "${temporaryImageFile}"`)
const snapProcess = spawn('ffmpeg',ffmpegCmd,{detached: true})
const snapProcess = spawn(config.ffmpegDir, ffmpegCmd, {detached: true})
snapProcess.stderr.on('data',function(data){
// s.debugLog(data.toString())
})
@ -447,8 +447,22 @@ module.exports = (s,config,lang) => {
ffmpegProcess.stdio[pipeNumber].pipe(activeMonitor.mp4frag[pipeNumber],{ end: false })
break;
case'mjpeg':
frameToStreamAdded = function (d) {
activeMonitor.emitterChannel[pipeNumber].emit('data', d)
}
break;
case'b64':
var buffer
frameToStreamAdded = function(d){
activeMonitor.emitterChannel[pipeNumber].emit('data',d)
if(!buffer){
buffer=[d]
}else{
buffer.push(d)
}
if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){
activeMonitor.emitterChannel[pipeNumber].emit('data',Buffer.concat(buffer))
buffer = null
}
}
break;
case'flv':
@ -1034,19 +1048,47 @@ module.exports = (s,config,lang) => {
}
}, 1000 * creationInterval * 2);
}
function setUpChosenDetector(e){
const groupKey = e.ke
const monitorId = e.mid || e.id
const activeMonitor = getActiveMonitor(groupKey,monitorId);
const monitorConfig = getMonitorConfiguration(groupKey,monitorId);
const monitorDetails = monitorConfig.details;
let chosenDetector = monitorDetails.detectors_selected;
if(chosenDetector instanceof Array)chosenDetector = chosenDetector.join(',');
let sendToDetector = (data) => {
s.ocvTx({
f : 'frame',
mon : monitorDetails,
ke : groupKey,
id : monitorId,
time : s.formattedTime(),
frame : data
})
}
if(chosenDetector && !(chosenDetector.includes('all'))){
const pluginsGettingIt = chosenDetector.split(',').map(item => item.trim()).filter(item => !!item);
sendToDetector = (data) => {
for(pluginName of pluginsGettingIt){
s.sendToDetector(pluginName, {
f : 'frame',
mon : monitorDetails,
ke : groupKey,
id : monitorId,
time : s.formattedTime(),
frame : data
})
}
}
}
activeMonitor.forDetectorJpegOutputAlone = sendToDetector;
}
function onDetectorJpegOutputAlone(e,d){
if(s.isAtleatOneDetectorPluginConnected){
const groupKey = e.ke
const monitorId = e.mid || e.id
const monitorConfig = getMonitorConfiguration(groupKey,monitorId);
s.ocvTx({
f: 'frame',
mon: monitorConfig.details,
ke: groupKey,
id: monitorId,
time: s.formattedTime(),
frame: d
})
const activeMonitor = getActiveMonitor(groupKey,monitorId);
activeMonitor.forDetectorJpegOutputAlone(d)
}
}
function onDetectorJpegOutputSecondary(e,buffer){
@ -1213,6 +1255,7 @@ module.exports = (s,config,lang) => {
onDetectorJpegOutputSecondary(e,data)
})
}else{
setUpChosenDetector(e)
activeMonitor.spawn.stdio[4].on('data',function(data){
onDetectorJpegOutputAlone(e,data)
})
@ -1222,6 +1265,7 @@ module.exports = (s,config,lang) => {
onDetectorJpegOutputSecondary(e,data)
})
}else{
setUpChosenDetector(e)
activeMonitor.spawn.stdio[4].on('data',function(data){
onDetectorJpegOutputAlone(e,data)
})

133
libs/mountManager.js Normal file
View File

@ -0,0 +1,133 @@
const path = require('path')
module.exports = (s,config,lang,app,io) => {
// for unix-based systems only (has /etc/fstab)
if(s.isWin){
app.all([
'list',
'mount',
'removeMount',
'setVideosDir',
].map(item => `${config.webPaths.superApiPrefix}:auth/mountManager/${item}`), function (req,res){
s.closeJsonResponse(res, {
ok: false,
msg: lang.windowsCantUseFeature,
error: lang.windowsCantUseFeature
});
});
return;
}
const {
modifyConfiguration,
} = require('./system/utils.js')(config)
const {
mount,
update,
remove,
list,
remountAll,
remount,
unmount,
createMountPoint,
checkDiskPathExists,
} = require('node-fstab');
/**
* API : Remount All in fstab
*/
app.get(config.webPaths.superApiPrefix+':auth/mountManager/list', function (req,res){
s.superAuth(req.params, async (resp) => {
const response = await list();
s.closeJsonResponse(res, response);
},res,req);
});
/**
* API : Add Mount to fstab
*/
app.post(config.webPaths.superApiPrefix+':auth/mountManager/mount', function (req,res){
s.superAuth(req.params, async (resp) => {
const { sourceTarget, localPath, mountType, options } = req.body;
const response = { ok: false }
if(sourceTarget && localPath){
try{
const createDirResponse = await createMountPoint(localPath)
response.createDirResponse = createDirResponse
}catch(err){
console.error(err)
}
try{
const { exists } = await checkDiskPathExists(localPath)
if(exists){
const unmountResponse = await unmount(localPath)
response.unmountResponse = unmountResponse
}
}catch(err){
console.error(err)
}
const updateResponse = await update(sourceTarget, localPath, mountType, options);
response.updateResponse = updateResponse
response.ok = updateResponse.ok
const remountResponse = await remount(localPath)
response.remountResponse = remountResponse
if(!remountResponse.ok){
await remove(localPath);
response.ok = false;
response.error = remountResponse.error;
}
response.mount = {
device: sourceTarget,
mountPoint: localPath,
type: mountType,
options,
}
}else{
response.error = lang['Invalid Data']
}
s.closeJsonResponse(res, response);
},res,req);
});
/**
* API : Remove Mount to fstab
*/
app.post(config.webPaths.superApiPrefix+':auth/mountManager/removeMount', function (req,res){
s.superAuth(req.params, async (resp) => {
const { localPath } = req.body;
try{
await unmount(localPath)
if(config.videosDir.startsWith(localPath)){
const configError = await modifyConfiguration({
videosDir: '__DIR__/videos',
}, true);
}
}catch(err){
console.error(err)
}
const response = await remove(localPath);
s.closeJsonResponse(res, response);
},res,req);
});
/**
* API : Set Mount Point as Videos Directory (videosDir)
*/
app.post(config.webPaths.superApiPrefix+':auth/mountManager/setVideosDir', function (req,res){
s.superAuth(req.params, async (resp) => {
const { localPath, pathInside } = req.body;
const isDefaultDir = localPath === '__DIR__/videos';
const response = { ok: false }
try{
const { exists } = isDefaultDir ? { exists: true } : await checkDiskPathExists(localPath)
if(exists){
const newVideosDirPath = pathInside ? path.join(localPath, pathInside) : localPath;
const createDirResponse = isDefaultDir ? true : await createMountPoint(newVideosDirPath)
const configError = await modifyConfiguration({
videosDir: newVideosDirPath,
}, true);
response.ok = true;
response.configError = configError;
response.createDirResponse = createDirResponse;
}
}catch(err){
console.error(err)
}
s.closeJsonResponse(res, response);
},res,req);
});
}

View File

@ -383,7 +383,7 @@ module.exports = function(s,config,lang,app,io){
/**
* API : Get List of Connected Plugins
*/
app.get(config.webPaths.apiPrefix+':auth/plugins/list', async (req,res) => {
app.get(config.webPaths.apiPrefix+':auth/plugins/:ke/list', async (req,res) => {
s.auth(req.params, async (resp) => {
s.closeJsonResponse(res,{
ok: true,

View File

@ -336,6 +336,7 @@ module.exports = function(s,config,lang,io){
tx({f:'users_online',users:s.group[d.ke].users})
s.tx({f:'user_status_change',ke:d.ke,uid:cn.uid,status:1,user:s.group[d.ke].users[d.auth]},'GRP_'+d.ke)
s.sendDiskUsedAmountToClients(d.ke)
s.sendCloudDiskUsedAmountToClients(d.ke)
tx({
f:'init_success',
users:s.group[d.ke].vid,
@ -728,81 +729,77 @@ module.exports = function(s,config,lang,io){
// super page socket functions
cn.on('super',function(d){
if(!cn.init&&d.f=='init'){
d.ok=s.superAuth({mail:d.mail,pass:d.pass},function(data){
cn.mail=d.mail
d.ok=s.superAuth(d.auth ? { auth: d.auth } : {mail:d.mail,pass:d.pass},function(data){
cn.mail = data.$user.mail
cn.join('$');
var tempSessionKey = s.gid(30)
var tempSessionKey = d.auth || s.gid(30)
cn.superSessionKey = tempSessionKey
s.superUsersApi[tempSessionKey] = data
s.superUsersApi[tempSessionKey].cnid = cn.id
if(!data.$user.tokens)data.$user.tokens = {}
data.$user.tokens[tempSessionKey] = {}
cn.ip=cn.request.connection.remoteAddress
s.userLog({ke:'$',mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}})
cn.init='super';
s.tx({f:'init_success',mail:d.mail,superSessionKey:tempSessionKey},cn.id);
s.tx({f:'init_success',mail:cn.mail,superSessionKey:tempSessionKey},cn.id);
})
if(d.ok===false){
cn.disconnect();
}
}else{
if(cn.mail&&cn.init=='super'){
switch(d.f){
case'logs':
switch(d.ff){
case'delete':
s.knexQuery({
action: "delete",
table: "Logs",
where: {
ke: d.ke,
}
})
break;
}
break;
case'system':
switch(d.ff){
case'update':
}else if(cn.mail&&cn.init=='super'){
switch(d.f){
case'logs':
switch(d.ff){
case'delete':
s.knexQuery({
action: "delete",
table: "Logs",
where: {
ke: d.ke,
}
})
break;
}
break;
case'system':
switch(d.ff){
case'update':
s.ffmpegKill()
s.systemLog('Shinobi ordered to update',{
by:cn.mail,
ip:cn.ip
})
var updateProcess = spawn('sh',(s.mainDirectory+'/UPDATE.sh').split(' '),{detached: true})
updateProcess.stderr.on('data',function(data){
s.systemLog('Update Info',data.toString())
})
updateProcess.stdout.on('data',function(data){
s.systemLog('Update Info',data.toString())
})
break;
case'restart':
//config.webPaths.superApiPrefix+':auth/restart/:script'
d.check=function(x){return d.target.indexOf(x)>-1}
if(d.check('system')){
s.systemLog('Shinobi ordered to restart',{by:cn.mail,ip:cn.ip})
s.ffmpegKill()
s.systemLog('Shinobi ordered to update',{
by:cn.mail,
ip:cn.ip
})
var updateProcess = spawn('sh',(s.mainDirectory+'/UPDATE.sh').split(' '),{detached: true})
updateProcess.stderr.on('data',function(data){
s.systemLog('Update Info',data.toString())
})
updateProcess.stdout.on('data',function(data){
s.systemLog('Update Info',data.toString())
})
break;
case'restart':
//config.webPaths.superApiPrefix+':auth/restart/:script'
d.check=function(x){return d.target.indexOf(x)>-1}
if(d.check('system')){
s.systemLog('Shinobi ordered to restart',{by:cn.mail,ip:cn.ip})
s.ffmpegKill()
exec('pm2 restart '+s.mainDirectory+'/camera.js')
}
if(d.check('cron')){
s.systemLog('Shinobi CRON ordered to restart',{by:cn.mail,ip:cn.ip})
exec('pm2 restart '+s.mainDirectory+'/cron.js')
}
if(d.check('logs')){
s.systemLog('Flush PM2 Logs',{by:cn.mail,ip:cn.ip})
exec('pm2 flush')
}
break;
case'configure':
s.systemLog('conf.json Modified',{by:cn.mail,ip:cn.ip,old:jsonfile.readFileSync(s.location.config)})
jsonfile.writeFile(s.location.config,d.data,{spaces: 2},function(){
s.tx({f:'save_configuration'},cn.id)
})
break;
}
break;
}
exec('pm2 restart '+s.mainDirectory+'/camera.js')
}
if(d.check('cron')){
s.systemLog('Shinobi CRON ordered to restart',{by:cn.mail,ip:cn.ip})
exec('pm2 restart '+s.mainDirectory+'/cron.js')
}
if(d.check('logs')){
s.systemLog('Flush PM2 Logs',{by:cn.mail,ip:cn.ip})
exec('pm2 flush')
}
break;
case'configure':
s.systemLog('conf.json Modified',{by:cn.mail,ip:cn.ip,old:jsonfile.readFileSync(s.location.config)})
jsonfile.writeFile(s.location.config,d.data,{spaces: 2},function(){
s.tx({f:'save_configuration'},cn.id)
})
break;
}
break;
}
}
})

View File

@ -3,6 +3,7 @@ module.exports = (s,config,databaseOptions) => {
var databaseOptions = {
client: config.databaseType,
connection: config.db,
pool: { min: 0, max: 10, propagateCreateError: true }
}
if(databaseOptions.client.indexOf('sqlite')>-1){
databaseOptions.client = 'sqlite3';

View File

@ -9,7 +9,8 @@ module.exports = function(s,config,lang,io){
scanForOrphanedVideos
} = require('./video/utils.js')(s,config,lang)
const {
checkSubscription
checkSubscription,
checkAgainSubscription,
} = require('./basic/utils.js')(process.cwd(),config)
const {
checkForStaticUsers
@ -60,7 +61,7 @@ module.exports = function(s,config,lang,io){
table: "Monitors",
},function(err,monitors) {
foundMonitors = monitors
if(err){s.systemLog(err)}
if(err){s.systemLog('Startup Error', err.toString())}
if(monitors && monitors[0]){
var didNotLoad = 0
var loadCompleted = 0
@ -89,7 +90,7 @@ module.exports = function(s,config,lang,io){
});
const monObj = Object.assign({},monitor,{id : monitor.mid})
await s.camera('stop',monObj);
await s.camera(monitor.mode,monObj);
if(!config.safeMode)await s.camera(monitor.mode,monObj);
checkAnother()
},1000)
}else{
@ -120,11 +121,12 @@ module.exports = function(s,config,lang,io){
if(orphanedFilesCount){
orphanedVideosForMonitors[monitor.ke][monitor.mid] += orphanedFilesCount
}
if(orphanedVideosForMonitors[monitor.ke][monitor.mid] == 0)delete(orphanedVideosForMonitors[monitor.ke][monitor.mid]);
++loadCompleted
if(monitors[loadCompleted]){
await checkForOrphanedVideosForMonitor(monitors[loadCompleted])
}else{
s.systemLog(lang.startUpText6+' : '+s.s(orphanedVideosForMonitors))
s.systemLog(lang.startUpText6, s.s(orphanedVideosForMonitors))
delete(foundMonitors)
callback()
}
@ -135,7 +137,7 @@ module.exports = function(s,config,lang,io){
}
}
var loadDiskUseForUser = function(user,callback){
s.systemLog(user.mail+' : '+lang.startUpText0)
s.systemLog(lang.startUpText0, user.mail)
var userDetails = JSON.parse(user.details)
s.group[user.ke].sizeLimit = parseFloat(userDetails.size) || 10000
s.group[user.ke].sizeLimitVideoPercent = parseFloat(userDetails.size_video_percent) || 90
@ -248,7 +250,7 @@ module.exports = function(s,config,lang,io){
})
s.cloudDisksLoaded.forEach(function(storageType){
var firstCount = user.cloudDiskUse[storageType].firstCount
s.systemLog(user.mail+' : '+lang.startUpText1+' : '+firstCount,storageType,user.cloudDiskUse[storageType].usedSpace)
// s.systemLog(lang.startUpText1, user.mail+' : '+firstCount,storageType,user.cloudDiskUse[storageType].usedSpace)
delete(user.cloudDiskUse[storageType].firstCount)
})
}
@ -343,7 +345,7 @@ module.exports = function(s,config,lang,io){
storageIndex.usedSpaceVideos = usedSpaceVideos / 1048576
storageIndex.usedSpaceFilebin = usedSpaceFilebin / 1048576
storageIndex.usedSpaceTimelapseFrames = usedSpaceTimelapseFrames / 1048576
s.systemLog(user.mail+' : '+path+' : '+videos.length,storageIndex.usedSpace)
// s.systemLog(user.mail+' : '+path+' : '+videos.length,storageIndex.usedSpace)
++currentStorageNumber
readStorageArray()
}
@ -420,12 +422,13 @@ module.exports = function(s,config,lang,io){
setTimeout(async () => {
await checkForStaticUsers()
//check for subscription
checkSubscription(config.subscriptionId,function(hasSubcribed){
checkSubscription(config.subscriptionId || config.peerConnectKey || config.p2pApiKey, function(hasSubcribed){
config.userHasSubscribed = hasSubcribed
//check terminal commander
checkForTerminalCommands(function(){
//load administrators (groups)
loadAdminUsers(function(){
s.runExtensionsForArray('onLoadedUsersAtStartup', null, [])
//load monitors (for groups)
loadMonitors(function(){
//check for orphaned videos
@ -436,7 +439,8 @@ module.exports = function(s,config,lang,io){
})
})
})
},1500)
},1500);
s.subscriptionIntervalCheck = checkAgainSubscription();
})
}
})

View File

@ -1,5 +1,8 @@
const fs = require('fs');
const spawn = require('child_process').spawn;
const {
mergeDeep,
} = require('../common.js')
module.exports = (config) => {
var currentlyUpdating = false
return {
@ -11,13 +14,15 @@ module.exports = (config) => {
"Shinobi": s.currentVersion,
"Node.js": process.version,
"FFmpeg": s.ffmpegVersion,
"isActivated": config.userHasSubscribed
"isActivated": config.userHasSubscribed,
"previousShinobi": s.versionsUsed,
},
Machine: {
"CPU Core Count": s.coreCount,
"Total RAM": s.totalmem,
"Operating System Platform": s.platform,
},
}
if(s.expiryDate)response.Versions["License Expires On"] = s.expiryDate
return response
@ -31,13 +36,20 @@ module.exports = (config) => {
});
});
},
modifyConfiguration: (postBody) => {
modifyConfiguration: (postBody, useBase) => {
return new Promise((resolve, reject) => {
console.log(config)
const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config;
const configData = JSON.stringify(postBody,null,3);
let configToPost = postBody;
if(useBase){
try{
const configBase = s.parseJSON(fs.readFileSync(configPath),{});
configToPost = mergeDeep(configBase, postBody)
}catch(err){
console.error('modifyConfiguration : Failed to use Config base!')
}
}
const configData = JSON.stringify(configToPost, null, 3);
fs.writeFile(configPath, configData, resolve);
});
},

View File

@ -8,6 +8,8 @@ module.exports = function(s,config,lang,app,io){
backblazeB2: require('./uploaders/backblazeB2.js'),
amazonS3: require('./uploaders/amazonS3.js'),
webdav: require('./uploaders/webdav.js'),
//local storage
mnt: require('./uploaders/mount.js'),
//oauth
googleDrive: require('./uploaders/googleDrive.js'),
//simple storage
@ -15,7 +17,9 @@ module.exports = function(s,config,lang,app,io){
}
Object.keys(loadedLibraries).forEach((key) => {
var loadedLib = loadedLibraries[key](s,config,lang,app,io)
loadedLib.isFormGroupGroup = true
s.definitions["Account Settings"].blocks["Uploaders"].info.push(loadedLib)
if(loadedLib.info){
loadedLib.isFormGroupGroup = true
s.definitions["Account Settings"].blocks["Uploaders"].info.push(loadedLib)
}
})
}

View File

@ -253,6 +253,7 @@ module.exports = function(s,config,lang){
"evaluation": "details.use_aws_s3 !== '0'",
"name": lang["Amazon S3"],
"color": "forestgreen",
"uploaderId": 's3',
"info": [
{
"name": "detail=aws_s3_save",

View File

@ -206,6 +206,7 @@ module.exports = function(s,config,lang){
"evaluation": "details.use_bb_b2 !== '0'",
"name": lang["Backblaze B2"],
"color": "forestgreen",
"uploaderId": 'b2',
"info": [
{
"name": "detail=bb_b2_save",

View File

@ -303,6 +303,7 @@ module.exports = (s,config,lang,app,io) => {
"evaluation": "details.use_googd !== '0'",
"name": lang["Google Drive"],
"color": "forestgreen",
"uploaderId": 'googd',
"info": [
{
"name": "detail=googd_save",

327
libs/uploaders/mount.js Normal file
View File

@ -0,0 +1,327 @@
const fs = require('fs').promises;
const { createReadStream } = require('fs');
const path = require('path');
const {
writeReadStream,
checkDiskPathExists,
} = require('node-fstab');
module.exports = function(s,config,lang){
if(s.isWin){
return {}
}
function constructFilePath(groupKey, filePath){
return path.join(s.group[groupKey].init.mnt_path, filePath)
}
const deleteObject = async (groupKey, filePath) => {
const fullPath = constructFilePath(groupKey, filePath)
const response = { ok: true }
try{
await fs.rm(fullPath)
}catch(err){
response.ok = false;
response.err = err.toString();
}
return response
};
const uploadObject = async (groupKey, { filePath, readStream }) => {
const fullPath = constructFilePath(groupKey, filePath)
return await writeReadStream(readStream, fullPath);
};
const getObject = async (groupKey, filePath) => {
const fullPath = constructFilePath(groupKey, filePath)
return createReadStream(fullPath)
};
function beforeAccountSave(d){
//d = save event
d.formDetails.mnt_use_global = d.d.mnt_use_global
d.formDetails.use_mnt = d.d.use_mnt
}
function cloudDiskUseStartup(group,userDetails){
group.cloudDiskUse['mnt'].name = 'Mounted Drive'
group.cloudDiskUse['mnt'].sizeLimitCheck = (userDetails.use_mnt_size_limit === '1')
if(!userDetails.mnt_size_limit || userDetails.mnt_size_limit === ''){
group.cloudDiskUse['mnt'].sizeLimit = 10000
}else{
group.cloudDiskUse['mnt'].sizeLimit = parseFloat(userDetails.mnt_size_limit)
}
}
function loadGroupApp(e){
// e = user
var userDetails = JSON.parse(e.details)
if(userDetails.mnt_use_global === '1' && config.cloudUploaders && config.cloudUploaders.WasabiHotCloudStorage){
userDetails = Object.assign(userDetails,config.cloudUploaders.mountedDrive)
}
//Mounted Drive Storage
if(
!s.group[e.ke].mnt &&
userDetails.mnt !== '0' &&
userDetails.mnt_path
){
checkDiskPathExists(userDetails.mnt_path).then((response) => {
if(response.exists){
s.group[e.ke].mnt = userDetails.mnt_path;
}
})
}
}
function unloadGroupApp(user){
s.group[user.ke].mnt = null
}
function deleteVideo(e,video,callback){
// e = user
try{
var videoDetails = JSON.parse(video.details)
}catch(err){
var videoDetails = video.details
}
if(video.type !== 'mnt'){
callback()
return
}
deleteObject(video.ke, videoDetails.location).then((response) => {
if (response.err){
console.error('Mounted Drive Storage DELETE Error')
console.error(err);
}
callback()
});
}
function onMonitorStart(monitorConfig){
const groupKey = monitorConfig.ke;
const monitorId = monitorConfig.mid;
if(s.group[groupKey].mnt){
const saveLocation = constructFilePath(groupKey, s.group[groupKey].init.mnt_dir + groupKey + '/' + monitorId);
fs.mkdir(saveLocation, { recursive: true }).catch((err) => {
console.error('Making Directory fail', err)
});
}
}
function uploadVideo(e,k,insertQuery){
//e = video object
//k = temporary values
if(!k)k={};
//cloud saver - Mounted Drive
const groupKey = insertQuery.ke
if(s.group[groupKey].mnt && s.group[groupKey].init.use_mnt !== '0' && s.group[groupKey].init.mnt_save === '1'){
const filename = `${s.formattedTime(insertQuery.time)}.${insertQuery.ext}`
var fileStream = createReadStream(k.dir + filename);
var saveLocation = s.group[groupKey].init.mnt_dir+groupKey+'/'+e.mid+'/'+filename
uploadObject(groupKey, {
filePath: saveLocation,
readStream: fileStream,
}).then((response) => {
if(response.err){
s.userLog(e,{type:lang['Mounted Drive Storage Upload Error'],msg:response.err})
}
if(s.group[groupKey].init.mnt_log === '1' && response.ok){
s.knexQuery({
action: "insert",
table: "Cloud Videos",
insert: {
mid: e.mid,
ke: groupKey,
ext: insertQuery.ext,
time: insertQuery.time,
status: 1,
type : 'mnt',
details: s.s({
location : saveLocation
}),
size: k.filesize,
end: k.endTime,
href: ''
}
})
s.setCloudDiskUsedForGroup(groupKey,{
amount: k.filesizeMB,
storageType: 'mnt'
})
s.purgeCloudDiskForGroup(e,'mnt')
}
});
}
}
function onInsertTimelapseFrame(monitorObject,queryInfo,filePath){
var e = monitorObject
if(s.group[e.ke].mnt && s.group[e.ke].init.use_mnt !== '0' && s.group[e.ke].init.mnt_save === '1'){
var fileStream = createReadStream(filePath)
fileStream.on('error', function (err) {
console.error(err)
})
var saveLocation = s.group[e.ke].init.mnt_dir + e.ke + '/' + e.mid + '_timelapse/' + queryInfo.filename
uploadObject(e.ke, {
filePath: saveLocation,
readStream: fileStream,
}).then((response) => {
if(response.err){
s.userLog(e,{type:lang['Wasabi Hot Cloud Storage Upload Error'],msg:response.err})
}
if(s.group[e.ke].init.mnt_log === '1' && response.ok){
s.knexQuery({
action: "insert",
table: "Cloud Timelapse Frames",
insert: {
mid: queryInfo.mid,
ke: queryInfo.ke,
time: queryInfo.time,
filename: queryInfo.filename,
type : 'mnt',
details: s.s({
location : saveLocation
}),
size: queryInfo.size,
href: ''
}
})
s.setCloudDiskUsedForGroup(e.ke,{
amount : s.kilobyteToMegabyte(queryInfo.size),
storageType : 'mnt'
},'timelapseFrames')
s.purgeCloudDiskForGroup(e,'mnt','timelapseFrames')
}
})
}
}
function onDeleteTimelapseFrameFromCloud(e,frame,callback){
// e = user
try{
var frameDetails = JSON.parse(frame.details)
}catch(err){
var frameDetails = frame.details
}
if(video.type !== 'mnt'){
callback()
return
}
if(!frameDetails.location){
frameDetails.location = frame.href.split(locationUrl)[1]
}
deleteObject(e.ke, frameDetails.location).then((response) => {
if (response.err){
console.error('Mounted Drive Storage DELETE Error')
console.error(err);
}
callback()
});
}
async function onGetVideoData(video){
const videoDetails = s.parseJSON(video.details)
const saveLocation = videoDetails.location
var fileStream = await getObject(video.ke, saveLocation);
return fileStream
}
//Mounted Drive Storage
s.addCloudUploader({
name: 'mnt',
loadGroupAppExtender: loadGroupApp,
unloadGroupAppExtender: unloadGroupApp,
insertCompletedVideoExtender: uploadVideo,
deleteVideoFromCloudExtensions: deleteVideo,
cloudDiskUseStartupExtensions: cloudDiskUseStartup,
beforeAccountSave: beforeAccountSave,
onAccountSave: cloudDiskUseStartup,
onInsertTimelapseFrame: onInsertTimelapseFrame,
onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud,
onGetVideoData,
});
s.onMonitorStart(onMonitorStart);
//return fields that will appear in settings
return {
"evaluation": "details.use_mnt !== '0'",
"name": lang["Mounted Drive Storage"],
"color": "forestgreen",
"uploaderId": 'mnt',
"info": [
{
"name": "detail=mnt_save",
"selector":"autosave_mnt",
"field": lang.Autosave,
"description": "",
"default": lang.No,
"example": "",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"hidden": true,
"field": lang['Mount Point'],
"name": "detail=mnt_path",
"placeholder": "/mnt/yourdrive",
"form-group-class": "autosave_mnt_input autosave_mnt_1",
},
{
"hidden": true,
"name": "detail=mnt_log",
"field": lang['Save Links to Database'],
"fieldType": "select",
"selector": "h_mntsld",
"form-group-class":"autosave_mnt_input autosave_mnt_1",
"description": "",
"default": "",
"example": "",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"hidden": true,
"name": "detail=use_mnt_size_limit",
"field": lang['Use Max Storage Amount'],
"fieldType": "select",
"selector": "h_mntzl",
"form-group-class":"autosave_mnt_input autosave_mnt_1",
"form-group-class-pre-layer":"h_mntsld_input h_mntsld_1",
"description": "",
"default": "",
"example": "",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"hidden": true,
"name": "detail=mnt_size_limit",
"field": lang['Max Storage Amount'],
"form-group-class":"autosave_mnt_input autosave_mnt_1",
"form-group-class-pre-layer":"h_mntsld_input h_mntsld_1",
"description": "",
"default": "10000",
"example": "",
"possible": ""
},
{
"hidden": true,
"name": "detail=mnt_dir",
"field": lang['Save Directory'],
"form-group-class":"autosave_mnt_input autosave_mnt_1",
"description": "",
"default": "/",
"example": "",
"possible": ""
},
]
}
}

View File

@ -255,6 +255,7 @@ module.exports = function(s,config,lang){
"evaluation": "details.use_whcs !== '0'",
"name": lang["S3-Based Network Storage"],
"color": "forestgreen",
"uploaderId": 'whcs',
"info": [
{
"name": "detail=whcs_save",

View File

@ -94,6 +94,8 @@ module.exports = function(s,config,lang){
"evaluation": "details.use_sftp !== '0'",
"name": lang['SFTP (SSH File Transfer)'],
"color": "forestgreen",
"uploaderId": 'sftp',
"simpleUploader": true,
"info": [
{
"name": "detail=sftp_save",

View File

@ -239,6 +239,7 @@ module.exports = async function(s,config,lang){
"evaluation": "details.use_webdav !== '0'",
"name": lang.WebDAV,
"color": "forestgreen",
"uploaderId": 'webdav',
"info": [
{
"name": "detail=webdav_save",

View File

@ -90,7 +90,16 @@ module.exports = function(s,config,lang){
usedSpaceFilebin: s.group[groupKey].usedSpaceFilebin,
usedSpaceTimelapseFrames: s.group[groupKey].usedSpaceTimelapseFrames,
limit: s.group[groupKey].sizeLimit,
addStorage: s.group[groupKey].addStorageUse
addStorage: s.group[groupKey].addStorageUse,
},'GRP_'+groupKey);
}
}
s.sendCloudDiskUsedAmountToClients = function(groupKey){
//send the amount used disk space to connected users
if(s.group[groupKey]&&s.group[groupKey].init){
s.tx({
f: 'cloudDiskUsed',
cloudDisks: s.group[groupKey].cloudDiskUse,
},'GRP_'+groupKey);
}
}
@ -151,6 +160,7 @@ module.exports = function(s,config,lang){
theGroup.usedSpace = theGroup.usedSpace || ((e.size || 0) / 1048576)
//emit the changes to connected users
s.sendDiskUsedAmountToClients(e.ke)
s.sendCloudDiskUsedAmountToClients(e.ke)
// create monitor management queue
theGroup.startMonitorInQueue = createQueueAwaited(0.5, 1)
}
@ -205,6 +215,7 @@ module.exports = function(s,config,lang){
cloudDisk.usedSpaceVideos += amount
break;
}
s.sendCloudDiskUsedAmountToClients(e.ke)
})
if(config.cron.deleteOverMax === true){
s.group[e.ke].diskUsedEmitter.on('purgeCloud',function(storageType,storagePoint){

View File

@ -539,7 +539,28 @@ module.exports = (s,config,lang) => {
})
})
}
async function getAdminUser(groupKey, uid){
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "ke,uid,details,mail",
table: "Users",
where: [
['uid','=', uid],
['ke','=', groupKey],
],
limit: 1,
});
try{
const user = rows[0];
user.details = JSON.parse(user.details)
return user
}catch(err){
s.systemLog(err)
return null;
}
}
return {
getAdminUser,
deleteSetOfVideos: deleteSetOfVideos,
deleteSetOfTimelapseFrames: deleteSetOfTimelapseFrames,
deleteSetOfFileBinFiles: deleteSetOfFileBinFiles,

View File

@ -1,5 +1,26 @@
var exec = require('child_process').exec
module.exports = function(s,config,lang,app,io){
function getLastUsedCommits(numberOfCommits = 3){
return new Promise((resolve) => {
exec(`git log -${numberOfCommits} --pretty=format:"%H"`, function(err, response){
try{
if(err){
console.error(err)
resolve([])
}else if(response){
const commitIds = response.toString().split('\n');
commitIds.shift();
resolve(commitIds)
}else{
resolve([])
}
}catch(err){
console.error(err)
resolve([])
}
})
})
}
var getRepositoryCommitId = function(callback){
exec(`git rev-parse HEAD`,function(err,response){
if(response){
@ -8,7 +29,7 @@ module.exports = function(s,config,lang,app,io){
if(data.indexOf('not a git repository') === -1){
s.currentVersion = data.trim()
isGitRespository = true
s.systemLog(`Current Version : ${s.currentVersion}`)
s.systemLog(`Current Version`, s.currentVersion)
}
}else if(err){
s.debugLog('Git is not installed.')
@ -18,5 +39,6 @@ module.exports = function(s,config,lang,app,io){
}
s.onProcessReady(async () => {
getRepositoryCommitId()
s.versionsUsed = await getLastUsedCommits(3);
})
}

View File

@ -54,11 +54,8 @@ module.exports = (s,config,lang) => {
})
})
}
const scanForOrphanedVideos = (monitor,options) => {
// const options = {
// checkMax: 2
// }
options = options ? options : {}
const scanForOrphanedVideos = (monitor, options) => {
options = options || {}
return new Promise((resolve,reject) => {
const response = {ok: false}
if(options.forceCheck === true || config.insertOrphans === true){
@ -70,30 +67,32 @@ module.exports = (s,config,lang) => {
let videosFound = 0;
const videosDirectory = s.getVideoDirectory(monitor)
const tempDirectory = s.getStreamsDirectory(monitor)
// const findCmd = [videosDirectory].concat(options.flags || ['-maxdepth','1'])
// Write the `sh` script
try{
fs.writeFileSync(
tempDirectory + 'orphanCheck.sh',
`find "${s.checkCorrectPathEnding(videosDirectory,true)}" -maxdepth 1 -type f -exec stat -c "%n" {} + | sort -r | head -n ${options.checkMax}`
);
}catch(err){
} catch(err) {
console.log('Failed scanForOrphanedVideos', monitor.ke, monitor.mid)
response.err = err.toString()
resolve(response)
return
return resolve(response)
}
let listing = spawn('sh',[tempDirectory + 'orphanCheck.sh'])
// const onData = options.onData ? options.onData : () => {}
const onError = options.onError ? options.onError : s.systemLog
const onExit = async () => {
try{
try {
listing.kill('SIGTERM')
await fs.promises.rm(tempDirectory + 'orphanCheck.sh')
}catch(err){
} catch(err) {
s.debugLog(err)
}
delete(listing)
}
const onFinish = () => {
if(!finished){
finished = true
@ -103,9 +102,9 @@ module.exports = (s,config,lang) => {
onExit()
}
}
const processLine = async (filePath) => {
let filename = filePath.split('/')
filename = `${filename[filename.length - 1]}`.trim()
let filename = filePath.split('/').pop().trim()
if(filename && filename.indexOf('-') > -1 && filename.indexOf('.') > -1){
const { status } = await checkIfVideoIsOrphaned(monitor,videosDirectory,filename)
if(status === 2){
@ -117,21 +116,45 @@ module.exports = (s,config,lang) => {
}
}
}
// ------------------------------------------------------------------------------
// Inactivity logic: if no data has arrived for 10 seconds, kill the process
// ------------------------------------------------------------------------------
let lastDataTimestamp = Date.now()
const INACTIVITY_TIMEOUT = 10000
const checkInactivity = () => {
if(finished) return // If we've already finished, do nothing
const now = Date.now()
if(now - lastDataTimestamp >= INACTIVITY_TIMEOUT){
// It's been more than 10 seconds since the last data event
onFinish()
} else {
// Check again in 1 second
setTimeout(checkInactivity, 1000)
}
}
// Start the inactivity checker
setTimeout(checkInactivity, 1000)
// ------------------------------------------------------------------------------
listing.stdout.on('data', async (d) => {
// Reset the inactivity timer
lastDataTimestamp = Date.now()
const filePathLines = d.toString().split('\n')
var i;
for (i = 0; i < filePathLines.length; i++) {
for (let i = 0; i < filePathLines.length; i++) {
await processLine(filePathLines[i])
}
})
listing.stderr.on('data', d=>onError(d.toString()))
listing.stderr.on('data', d => onError(d.toString()))
listing.on('close', (code) => {
// s.debugLog(`findOrphanedVideos ${monitor.ke} : ${monitor.mid} process exited with code ${code}`);
setTimeout(() => {
onFinish()
},1000)
});
}else{
} else {
// If we are not going to check for orphans, just resolve
resolve(response)
}
})
@ -244,13 +267,16 @@ module.exports = (s,config,lang) => {
startTime,
endTime,
searchQuery,
monitorRestrictions
monitorRestrictions,
andOnly
}){
const theSearches = searchQuery.split(',').map(query => ['objects','LIKE',`%${query.trim()}%`]);
const lastIndex = theSearches.length - 1;
theSearches.forEach(function(item, n){
if(n !== 0)theSearches[n] = ['or', ...item];
});
if(!andOnly){
theSearches.forEach(function(item, n){
if(n !== 0)theSearches[n] = ['or', ...item];
});
}
const initialEventQuery = [
['ke','=',groupKey],
];
@ -897,7 +923,7 @@ module.exports = (s,config,lang) => {
await fsP.writeFile(framePath, frameBuffer)
await s.createTimelapseFrameAndInsert(activeMonitor,location,frameFilename, frameTime._d)
}catch(err){
console.error(err)
s.debugLog(err)
}
}
// console.error('Completed Saving Frame from New Video!', framePath)

View File

@ -127,7 +127,11 @@ 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)].filter(item => !!item).join(',');
if(listOTags && !k.objects){
k.objects = [...new Set(listOTags)].filter(item => !!item).join(',');
}else if(k.objects[0] instanceof Object){
k.objects = k.objects.map(matrix => matrix.tag).join(',').split(',').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)
@ -152,6 +156,7 @@ module.exports = function(s,config,lang){
sendVideoToMasterNode(filePath,response)
}else{
var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename
const monitorEventsCounted = activeMonitor.detector_motion_count
s.txWithSubPermissions({
f: 'video_build_success',

View File

@ -163,9 +163,20 @@ module.exports = function(s,config,lang,app,io){
app.get(config.webPaths.apiPrefix+':auth/userInfo/:ke',function (req,res){
var response = {ok:false};
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,function(user){
s.auth(req.params, async function(user){
response.ok = true
response.user = user
const { rows } = await s.knexQueryPromise({
action: "select",
columns: "ke,uid,mail,details",
table: "Users",
where: [
['ke','=', req.params.ke],
['uid','=', user.uid],
]
});
const userInfo = rows[0];
userInfo.details = JSON.parse(userInfo.details)
response.user = userInfo;
res.end(s.prettyPrint(response));
},res,req);
})
@ -512,115 +523,6 @@ module.exports = function(s,config,lang,app,io){
res.end(s.prettyPrint({ok:true}))
})
})
// /**
// * Page : Montage - stand alone squished view with gridstackjs
// */
// app.get([
// config.webPaths.apiPrefix+':auth/grid/:ke',
// config.webPaths.apiPrefix+':auth/grid/:ke/:group',
// config.webPaths.apiPrefix+':auth/cycle/:ke',
// config.webPaths.apiPrefix+':auth/cycle/:ke/:group'
// ], function(req,res) {
// s.auth(req.params,function(user){
// if(user.permissions.get_monitors==="0"){
// res.end(user.lang['Not Permitted'])
// return
// }
//
// req.params.protocol=req.protocol;
// req.sql='SELECT * FROM Monitors WHERE mode!=? AND mode!=? AND ke=?';req.ar=['stop','idle',req.params.ke];
// if(!req.params.id){
// if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){
// try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){}
// req.or=[];
// user.details.monitors.forEach(function(v,n){
// req.or.push('mid=?');req.ar.push(v)
// })
// req.sql+=' AND ('+req.or.join(' OR ')+')'
// }
// }else{
// if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){
// req.sql+=' and mid=?';req.ar.push(req.params.id)
// }else{
// res.end(user.lang['There are no monitors that you can view with this account.']);
// return;
// }
// }
// s.sqlQuery(req.sql,req.ar,function(err,r){
// if(req.params.group){
// var filteredByGroupCheck = {};
// var filteredByGroup = [];
// r.forEach(function(v,n){
// var details = JSON.parse(r[n].details);
// try{
// req.params.group.split('|').forEach(function(group){
// var groups = JSON.parse(details.groups);
// if(groups.indexOf(group) > -1 && !filteredByGroupCheck[v.mid]){
// filteredByGroupCheck[v.mid] = true;
// filteredByGroup.push(v)
// }
// })
// }catch(err){
//
// }
// })
// r = filteredByGroup;
// }
// r.forEach(function(v,n){
// if(s.group[v.ke]&&s.group[v.ke].activeMonitors[v.mid]&&s.group[v.ke].activeMonitors[v.mid].watch){
// r[n].currentlyWatching=Object.keys(s.group[v.ke].activeMonitors[v.mid].watch).length
// }
// r[n].subStream={}
// var details = JSON.parse(r[n].details)
// if(details.snap==='1'){
// r[n].subStream.jpeg = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg'
// }
// if(details.stream_channels&&details.stream_channels!==''){
// try{
// details.stream_channels=JSON.parse(details.stream_channels)
// r[n].channels=[]
// details.stream_channels.forEach(function(b,m){
// var streamURL
// switch(b.stream_type){
// case'mjpeg':
// streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+'/'+m
// break;
// case'hls':
// streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+'/'+m+'/s.m3u8'
// break;
// case'h264':
// streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+'/'+m
// break;
// case'flv':
// streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+'/'+m+'/s.flv'
// break;
// case'mp4':
// streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+'/'+m+'/s.mp4'
// break;
// }
// r[n].channels.push(streamURL)
// })
// }catch(err){
// s.userLog(req.params,{type:'Broken Monitor Object',msg:'Stream Channels Field is damaged. Skipping.'})
// }
// }
// })
// var page = config.renderPaths.grid
// if(req.path.indexOf('/cycle/') > -1){
// page = config.renderPaths.cycle
// }
// s.renderPage(req,res,page,{
// data:Object.assign(req.params,req.query),
// baseUrl:req.protocol+'://'+req.hostname,
// config: s.getConfigWithBranding(req.hostname),
// lang:user.lang,
// $user:user,
// monitors:r,
// query:req.query
// });
// })
// },res,req)
// });
/**
* API : Get TV Channels (Monitor Streams) json
*/
@ -1128,6 +1030,7 @@ module.exports = function(s,config,lang,app,io){
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,function(user){
const searchQuery = s.getPostData(req,'search')
const andOnly = s.getPostData(req,'andOnly') == '1'
const startTime = s.getPostData(req,'start')
const endTime = s.getPostData(req,'end')
const monitorId = req.params.id
@ -1158,6 +1061,7 @@ module.exports = function(s,config,lang,app,io){
endTime,
searchQuery,
monitorRestrictions,
andOnly,
}).then((response) => {
if(response && response.rows){
s.buildVideoLinks(response.rows,{
@ -1528,8 +1432,8 @@ module.exports = function(s,config,lang,app,io){
/**
* API : Get Video File
*/
const videoRowCaches = {}
const videoRowCacheTimeouts = {}
const videoRowCaches = {}
const videoRowCacheTimeouts = {}
app.get(config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file', function (req,res){
s.auth(req.params,function(user){
const groupKey = req.params.ke
@ -2244,6 +2148,83 @@ module.exports = function(s,config,lang,app,io){
},res,req)
})
/**
* API : Get Available Languages
*/
app.get(config.webPaths.apiPrefix+':auth/languages/:ke',function (req,res){
s.auth(req.params,function(user){
var endData = {
ok: true,
list: s.listOfPossibleLanguages
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Get Storage Locations
*/
app.get(config.webPaths.apiPrefix+':auth/storageLocations/:ke',function (req,res){
s.auth(req.params,function(user){
const endData = {
ok: true,
list: s.listOfStorage
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Get Hardware Acceleration choices
*/
app.get(config.webPaths.apiPrefix+':auth/hardwareAccels/:ke',function (req,res){
s.auth(req.params,function(user){
const endData = {
ok: true,
list: s.listOfHwAccels
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Get Audio File choices
*/
app.get(config.webPaths.apiPrefix+':auth/addStorage/:ke',function (req,res){
s.auth(req.params,function(user){
const endData = {
ok: true,
list: s.dir.addStorage
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Get Uploader choices
*/
app.get(config.webPaths.apiPrefix+':auth/uploaderFields/:ke',function (req,res){
s.auth(req.params,function(user){
const fields = s.definitions["Account Settings"].blocks["Uploaders"];
const endData = {
ok: true,
fields
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Get Admin API Prefix
*/
app.get(config.webPaths.apiPrefix+':auth/getAdminApiPrefix/:ke',function (req,res){
s.auth(req.params,function(user){
const endData = {
ok: true,
adminApiPrefix: config.webPaths.adminApiPrefix
}
if(user.details.sub){
endData.ok = false;
endData.adminApiPrefix = null;
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* Robots.txt
*/
app.get('/robots.txt', function (req,res){

View File

@ -10,6 +10,7 @@ var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({})
var ejs = require('ejs');
module.exports = function(s,config,lang,app){
const { getAdminUser } = require('./user/utils.js')(s,config,lang);
function cantLiveStreamPermission(user,monitorId,permission){
const {
monitorPermissions,
@ -419,7 +420,7 @@ module.exports = function(s,config,lang,app){
* Page : Get WallView
*/
app.get(config.webPaths.apiPrefix+':auth/wallview/:ke', function (req,res){
s.auth(req.params,function(user){
s.auth(req.params, async function(user){
const authKey = req.params.auth
const groupKey = req.params.ke
if(
@ -429,6 +430,7 @@ module.exports = function(s,config,lang,app){
res.end(user.lang['Not Permitted'])
return
}
const $user = await getAdminUser(groupKey, user.uid);
s.renderPage(req,res,config.renderPaths.wallview,{
forceUrlPrefix: req.query.host || '',
data: req.params,
@ -437,7 +439,7 @@ module.exports = function(s,config,lang,app){
config: s.getConfigWithBranding(req.hostname),
define: s.getDefinitonFile(user.details ? user.details.lang : config.lang),
lang: lang,
$user: user,
$user,
authKey: authKey,
groupKey: groupKey,
originalURL: s.getOriginalUrl(req)

View File

@ -26,8 +26,8 @@ module.exports = function(s,config,lang,app){
startDate: req.query.start,
endDate: req.query.end,
startOperator: req.query.startOperator,
endOperator: req.query.endOperator,
limit: req.query.limit || 30,
endIsStartTo: true,
limit: req.query.limit,
},(response) => {
response.rows.forEach(function(v,n){
response.rows[n].info = JSON.parse(v.info)
@ -161,6 +161,7 @@ module.exports = function(s,config,lang,app){
const configError = await modifyConfiguration(Object.assign(currentConfig,{
subscriptionId: subscriptionId,
}))
config.subscriptionId = subscriptionId;
if(configError)s.systemLog(configError)
s.tx({f:'save_configuration'},'$')
}

6590
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,10 +42,11 @@
"mysql2": "^3.9.7",
"node-abort-controller": "^3.0.1",
"node-fetch": "^2.6.7",
"node-fstab": "^1.0.12",
"node-shinobi": "^1.0.4",
"node-ssh": "^12.0.4",
"nodemailer": "^6.7.1",
"onvif": "^0.7.1",
"onvif": "^0.8.0",
"pam-diff": "^1.1.0",
"path": "^0.12.7",
"pipe2pam": "^0.6.2",

View File

@ -17,11 +17,12 @@
#wallview-controls{
position: absolute;
top: 0;
right: 0;
right: 30px;
padding: 5px;
opacity: 0;
transition: 0.2s;
z-index: 11;
z-index: 12;
border-radius: 0 0 5px 5px;
}
#wallview-controls:hover{
@ -42,7 +43,7 @@
}
#wallview-monitorList .active i{
display: inlin-block;
display: inline-block;
}
#wallview-canvas .wallview-video {
@ -87,3 +88,22 @@
#wallview-canvas .wallview-video:hover .wallview-item-controls{
opacity: 0.5;
}
#wallview-monitorList {
z-index: 11;
width: 100%;
overflow: auto;
}
#wallview-monitorList-container {
position: absolute;
display: flex;
flex-flow: column;
z-index: 11;
width: 300px;
height: 70vh;
background: rgba(0,0,0,0.6);
right: 0;
bottom: 10vh;
border-radius: 10px 0 0 10px;;
}

View File

View File

@ -10,7 +10,7 @@ $(document).ready(function(){
var addStorageData = safeJsonParse($user.details.addStorage || '{}')
var html = ''
$.each(addStorage,function(n,storage){
var theStorage = addStorageData[storage.path]
var theStorage = addStorageData[storage.path] || {}
html += `
<div addStorageFields="${storage.path}">
<div class="form-group">

View File

@ -2,7 +2,6 @@ $(document).ready(function(){
onInitWebsocket(function(){
loadMonitorsIntoMemory(function(data){
setInterfaceCounts(data)
openTab('initial')
onDashboardReadyExecute()
})
});

View File

@ -1089,6 +1089,9 @@ function onDashboardReadyExecute(theAction){
function popImage(imageSrc){
$('body').append(`<div class="popped-image"><img src="${imageSrc}"></div>`)
}
function popImageClose(imageSrc){
$('.popped-image').remove()
}
function setSubmitButton(editorForm,text,icon,toggle){
var submitButtons = editorForm.find('[type="submit"]').prop('disabled',toggle)
submitButtons.html(`<i class="fa fa-${icon}"></i> ${text}`)

View File

@ -189,45 +189,43 @@ function initiateLiveGridPlayer(monitor){
})
break;
case'mp4':
setTimeout(function(){
var stream = containerElement.find('.stream-element');
var onPoseidonError = function(){
// setTimeout(function(){
// mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId})
// },2000)
var stream = containerElement.find('.stream-element');
var onPoseidonError = function(){
// setTimeout(function(){
// mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId})
// },2000)
}
if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0
if(loadedPlayer.PoseidonErrorCount >= 5)return
if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){
if(loadedPlayer.Poseidon){
loadedPlayer.Poseidon.stop()
revokeVideoPlayerUrl(monitorId)
}
if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0
if(loadedPlayer.PoseidonErrorCount >= 5)return
if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){
if(loadedPlayer.Poseidon){
loadedPlayer.Poseidon.stop()
revokeVideoPlayerUrl(monitorId)
}
try{
loadedPlayer.Poseidon = new Poseidon({
video: stream[0],
auth_token: $user.auth_token,
ke: monitor.ke,
uid: $user.uid,
id: monitor.mid,
url: location.origin,
path: websocketPath,
query: websocketQuery,
onError : onPoseidonError,
channel : subStreamChannel
})
loadedPlayer.Poseidon.start();
}catch(err){
// onPoseidonError()
console.log('onTryPoseidonError',err)
}
}else{
stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime())
stream[0].onerror = function(err){
console.error(err)
}
try{
loadedPlayer.Poseidon = new Poseidon({
video: stream[0],
auth_token: $user.auth_token,
ke: monitor.ke,
uid: $user.uid,
id: monitor.mid,
url: location.origin,
path: websocketPath,
query: websocketQuery,
onError : onPoseidonError,
channel : subStreamChannel
})
loadedPlayer.Poseidon.start();
}catch(err){
// onPoseidonError()
console.log('onTryPoseidonError',err)
}
},1000)
}else{
stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime())
stream[0].onerror = function(err){
console.error(err)
}
}
break;
case'flv':
if (flvjs.isSupported()) {
@ -345,6 +343,7 @@ function closeLiveGridPlayer(monitorId,killElement){
var livePlayerElement = loadedLiveGrids[monitorId]
if(livePlayerElement){
if(livePlayerElement.hls){livePlayerElement.hls.destroy()}
clearTimeout(livePlayerElement.m3uCheck)
if(livePlayerElement.Poseidon){livePlayerElement.Poseidon.stop()}
if(livePlayerElement.Base64){livePlayerElement.Base64.disconnect()}
if(livePlayerElement.dash){livePlayerElement.dash.reset()}
@ -452,7 +451,7 @@ function signalCheckLiveStream(options){
}
})
}
mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId});
requestMonitorInit()
}
function succeededStreamCheck(){
if(monitorConfig.signal_check_log == 1){
@ -513,7 +512,7 @@ function signalCheckLiveStream(options){
var errorStack = err.stack;
function phraseFoundInErrorStack(x){return errorStack.indexOf(x) > -1}
if(phraseFoundInErrorStack("The HTMLImageElement provided is in the 'broken' state.")){
mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId});
requestMonitorInit()
}
clearInterval(liveGridData.signal);
delete(liveGridData.signal);
@ -563,12 +562,7 @@ $(document).ready(function(e){
fullScreenLiveGridStream(monitorItem)
})
.on('click','.reconnect-live-grid-monitor',function(){
var monitorId = $(this).parents('[data-mid]').attr('data-mid')
mainSocket.f({
f: 'monitor',
ff: 'watch_on',
id: monitorId
})
requestMonitorInit()
})
.on('click','.toggle-live-grid-monitor-fullscreen',function(){
var monitorItem = $(this).parents('[data-mid]')
@ -580,11 +574,12 @@ $(document).ready(function(e){
// loadPreviouslyOpenedLiveGridBlocks()
break;
case'monitor_watch_off':case'monitor_stopping':
var monitorId = d.mid || d.id
closeLiveGridPlayer(monitorId,(d.f === 'monitor_watch_off'))
if(monitorId === d.id){
closeLiveGridPlayer(monitorId,(d.f === 'monitor_watch_off'))
}
break;
case'monitor_status':
if(monitorId === d.mid && d.code === 2 || d.code === 3){
if(monitorId === d.id && (d.code === 2 || d.code === 3)){
setTimeout(function(){
requestMonitorInit()
},2000)

View File

@ -81,3 +81,15 @@ function getQueryString(){
})
return theObject
}
function getListOfTagsFromMonitors(){
var listOftags = {}
$.each(loadedMonitors,function(monitorId,monitor){
if(monitor.tags){
monitor.tags.split(',').forEach((tag) => {
if(!listOftags[tag])listOftags[tag] = [];
listOftags[tag].push(monitorId)
})
}
})
return listOftags
}

View File

@ -6,7 +6,10 @@ async function addExtender(extenderContainer){
dashboardExtensions[extenderContainer].push(...extender)
};
}
async function executeExtender(extenderContainer, args){
async function addActionToExtender(extenderContainer, action){
return on[extenderContainer](action)
}
async function executeExtender(extenderContainer, args = []){
for(extender of dashboardExtensions[extenderContainer]){
await extender(...args)
}

View File

@ -0,0 +1,65 @@
function setGamepadMonitorSelection(monitorId) => {
dashboardOptions('gamepadMonitorSelection', monitorId);
selectedMonitor = `${monitorId}`;
}
function canGame() {
return "getGamepads" in navigator;
}
function convertStickAxisTo2048(value){
var newVal = parseInt((stickMax - stickBase) * value + stickBase)
return newVal
}
function getAnalogStickValues(gp, i, callback){
var label = i === 0 ? 'left' : 'right'
var horizontal = gp.axes[i] * outerDeadZone
var vertical = gp.axes[i + 1] * outerDeadZone
var newH = convertStickAxisTo2048(horizontal > deadZoneThreshold || horizontal < -deadZoneThreshold ? horizontal : 0)
var newV = convertStickAxisTo2048((vertical > deadZoneThreshold || vertical < -deadZoneThreshold ? vertical : 0) * -1)
if(
newH !== lastState.sticks[label].h ||
newV !== lastState.sticks[label].v
){
callback(label, newH, newV)
}
lastState.sticks[label].h = newH
lastState.sticks[label].v = newV
}
function getStickValue(gp, i, callback){
var label = `axis${axis}`;
var axis = gp.axes[i] * outerDeadZone
var newH = convertStickAxisTo2048(axis > deadZoneThreshold || axis < -deadZoneThreshold ? axis : 0)
if(newH !== lastState[label]){
callback(newH)
}
lastState[label] = newH
}
function getButtonsPressed(gp, callback, offCallback = () => {}){
$.each(keyLegend,function(code,key){
if(gp.buttons[code] && gp.buttons[code].pressed){
if(!lastState[key]){
buttonsPressed[code] = true;
callback(code)
}
lastState[key] = true
}else{
if(lastState[key]){
buttonsPressed[code] = false;
offCallback(code)
}
lastState[key] = false
}
})
}
function sendPtzCommand(direction, doMove){
runPtzMove(selectedMonitor, direction, doMove)
}
function sentPtzToHome(){
runPtzCommand(selectedMonitor, 'center')
}

View File

@ -0,0 +1,390 @@
$(document).ready(function() {
var selectedController = 0;
var keyLegend = {
"0": "b",
"1": "a",
"2": "y",
"3": "x",
"4": "l",
"5": "r",
"6": "zl",
"7": "zr",
"8": "minus",
"9": "plus",
"10": "l_stick",
"11": "r_stick",
"12": "up",
"13": "down",
"14": "left",
"15": "right",
}
var lastState = {
sticks: {
left: {},
right: {},
}
}
var lastPtzDirection = {}
var buttonsPressed = {}
var hasGP = false;
var repGP;
var reportInterval = 200
var stickBase = 2048
var stickMax = 4096
var deadZoneThreshold = 0.35
var outerDeadZone = 1.01
var selectedMonitor = dashboardOptions().gamepadMonitorSelection;
var monitorKeys = {};
var onMonitorOpenForGamepad = () => {}
var sequenceButtonPressList = []
var sequenceButtonPressTimeout = null
var buttonPressAction = null;
window.setGamepadMonitorSelection = (monitorId) => {
dashboardOptions('gamepadMonitorSelection', monitorId);
selectedMonitor = `${monitorId}`;
}
function canGame() {
return "getGamepads" in navigator;
}
function convertStickAxisTo2048(value){
var newVal = parseInt((stickMax - stickBase) * value + stickBase)
return newVal
}
function getAnalogStickValues(gp, i, callback){
var label = i === 0 ? 'left' : 'right'
var horizontal = gp.axes[i] * outerDeadZone
var vertical = gp.axes[i + 1] * outerDeadZone
var newH = convertStickAxisTo2048(horizontal > deadZoneThreshold || horizontal < -deadZoneThreshold ? horizontal : 0)
var newV = convertStickAxisTo2048((vertical > deadZoneThreshold || vertical < -deadZoneThreshold ? vertical : 0) * -1)
if(
newH !== lastState.sticks[label].h ||
newV !== lastState.sticks[label].v
){
callback(label, newH, newV)
}
lastState.sticks[label].h = newH
lastState.sticks[label].v = newV
}
function getStickValue(gp, i, callback){
var label = `axis${axis}`;
var axis = gp.axes[i] * outerDeadZone
var newH = convertStickAxisTo2048(axis > deadZoneThreshold || axis < -deadZoneThreshold ? axis : 0)
if(newH !== lastState[label]){
callback(newH)
}
lastState[label] = newH
}
function getButtonsPressed(gp, callback, offCallback = () => {}){
$.each(keyLegend,function(code,key){
if(gp.buttons[code] && gp.buttons[code].pressed){
if(!lastState[key]){
buttonsPressed[code] = true;
callback(code)
}
lastState[key] = true
}else{
if(lastState[key]){
buttonsPressed[code] = false;
offCallback(code)
}
lastState[key] = false
}
})
}
function setCameraFromButtonCode(buttonCode = 0, preAdded){
const addedOneToButtonCode = preAdded ? buttonCode : parseInt(buttonCode) + 1
try{
const monitor = loadedMonitors[monitorKeys[addedOneToButtonCode]];
const isFullscreened = !!document.fullscreenElement;
if(isFullscreened) {
document.exitFullscreen()
closeAllLiveGridPlayers(true)
}
openMonitorInLiveGrid(monitor.mid, function(){
if(isFullscreened) {
fullScreenLiveGridStreamById(monitor.mid)
}
})
}catch(err){
new PNotify({
title: lang['Invalid Action'],
text: `${lang.ptzControlIdNotFound}<br><br>${lang['Button Code']} : ${addedOneToButtonCode}`,
type: 'warning'
});
console.log('No Monitor Associated :', buttonCode)
}
}
// function setCameraFromButtonNumbers(){
// const buttons = Object.keys(buttonsPressed).filter(code => buttonsPressed[code]);
// console.log('pressed', buttons)
// if(buttons.length > 1){
//
// }else if(buttons.length > 0){
// const monitor = loadedMonitors[monitorKeys[buttons[0]]];
// console.log(monitorKeys[buttons[0]])
// openMonitorInLiveGrid(monitor.mid)
// }
// }
function openMonitorInLiveGrid(monitorId, callback){
lastPtzDirection = {};
setGamepadMonitorSelection(monitorId)
mainSocket.f({
f: 'monitor',
ff: 'watch_on',
id: monitorId
})
onMonitorOpenForGamepad = (monitorId) => {
setTimeout(() => {
if(monitorId === selectedMonitor){
onMonitorOpenForGamepad = () => {}
if(callback)callback()
}
}, 200)
}
}
function sendPtzCommand(direction, doMove){
runPtzMove(selectedMonitor, direction, doMove)
}
function sentPtzToHome(){
runPtzCommand(selectedMonitor, 'center')
}
function translatePointTiltStick(x, y){
if(x > stickBase && !lastPtzDirection['right']){
lastPtzDirection['right'] = true
lastPtzDirection['left'] = false
// sendPtzCommand('left', false)
sendPtzCommand('right', true)
}else if(x < stickBase && !lastPtzDirection['left']){
lastPtzDirection['left'] = true
lastPtzDirection['right'] = false
// sendPtzCommand('right', false)
sendPtzCommand('left', true)
}else if(x === stickBase){
if(lastPtzDirection['right'])sendPtzCommand('right', false)
if(lastPtzDirection['left'])sendPtzCommand('left', false)
lastPtzDirection['right'] = false
lastPtzDirection['left'] = false
}
if(y > stickBase && !lastPtzDirection['up']){
lastPtzDirection['up'] = true
lastPtzDirection['down'] = false
// sendPtzCommand('down', false)
sendPtzCommand('up', true)
}else if(y < stickBase && !lastPtzDirection['down']){
lastPtzDirection['down'] = true
lastPtzDirection['up'] = false
// sendPtzCommand('up', false)
sendPtzCommand('down', true)
}else if(y === stickBase){
if(lastPtzDirection['up'])sendPtzCommand('up', false)
if(lastPtzDirection['down'])sendPtzCommand('down', false)
lastPtzDirection['down'] = false
lastPtzDirection['up'] = false
}
console.log(lastPtzDirection)
}
function translateZoomAxis(value){
if(value > stickBase && !lastPtzDirection['zoom_in']){
lastPtzDirection['zoom_in'] = true
lastPtzDirection['zoom_out'] = false
// sendPtzCommand('zoom_out', false)
sendPtzCommand('zoom_in', true)
}else if(value < stickBase && !lastPtzDirection['zoom_out']){
lastPtzDirection['zoom_out'] = true
lastPtzDirection['zoom_in'] = false
// sendPtzCommand('zoom_in', false)
sendPtzCommand('zoom_out', true)
}else if(value === stickBase){
if(lastPtzDirection['zoom_in'])sendPtzCommand('zoom_in', false)
if(lastPtzDirection['zoom_out'])sendPtzCommand('zoom_out', false)
lastPtzDirection['zoom_in'] = false
lastPtzDirection['zoom_out'] = false
}
}
function reportOnXboxGamepad() {
try{
var gp = navigator.getGamepads()[0];
getButtonsPressed(gp, function(buttonCode){
if(buttonCode == 6){
sendPtzCommand('zoom_out', true)
}else if(buttonCode == 7){
sendPtzCommand('zoom_in', true)
}else if(buttonCode == 8){
if($('.popped-image').length > 0){
closeSnapshot()
}else{
openSnapshot()
}
}else if(buttonCode == 9){
sentPtzToHome()
}else if(buttonCode == 11){
if (!document.fullscreenElement) {
fullScreenLiveGridStreamById(selectedMonitor)
}else{
document.exitFullscreen()
}
}else{
buttonPressAction(buttonCode)
}
}, function(buttonCode){
if(buttonCode == 6){
sendPtzCommand('zoom_out', false)
}else if(buttonCode == 7){
sendPtzCommand('zoom_in', false)
}
})
getAnalogStickValues(gp, 0, function(stick, x, y){
translatePointTiltStick(x, y)
})
getAnalogStickValues(gp, 2, function(stick, x, y){
translateZoomAxis(y)
})
}catch(err){
console.log(err)
// stopReporting()
}
}
function reportOnGenericGamepad() {
try{
const gp = navigator.getGamepads()[0];
getButtonsPressed(gp, function(buttonCode){
if(buttonCode == 10){
closeSnapshot()
openSnapshot()
}else if(buttonCode == 11){
closeSnapshot()
}else{
buttonPressAction(buttonCode)
}
},function(buttonCode){
})
getAnalogStickValues(gp, 0, function(stick, x, y){
translatePointTiltStick(x, y)
})
getStickValue(gp, 2,function(value){
translateZoomAxis(value)
})
}catch(err){
console.log(err)
// stopReporting()
}
}
function openSnapshot(){
getSnapshot(loadedMonitors[selectedMonitor],function(url){
popImage(url)
})
}
function closeSnapshot(){
popImageClose()
}
function startReporting(){
if(hasGP){
console.log('Reading Gamepad')
repGP = window.setInterval(reportOnGamepad, reportInterval);
}
}
function stopReporting(){
console.log('Stopping Gamepad')
window.clearInterval(repGP)
}
function generateMonitorKeysFromPtzIds(){
monitorKeys = []
Object.values(loadedMonitors)
.filter(item => !!parseInt(item.details.ptz_id))
.sort((a, b) => parseInt(b.details.ptz_id) - parseInt(a.details.ptz_id))
.forEach((item) => {
console.log(item.details.ptz_id)
monitorKeys[item.details.ptz_id] = item.mid;
});
console.log(monitorKeys)
}
function setControllerType(gamepadId){
switch(true){
case gamepadId.includes('Xbox'):
reportInterval = 200;
reportOnGamepad = reportOnXboxGamepad
buttonPressAction = setCameraFromButtonCode
console.log('Xbox Controller found!')
break;
default:
reportInterval = 50;
reportOnGamepad = reportOnGenericGamepad
buttonPressAction = sequenceButtonPress
break;
}
}
var reportOnGamepad = reportOnXboxGamepad;
function sequenceButtonPress(buttonCode){
sequenceButtonPressList.push(buttonCode)
clearTimeout(sequenceButtonPressTimeout)
sequenceButtonPressTimeout = setTimeout(() => {
const newButtonCode = parseInt(sequenceButtonPressList.map(item => `${parseInt(item) + 1}`).join(''))
setCameraFromButtonCode(newButtonCode, true)
sequenceButtonPressList = []
},300)
}
if(canGame()) {
$(window).on("gamepadconnected", function(e) {
hasGP = true;
if(tabTree.name === 'liveGrid'){
startReporting()
}
const gamepadName = e.originalEvent.gamepad.id;
setControllerType(gamepadName)
console.log('Gamepad Connected!', gamepadName)
})
.on("gamepaddisconnected", function() {
if(!navigator.getGamepads()[0]){
hasGP = false;
console.log('Gamepad Disconnected!')
}
})
}
onDashboardReady(function(d){
generateMonitorKeysFromPtzIds();
})
onWebSocketEvent(function(d){
switch(d.f){
case'monitor_edit':
generateMonitorKeysFromPtzIds();
break;
case'monitor_watch_on':
var monitorId = d.mid || d.id;
onMonitorOpenForGamepad(monitorId)
break;
}
})
addOnTabOpen('liveGrid', function () {
startReporting()
})
addOnTabReopen('liveGrid', function () {
startReporting()
})
addOnTabAway('liveGrid', function () {
stopReporting()
})
});

View File

@ -35,6 +35,9 @@ function getLiveGridData(){
}
function getMonitorsPerRow(){
var x
if(dashboardOptions().montage_use != '1'){
return '4'
}
switch(dashboardOptions().montage){
case'1':
x = '12'
@ -422,45 +425,43 @@ function initiateLiveGridPlayer(monitor,subStreamChannel){
})
break;
case'mp4':
setTimeout(function(){
var stream = containerElement.find('.stream-element');
var onPoseidonError = function(){
// setTimeout(function(){
// mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId})
// },2000)
var stream = containerElement.find('.stream-element');
var onPoseidonError = function(){
// setTimeout(function(){
// mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId})
// },2000)
}
if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0
if(loadedPlayer.PoseidonErrorCount >= 5)return
if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){
if(loadedPlayer.Poseidon){
loadedPlayer.Poseidon.stop()
revokeVideoPlayerUrl(monitorId)
}
if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0
if(loadedPlayer.PoseidonErrorCount >= 5)return
if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){
if(loadedPlayer.Poseidon){
loadedPlayer.Poseidon.stop()
revokeVideoPlayerUrl(monitorId)
}
try{
loadedPlayer.Poseidon = new Poseidon({
video: stream[0],
auth_token: $user.auth_token,
ke: monitor.ke,
uid: $user.uid,
id: monitor.mid,
url: location.origin,
path: websocketPath,
query: websocketQuery,
onError : onPoseidonError,
channel : subStreamChannel
})
loadedPlayer.Poseidon.start();
}catch(err){
// onPoseidonError()
console.log('onTryPoseidonError',err)
}
}else{
stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime())
stream[0].onerror = function(err){
console.error(err)
}
try{
loadedPlayer.Poseidon = new Poseidon({
video: stream[0],
auth_token: $user.auth_token,
ke: monitor.ke,
uid: $user.uid,
id: monitor.mid,
url: location.origin,
path: websocketPath,
query: websocketQuery,
onError : onPoseidonError,
channel : subStreamChannel
})
loadedPlayer.Poseidon.start();
}catch(err){
// onPoseidonError()
console.log('onTryPoseidonError',err)
}
},1000)
}else{
stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime())
stream[0].onerror = function(err){
console.error(err)
}
}
break;
case'flv':
if (flvjs.isSupported()) {
@ -630,6 +631,7 @@ function closeLiveGridPlayer(monitorId,killElement){
var loadedPlayer = loadedLiveGrids[monitorId]
if(loadedPlayer){
if(loadedPlayer.hls){loadedPlayer.hls.destroy()}
clearTimeout(loadedPlayer.m3uCheck)
if(loadedPlayer.Poseidon){loadedPlayer.Poseidon.stop()}
if(loadedPlayer.Base64){loadedPlayer.Base64.disconnect()}
if(loadedPlayer.dash){loadedPlayer.dash.reset()}
@ -773,6 +775,10 @@ function fullScreenLiveGridStream(monitorItem){
}
fullScreenInit(videoElement[0])
}
function fullScreenLiveGridStreamById(monitorId){
const monitorItem = liveGrid.find(`[data-mid="${monitorId}"]`)
fullScreenLiveGridStream(monitorItem)
}
function toggleJpegMode(){
var sendData = {
f: 'monitor',
@ -977,7 +983,7 @@ function setPauseScrollTimeout(){
if(tabTree.name === 'liveGrid'){
liveGridPauseScrollTimeout = setTimeout(function(){
setPauseStatusForMonitorItems()
},700)
},200)
}
}
function openAllLiveGridPlayers(){
@ -1101,6 +1107,7 @@ $(document).ready(function(e){
.on('click','.toggle-live-grid-monitor-ptz-controls',function(){
var monitorItem = $(this).parents('[data-mid]').attr('data-mid')
drawPtzControlsOnLiveGridBlock(monitorItem)
setGamepadMonitorSelection()
})
.on('click','.toggle-live-grid-monitor-menu,.mdl-overlay-menu-backdrop',function(){
var monitorItem = $(this).parents('[data-mid]')
@ -1114,6 +1121,7 @@ $(document).ready(function(e){
.on('click','.toggle-live-grid-monitor-fullscreen',function(){
var monitorItem = $(this).parents('[data-mid]')
fullScreenLiveGridStream(monitorItem)
setGamepadMonitorSelection()
})
.on('click','.run-live-grid-monitor-pop',function(){
var monitorId = $(this).parents('[data-mid]').attr('data-mid')
@ -1277,6 +1285,7 @@ $(document).ready(function(e){
break;
case'substream_start':
loadedMonitors[d.mid].subStreamChannel = d.channel
loadedMonitors[d.mid].subStreamActive = true
showHideSubstreamActiveIcon(d.mid,true)
setTimeout(() => {
resetMonitorCanvas(d.mid,true,d.channel)
@ -1284,6 +1293,7 @@ $(document).ready(function(e){
break;
case'substream_end':
loadedMonitors[d.mid].subStreamChannel = null
loadedMonitors[d.mid].subStreamActive = false
resetMonitorCanvas(d.mid,true,null)
showHideSubstreamActiveIcon(d.mid,false)
break;

View File

@ -424,7 +424,9 @@ var copyMonitorSettingsToSelected = function(monitorConfig){
})
$.post(getApiPrefix()+'/configureMonitor/'+$user.ke+'/'+monitor.mid,{data:JSON.stringify(monitor)},function(d){
debugLog(d)
})
}).fail(function(xhr, status, error) {
console.error(error)
});
chosenMonitors[monitor.mid] = monitor;
})
}
@ -570,7 +572,7 @@ function drawInputMapSelectorHtml(options,parent){
function getPluginsList(monitorConfig){
return new Promise((resolve) => {
const chosenDetectors = safeJsonParse(monitorConfig.details).detectors_selected || [];
$.get(getApiPrefix() + '/plugins/list',function(data){
$.get(getApiPrefix('plugins') + '/list',function(data){
var plugins = data.plugins || {};
var pluginNames = Object.keys(plugins)
var disconnectedPlugins = chosenDetectors.filter(item => !pluginNames.includes(item));
@ -594,6 +596,9 @@ function getPluginsList(monitorConfig){
});
detectorsSelected.html(html)
resolve(plugins)
}).fail((err) => {
console.error(err);
resolve({})
})
})
}
@ -827,6 +832,13 @@ editorForm.submit(function(e){
}
debugLog(d)
setSubmitButton(editorForm, lang.Save, `check`, false)
}).fail((err) => {
new PNotify({
title: lang['Action Failed'],
text: JSON.stringify(err, null, 3),
type: 'danger'
})
setSubmitButton(editorForm, lang.Save, `check`, false)
})
//
if(copySettingsSelector.val() === '1'){

View File

@ -646,7 +646,7 @@ function launchImportMonitorWindow(callback){
reader.readAsText(f);
});
}
function readAlertNotice(title, text, type) {
function redAlertNotify({ title, text, type }) {
var redAlertNotice = redAlertNotices[title];
if (redAlertNotice) {
redAlertNotice.update({
@ -669,29 +669,6 @@ function readAlertNotice(title, text, type) {
});
}
}
function redAlertNotify(options) {
var title = options.title;
var redAlertNotice = redAlertNotices[title];
var notifyOptions = {
title: title,
text: options.text,
type: options.type,
hide: options.hide === undefined ? false : options.hide,
delay: options.delay || 30000
};
try{
if (redAlertNotice) {
redAlertNotice.update(notifyOptions);
} else {
redAlertNotices[title] = new PNotify(notifyOptions);
redAlertNotices[title].on('close', function() {
redAlertNotices[title] = null;
});
}
}catch(err){
console.error('redAlertNotify ERROR',err)
}
}
function buildPosePoints(bodyParts, x, y){
let theArray = []
for(const point of bodyParts){
@ -760,7 +737,11 @@ function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
}
if(matrix.redAlert){
var monitor = loadedMonitors[monitorId]
readAlertNotice(`${monitor.name}`,`${matrix.tag} (${matrix.id})<br>${matrix.notice}`,'danger');
redAlertNotify({
title: `${monitor.name}`,
text: `${matrix.tag} (${matrix.id})<br>${matrix.notice}`,
type: 'danger'
});
}
html += '</div>'
}

View File

@ -152,7 +152,9 @@ $(document).ready(function(e){
var newMon = mergeDeep(generateDefaultMonitorSettings(),monitorToPost)
$.post(getApiPrefix(`configureMonitor`) + '/' + monitorToPost.mid,{data:JSON.stringify(newMon,null,3)},function(d){
debugLog(d)
})
}).fail(function(xhr, status, error) {
console.error(error)
});
}
function loadLocalOptions(){
var currentOptions = dashboardOptions()

View File

@ -94,7 +94,11 @@ $(document).ready(function(){
liveStamp()
})
});
var loadedOnce = false;
onDashboardReady(function(){
if(loadedOnce)return;
loadedOnce = true;
openTab('initial');
drawMonitorListToSelector(monitorList.find('optgroup'))
loadVideos({
limit: 0,

View File

@ -238,8 +238,6 @@ onWebSocketEvent(function(d){
case'monitor_snapshot':
setTimeout(function(){
var snapElement = $(`[data-mid="${d.mid}"] .snapshot`)
console.log(d)
console.log(snapElement)
switch(d.snapshot_format){
case'plc':
snapElement.attr('src',placeholder.getData(placeholder.plcimg({text:d.snapshot.toUpperCase().split('').join(' '), fsize: 25, bgcolor:'#1462a5'})))

View File

@ -9,6 +9,7 @@ $(document).ready(function(){
var dateSelector = $('#timeline-date-selector');
var sideMenuList = $(`#side-menu-link-timeline ul`)
var monitorList = $(`#timeline-monitor-list`)
var videoTypeSelector = $(`#timeline-video-type`)
var playToggles = timeStripControls.find('[timeline-action="playpause"]')
var speedButtons = timeStripControls.find('[timeline-action="speed"]')
var gridSizeButtons = timeStripControls.find('[timeline-action="gridSize"]')
@ -106,8 +107,13 @@ $(document).ready(function(){
}
async function getVideosInGaps(gaps,monitorIds){
var searchQuery = timeStripObjectSearchInput.val()
var videoType = videoTypeSelector.val()
var wantArchivedVideo = videoType === 'archive'
var wantCloudVideo = videoType === 'cloud' && !wantArchivedVideo
var andOnly = searchQuery.startsWith('and:')
var videos = []
var eventLimit = Object.values(loadedMonitors).length * 300
searchQuery = andOnly ? searchQuery.replace('and:','') : searchQuery;
async function loopOnGaps(monitorId){
for (let i = 0; i < gaps.length; i++) {
var range = gaps[i]
@ -117,8 +123,9 @@ $(document).ready(function(){
endDate: range[1],
eventLimit,
searchQuery,
// archived: false,
// customVideoSet: wantCloudVideo ? 'cloudVideos' : null,
andOnly: andOnly ? '1' : '0',
archived: wantArchivedVideo,
customVideoSet: wantCloudVideo ? 'cloudVideos' : null,
},null,dontShowDetectionOnTimeline)).videos;
videos.push(...videosFound);
executeExtender('timelineGetVideosByMonitor', [monitorId, videosFound])
@ -460,7 +467,7 @@ $(document).ready(function(){
function setVideoInCanvas(newVideo){
var monitorId = newVideo.mid
var container = getVideoContainerInCanvas(newVideo)
.removeClass('no-video').find('.film').html(`<video muted src="${getApiPrefix('videos')}/${monitorId}/${newVideo.filename}"></video>`)
.removeClass('no-video').find('.film').html(`<video muted src="${newVideo.href}"></video>`)
var vidEl = getVideoElInCanvas(newVideo)
var objectContainer = getObjectContainerInCanvas(newVideo)
vidEl.playbackRate = timelineSpeed
@ -1055,6 +1062,9 @@ $(document).ready(function(){
timeStripObjectSearchInput.change(function(){
refreshTimeline()
})
videoTypeSelector.change(function(){
refreshTimeline()
})
timeStripVideoCanvas.on('dblclick','.timeline-video',function(){
var monitorId = $(this).attr('data-mid')
var video = loadedVideosOnCanvas[monitorId];

View File

@ -445,6 +445,7 @@ function loadEventsData(videoEvents){
function getVideoSearchRequestQueries(options){
var searchQuery = options.searchQuery
var requestQueries = []
var andOnly = options.andOnly === '1'
var monitorId = options.monitorId
var archived = options.archived
var customVideoSet = options.customVideoSet
@ -467,8 +468,12 @@ function getVideoSearchRequestQueries(options){
if(archived){
requestQueries.push(`archived=1`)
}
if(andOnly){
requestQueries.push(`andOnly=1`)
}
return {
searchQuery,
andOnly,
monitorId,
archived,
customVideoSet,
@ -483,6 +488,7 @@ function getVideoSearchRequestQueries(options){
function mergeVideosAndBin(options,callback){
const {
searchQuery,
andOnly,
monitorId,
archived,
customVideoSet,
@ -520,6 +526,7 @@ function getVideos(options,callback,noEvents){
options = options ? options : {}
const {
searchQuery,
andOnly,
monitorId,
archived,
customVideoSet,
@ -533,7 +540,7 @@ function getVideos(options,callback,noEvents){
$.getJSON(`${getApiPrefix(customVideoSet ? customVideoSet : searchQuery ? `videosByEventTag` : `videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([limit ? `limit=${limit}` : `noLimit=1`]).join('&')}`,function(data){
var videos = data.videos.map((video) => {
return Object.assign({},video,{
href: getFullOrigin(true) + video.href
href: `${getFullOrigin(true) + video.href}${customVideoSet === 'cloudVideos' ? `?type=${video.type}` : ''}`
})
})
$.getJSON(`${getApiPrefix(`timelapse`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(timelapseFrames){

View File

@ -5,6 +5,7 @@ $(document).ready(function(e){
var videosTableDrawArea = $('#videosTable_draw_area')
var videosTablePreviewArea = $('#videosTable_preview_area')
var objectTagSearchField = $('#videosTable_tag_search')
var objectTagSearchFieldAndOnly = $('#videosTable_tag_search_andonly')
var cloudVideoCheckSwitch = $('#videosTable_cloudVideos')
var sideLinkListBox = $('#side-menu-link-videosTableView ul')
var loadedVideosTable = [];
@ -28,18 +29,18 @@ $(document).ready(function(e){
}
//Lazy Load Thumbnails
function loadFramesForVideosInView() {
videosTableDrawArea.find('.video-thumbnail').each(async (n, imgEl) => {
const el = $(imgEl);
const monitorId = el.attr('data-mid');
const startDate = el.attr('data-time');
const endDate = el.attr('data-end');
const imgBlock = el.find('.video-thumbnail-img-block');
videosTableDrawArea.find('.video-thumbnail').each(async (n, imgEl) => {
const el = $(imgEl);
const monitorId = el.attr('data-mid');
const startDate = el.attr('data-time');
const endDate = el.attr('data-end');
const imgBlock = el.find('.video-thumbnail-img-block');
if (el.is(':visible')) { // Only load if visible
const href = await getSnapshotFromVideoTimeFrame(monitorId, startDate, endDate);
imgBlock.find('img').attr('src', href);
}
});
if (el.is(':visible')) { // Only load if visible
const href = await getSnapshotFromVideoTimeFrame(monitorId, startDate, endDate);
imgBlock.find('img').attr('src', href);
}
});
}
window.openVideosTableView = function(monitorId){
drawMonitorListToSelector(monitorsList,null,null,true)
@ -54,32 +55,29 @@ $(document).ready(function(e){
}
})
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
};
}
monitorsList.change(debounce(function(){
videosTableDrawArea.bootstrapTable('destroy');
drawVideosTableViewElements();
}, 300));
objectTagSearchField.change(debounce(function(){
videosTableDrawArea.bootstrapTable('destroy');
drawVideosTableViewElements();
}, 300));
cloudVideoCheckSwitch.change(debounce(function(){
videosTableDrawArea.bootstrapTable('destroy');
drawVideosTableViewElements();
}, 300));
[
monitorsList,
objectTagSearchField,
objectTagSearchFieldAndOnly,
cloudVideoCheckSwitch,
].forEach(function(theElement){
theElement.change(debounce(function(){
videosTableDrawArea.bootstrapTable('destroy');
drawVideosTableViewElements();
}, 300));
});
// Event listener for the Refresh link styled as a button
$('.refresh-data').click(function(e) {
e.preventDefault();
videosTableDrawArea.bootstrapTable('destroy');
videosTableDrawArea.bootstrapTable('destroy');
drawVideosTableViewElements();
});
@ -90,18 +88,19 @@ $(document).ready(function(e){
var dateRange = getSelectedTime(dateSelector);
var searchQuery = objectTagSearchField.val() || null;
var andOnly = objectTagSearchFieldAndOnly.val() || '0';
var startDate = dateRange.startDate;
var endDate = dateRange.endDate;
var monitorId = monitorsList.val();
var wantsArchivedVideo = getVideoSetSelected() === 'archive';
var wantCloudVideo = wantCloudVideos();
if (!usePreloadedData) {
var result = await getVideos({
monitorId,
startDate,
endDate,
searchQuery,
andOnly,
archived: wantsArchivedVideo,
customVideoSet: wantCloudVideo ? 'cloudVideos' : null,
pageSize: pageSize, // Pass pageSize to server

View File

@ -0,0 +1,178 @@
var wallViewCycleTimer = null;
var cycleLiveOptionsBefore = null;
var cycleLiveOptions = null;
var cycleLiveMoveNext = function(){}
var cycleLiveMovePrev = function(){}
var cycleLiveFullList = null
var cycleLiveCurrentPart = null
function getRunningMonitors(asArray){
const foundMonitors = {}
$.each(loadedMonitors,function(monitorId,monitor){
if(
monitor.mode === 'start' ||
monitor.mode === 'record'
){
foundMonitors[monitorId] = monitor
}
})
return asArray ? Object.values(foundMonitors) : foundMonitors
}
function getListOfMonitorsToCycleLive(chosenTags,useMonitorIds){
var monitors = []
if(useMonitorIds){
monitors = getMonitorsFromIds(chosenTags)
}else if(chosenTags){
var tags = sanitizeTagList(chosenTags)
monitors = getMonitorsFromTags(tags)
}else{
monitors = getRunningMonitors(true)
}
return monitors;
}
function getPartForCycleLive(fullList, afterMonitorId, numberOfMonitors) {
const startIndex = afterMonitorId ? fullList.findIndex(monitor => monitor.mid === afterMonitorId) : -1;
const result = [];
for (let i = 1; i <= numberOfMonitors; i++) {
const index = (startIndex + i) % fullList.length;
result.push(fullList[index]);
}
return result;
}
function displayCycleSetOnLiveGrid(monitorsList){
cycleLiveCurrentPart = [].concat(monitorsList)
closeAllMonitors()
openMonitors(monitorsList.map(monitor => monitor.mid))
}
// rotator
function stopCycleLive(){
clearTimeout(wallViewCycleTimer)
wallViewCycleTimer = null
}
function resumeCycleLive(fullList,partForCycle,numberOfMonitors){
const theLocalStorage = dashboardOptions()
const cycleLiveTimerAmount = parseInt(theLocalStorage.cycleLiveTimerAmount) || 30000
function next(){
var afterMonitorId = partForCycle.slice(-1)[0].mid;
partForCycle = getPartForCycleLive(fullList,afterMonitorId,numberOfMonitors)
displayCycleSetOnLiveGrid(partForCycle)
reset()
}
function prev(){
var firstInPart = partForCycle[0].mid;
var firstPartIndex = fullList.findIndex(monitor => monitor.mid === firstInPart)
var backedToIndex = (firstPartIndex - (numberOfMonitors + 1) + fullList.length) % fullList.length;
var beforeMonitorId = fullList[backedToIndex].mid
partForCycle = getPartForCycleLive(fullList,beforeMonitorId,numberOfMonitors, true)
displayCycleSetOnLiveGrid(partForCycle)
reset()
}
function reset(){
clearTimeout(wallViewCycleTimer)
wallViewCycleTimer = setTimeout(function(){
next()
},cycleLiveTimerAmount)
}
reset()
cycleLiveMoveNext = next
cycleLiveMovePrev = prev
}
function beginCycleLive({
chosenTags,
useMonitorIds,
numberOfMonitors,
monitorHeight,
}){
var fullList = getListOfMonitorsToCycleLive(chosenTags,useMonitorIds)
var partForCycle = getPartForCycleLive(fullList,null,numberOfMonitors)
cycleLiveFullList = [].concat(fullList)
displayCycleSetOnLiveGrid(partForCycle)
stopCycleLive()
resumeCycleLive(fullList,partForCycle,numberOfMonitors)
}
function toggleCycleLiveState(toggleState){
console.log('Toggle Cycle', toggleState)
const cycleControls = $('.wallview-cycle-control')
if(toggleState){
cycleControls.hide()
cycleLiveOptions = null
cycleLiveOptionsBefore = null
stopCycleLive()
}else{
cycleControls.show()
cycleLiveOptionsBefore = cycleLiveOptions ? Object.assign({},cycleLiveOptions) : null
const theLocalStorage = dashboardOptions()
const cycleLivePerRow = parseInt(theLocalStorage.cycleLivePerRow) || 2
const cycleLiveNumberOfMonitors = parseInt(theLocalStorage.cycleLiveNumberOfMonitors) || 4
const cycleLiveMonitorHeight = parseInt(theLocalStorage.cycleLiveMonitorHeight) || 4
cycleLiveOptions = {
chosenTags: null,
useMonitorIds: null,
monitorsPerRow: cycleLivePerRow,
numberOfMonitors: cycleLiveNumberOfMonitors,
monitorHeight: cycleLiveMonitorHeight,
}
beginCycleLive(cycleLiveOptions)
}
}
function keyShortcutsForCycleLive(enable) {
function cleanup(){
document.removeEventListener('keydown', keyShortcuts['cycleLive'].keydown);
document.removeEventListener('keyup', keyShortcuts['cycleLive'].keyup);
delete(keyShortcuts['cycleLive'])
}
if(enable){
let isKeyPressed = false;
function handleKeyboard(event){
if (isKeyPressed) {
return;
}
event.preventDefault();
switch(event.code){
case 'Space':
isKeyPressed = true;
if(wallViewCycleTimer){
stopCycleLive()
}else{
resumeCycleLive(
cycleLiveFullList,
cycleLiveCurrentPart,
cycleLiveOptions.numberOfMonitors
)
}
break;
case 'ArrowLeft':
isKeyPressed = true;
cycleLiveMovePrev();
break;
case 'ArrowRight':
isKeyPressed = true;
cycleLiveMoveNext();
break;
}
}
function handleKeyup(event) {
isKeyPressed = false;
}
keyShortcuts['cycleLive'] = {
keydown: handleKeyboard,
keyup: handleKeyup,
}
document.addEventListener('keydown', keyShortcuts['cycleLive'].keydown);
document.addEventListener('keyup', keyShortcuts['cycleLive'].keyup);
}else{
cleanup()
}
}
$(document).ready(function(){
$('body')
.on('click', '.wallview-cycle', function(e){
toggleCycleLiveState(!!wallViewCycleTimer)
})
.on('click', '.wallview-cycle-back', function(e){
cycleLiveMovePrev();
})
.on('click', '.wallview-cycle-front', function(e){
cycleLiveMoveNext();
})
})

View File

@ -0,0 +1,408 @@
$(document).ready(function() {
var selectedController = 0;
var keyLegend = {
"0": "b",
"1": "a",
"2": "y",
"3": "x",
"4": "l",
"5": "r",
"6": "zl",
"7": "zr",
"8": "minus",
"9": "plus",
"10": "l_stick",
"11": "r_stick",
"12": "up",
"13": "down",
"14": "left",
"15": "right",
}
var lastState = {
sticks: {
left: {},
right: {},
}
}
var lastPtzDirection = {}
var buttonsPressed = {}
var hasGP = false;
var repGP;
var reportInterval = 200
var stickBase = 2048
var stickMax = 4096
var deadZoneThreshold = 0.35
var outerDeadZone = 1.01
var selectedMonitor = dashboardOptions().gamepadMonitorSelection;
var monitorKeys = {};
var sequenceButtonPressList = []
var sequenceButtonPressTimeout = null
var buttonPressAction = null;
function runPtzCommand(monitorId,switchChosen){
switch(switchChosen){
case'setHome':
$.getJSON(getApiPrefix(`control`) + '/' + monitorId + '/setHome',function(data){
console.log(data)
})
break;
default:
mainSocket.f({
f: 'control',
direction: switchChosen,
id: monitorId,
ke: $user.ke
})
break;
}
}
function runPtzMove(monitorId,switchChosen,doMove){
mainSocket.f({
f: doMove ? 'startMove' : 'stopMove',
direction: switchChosen,
id: monitorId,
ke: $user.ke
})
}
window.setGamepadMonitorSelection = (monitorId) => {
dashboardOptions('gamepadMonitorSelection', monitorId);
selectedMonitor = `${monitorId}`;
}
function canGame() {
return "getGamepads" in navigator;
}
function convertStickAxisTo2048(value){
var newVal = parseInt((stickMax - stickBase) * value + stickBase)
return newVal
}
function getAnalogStickValues(gp, i, callback){
var label = i === 0 ? 'left' : 'right'
var horizontal = gp.axes[i] * outerDeadZone
var vertical = gp.axes[i + 1] * outerDeadZone
var newH = convertStickAxisTo2048(horizontal > deadZoneThreshold || horizontal < -deadZoneThreshold ? horizontal : 0)
var newV = convertStickAxisTo2048((vertical > deadZoneThreshold || vertical < -deadZoneThreshold ? vertical : 0) * -1)
if(
newH !== lastState.sticks[label].h ||
newV !== lastState.sticks[label].v
){
callback(label, newH, newV)
}
lastState.sticks[label].h = newH
lastState.sticks[label].v = newV
}
function getStickValue(gp, i, callback){
var label = `axis${axis}`;
var axis = gp.axes[i] * outerDeadZone
var newH = convertStickAxisTo2048(axis > deadZoneThreshold || axis < -deadZoneThreshold ? axis : 0)
if(newH !== lastState[label]){
callback(newH)
}
lastState[label] = newH
}
function getButtonsPressed(gp, callback, offCallback = () => {}){
$.each(keyLegend,function(code,key){
if(gp.buttons[code] && gp.buttons[code].pressed){
if(!lastState[key]){
buttonsPressed[code] = true;
callback(code)
}
lastState[key] = true
}else{
if(lastState[key]){
buttonsPressed[code] = false;
offCallback(code)
}
lastState[key] = false
}
})
}
function fullScreenInit(target){
if (target.requestFullscreen) {
target.requestFullscreen();
} else if (target.mozRequestFullScreen) {
target.mozRequestFullScreen();
} else if (target.webkitRequestFullscreen) {
target.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
}
function setCameraFromButtonCode(buttonCode = 0, preAdded){
const addedOneToButtonCode = preAdded ? buttonCode : parseInt(buttonCode) + 1
try{
console.log('addedOneToButtonCode', addedOneToButtonCode)
console.log('monitorKeys', monitorKeys)
const monitor = loadedMonitors[monitorKeys[addedOneToButtonCode]];
console.log('monitor', monitor)
const monitorId = monitor.mid;
const isFullscreened = !!document.fullscreenElement;
if(isFullscreened) {
document.exitFullscreen()
}
closeAllMonitors()
openMonitorInLiveGrid(monitorId)
if(isFullscreened) {
const streamElement = $(`[live-stream="${monitorId}"]`)[0];
fullScreenInit(streamElement)
}
}catch(err){
console.log(err)
new PNotify({
title: lang['Invalid Action'],
text: `${lang.ptzControlIdNotFound}<br><br>${lang['Button Code']} : ${addedOneToButtonCode}`,
type: 'warning'
});
console.log('No Monitor Associated :', buttonCode)
}
}
function openMonitorInLiveGrid(monitorId, callback){
lastPtzDirection = {};
setGamepadMonitorSelection(monitorId)
selectMonitor(monitorId)
displayInfoScreen()
autoPlaceCurrentMonitorItems()
saveLayout()
}
function sendPtzCommand(direction, doMove){
runPtzMove(selectedMonitor, direction, doMove)
}
function sentPtzToHome(){
runPtzCommand(selectedMonitor, 'center')
}
function translatePointTiltStick(x, y){
if(x > stickBase && !lastPtzDirection['right']){
lastPtzDirection['right'] = true
lastPtzDirection['left'] = false
// sendPtzCommand('left', false)
sendPtzCommand('right', true)
}else if(x < stickBase && !lastPtzDirection['left']){
lastPtzDirection['left'] = true
lastPtzDirection['right'] = false
// sendPtzCommand('right', false)
sendPtzCommand('left', true)
}else if(x === stickBase){
if(lastPtzDirection['right'])sendPtzCommand('right', false)
if(lastPtzDirection['left'])sendPtzCommand('left', false)
lastPtzDirection['right'] = false
lastPtzDirection['left'] = false
}
if(y > stickBase && !lastPtzDirection['up']){
lastPtzDirection['up'] = true
lastPtzDirection['down'] = false
// sendPtzCommand('down', false)
sendPtzCommand('up', true)
}else if(y < stickBase && !lastPtzDirection['down']){
lastPtzDirection['down'] = true
lastPtzDirection['up'] = false
// sendPtzCommand('up', false)
sendPtzCommand('down', true)
}else if(y === stickBase){
if(lastPtzDirection['up'])sendPtzCommand('up', false)
if(lastPtzDirection['down'])sendPtzCommand('down', false)
lastPtzDirection['down'] = false
lastPtzDirection['up'] = false
}
}
function translateZoomAxis(value){
if(value > stickBase && !lastPtzDirection['zoom_in']){
lastPtzDirection['zoom_in'] = true
lastPtzDirection['zoom_out'] = false
// sendPtzCommand('zoom_out', false)
sendPtzCommand('zoom_in', true)
}else if(value < stickBase && !lastPtzDirection['zoom_out']){
lastPtzDirection['zoom_out'] = true
lastPtzDirection['zoom_in'] = false
// sendPtzCommand('zoom_in', false)
sendPtzCommand('zoom_out', true)
}else if(value === stickBase){
if(lastPtzDirection['zoom_in'])sendPtzCommand('zoom_in', false)
if(lastPtzDirection['zoom_out'])sendPtzCommand('zoom_out', false)
lastPtzDirection['zoom_in'] = false
lastPtzDirection['zoom_out'] = false
}
}
function reportOnXboxGamepad() {
try{
var gp = navigator.getGamepads()[0];
getButtonsPressed(gp, function(buttonCode){
if(buttonCode == 6){
sendPtzCommand('zoom_out', true)
}else if(buttonCode == 7){
sendPtzCommand('zoom_in', true)
}else if(buttonCode == 8){
// closeSnapshot()
// openSnapshot()
}else if(buttonCode == 9){
sentPtzToHome()
}else if(buttonCode == 11){
if (!document.fullscreenElement) {
fullScreenLiveGridStreamById(selectedMonitor)
}else{
document.exitFullscreen()
}
}else if(buttonCode == 12){
toggleCycleLiveState(!!wallViewCycleTimer)
}else if(buttonCode == 14){
cycleLiveMovePrev();
}else if(buttonCode == 15){
cycleLiveMoveNext();
}else{
buttonPressAction(buttonCode)
}
}, function(buttonCode){
if(buttonCode == 6){
sendPtzCommand('zoom_out', false)
}else if(buttonCode == 7){
sendPtzCommand('zoom_in', false)
}
})
getAnalogStickValues(gp, 0, function(stick, x, y){
translatePointTiltStick(x, y)
})
getAnalogStickValues(gp, 2, function(stick, x, y){
translateZoomAxis(y)
})
}catch(err){
console.log(err)
// stopReporting()
}
}
function reportOnGenericGamepad() {
try{
const gp = navigator.getGamepads()[0];
getButtonsPressed(gp, function(buttonCode){
if(buttonCode == 10){
// closeSnapshot()
}else if(buttonCode == 11){
// closeSnapshot()
// openSnapshot()
}else{
buttonPressAction(buttonCode)
}
},function(buttonCode){
})
getAnalogStickValues(gp, 0, function(stick, x, y){
translatePointTiltStick(x, y)
})
getStickValue(gp, 2,function(value){
translateZoomAxis(value)
})
}catch(err){
console.log(err)
// stopReporting()
}
}
function openSnapshot(){
if(!$.confirm.e.is(':visible')){
getSnapshot(loadedMonitors[selectedMonitor],function(url){
$.confirm.create({
title: lang.Snapshot,
body: `<img src="${url}">`,
clickOptions: {
class: 'btn-primary',
title: lang.Close,
},
clickCallback: async function(){}
})
})
}
}
function closeSnapshot(){
$.confirm.e.modal('hide')
}
function startReporting(){
if(hasGP){
console.log('Reading Gamepad')
repGP = window.setInterval(reportOnGamepad, reportInterval);
}
}
function stopReporting(){
console.log('Stopping Gamepad')
window.clearInterval(repGP)
}
function generateMonitorKeysFromPtzIds(){
monitorKeys = []
Object.values(loadedMonitors)
.filter(item => !!parseInt(item.details.ptz_id))
.sort((a, b) => parseInt(b.details.ptz_id) - parseInt(a.details.ptz_id))
.forEach((item) => {
console.log(item.details.ptz_id)
monitorKeys[item.details.ptz_id] = item.mid;
});
console.log(monitorKeys)
}
function setControllerType(gamepadId){
switch(true){
case gamepadId.includes('Xbox'):
reportInterval = 200;
reportOnGamepad = reportOnXboxGamepad
buttonPressAction = setCameraFromButtonCode
console.log('Xbox Controller found!')
break;
default:
reportInterval = 50;
reportOnGamepad = reportOnGenericGamepad
buttonPressAction = sequenceButtonPress
break;
}
}
var reportOnGamepad = reportOnXboxGamepad;
function sequenceButtonPress(buttonCode){
sequenceButtonPressList.push(buttonCode)
clearTimeout(sequenceButtonPressTimeout)
sequenceButtonPressTimeout = setTimeout(() => {
const newButtonCode = parseInt(sequenceButtonPressList.map(item => `${parseInt(item) + 1}`).join(''))
setCameraFromButtonCode(newButtonCode, true)
sequenceButtonPressList = []
},300)
}
if(canGame()) {
$(window).on("gamepadconnected", function(e) {
hasGP = true;
startReporting()
const gamepadName = e.originalEvent.gamepad.id;
setControllerType(gamepadName)
console.log('Gamepad Connected!', gamepadName)
})
.on("gamepaddisconnected", function() {
if(!navigator.getGamepads()[0]){
hasGP = false;
console.log('Gamepad Disconnected!')
}
})
}
addActionToExtender('onDashboardReady',function(){
generateMonitorKeysFromPtzIds();
startReporting()
})
// onWebSocketEvent(function(d){
// switch(d.f){
// case'monitor_edit':
// generateMonitorKeysFromPtzIds();
// break;
// }
// })
});

View File

@ -1,281 +1,345 @@
var loadedMonitors = {}
var selectedMonitors = {}
$(document).ready(function(){
PNotify.prototype.options.styling = "fontawesome";
var wallViewMonitorList = $('#wallview-monitorList')
var wallViewControls = $('#wallview-controls')
var wallViewCanvas = $('#wallview-canvas')
var wallViewInfoScreen = $('#wallview-info-screen')
var theWindow = $(window);
var lastWindowWidth = theWindow.width()
var lastWindowHeight = theWindow.height()
function getQueryString(){
var theObject = {}
location.search.substring(1).split('&').forEach(function(string){
var parts = string.split('=')
theObject[parts[0]] = parts[1]
})
return theObject
PNotify.prototype.options.styling = "fontawesome";
var wallViewMonitorList = $('#wallview-monitorList')
var wallViewMonitorListContainer = $('#wallview-monitorList-container')
var wallViewControls = $('#wallview-controls')
var wallViewCanvas = $('#wallview-canvas')
var wallViewInfoScreen = $('#wallview-info-screen')
var theWindow = $(window);
var lastWindowWidth = theWindow.width()
var lastWindowHeight = theWindow.height()
var websocketPath = checkCorrectPathEnding(urlPrefix.replace(location.origin, '')) + 'socket.io'
function checkCorrectPathEnding(x){
var length=x.length
if(x.charAt(length-1)!=='/'){
x=x+'/'
}
function featureIsActivated(showNotice){
if(userHasSubscribed){
return true
}else{
if(showNotice){
new PNotify({
title: lang.activationRequired,
text: lang.featureRequiresActivationText,
type: 'warning'
})
}
return false
}
return x
}
function dashboardOptions(r,rr,rrr){
if(!rrr){rrr={};};if(typeof rrr === 'string'){rrr={n:rrr}};if(!rrr.n){rrr.n='ShinobiOptions_'+location.host+'_'+$user.ke+$user.uid}
ii={o:localStorage.getItem(rrr.n)};try{ii.o=JSON.parse(ii.o)}catch(e){ii.o={}}
if(!ii.o){ii.o={}}
if(r&&rr&&!rrr.x){
ii.o[r]=rr;
}
function createWallViewWindow(windowName){
var el = $(document)
var width = el.width()
var height = el.height()
window.open(getApiPrefix() + '/wallview/' + groupKey + (windowName ? '?window=' + windowName : ''), 'wallview_'+windowName, 'height='+height+',width='+width)
switch(rrr.x){
case 0:
delete(ii.o[r])
break;
case 1:
delete(ii.o[r][rr])
break;
}
function getApiPrefix(innerPart){
return `${urlPrefix}${authKey}${innerPart ? `/${innerPart}/${groupKey}` : ''}`
}
function getWindowName(){
const urlParams = new URLSearchParams(window.location.search);
const theWindowChoice = urlParams.get('window');
return theWindowChoice || '1'
}
function drawMonitorListItem(monitor){
wallViewMonitorList.append(`<li><a class="dropdown-item" select-monitor="${monitor.mid}" href="#"><i class="fa fa-check"></i> ${monitor.name}</a></li>`)
}
function drawMonitorList(){
return new Promise((resolve) => {
$.get(getApiPrefix('monitor'),function(monitors){
$.each(monitors, function(n,monitor){
if(monitor.mode !== 'stop' && monitor.mode !== 'idle'){
loadedMonitors[monitor.mid] = monitor;
drawMonitorListItem(monitor)
}
})
resolve(monitors)
localStorage.setItem(rrr.n,JSON.stringify(ii.o))
return ii.o
}
function getQueryString(){
var theObject = {}
location.search.substring(1).split('&').forEach(function(string){
var parts = string.split('=')
theObject[parts[0]] = parts[1]
})
return theObject
}
function featureIsActivated(showNotice){
if(userHasSubscribed){
return true
}else{
if(showNotice){
new PNotify({
title: lang.activationRequired,
text: lang.featureRequiresActivationText,
type: 'warning'
})
})
}
function getMonitorListItem(monitorId){
return wallViewMonitorList.find(`[select-monitor="${monitorId}"]`)
}
function selectMonitor(monitorId, css){
css = css || {};
var embedHost = getQueryString().host || `/`;
var isSelected = selectedMonitors[monitorId]
if(isSelected)return;
var numberOfSelected = Object.keys(selectedMonitors)
if(numberOfSelected > 3 && !featureIsActivated(true)){
return
}
selectedMonitors[monitorId] = Object.assign({}, loadedMonitors[monitorId]);
wallViewCanvas.append(`<div class="wallview-video p-0 m-0" live-stream="${monitorId}" style="left:${css.left || 0}px;top:${css.top || 0}px;width:${css.width ? css.width + 'px' : '50vw'};height:${css.height ? css.height + 'px' : '50vh'};"><div class="overlay"><div class="wallview-item-controls text-end"><a class="btn btn-sm btn-outline-danger wallview-item-close"><i class="fa fa-times"></i></a></div></div><iframe src="${getApiPrefix('embed')}/${monitorId}/fullscreen%7Cjquery%7Crelative?host=${embedHost}"></iframe></div>`)
wallViewCanvas.find(`[live-stream="${monitorId}"]`)
.draggable({
grid: [40, 40],
snap: '#wallview-canvas',
containment: "window",
stop: function(){
saveLayout()
}
})
.resizable({
grid: [40, 40],
snap: '#wallview-container',
stop: function(){
saveLayout()
}
});
getMonitorListItem(monitorId).addClass('active')
return false
}
function deselectMonitor(monitorId){
delete(selectedMonitors[monitorId])
var monitorItem = wallViewCanvas.find(`[live-stream="${monitorId}"]`);
monitorItem.find('iframe').attr('src','about:blank')
monitorItem.remove()
getMonitorListItem(monitorId).removeClass('active')
}
function getCurrentLayout(){
var layout = []
wallViewCanvas.find('.wallview-video').each(function(n,v){
var el = $(v)
var monitorId = el.attr('live-stream')
var position = el.position()
layout.push({
monitorId,
css: {
left: position.left,
top: position.top,
width: el.width(),
height: el.height(),
}
function createWallViewWindow(windowName){
var el = $(document)
var width = el.width()
var height = el.height()
window.open(getApiPrefix() + '/wallview/' + groupKey + (windowName ? '?window=' + windowName : ''), 'wallview_'+windowName, 'height='+height+',width='+width)
}
function getApiPrefix(innerPart){
return `${urlPrefix}${authKey}${innerPart ? `/${innerPart}/${groupKey}` : ''}`
}
function getWindowName(){
const urlParams = new URLSearchParams(window.location.search);
const theWindowChoice = urlParams.get('window');
return theWindowChoice || '1'
}
function drawMonitorList(){
return new Promise((resolve) => {
$.get(getApiPrefix('monitor'),function(monitors){
$.each(monitors, function(n,monitor){
if(monitor.mode !== 'stop' && monitor.mode !== 'idle'){
loadedMonitors[monitor.mid] = monitor;
}
})
})
return layout
}
function saveLayout(){
var windowName = getWindowName();
var layouts = getAllLayouts();
var layout = getCurrentLayout();
var saveContainer = {
layout,
windowInnerWidth: window.innerWidth,
windowInnerHeight: window.innerHeight,
}
layouts[windowName] = saveContainer;
localStorage.setItem('windowLayouts', JSON.stringify(layouts));
}
function getAllLayouts(){
return JSON.parse(localStorage.getItem(`windowLayouts`) || '{}');
}
function getLayout(full){
var windowName = getWindowName();
var saveContainer = getAllLayouts()[windowName]
if(full)return saveContainer || { layout: [] };
var layout = saveContainer.layout || []
return layout;
}
function resetWindowDimensions(){
var saveContainer = getLayout(true);
if(saveContainer.windowInnerWidth && saveContainer.windowInnerHeight){
var widthDiff = window.outerWidth - window.innerWidth;
var heightDiff = window.outerHeight - window.innerHeight;
lastWindowWidth = saveContainer.windowInnerWidth
lastWindowHeight = saveContainer.windowInnerHeight
window.resizeTo(saveContainer.windowInnerWidth + widthDiff, saveContainer.windowInnerHeight + heightDiff);
}
}
function loadSavedLayout(){
var saveContainer = getLayout(true);
resetWindowDimensions()
saveContainer.layout.forEach(function({ monitorId, css }, n){
selectMonitor(monitorId, css);
});
displayInfoScreen();
}
function displayInfoScreen(){
if(getCurrentLayout().length === 0){
wallViewInfoScreen.css('display','flex')
}else{
wallViewInfoScreen.hide()
}
}
function resizeMonitorItem({ monitorId, css }, oldWidth, oldHeight, newWidth, newHeight){
var monitorItem = wallViewCanvas.find(`[live-stream="${monitorId}"]`);
var newCss = rescaleMatrix(css, oldWidth, oldHeight, newWidth, newHeight)
monitorItem.css(newCss)
}
function rescaleMatrix(matrix, oldWidth, oldHeight, newWidth, newHeight) {
const scaleX = newWidth / oldWidth;
const scaleY = newHeight / oldHeight;
return {
left: matrix.left * scaleX,
top: matrix.top * scaleY,
width: matrix.width * scaleX,
height: matrix.height * scaleY
};
}
function onWindowResize(){
var currentWindowWidth = theWindow.width()
var currentWindowHeight = theWindow.height()
var layout = getCurrentLayout();
for(item of layout){
resizeMonitorItem(item,lastWindowWidth,lastWindowHeight,currentWindowWidth,currentWindowHeight)
}
lastWindowWidth = currentWindowWidth
lastWindowHeight = currentWindowHeight
}
function autoPlaceCurrentMonitorItems() {
const wallviewVideos = wallViewCanvas.find('.wallview-video');
const totalItems = wallviewVideos.length;
let numRows, numCols;
if (totalItems === 6 || totalItems === 5) {
numCols = 3;
numRows = 2;
} else {
numRows = Math.ceil(Math.sqrt(totalItems));
numCols = Math.ceil(totalItems / numRows);
}
const containerWidth = wallViewCanvas.width();
const containerHeight = wallViewCanvas.height();
const itemWidth = containerWidth / numCols;
const itemHeight = containerHeight / numRows;
wallviewVideos.each(function(index, element) {
const row = Math.floor(index / numCols);
const col = index % numCols;
$(element).css({
left: col * itemWidth,
top: row * itemHeight,
width: itemWidth,
height: itemHeight
});
});
}
function openAllMonitors(){
$.each(loadedMonitors,function(monitorId, monitor){
var modeAccepted = monitor.mode !== 'stop' && monitor.mode !== 'idle'
if(modeAccepted)selectMonitor(monitorId)
})
autoPlaceCurrentMonitorItems()
displayInfoScreen()
saveLayout()
}
function openNextMonitors(numberOf){
var allLayouts = getAllLayouts()
var ignoreMonitors = []
var availableMonitors = []
var numberToOpen = parseInt(numberOf) || 4;
$.each(allLayouts,function(windowName, { layout }){
$.each(layout,function(n, { monitorId }){
ignoreMonitors.push(monitorId)
var tags = getListOfTagsFromMonitors()
var monitorsOrdered = Object.values(loadedMonitors).sort((a, b) => a.name.localeCompare(b.name));
var allFound = [
{
attributes: `tag=""`,
class: `cursor-pointer wallview-open-monitor-group`,
color: 'forestgreen',
label: lang['All Monitors'],
}
]
$.each(tags,function(tag,monitors){
allFound.push({
attributes: `tag="${tag}"`,
class: `cursor-pointer wallview-open-monitor-group`,
color: 'blue',
label: tag,
})
})
});
$.each(loadedMonitors,function(monitorId, monitor){
if(ignoreMonitors.indexOf(monitor.mid) === -1){
var modeAccepted = monitor.mode !== 'stop' && monitor.mode !== 'idle'
if(modeAccepted)availableMonitors.push(monitorId)
}
});
for (let i = 0; i < numberToOpen; i++) {
selectMonitor(availableMonitors[i])
}
autoPlaceCurrentMonitorItems()
displayInfoScreen()
saveLayout()
}
function closeAllMonitors(){
$.each(loadedMonitors,function(monitorId, monitor){
deselectMonitor(monitorId)
$.each(monitorsOrdered,function(monitorKey,monitor){
var monitorId = monitor.mid
var label = monitor.name
allFound.push({
attributes: `select-monitor="${monitorId}"`,
class: `cursor-pointer`,
color: 'grey',
label,
})
})
var html = allFound.map(item => `<div class="mb-1 search-row"><a class="btn d-block btn-primary btn-sm ${item.class}" ${item.attributes} href="#">${item.label}</a></div>`).join('')
wallViewMonitorList.html(html)
resolve(monitors)
})
displayInfoScreen()
saveLayout()
})
}
function getMonitorListItem(monitorId){
return wallViewMonitorList.find(`[select-monitor="${monitorId}"]`)
}
function selectMonitor(monitorId, css){
css = css || {};
var embedHost = getQueryString().host || `/`;
var isSelected = selectedMonitors[monitorId]
if(isSelected)return;
var numberOfSelected = Object.keys(selectedMonitors)
if(numberOfSelected > 3 && !featureIsActivated(true)){
return
}
selectedMonitors[monitorId] = Object.assign({}, loadedMonitors[monitorId]);
wallViewCanvas.append(`<div class="wallview-video p-0 m-0" live-stream="${monitorId}" style="left:${css.left || 0}px;top:${css.top || 0}px;width:${css.width ? css.width + 'px' : '50vw'};height:${css.height ? css.height + 'px' : '50vh'};"><div class="overlay"><div class="wallview-item-controls text-end"><a class="btn btn-sm btn-outline-danger wallview-item-close"><i class="fa fa-times"></i></a></div></div><iframe src="${getApiPrefix('embed')}/${monitorId}/fullscreen%7Cjquery%7Crelative?host=${embedHost}"></iframe></div>`)
wallViewCanvas.find(`[live-stream="${monitorId}"]`)
.draggable({
grid: [40, 40],
snap: '#wallview-canvas',
containment: "window",
stop: function(){
saveLayout()
}
})
.resizable({
grid: [40, 40],
snap: '#wallview-container',
stop: function(){
saveLayout()
}
});
getMonitorListItem(monitorId).removeClass('btn-primary').addClass('btn-warning')
}
function deselectMonitor(monitorId){
delete(selectedMonitors[monitorId])
var monitorItem = wallViewCanvas.find(`[live-stream="${monitorId}"]`);
monitorItem.find('iframe').attr('src','about:blank')
monitorItem.remove()
getMonitorListItem(monitorId).removeClass('btn-warning').addClass('btn-primary')
}
function getCurrentLayout(){
var layout = []
wallViewCanvas.find('.wallview-video').each(function(n,v){
var el = $(v)
var monitorId = el.attr('live-stream')
var position = el.position()
layout.push({
monitorId,
css: {
left: position.left,
top: position.top,
width: el.width(),
height: el.height(),
}
})
})
return layout
}
function saveLayout(){
var windowName = getWindowName();
var layouts = getAllLayouts();
var layout = getCurrentLayout();
var saveContainer = {
layout,
windowInnerWidth: window.innerWidth,
windowInnerHeight: window.innerHeight,
}
layouts[windowName] = saveContainer;
localStorage.setItem('windowLayouts', JSON.stringify(layouts));
}
function getAllLayouts(){
return JSON.parse(localStorage.getItem(`windowLayouts`) || '{}');
}
function getLayout(full){
var windowName = getWindowName();
var saveContainer = getAllLayouts()[windowName]
if(full)return saveContainer || { layout: [] };
var layout = saveContainer.layout || []
return layout;
}
function resetWindowDimensions(){
var saveContainer = getLayout(true);
if(saveContainer.windowInnerWidth && saveContainer.windowInnerHeight){
var widthDiff = window.outerWidth - window.innerWidth;
var heightDiff = window.outerHeight - window.innerHeight;
lastWindowWidth = saveContainer.windowInnerWidth
lastWindowHeight = saveContainer.windowInnerHeight
window.resizeTo(saveContainer.windowInnerWidth + widthDiff, saveContainer.windowInnerHeight + heightDiff);
}
}
function loadSavedLayout(){
var saveContainer = getLayout(true);
resetWindowDimensions()
saveContainer.layout.forEach(function({ monitorId, css }, n){
selectMonitor(monitorId, css);
});
displayInfoScreen();
}
function displayInfoScreen(){
if(getCurrentLayout().length === 0){
wallViewInfoScreen.css('display','flex')
}else{
wallViewInfoScreen.hide()
}
}
function resizeMonitorItem({ monitorId, css }, oldWidth, oldHeight, newWidth, newHeight){
var monitorItem = wallViewCanvas.find(`[live-stream="${monitorId}"]`);
var newCss = rescaleMatrix(css, oldWidth, oldHeight, newWidth, newHeight)
monitorItem.css(newCss)
}
function rescaleMatrix(matrix, oldWidth, oldHeight, newWidth, newHeight) {
const scaleX = newWidth / oldWidth;
const scaleY = newHeight / oldHeight;
return {
left: matrix.left * scaleX,
top: matrix.top * scaleY,
width: matrix.width * scaleX,
height: matrix.height * scaleY
};
}
function onWindowResize(){
var currentWindowWidth = theWindow.width()
var currentWindowHeight = theWindow.height()
var layout = getCurrentLayout();
for(item of layout){
resizeMonitorItem(item,lastWindowWidth,lastWindowHeight,currentWindowWidth,currentWindowHeight)
}
lastWindowWidth = currentWindowWidth
lastWindowHeight = currentWindowHeight
}
function autoPlaceCurrentMonitorItems() {
const wallviewVideos = wallViewCanvas.find('.wallview-video');
const totalItems = wallviewVideos.length;
let numRows, numCols;
if (totalItems === 6 || totalItems === 5) {
numCols = 3;
numRows = 2;
} else {
numRows = Math.ceil(Math.sqrt(totalItems));
numCols = Math.ceil(totalItems / numRows);
}
const containerWidth = wallViewCanvas.width();
const containerHeight = wallViewCanvas.height();
const itemWidth = containerWidth / numCols;
const itemHeight = containerHeight / numRows;
wallviewVideos.each(function(index, element) {
const row = Math.floor(index / numCols);
const col = index % numCols;
$(element).css({
left: col * itemWidth,
top: row * itemHeight,
width: itemWidth,
height: itemHeight
});
});
}
function openAllMonitors(){
$.each(loadedMonitors,function(monitorId, monitor){
var modeAccepted = monitor.mode !== 'stop' && monitor.mode !== 'idle'
if(modeAccepted)selectMonitor(monitorId)
})
autoPlaceCurrentMonitorItems()
displayInfoScreen()
saveLayout()
}
function openMonitors(monitorIds, savePlaces){
$.each(monitorIds,function(n, monitorId){
selectMonitor(monitorId)
})
autoPlaceCurrentMonitorItems()
displayInfoScreen()
if(savePlaces)saveLayout()
}
function openNextMonitors(numberOf){
var allLayouts = getAllLayouts()
var ignoreMonitors = []
var availableMonitors = []
var numberToOpen = parseInt(numberOf) || 4;
$.each(allLayouts,function(windowName, { layout }){
$.each(layout,function(n, { monitorId }){
ignoreMonitors.push(monitorId)
})
});
$.each(loadedMonitors,function(monitorId, monitor){
if(ignoreMonitors.indexOf(monitor.mid) === -1){
var modeAccepted = monitor.mode !== 'stop' && monitor.mode !== 'idle'
if(modeAccepted)availableMonitors.push(monitorId)
}
});
for (let i = 0; i < numberToOpen; i++) {
selectMonitor(availableMonitors[i])
}
autoPlaceCurrentMonitorItems()
displayInfoScreen()
saveLayout()
}
function closeAllMonitors(){
$.each(loadedMonitors,function(monitorId, monitor){
deselectMonitor(monitorId)
})
displayInfoScreen()
saveLayout()
}
addExtender('onDashboardReady')
$(document).ready(function(){
drawMonitorList().then(() => {
loadSavedLayout()
setTimeout(() => {
@ -283,6 +347,7 @@ $(document).ready(function(){
onWindowResize()
saveLayout()
})
executeExtender('onDashboardReady')
},500)
})
$('body')
@ -332,4 +397,42 @@ $(document).ready(function(){
e.preventDefault()
closeAllMonitors()
})
})
.on('click', '.wallview-toggle-monitor-list', function(e){
e.preventDefault();
wallViewMonitorListContainer.toggleClass('d-none')
return false;
})
.on('click', '.wallview-open-monitor-group', function(e){
e.preventDefault();
var el = $(this)
var tag = el.attr('tag')
if(!tag){
for(monitorId of Object.keys(loadedMonitors)){
selectMonitor(monitorId)
}
}else{
var tags = getListOfTagsFromMonitors()
var monitorIds = tags[tag]
for(monitorId of monitorIds){
selectMonitor(monitorId)
}
}
autoPlaceCurrentMonitorItems()
displayInfoScreen()
saveLayout()
return false;
})
.on('keyup','.search-parent .search-controller',function(){
var _this = this;
var parent = $(this).parents('.search-parent')
$.each(parent.find(".search-body .search-row"), function() {
if($(this).text().toLowerCase().indexOf($(_this).val().toLowerCase()) === -1)
$(this).hide();
else
$(this).show();
});
});
createWebsocket(location.origin,{
path: websocketPath
});
});

View File

@ -28,7 +28,7 @@ function createWebsocket(theURL,thePath){
data.callbackId = callbackId
queuedCallbacks[callbackId] = callback
}
console.log('Sending Data',data)
// console.log('Sending Data',data)
return mainSocket.emit('f',data)
}
mainSocket.on('ping', function(){
@ -36,25 +36,12 @@ function createWebsocket(theURL,thePath){
})
mainSocket.on('connect',function (d){
console.log('Connected to Websocket!')
if(location.search === '?p2p=1'){
mainSocket.emit('p2pInitUser',{
user: {
ke: $user.ke,
mail: $user.mail,
auth_token: $user.auth_token,
details: $user.details,
uid: $user.uid,
},
machineId: machineId
})
}else{
mainSocket.f({
f: 'init',
ke: $user.ke,
auth: $user.auth_token,
uid: $user.uid
})
}
mainSocket.f({
f: 'init',
ke: $user.ke,
auth: $user.auth_token,
uid: $user.uid
})
})
mainSocket.on('f',function (d){
switch(d.f){

View File

@ -0,0 +1,24 @@
$(document).ready(function(){
var theEnclosure = $('#centralManagement')
var theForm = theEnclosure.find('form')
theEnclosure.find('.submit').click(function(){
theForm.submit()
})
theForm.submit(function(e){
e.preventDefault()
var formValues = $(this).serializeObject()
$.post(superApiPrefix + $user.sessionKey + '/mgmt/save',{
data: JSON.stringify(formValues)
},function(data){
console.log(data)
if(data.ok){
new PNotify({
type: 'success',
title: lang['Settings Changed'],
text: lang.centralManagementSaved,
})
}
})
return false
})
})

View File

@ -343,6 +343,7 @@ $(document).ready(function () {
schema: schema,
}
);
window.configurationEditor = configurationEditor;
configurationEditor.setValue(data.config);
@ -404,5 +405,4 @@ $(document).ready(function () {
}
});
window.configurationEditor = configurationEditor;
})

View File

@ -0,0 +1,195 @@
$(document).ready(function(){
const loadedMounts = {}
const theEnclosure = $('#superMountManager')
const theSearch = $('#mountManagerListSearch')
const theTable = $('#mountManagerListTable tbody')
const newMountForm = $('#mountManagerNewMount')
function getMountId(theMount){
return `${theMount.mountPoint.split('/').join('_')}`
}
function drawMountToTable(theMount){
var html = `
<tr row-mounted="${getMountId(theMount)}">
<td class="align-middle">
<div>${theMount.device}</div>
<div><small>${theMount.mountPoint}</small></div>
</td>
<td class="align-middle">
${theMount.type}
</td>
<td class="align-middle">
${theMount.options}
</td>
<td class="align-middle">
<a class="btn btn-primary btn-sm cursor-pointer edit" title="${lang.Edit}"><i class="fa fa-pencil-square-o"></i></a>
<a class="btn btn-success btn-sm cursor-pointer setVideosDir" title="${lang.videosDir}"><i class="fa fa-download"></i></a>
<a class="btn btn-danger btn-sm cursor-pointer delete" title="${lang.Delete}"><i class="fa fa-trash-o"></i></a>
</td>
</tr>`
theTable.append(html)
}
function drawMountsTable(data){
theTable.empty()
$.each(data,function(n,theMount){
drawMountToTable(theMount)
});
}
function filterMountsTable(theSearch = '') {
var searchQuery = theSearch.trim().toLowerCase();
if(searchQuery === ''){
theTable.find(`[row-mounted]`).show()
return;
}
var rows = Object.values(loadedMounts);
var filtered = []
rows.forEach((row) => {
var searchInString = JSON.stringify(row).toLowerCase();
var theElement = theTable.find(`[row-mounted="${getMountId(row)}"]`)
if(searchInString.indexOf(searchQuery) > -1){
theElement.show()
}else{
theElement.hide()
}
})
return filtered
}
function loadMounts(callback) {
return new Promise((resolve,reject) => {
$.getJSON(superApiPrefix + $user.sessionKey + '/mountManager/list',function(data){
$.each(data.mounts,function(n,theMount){
loadedMounts[getMountId(theMount)] = theMount;
})
drawMountsTable(data.mounts)
resolve(data)
})
})
}
function addMount(form) {
return new Promise((resolve,reject) => {
// const { sourceTarget, localPath, mountType, options } = form;
$.post(superApiPrefix + $user.sessionKey + '/mountManager/mount', form,function(data){
resolve(data)
})
})
}
function removeMount(localPath) {
return new Promise((resolve,reject) => {
$.post(superApiPrefix + $user.sessionKey + '/mountManager/removeMount',{
localPath
},function(data){
resolve(data)
})
})
}
function setVideosDir(localPath, pathInside) {
return new Promise((resolve,reject) => {
$.post(superApiPrefix + $user.sessionKey + '/mountManager/setVideosDir',{
localPath,
pathInside
},function(data){
resolve(data)
})
})
}
function launchSetVideoDirConfirm(localPath){
$.confirm.create({
title: lang['Set New Videos Directory'],
body: `<b>${lang['Mount Path']} : ${localPath}</b><br>${lang.setVideosDirWarning} ${lang.restartRequired}<br><br><input placeholder="${lang['Path Inside Mount']}" class="form-control" id="newVideosDirInnerPath">`,
clickOptions: {
class: 'btn-success',
title: lang.Save,
},
clickCallback: async function(){
const pathInside = $('#newVideosDirInnerPath').val().trim();
const response = await setVideosDir(localPath, pathInside);
if(response.ok){
new PNotify({
title: lang['New Videos Directory Set'],
text: lang.restartRequired,
type: 'success'
})
}else{
new PNotify({
title: lang['Action Failed'],
text: lang['See System Logs'],
type: 'danger'
})
}
}
})
}
newMountForm.submit(async function(e){
e.preventDefault();
const form = newMountForm.serializeObject();
$.each(form, function(key,val){form[key] = val.trim()});
const response = await addMount(form);
const notify = {
title: lang['Mount Added'],
text: lang.mountAddedText,
type: 'success'
}
if(!response.ok){
notify.title = lang['Failed to Add Mount']
notify.text = response.error
notify.type = 'danger'
}else{
const theMount = response.mount
const mountId = getMountId(theMount);
theTable.find(`[row-mounted="${mountId}"]`).remove()
loadedMounts[mountId] = theMount;
drawMountToTable(theMount);
}
new PNotify(notify)
return false;
});
theTable.on('click','.delete', async function(e){
const el = $(this).parents('[row-mounted]')
const mountId = el.attr('row-mounted');
const theMount = loadedMounts[mountId]
const localPath = theMount.mountPoint
$.confirm.create({
title: lang['Delete Mount'],
body: `<b>${lang['Mount Path']} : ${localPath} (${theMount.type})</b><br><small>${theMount.device}</small><br>${lang.setVideosDirWarning}`,
clickOptions: {
class: 'btn-danger',
title: lang.Delete,
},
clickCallback: async function(){
const response = await removeMount(localPath);
if(response.ok){
el.remove()
}else{
new PNotify({
title: lang['Failed to Remove Mount'],
text: lang['See System Logs'],
type: 'danger'
})
}
}
})
})
theTable.on('click','.edit', async function(e){
const el = $(this).parents('[row-mounted]')
const mountId = el.attr('row-mounted');
const theMount = loadedMounts[mountId]
newMountForm.find('[name="sourceTarget"]').val(theMount.device)
newMountForm.find('[name="localPath"]').val(theMount.mountPoint)
newMountForm.find('[name="mountType"]').val(theMount.type)
newMountForm.find('[name="options"]').val(theMount.options)
})
theTable.on('click','.setVideosDir', function(e){
const el = $(this).parents('[row-mounted]')
const mountId = el.attr('row-mounted');
const theMount = loadedMounts[mountId]
const localPath = theMount.mountPoint
launchSetVideoDirConfirm(localPath)
})
theEnclosure.on('click','.setDefaultVideosDir', function(e){
launchSetVideoDirConfirm('__DIR__/videos')
})
theSearch.keydown(function(){
const value = $(this).val().trim()
filterMountsTable(value)
})
onInitSuccess(loadMounts)
})

View File

@ -39,6 +39,7 @@
<script src="<%-window.libURL%>assets/js/bs5.schedules.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.liveGrid.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.liveGrid.cycle.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.liveGrid.gamepad.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.liveGrid.keyboard.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.regionEditor.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.timelapseViewer.js"></script>

View File

@ -6,7 +6,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title><%-pageTitle%></title>
<%- include('header-title'); %>
<meta charset="utf-8">
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no' name='viewport' />
<meta name="description" content="Shinobi">
@ -70,7 +70,7 @@
%>
<script>
window.getAdminApiPrefix = function(piece){
return "<%=originalURL%><%= config.webPaths.adminApiPrefix %><%- $user.auth_token %>/"
return "<%= config.webPaths.adminApiPrefix %><%- $user.auth_token %>/"
}
</script>
<% customAutoLoad.LibsCss.forEach(function(lib){ %>

View File

@ -2,9 +2,9 @@
const showMonitors = define.SideMenu.showMonitors; %>
<nav id="<%- menuInfo.id || 'sidebarMenu' %>" class="<%- menuInfo.class || 'col-md-3 col-lg-2 d-md-block sidebar collapse' %>">
<div id="menu-side" class="position-sticky">
<div class="align-items-center d-flex flex-row my-3 m-3 btn-default rounded shadow-sm cursor-pointer" style="overflow: hidden;">
<div class="align-items-center d-flex flex-row my-3 m-3 btn-default rounded shadow-sm cursor-pointer" style="overflow: hidden;border-bottom: 2px solid <%- config.userHasSubscribed ? '#1f80f9' : 'red' %>;">
<div class="align-items-center mr-3 py-3 pl-3">
<div class="lh-1 <%- showMonitors ? `toggle-menu-collapse` : '' %>">
<div class="d-sm-none lh-1 <%- showMonitors ? `toggle-menu-collapse` : '' %>">
<img src="<%- window.libURL + config.logoLocation196x196 %>" width="42" style="<%- config.logoLocation76x76Style %>">
</div>
</div>

View File

@ -0,0 +1,31 @@
<div class="tab-pane text-left" id="centralManagement" role="tabpanel">
<form>
<div class="row" style="display:flex">
<div class="col-md-12">
<div class="card bg-dark text-white mb-4">
<div class="card-header">
<%- lang['Central Management'] %>
</div>
<div class="card-body">
<%
if(!config.enableMgmtConnect || !config.userHasSubscribed){ %>
<div class="text-center"><%- lang.centralManagementNotEnabled %></div>
<% }else{ %>
<div class="form-group">
<input placeholder="<%-lang['Server URL']%>" class="form-control btn btn-dark text-left text-white" type="text" name="managementServer" value="<%- config.managementServer %>" placeholder="ws://127.0.0.1:8663">
</div>
<div class="form-group">
<input placeholder="<%-lang['P2P API Key']%>" class="form-control btn btn-dark text-left text-white" type="text" name="peerConnectKey" value="<%- config.peerConnectKey %>" placeholder="API Key from https://licenses.shinobi.video">
</div>
<div class="btn-group d-flex flex-row">
<a href="#" class="submit flex-grow-1 btn btn-success"><i class="fa fa-check"></i> <%- lang.Save %></a>
</div>
<% }
%>
</div>
</div>
</div>
</div>
</form>
<script src="<%-window.libURL%>assets/js/super.centralManagement.js" type="text/javascript"></script>
</div>

View File

@ -0,0 +1,47 @@
<div class="row">
<div class="col-md-12">
<form class="card bg-dark grey mt-1" id="mountManagerNewMount">
<div class="card-header">
<%- lang['Add Mount'] %>
</div>
<div class="card-body">
<div class="form-group">
<label><%- lang.Source %></label>
<input type="text" placeholder="//192.168.1.200/shared/folder" class="form-control" name="sourceTarget" />
</div>
<div class="form-group">
<label><%- lang['Mount Path'] %></label>
<input type="text" placeholder="/mnt/newmount" class="form-control" name="localPath" />
</div>
<div class="form-group">
<label><%- lang['Mount Type'] %></label>
<select type="text" class="form-control" name="mountType">
<option value="cifs" selected>CIFS</option>
<option value="nfs">NFS</option>
<option value="ext4">ext4</option>
</select>
</div>
<div class="form-group">
<label><%- lang.Options %></label>
<input type="text" placeholder="username=username,password=password,rw,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0" value="username=username,password=password,rw,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0" class="form-control" name="options" />
</div>
<div><button type="submit" class="btn btn-round btn-block btn-default mb-0"><i class="fa fa-check"></i> <%- lang.Save %></button></div>
</div>
</form>
</div>
</div>
<div class="pt-4 pb-0 m-0">
<div id="mountManagerList">
<div class="form-group">
<a class="btn btn-block btn-info setDefaultVideosDir"><%- lang['Use Default Videos Directory'] %></a>
</div>
<div class="form-group">
<input id="mountManagerListSearch" type="text" placeholder="<%- lang.Search %>" class="form-control" />
</div>
<table class="table table-striped" id="mountManagerListTable">
<tbody></tbody>
</table>
</div>
</div>
<link rel="stylesheet" href="<%-window.libURL%>assets/css/super.mountManager.css">
<script src="<%-window.libURL%>assets/js/super.mountManager.js" type="text/javascript"></script>

View File

@ -1,5 +1,4 @@
<div class="row">
<div class="col-md-6">
<div class="card bg-dark grey mt-1">
<div class="card-header">

View File

@ -18,7 +18,10 @@
var hasGUI = getAddon('gui')
var isFullscreen = getAddon('fullscreen')
var isRelativeUrl = getAddon('relative')
if(forceUrlPrefix){
var hasHost = getAddon('host')
if(hasHost){
urlPrefix = decodeURI(hasHost)
}else if(forceUrlPrefix){
urlPrefix = forceUrlPrefix
}else if(isRelativeUrl){
urlPrefix = ''

View File

@ -1,6 +1,4 @@
<% var pageTitle = lang.Shinobi %>
<%- include('blocks/header', {pageTitle: pageTitle}); %>
<%- include('blocks/header'); %>
<%- include('blocks/header-loggedIn'); %>
<!-- Page -->
<%- include('blocks/home'); %>

View File

@ -89,6 +89,17 @@
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#superPluginManager" role="tab"><%-lang['Plugin Manager']%></a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#superMountManager" role="tab"><%-lang['Mount Manager']%></a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#centralManagement" role="tab"><%-lang['Central Management']%></a>
</li>
<% customAutoLoad.superPageTabs.forEach(function(block){ %>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#<%- block.target %>" role="tab"><%- block.label %></a>
</li>
<% }) %>
</ul>
<div class="card-body text-white" style="background:#263343">
<!-- Tab panes -->
@ -125,6 +136,10 @@
<div class="tab-pane text-left" id="superPluginManager" role="tabpanel">
<%- include('blocks/superPluginManager'); %>
</div>
<div class="tab-pane text-left" id="superMountManager" role="tabpanel">
<%- include('blocks/superMountManager'); %>
</div>
<%- include('blocks/superCentralManagement'); %>
<% customAutoLoad.superPageBlocks.forEach(function(block){ %>
<%- include(block) %>
<% }) %>
@ -220,7 +235,10 @@ switch($user.lang){
$.ccio.ws = io(`${location.origin.split('/super')[0]}/`,{
path : tool.checkCorrectPathEnding(location.pathname)+'socket.io'
});
const onInitSuccessExtensions = [];
function onInitSuccess(theAction){
onInitSuccessExtensions.push(theAction)
}
$.ccio.cx=function(x){return $.ccio.ws.emit('super',x)}
$.ccio.ws.on('connect',function(d){
$.ccio.cx({f:'init',mail:$user.mail,pass:$user.pass,machineId: `<%- config.machineId %>`})
@ -232,6 +250,9 @@ $.ccio.ws.on('f',function(d){
drawUserList()
drawSystemLogs()
drawSystemInfo()
onInitSuccessExtensions.forEach((theAction) => {
theAction()
})
break;
case'log':
$.ccio.tm(4,d.log,'#logs-list')

View File

@ -36,10 +36,11 @@
var originalURL = `${urlPrefix}`
%>
<%- include('blocks/header-favicon') %>
<script>window.lang=<%- JSON.stringify(lang) %>;</script>
<script>window.$user=<%- JSON.stringify($user) %>;</script>
<script>window.urlPrefix = "<%- urlPrefix || '' %>";</script>
<script>window.groupKey = "<%- groupKey || '' %>";</script>
<script>window.authKey = "<%- authKey || '' %>";</script>
<script>window.groupKey = "<%- groupKey || '' %>";$user.ke = groupKey;</script>
<script>window.authKey = "<%- authKey || '' %>";$user.auth_token = authKey;</script>
<script>window.userHasSubscribed = <%- config.userHasSubscribed ? 'true' : 'false' %>;</script>
<script src="<%- urlPrefix %>assets/vendor/js/socket.io.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/jquery.min.js"></script>
@ -59,6 +60,9 @@
</div>
<div id="wallview-controls" class="text-end">
<a class="btn btn-primary wallview-cycle-control wallview-cycle-back" style="display:none" href="#"><i class="fa fa-arrow-left"></i></a>
<a class="btn btn-primary wallview-cycle" href="#"><%- lang['Cycle'] %></a>
<a class="btn btn-primary wallview-cycle-control wallview-cycle-front" style="display:none" href="#"><i class="fa fa-arrow-right"></i></a>
<a class="btn btn-primary wallview-open-all" href="#"><%- lang['Open All'] %></a>
<a class="btn btn-danger wallview-close-all" href="#"><%- lang['Close All'] %></a>
<a class="btn btn-success wallview-autoplace" href="#"><%- lang['Auto Placement'] %></a>
@ -73,16 +77,27 @@
<% }) %>
</ul>
</div>
<div class="dropdown d-inline-block">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="monitorListButton" data-bs-toggle="dropdown" aria-expanded="false">
<%- lang.Monitors %>
</a>
<ul id="wallview-monitorList" class="dropdown-menu" aria-labelledby="monitorListButton"></ul>
<a class="btn btn-secondary wallview-toggle-monitor-list" href="#"><%- lang.Monitors %></a>
</div>
<div id="wallview-monitorList-container" class="d-none search-parent">
<div class="p-2">
<a class="btn btn-success d-block wallview-autoplace" href="#"><%- lang['Auto Placement'] %></a>
</div>
<div class="px-2 pb-2">
<input class="form-control search-controller" placeholder="<%- lang.Search %>">
</div>
<div id="wallview-monitorList" class="px-2 pb-2 search-body">
</div>
</div>
</div>
<!-- <script src="<%- urlPrefix %>assets/js/bs5.embed.utils.js"></script> -->
<script src="<%- urlPrefix %>assets/js/bs5.embed.utils.js"></script>
<script src="<%- urlPrefix %>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/pnotify.custom.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/jquery-ui.min.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.extenders.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.websocket.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.wallview.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.wallview.gamepad.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.wallview.cycle.js"></script>