Merge branch 'master' into add_mqtt

This commit is contained in:
Isaac Connor
2022-08-25 18:37:54 -04:00
244 changed files with 16940 additions and 4456 deletions

View File

@@ -2,7 +2,7 @@ task:
name: freebsd-build
freebsd_instance:
matrix:
- image_family: freebsd-12-2
- image_family: freebsd-12-3
- image_family: freebsd-13-0
prepare_script:

6
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -0,0 +1,11 @@
FROM centos:7
LABEL name="centos7-gcc8-zm" \
version="1"
RUN yum -y install https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \
yum -y install https://repo.ius.io/ius-release-el7.rpm && yum -y install git236 && \
yum -y update && yum -y install make cmake3 gcc-c++ mariadb-devel ffmpeg-devel libcurl-devel vlc-devel libvncserver-devel libjpeg-turbo-devel "perl(Date::Manip)" "perl(DBD::mysql)" "perl(ExtUtils::MakeMaker)" "perl(Sys::Mmap)" "perl(Sys::Syslog)" "perl(LWP::UserAgent)" polkit-devel libjwt-devel && \
yum -y install centos-release-scl-rh && \
INSTALL_PKGS="devtoolset-8-gcc devtoolset-8-gcc-c++" && \
yum -y install --setopt=tsflags=nodocs $INSTALL_PKGS

View File

@@ -7,6 +7,9 @@ on:
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
build:
defaults:
@@ -31,14 +34,14 @@ jobs:
add-apt-repository ppa:git-core/ppa
apt-get -qq update
apt-get -qq install git
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install dependencies
run: >
apt-get -qq install make cmake g++
default-libmysqlclient-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libavdevice-dev
libcurl4-gnutls-dev libvlc-dev libvncserver-dev
libdate-manip-perl libdbd-mysql-perl libsys-mmap-perl libwww-perl
libpolkit-gobject-1-dev

View File

@@ -7,6 +7,9 @@ on:
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
build:
defaults:
@@ -34,14 +37,14 @@ jobs:
run: apt-get -qq update && apt-get -qq upgrade
- name: Install git
run: apt-get -qq install git
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install dependencies
run: >
apt-get -qq install make cmake g++
default-libmysqlclient-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libavdevice-dev
libcurl4-gnutls-dev libvlc-dev libvncserver-dev
libdate-manip-perl libdbd-mysql-perl libsys-mmap-perl libwww-perl
libpolkit-gobject-1-dev

View File

@@ -7,6 +7,9 @@ on:
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
build:
defaults:
@@ -34,14 +37,14 @@ jobs:
run: apt-get -qq update && apt-get -qq upgrade
- name: Install git
run: apt-get -qq install git
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install dependencies
run: >
apt-get -qq install make cmake g++
default-libmysqlclient-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libavdevice-dev
libcurl4-gnutls-dev libvlc-dev libvncserver-dev
libdate-manip-perl libdbd-mysql-perl libsys-mmap-perl libwww-perl
libpolkit-gobject-1-dev

View File

@@ -7,6 +7,10 @@ on:
pull_request:
branches: [ master ]
permissions:
contents: read
packages: read
jobs:
build:
strategy:
@@ -19,21 +23,15 @@ jobs:
- crypto_backend: gnutls
jwt_backend: libjwt
runs-on: ubuntu-latest
container: centos:7
container: ghcr.io/dougnazar/centos7-gcc8-zm:latest
steps:
- name: Enable RPMFusion and EPEL
run: yum -y install https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
- name: Install git
run: yum -y install https://repo.ius.io/ius-release-el7.rpm && yum -y install git224
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install dependencies
run: yum -y update && yum -y install make cmake3 gcc-c++ mariadb-devel ffmpeg-devel libcurl-devel vlc-devel libvncserver-devel libjpeg-turbo-devel "perl(Date::Manip)" "perl(DBD::mysql)" "perl(ExtUtils::MakeMaker)" "perl(Sys::Mmap)" "perl(Sys::Syslog)" "perl(LWP::UserAgent)" polkit-devel libjwt-devel
- name: Prepare
run: mkdir build
- name: Configure
run: cd build && cmake3 --version && cmake3 .. -DBUILD_MAN=0 -DENABLE_WERROR=1 -DZM_CRYPTO_BACKEND=${{ matrix.crypto_backend }} -DZM_JWT_BACKEND=${{ matrix.jwt_backend }}
run: source /usr/bin/scl_source enable devtoolset-8 && cd build && cmake3 --version && cmake3 .. -DBUILD_MAN=0 -DENABLE_WERROR=1 -DZM_CRYPTO_BACKEND=${{ matrix.crypto_backend }} -DZM_JWT_BACKEND=${{ matrix.jwt_backend }}
- name: Build
run: cd build && make -j3 | grep --line-buffered -Ev '^(cp lib\/|Installing.+\.pm)' && (exit ${PIPESTATUS[0]})
run: source /usr/bin/scl_source enable devtoolset-8 && cd build && make -j3 | grep --line-buffered -Ev '^(cp lib\/|Installing.+\.pm)' && (exit ${PIPESTATUS[0]})

View File

@@ -7,6 +7,9 @@ on:
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
build:
strategy:
@@ -26,7 +29,7 @@ jobs:
run: yum -y install "dnf-command(config-manager)" https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && yum config-manager --set-enabled powertools
- name: Install git
run: yum -y install git
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install dependencies

View File

@@ -7,12 +7,15 @@ on:
pull_request:
branches: [ master ]
permissions:
contents: read
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install ESLint

View File

@@ -1,41 +0,0 @@
name: CI Debian Stretch
on:
push:
branches:
- '*'
pull_request:
branches: [ master ]
jobs:
build:
defaults:
run:
shell: bash
runs-on: ubuntu-latest
container: debian:stretch-backports
steps:
- name: Update packages
run: apt-get -qq update && apt-get -qq upgrade
- name: Install git
run: apt-get -qq install git/stretch-backports git-man/stretch-backports
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Install dependencies
run: >
apt-get -qq install make cmake g++
default-libmysqlclient-dev
libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libavdevice-dev
libcurl4-gnutls-dev libvlc-dev libvncserver-dev
libdate-manip-perl libdbd-mysql-perl libsys-mmap-perl libwww-perl
libpolkit-gobject-1-dev
libssl-dev
libgsoap-dev
- name: Prepare
run: mkdir build
- name: Configure
run: cd build && cmake --version && cmake .. -DBUILD_MAN=0 -DENABLE_WERROR=1
- name: Build
run: cd build && make -j3 | grep --line-buffered -Ev '^(cp lib\/|Installing.+\.pm)' && (exit ${PIPESTATUS[0]})

View File

@@ -14,8 +14,15 @@ on:
schedule:
- cron: '0 3 * * 5'
permissions:
contents: read
jobs:
analyze:
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/autobuild to send a status report
name: Analyze
runs-on: ubuntu-latest
@@ -30,7 +37,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -38,7 +45,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
@@ -52,7 +59,7 @@ jobs:
git submodule init
git submodule update --init --recursive
sudo apt-get update
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libjwt-gnutls-dev
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libjwt-gnutls-dev libavdevice-dev
sudo apt-get install libbz2-dev libcurl4-gnutls-dev libjpeg-turbo8-dev libturbojpeg0-dev
sudo apt-get install default-libmysqlclient-dev libpcre3-dev libpolkit-gobject-1-dev libv4l-dev libvlc-dev
sudo apt-get install libdate-manip-perl libdbd-mysql-perl libphp-serialization-perl libsys-mmap-perl
@@ -61,7 +68,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl-
@@ -76,4 +83,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -3,8 +3,6 @@ name: Create packages
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
package:
@@ -15,10 +13,14 @@ jobs:
dist: buster
- os: debian
dist: bullseye
- os: ubuntu
dist: bionic
- os: ubuntu
dist: focal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: '0'
submodules: recursive

14
.github/workflows/depsreview.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2

39
.github/workflows/release-packages.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Create Debian Release Packages
on:
release:
types: [ published ]
jobs:
package:
strategy:
matrix:
os_dist:
- os: debian
dist: buster
- os: debian
dist: bullseye
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: '0'
submodules: recursive
- name: Run packpack
env:
SMPFLAGS: -j4
OS: ${{ matrix.os_dist.os }}
DIST: ${{ matrix.os_dist.dist }}
DOCKER_REPO: iconzm/packpack
run: utils/packpack/startpackpack.sh
- name: Publish
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.ZMREPO_SSH_KEY }}
ARGS: "-rltgoDzvO"
SOURCE: build/
REMOTE_HOST: ${{ secrets.ZMREPO_HOST }}
REMOTE_USER: ${{ secrets.ZMREPO_SSH_USER }}
TARGET: debian/release-1.36/mini-dinstall/incoming/

View File

@@ -5,13 +5,28 @@ default:
- apt-get update -yq
- DEBIAN_FRONTEND=noninteractive apt-get install -yq devscripts sudo
deb:
ubuntu_deb:
stage: build
tags:
- docker
script:
- DEBIAN_FRONTEND=noninteractive TZ=America/Chicago apt-get -y install tzdata
- yes "" | ./utils/do_debian_package.sh --snapshot=stable --type=binary --interactive=no --dput=no --debbuild-extra=--no-sign || true
timeout: 2h
timeout: 3h
artifacts:
paths:
- '*.deb'
expire_in: 1 week
debian_deb:
stage: build
tags:
- docker
image:
name: debian:latest
script:
- yes "" | ./utils/do_debian_package.sh --snapshot=stable --type=binary --interactive=no --dput=no --debbuild-extra=--no-sign || true
timeout: 3h
artifacts:
paths:
- '*.deb'

View File

@@ -40,6 +40,10 @@ set(CMAKE_C_FLAGS_OPTIMISED "-O3")
set(CMAKE_CXX_FLAGS_OPTIMISED "-O3")
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
add_compile_options(-D_FILE_OFFSET_BITS=64)
include(ConfigureBaseTargets)
include(CheckPlatform)
@@ -86,6 +90,7 @@ mark_as_advanced(
ZM_PATH_ARP_SCAN
ZM_CONFIG_DIR
ZM_CONFIG_SUBDIR
ZM_DETECT_SYSTEMD
ZM_SYSTEMD
ZM_MANPAGE_DEST_PREFIX)
@@ -186,6 +191,8 @@ set(ZM_PERL_SEARCH_PATH "" CACHE PATH
installed outside Perl's default search path.")
set(ZM_TARGET_DISTRO "" CACHE STRING
"Build ZoneMinder for a specific distribution. Currently, valid names are: fc27, fc26, el7, OS13, FreeBSD")
set(ZM_DETECT_SYSTEMD "ON" CACHE BOOL
"Set to OFF to disable detection of systemd. default: ON")
set(ZM_SYSTEMD "OFF" CACHE BOOL
"Set to ON to force building ZM with systemd support. default: OFF")
set(ZM_MANPAGE_DEST_PREFIX "share/man" CACHE PATH
@@ -268,7 +275,7 @@ set(CMAKE_EXTRA_INCLUDE_FILES ${CMAKE_EXTRA_INCLUDE_FILES} stdio.h stdlib.h math
set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS ON)
# Set the systemd flag if systemd is autodetected or ZM_SYSTEMD has been set
if(ZM_SYSTEMD OR (IS_DIRECTORY /usr/lib/systemd/system) OR (IS_DIRECTORY /lib/systemd/system))
if(ZM_SYSTEMD OR (ZM_DETECT_SYSTEMD AND ((IS_DIRECTORY /usr/lib/systemd/system) OR (IS_DIRECTORY /lib/systemd/system))))
set(WITH_SYSTEMD 1)
endif()
@@ -474,6 +481,7 @@ endif()
find_package(FFMPEG 55.34.100 REQUIRED
COMPONENTS
avcodec
avdevice
avformat
avutil
swresample

View File

@@ -53,3 +53,6 @@ ZM_PATH_ARP_SCAN="@ZM_PATH_ARP_SCAN@"
#Full path to shutdown binary
ZM_PATH_SHUTDOWN="@ZM_PATH_SHUTDOWN@"
# Path to FFmpeg binary
ZM_PATH_FFMPEG="@FFMPEG_EXECUTABLE@"

1
db/monitors_dbupdate.sql Normal file
View File

@@ -0,0 +1 @@
ALTER TABLE zm.Monitors ADD Onvif_Alarm_Txt varchar(30) DEFAULT 'MotionAlarm' NULL;

View File

@@ -40,6 +40,7 @@ CREATE TABLE `Config` (
`Category` varchar(32) NOT NULL default '',
`Readonly` tinyint(3) unsigned NOT NULL default '0',
`Private` BOOLEAN NOT NULL DEFAULT FALSE,
`System` BOOLEAN NOT NULL DEFAULT FALSE,
`Requires` text,
PRIMARY KEY (`Name`)
) ENGINE=@ZM_MYSQL_ENGINE@;
@@ -457,11 +458,15 @@ CREATE TABLE `Monitors` (
`Capturing` enum('None','Ondemand', 'Always') NOT NULL default 'Always',
`Analysing` enum('None','Always') NOT NULL default 'Always',
`AnalysisSource` enum('Primary','Secondary') NOT NULL DEFAULT 'Primary',
`AnalysisImage` enum('FullColour','YChannel') NOT NULL DEFAULT 'FullColour',
`Recording` enum('None', 'OnMotion', 'Always') NOT NULL default 'Always',
`Enabled` tinyint(3) unsigned NOT NULL default '1',
`DecodingEnabled` tinyint(3) unsigned NOT NULL default '1',
`Decoding` enum('None','Ondemand','KeyFrames','KeyFrames+Ondemand', 'Always') NOT NULL default 'Always',
`JanusEnabled` BOOLEAN NOT NULL default false,
`JanusAudioEnabled` BOOLEAN NOT NULL default false,
`Janus_Profile_Override` VARCHAR(30) NOT NULL DEFAULT '',
`Janus_Use_RTSP_Restream` BOOLEAN NOT NULL default false,
`LinkedMonitors` varchar(255),
`Triggers` set('X10') NOT NULL default '',
`EventStartCommand` VARCHAR(255) NOT NULL DEFAULT '',
@@ -471,6 +476,7 @@ CREATE TABLE `Monitors` (
`ONVIF_Password` VARCHAR(64) NOT NULL DEFAULT '',
`ONVIF_Options` VARCHAR(64) NOT NULL DEFAULT '',
`ONVIF_Event_Listener` BOOLEAN NOT NULL DEFAULT FALSE,
`ONVIF_Alarm_Text` VARCHAR(30) DEFAULT 'MotionAlarm',
`use_Amcrest_API` BOOLEAN NOT NULL DEFAULT FALSE,
`Device` tinytext NOT NULL default '',
`Channel` tinyint(3) unsigned NOT NULL default '0',
@@ -499,7 +505,7 @@ CREATE TABLE `Monitors` (
`VideoWriter` TINYINT NOT NULL DEFAULT '0',
`OutputCodec` int(10) unsigned NOT NULL default 0,
`Encoder` varchar(32),
`OutputContainer` enum('auto','mp4','mkv'),
`OutputContainer` enum('auto','mp4','mkv','webm'),
`EncoderParameters` TEXT,
`RecordAudio` TINYINT NOT NULL DEFAULT '0',
`RecordingSource` enum('Primary','Secondary','Both') NOT NULL DEFAULT 'Primary',
@@ -665,7 +671,7 @@ CREATE TABLE `Stats` (
`MaxY` smallint(5) unsigned NOT NULL default '0',
`Score` smallint(5) unsigned NOT NULL default '0',
PRIMARY KEY (`Id`),
KEY `EventId` (`EventId`),
KEY `EventId_ZoneId` (`EventId`, `ZoneId`),
KEY `MonitorId` (`MonitorId`),
KEY `ZoneId` (`ZoneId`)
) ENGINE=@ZM_MYSQL_ENGINE@;
@@ -969,7 +975,7 @@ INSERT INTO `Controls` VALUES (NULL,'IPCC 7210W','Remote','IPCC7210W', 1, 1, 1,
INSERT INTO `Controls` VALUES (NULL,'Vivotek ePTZ','Remote','Vivotek_ePTZ',0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,1,0,5,0,0,1,0,0,0,0,1,0,5,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'Netcat ONVIF','Ffmpeg','Netcat',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,100,5,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,100,5,5,0,0,0,1,255,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'Keekoon','Remote','Keekoon', 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
INSERT INTO `Controls` VALUES (NULL,'HikVision','Local','',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,0,1,1,0,0,0,0,1,1,100,0,0,1,0,0,0,0,1,1,100,1,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'HikVision','Local','HikVision',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,0,1,1,0,0,0,0,1,1,100,0,0,1,0,0,0,0,1,1,100,1,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'Maginon Supra IPC','cURL','MaginonIPC',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'Floureon 1080P','Ffmpeg','Floureon',0,0,0,0,1,0,0,0,1,1,18,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,0,1,1,1,0,0,0,1,1,0,0,0,0,1,1,8,0,0,1,0,0,0,0,1,1,8,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-423','Ffmpeg','Reolink',0,0,1,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,64,1,1,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0);
@@ -1093,6 +1099,7 @@ DROP TABLE IF EXISTS MontageLayouts;
CREATE TABLE MontageLayouts (
`Id` int(10) unsigned NOT NULL auto_increment,
`Name` TEXT NOT NULL,
`UserId` int(10) UNSIGNED NOT NULL default 0,
`Positions` LONGTEXT,
/*`Positions` JSON,*/
PRIMARY KEY (`Id`)

18
db/zm_update-1.36.20.sql Normal file
View File

@@ -0,0 +1,18 @@
--
-- Update MontageLayout table to have UserId
--
SELECT 'Checking for UserId in MontageLayouts';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'MontageLayouts'
AND table_schema = DATABASE()
AND column_name = 'UserId'
) > 0,
"SELECT 'Column UserId already exists in MontageLayouts'",
"ALTER TABLE `MontageLayouts` ADD COLUMN `UserId` int(10) UNSIGNED NOT NULL DEFAULT 0 AFTER `Name`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@@ -52,9 +52,10 @@ SET @s = (SELECT IF(
PREPARE stmt FROM @s;
EXECUTE stmt;
UPDATE `Monitors` SET `Recording` = 'None' WHERE `Function` = 'Monitor';
UPDATE `Monitors` SET `Recording` = 'OnMotion' WHERE `Function` = 'Modect' OR `Function` = 'Nodect';
UPDATE `Monitors` SET `Recording` = 'Always' WHERE `Function` = 'Mocord';
UPDATE `Monitors` SET `Recording`='None', `Analysing`='None' WHERE `Function` = 'Monitor';
UPDATE `Monitors` SET `Recording`='OnMotion', `Analysing`='None' WHERE `Function` = 'Nodect';
UPDATE `Monitors` SET `Recording`='OnMotion' WHERE `Function` = 'Modect';
UPDATE `Monitors` SET `Recording`='Always' WHERE `Function` = 'Mocord';
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()

14
db/zm_update-1.37.13.sql Normal file
View File

@@ -0,0 +1,14 @@
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors'
AND column_name = 'Decoding'
) > 0,
"SELECT 'Column Decoding already exists in Monitors'",
"ALTER TABLE `Monitors` ADD `Decoding` enum('None','Ondemand','KeyFrames','Always') NOT NULL default 'Always' AFTER `DecodingEnabled`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
UPDATE `Monitors` SET `Decoding` = 'None' WHERE `DecodingEnabled` = 0;
UPDATE `Monitors` SET `Decoding` = 'Always' WHERE `DecodingEnabled` != 1;

1
db/zm_update-1.37.14.sql Normal file
View File

@@ -0,0 +1 @@
ALTER TABLE `Monitors` MODIFY `OutputContainer` enum('auto','mp4','mkv','webm') default 'auto';

20
db/zm_update-1.37.15.sql Normal file
View File

@@ -0,0 +1,20 @@
--
-- Update Config table to have System BOOLEAN field
--
SELECT 'Checking for System in Config';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Config'
AND table_schema = DATABASE()
AND column_name = 'System'
) > 0,
"SELECT 'Column System already exists in Config'",
"ALTER TABLE `Config` ADD COLUMN `System` BOOLEAN NOT NULL DEFAULT FALSE AFTER `Private`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

2
db/zm_update-1.37.16.sql Normal file
View File

@@ -0,0 +1,2 @@
/*ALTER TABLE `Monitors` MODIFY `Decoding` enum('None','Ondemand','KeyFrames','Always') NOT NULL default 'Always';*/
ALTER TABLE `Monitors` MODIFY `Decoding` enum('None','Ondemand','KeyFrames','KeyFrames+Ondemand', 'Always') NOT NULL default 'Always';

11
db/zm_update-1.37.17.sql Normal file
View File

@@ -0,0 +1,11 @@
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors'
AND column_name = 'AnalysisImage'
) > 0,
"SELECT 'Column AnalysisImage already exists in Monitors'",
"ALTER TABLE `Monitors` ADD `AnalysisImage` enum('FullColour','YChannel') NOT NULL DEFAULT 'FullColour' AFTER `AnalysisSource`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

18
db/zm_update-1.37.18.sql Normal file
View File

@@ -0,0 +1,18 @@
--
-- Update MontageLayout table to have UserId
--
SELECT 'Checking for UserId in MontageLayouts';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'MontageLayouts'
AND table_schema = DATABASE()
AND column_name = 'UserId'
) > 0,
"SELECT 'Column UserId already exists in MontageLayouts'",
"ALTER TABLE `MontageLayouts` ADD COLUMN `UserId` int(10) UNSIGNED NOT NULL DEFAULT 0 AFTER `Name`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

18
db/zm_update-1.37.19.sql Normal file
View File

@@ -0,0 +1,18 @@
--
-- Update Monitors table to have ONVIF_Alarm_Text
--
SELECT 'Checking for ONVIF_Alarm_Text in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'ONVIF_Alarm_Text'
) > 0,
"SELECT 'Column ONVIF_Alarm_Text already exists in Monitors'",
"ALTER TABLE Monitors ADD Onvif_Alarm_Text varchar(30) DEFAULT 'MotionAlarm' AFTER `ONVIF_Event_Listener`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

37
db/zm_update-1.37.20.sql Normal file
View File

@@ -0,0 +1,37 @@
--
-- Update Monitors Table to include Janus_Profile_Override
--
SELECT 'Checking for Janus_Profile_Override in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'Janus_Profile_Override'
) > 0,
"SELECT 'Column Janus_Profile_Override already exists in Monitors'",
"ALTER TABLE Monitors ADD Janus_Profile_Override varchar(30) DEFAULT '' AFTER `JanusAudioEnabled`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
--
-- Update Monitors Table to include Janus_Use_RTSP_Restream
--
SELECT 'Checking for Janus_Use_RTSP_Restream in Monitors';
SET @s = (SELECT IF(
(SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'Monitors'
AND table_schema = DATABASE()
AND column_name = 'Janus_Use_RTSP_Restream'
) > 0,
"SELECT 'Column Janus_Use_RTSP_Restream already exists in Monitors'",
"ALTER TABLE Monitors ADD Janus_Use_RTSP_Restream BOOLEAN NOT NULL DEFAULT false AFTER `Janus_Profile_Override`"
));
PREPARE stmt FROM @s;
EXECUTE stmt;

View File

@@ -19,6 +19,7 @@
# Newer php's keep json functions in a subpackage
%if 0%{?fedora} || 0%{?rhel} >= 8
%global with_gsoap 1
%global with_php_json 1
%endif
@@ -36,7 +37,7 @@
%global _hardened_build 1
Name: zoneminder
Version: 1.37.12
Version: 1.37.20
Release: 1%{?dist}
Summary: A camera monitoring and analysis tool
Group: System Environment/Daemons
@@ -93,6 +94,7 @@ BuildRequires: libv4l-devel
BuildRequires: desktop-file-utils
BuildRequires: gzip
BuildRequires: zlib-devel
%{?with_gsoap:BuildRequires: gsoap-devel}
# ZoneMinder looks for and records the location of the ffmpeg binary during build
BuildRequires: ffmpeg
@@ -120,10 +122,11 @@ Summary: Common files for ZoneMinder, not tied to a specific web server
Requires: php-mysqli
Requires: php-common
Requires: php-gd
Requires: php-intl
Requires: php-process
%{?with_php_json:Requires: php-json}
%{?fedora:Requires: php-pecl-memcached}
%{?rhel:Requires: php-pecl-apcu}
Requires: cambozola
Requires: net-tools
Requires: psmisc
Requires: polkit
@@ -139,8 +142,10 @@ Requires: perl(MIME::Lite)
Requires: perl(Net::SMTP)
Requires: perl(Net::FTP)
Requires: perl(LWP::Protocol::https)
Requires: perl(Module::Load::Conditional)
Requires: ca-certificates
Requires: zip
%{?with_gsoap:Requires: gsoap}
%{?systemd_requires}
Requires(post): %{_bindir}/gpasswd
@@ -212,7 +217,6 @@ rm -rf ./dep/RtspServer
mv -f RtspServer-%{rtspserver_commit} ./dep/RtspServer
# Change the following default values
./utils/zmeditconfigdata.sh ZM_OPT_CAMBOZOLA yes
./utils/zmeditconfigdata.sh ZM_OPT_CONTROL yes
./utils/zmeditconfigdata.sh ZM_CHECK_FOR_UPDATES no
@@ -224,8 +228,7 @@ mv -f RtspServer-%{rtspserver_commit} ./dep/RtspServer
%cmake \
-DZM_WEB_USER="%{zmuid_final}" \
-DZM_WEB_GROUP="%{zmgid_final}" \
-DZM_TARGET_DISTRO="%{zmtargetdistro}" \
.
-DZM_TARGET_DISTRO="%{zmtargetdistro}"
%cmake_build
@@ -375,6 +378,7 @@ ln -sf %{_sysconfdir}/zm/www/zoneminder.nginx.conf %{_sysconfdir}/zm/www/zonemin
%{_bindir}/zmonvif-trigger.pl
%{_bindir}/zmstats.pl
%{_bindir}/zmrecover.pl
%{_bindir}/zmeventtool.pl
%{_bindir}/zm_rtsp_server
%{perl_vendorlib}/ZoneMinder*

View File

@@ -5,6 +5,7 @@ Maintainer: Isaac Connor <isaac@zoneminder.com>
Build-Depends: debhelper (>= 11), sphinx-doc, python3-sphinx, dh-linktree, dh-apache2
,cmake
,libavcodec-dev
,libavdevice-dev
,libavformat-dev
,libavutil-dev
,libswresample-dev
@@ -39,6 +40,7 @@ Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,sudo
,javascript-common
,libavdevice58|libavdevice57
,libswscale5|libswscale4
,libswresample3|libswresample2
,ffmpeg
@@ -67,7 +69,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,libnumber-bytes-human-perl
,libfile-slurp-perl
,default-mysql-client | mariadb-client | virtual-mysql-client
,php-mysql, php-gd, php-apcu, php-apc | php-apcu-bc, php-json
,php-mysql, php-gd, php-apcu, php-json, php-intl
,policykit-1
,rsyslog | system-log-daemon
,zip
@@ -75,7 +77,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,libdata-entropy-perl
,libvncclient1|libvncclient0
,libjwt-gnutls0|libjwt0
,libgsoap-2.8.104|libgsoap-2.8.91|libgsoap-2.8.75|libgsoap-2.8.60|libgsoap10
,libgsoap-2.8.117|libgsoap-2.8.104|libgsoap-2.8.91|libgsoap-2.8.75|libgsoap-2.8.60|libgsoap10
Recommends: ${misc:Recommends}
,libapache2-mod-php | php-fpm
,default-mysql-server | mariadb-server | virtual-mysql-server

View File

@@ -24,7 +24,7 @@ Now your terminal session is back under your normal user. You can check that
you are now part of the sudo group with the command ``groups``, "sudo" should
appear in the list. If not, run ``newgrp sudo`` and check again with ``groups``.
**Step 2:** Update system and install zoneminder
**Step 2:** Update system
Run the following commands.
@@ -32,12 +32,45 @@ Run the following commands.
sudo apt update
sudo apt upgrade
**Step 3:** Install MariaDB and do initial database configuration
Run the following commands.
::
sudo apt install mariadb-server
sudo apt install zoneminder
Switch into root user and create database and database user
::
sudo su
mariadb
CREATE DATABASE zm;
CREATE USER zmuser@localhost IDENTIFIED BY 'zmpass';
GRANT ALL ON zm.* TO zmuser@localhost;
FLUSH PRIVILEGES;
exit;
exit
By default MariaDB uses `unix socket authentication`_, so no root user password is required (root MariaDB user access only available to local root Linux user). If you wish, you can set a root MariaDB password (and apply other security tweaks) by running `mariadb-secure-installation`_.
**Step 3:** Setup permissions for zm.conf
**Step 4:** Install zoneminder
Run the following commands.
::
sudo apt install zoneminder
**Step 5:** Configure database
::
mariadb -u zmuser -pzmpass < /usr/share/zoneminder/db/zm_create.sql
**Step 6:** Setup permissions for zm.conf
To make sure zoneminder can read the configuration file, run the following command.
@@ -45,7 +78,17 @@ To make sure zoneminder can read the configuration file, run the following comma
sudo chgrp -c www-data /etc/zm/zm.conf
Congratulations! You should now be able to access zoneminder at ``http://yourhostname/zm``
**Step 7:** Tweak Apache configuration
::
sudo a2enconf zoneminder
sudo systemctl reload apache2.service
sudo systemctl reload zoneminder.service
sudo systemctl restart zoneminder.service
sudo systemctl status zoneminder.service
If the zoneminder.service show to be active and without any errors, you should be able to access zoneminder at ``http://yourhostname/zm``
Easy Way: Debian Buster
------------------------

View File

@@ -29,6 +29,7 @@
<Directory "@WEB_PREFIX@">
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
<IfModule mod_authz_core.c>
# Apache 2.4
Require all granted
@@ -40,6 +41,7 @@
</IfModule>
</Directory>
# Remember to enable cgi mod (i.e. "a2enmod cgi").
ScriptAlias /cgi-bin "@CGI_PREFIX@"
<Directory "@CGI_PREFIX@">
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch

View File

@@ -19,6 +19,7 @@ configure_file(zmvideo.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmvideo.pl" @ONLY)
configure_file(zmwatch.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmwatch.pl" @ONLY)
configure_file(zmstats.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmstats.pl" @ONLY)
configure_file(zmcamtool.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmcamtool.pl" @ONLY)
configure_file(zmeventtool.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmeventtool.pl" @ONLY)
configure_file(zmsystemctl.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmsystemctl.pl" @ONLY)
configure_file(zmtelemetry.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmtelemetry.pl" @ONLY)
if(NOT ZM_NO_X10)
@@ -54,6 +55,7 @@ install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/zmvideo.pl"
"${CMAKE_CURRENT_BINARY_DIR}/zmwatch.pl"
"${CMAKE_CURRENT_BINARY_DIR}/zmcamtool.pl"
"${CMAKE_CURRENT_BINARY_DIR}/zmeventtool.pl"
"${CMAKE_CURRENT_BINARY_DIR}/zmtelemetry.pl"
"${CMAKE_CURRENT_BINARY_DIR}/zmstats.pl"
DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)

View File

@@ -162,15 +162,13 @@ sub loadConfigFromDB {
my $res = $sth->execute()
or croak( "Can't execute: ".$sth->errstr() );
my $option_count = 0;
while( my $config = $sth->fetchrow_hashref() ) {
while ( my $config = $sth->fetchrow_hashref() ) {
my ( $name, $value ) = ( $config->{Name}, $config->{Value} );
#print( "Name = '$name'\n" );
my $option = $options_hash{$name};
my $option = $ZoneMinder::ConfigData::options_hash{$name};
if ( !$option ) {
warn( "No option '$name' found, removing.\n" );
next;
}
#next if ( $option->{category} eq 'hidden' );
if ( defined($value) ) {
if ( $option->{type} == $types{boolean} ) {
$option->{value} = $value?'yes':'no';
@@ -178,11 +176,11 @@ sub loadConfigFromDB {
$option->{value} = $value;
}
}
$option_count++;;
$option_count++;
}
$sth->finish();
print( " $option_count entries\n" );
return( $option_count );
return $option_count;
} # end sub loadConfigFromDB
sub saveConfigToDB {
@@ -203,7 +201,7 @@ sub saveConfigToDB {
my $res = $dbh->do( $sql )
or croak( "Can't do '$sql': ".$dbh->errstr() );
$sql = "replace into Config set Id = ?, Name = ?, Value = ?, Type = ?, DefaultValue = ?, Hint = ?, Pattern = ?, Format = ?, Prompt = ?, Help = ?, Category = ?, Readonly = ?, Private = ?, Requires = ?";
$sql = 'REPLACE INTO Config SET Id=?, Name=?, Value=?, Type=?, DefaultValue=?, Hint=?, Pattern=?, Format=?, Prompt=?, Help=?, Category=?, Readonly=?, Private=?, `System`=?, Requires=?';
my $sth = $dbh->prepare_cached( $sql )
or croak( "Can't prepare '$sql': ".$dbh->errstr() );
foreach my $option ( @options ) {
@@ -241,6 +239,7 @@ sub saveConfigToDB {
$option->{category},
$option->{readonly} ? 1 : 0,
$option->{private} ? 1 : 0,
$option->{system} ? 1 : 0,
$option->{db_requires}
) or croak("Can't execute when updating config entry $$option{name}: ".$sth->errstr() );
} # end foreach option

View File

@@ -229,6 +229,50 @@ our @options = (
type => $types{string},
category => 'system',
},
{
name => 'ZM_LOCALE_DEFAULT',
default => '',
description => 'Default locale used when formatting date/time strings',
help => q`ZoneMinder will default to the system set locale. This
option allows to override it. The locale is used to determine the format
string used when formatting dates and times.
`,
type => $types{string},
category => 'system',
},
{
name => 'ZM_DATE_FORMAT_PATTERN',
default => '',
description => 'Date format override',
help => q`Pattern to use to override the format string used for dates.
Leave it empty to use the defaults for the set locale.
See [unicode-org](https://unicode-org.github.io/icu/userguide/format_parse/datetime/) for values.
`,
type => $types{string},
category => 'system',
},
{
name => 'ZM_TIME_FORMAT_PATTERN',
default => '',
description => 'Time format override',
help => q`Pattern to use to override the format string used for times without dates.
Leave it empty to use the defaults for the set locale.
See [unicode-org](https://unicode-org.github.io/icu/userguide/format_parse/datetime/) for values.
`,
type => $types{string},
category => 'system',
},
{
name => 'ZM_DATETIME_FORMAT_PATTERN',
default => '',
description => 'Date/time format override',
help => q`Pattern to use to override the format string used for dates
and times. Leave it empty to use the defaults for the set locale.
See [unicode-org](https://unicode-org.github.io/icu/userguide/format_parse/datetime/) for values.
`,
type => $types{string},
category => 'system',
},
{
name => 'ZM_OPT_USE_AUTH',
default => 'no',
@@ -736,55 +780,6 @@ our @options = (
type => $types{boolean},
category => 'images',
},
{
name => 'ZM_OPT_CAMBOZOLA',
default => 'no',
description => 'Is the (optional) cambozola java streaming client installed',
help => q`
Cambozola is a handy low fat cheese flavoured Java applet that
ZoneMinder uses to view image streams on browsers such as
Internet Explorer that don't natively support this format. If
you use this browser it is highly recommended to install this
from the [cambozola project site](http://www.charliemouse.com/code/cambozola/).
However, if it is not installed still images at a lower refresh rate can
still be viewed.
`,
type => $types{boolean},
category => 'images',
},
{
name => 'ZM_PATH_CAMBOZOLA',
default => 'cambozola.jar',
description => 'Web path to (optional) cambozola java streaming client',
help => q`
Cambozola is a handy low fat cheese flavoured Java applet that
ZoneMinder uses to view image streams on browsers such as
Internet Explorer that don't natively support this format. If
you use this browser it is highly recommended to install this
from the [cambozola project site](http://www.charliemouse.com/code/cambozola/).
However if it is not installed still images at a lower refresh rate can
still be viewed. Leave this as 'cambozola.jar' if cambozola is
installed in the same directory as the ZoneMinder web client
files.
`,
requires => [ { name=>'ZM_OPT_CAMBOZOLA', value=>'yes' } ],
type => $types{rel_path},
category => 'images',
},
{
name => 'ZM_RELOAD_CAMBOZOLA',
default => '0',
description => 'After how many seconds should Cambozola be reloaded in live view',
help => q`
Cambozola allows for the viewing of streaming MJPEG however it
caches the entire stream into cache space on the computer,
setting this to a number > 0 will cause it to automatically
reload after that many seconds to avoid filling up a hard
drive.
`,
type => $types{integer},
category => 'images',
},
{
name => 'ZM_TIMESTAMP_ON_CAPTURE',
default => 'yes',
@@ -989,13 +984,12 @@ our @options = (
{
name => 'ZM_MIN_RTSP_PORT',
default => '',
description => 'Start of port range to contact for RTSP streaming video.',
description => 'Port used for RTSP streaming video.',
help => q`
The beginng of a port range that will be used to offer
The port that will be used to offer
RTSP streaming of live captured video.
Each monitor will use this value plus the Monitor Id to stream
content. So a value of 2000 here will cause a stream for Monitor 1 to
hit port 2001.`,
Each camera to be streamed must be enabled
under its misc tab.`,
type => $types{integer},
category => 'network',
},
@@ -1056,10 +1050,15 @@ our @options = (
name => 'ZM_PATH_FFMPEG',
default => '@PATH_FFMPEG@',
description => 'Path to (optional) ffmpeg mpeg encoder',
help => 'This path should point to where ffmpeg has been installed.',
help => 'This path should point to where ffmpeg has been installed.~~
~~
Please note this cannot be edited through the Web UI or API.~~
It can only be changed through .conf files in @ZM_CONFIG_SUBDIR@~~
',
requires => [ { name=>'ZM_OPT_FFMPEG', value=>'yes' } ],
type => $types{abs_path},
category => 'images',
system => 1,
},
{
name => 'ZM_FFMPEG_INPUT_OPTIONS',
@@ -3880,6 +3879,53 @@ our @options = (
type => $types{string},
category => 'MQTT',
},
# Add options for Alarm Server
{
name => 'ZM_OPT_USE_ALARMSERVER',
default => 'no',
description => 'Enable NETSurveillance WEB Camera ALARM SERVER',
help => q`
Alarm Server that works with cameras that use Netsurveillance Web Server,
and has the Alarm Server option it receives alarms sent by this cameras
(once enabled), and pass to Zoneminder the events.
It requires pyzm installed, visit https://pyzm.readthedocs.io/en/latest/
for installation instructions.
`,
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_LOGENTRY',
default => 'no',
description => 'Makes ALARM SERVER create a log entry in ZoneMinder on Human Detected',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_ALARM',
default => 'no',
description => 'Send the Human Detected alarm from ALARM SERVER to ZoneMinder, It does not work along with OPT_ALS_TRIGGEREVENT',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_TRIGGEREVENT',
default => 'no',
description => 'Trigger an event on Human Detected alarm from ALARM SERVER to ZoneMinder. Requires the zmTrigger option Enabled',
help => '',
type => $types{boolean},
category => 'system',
},
{
name => 'ZM_OPT_ALS_PORT',
default => '15002',
description => 'Port Number to receive alarms from Alarm Server',
help => '',
type => $types{integer},
category => 'system',
},
);
our %options_hash = map { ( $_->{name}, $_ ) } @options;

View File

@@ -255,16 +255,55 @@ sub moveConDownLeft {
sub moveStop {
my $self = shift;
if ($$self{LastCmd}) {
Debug('Move Stop '.$$self{LastCmd});
$self->sendCmd('cgi-bin/ptz.cgi?action=stop&'.$$self{LastCmd});
$$self{LastCmd} = '';
$$self{Monitor}->resumeMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
if ( substr($$self{LastCmd},0,4) eq 'code' ) {
# last command was a PTZ move
Debug('Move Stop '.$$self{LastCmd});
$self->sendCmd('cgi-bin/ptz.cgi?action=stop&'.$$self{LastCmd});
$$self{LastCmd} = '';
$$self{Monitor}->resumeMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
} elsif ( substr($$self{LastCmd},0,5) eq 'focus' ) {
# last command was a focus adjustment
Debug('focus Stop '.$$self{LastCmd});
$self->sendCmd('cgi-bin/devVideoInput.cgi?action=adjustFocusContinuously&focus=0&zoom=0');
$$self{LastCmd} = '';
$$self{Monitor}->resumeMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
} else {
Debug('focus Stop '.$$self{LastCmd});
Error('Unknown or unaccounted for lastcmd value: ' . $$self{LastCmd});
$$self{LastCmd} = '';
}
} else {
Debug('Move Stop/Center');
$self->sendCmd('cgi-bin/ptz.cgi?action=start&code=PositionABS&channel=0&arg1=0&arg2=0&arg3=0&arg4=1');
}
}
#new focus stuff
sub focusAuto {
my $self = shift;
Debug('Set AutoFocus on');
$self->sendCmd('cgi-bin/devVideoInput.cgi?action=autoFocus');
}
# focusConNear, focusConFar, focusStop is implemented above in sub moveStop
sub focusConFar {
my $self = shift;
Debug('Set Focus far');
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
$$self{LastCmd} = 'code=FocusFar&channel=0&arg1=0&arg2=1&arg3=0';
$self->sendCmd('cgi-bin/ptz.cgi?action=start&'.$$self{LastCmd});
}
sub focusConNear {
my $self = shift;
Debug('Set Focus near');
$$self{Monitor}->suspendMotionDetection() if !$self->{Monitor}->{ModectDuringPTZ};
$$self{LastCmd} = 'code=FocusNear&channel=0&arg1=0&arg2=1&arg3=0';
$self->sendCmd('cgi-bin/ptz.cgi?action=start&'.$$self{LastCmd});
}
# end of new focus stuff
# Move Camera to Home Position
# The current API does not support a Home per se, so we'll just send the camera to preset #1
# NOTE: It goes without saying that the user must have set up preset #1 for this to work.

View File

@@ -163,57 +163,81 @@ sub cameraReset {
sub moveConUp {
my $self = shift;
my $params = shift;
my $panspeed = 0; # purely moving vertically
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 );
Debug('Move Up');
my $cmd = '/axis-cgi/com/ptz.cgi?move=up';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConDown {
my $self = shift;
my $params = shift;
my $panspeed = 0; # purely moving vertically
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 ) * -1 ;
Debug('Move Down');
my $cmd = '/axis-cgi/com/ptz.cgi?move=down';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConLeft {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 ) * -1 ;
my $tiltspeed = 0; # purely moving horizontally
Debug('Move Left');
my $cmd = '/axis-cgi/com/ptz.cgi?move=left';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConRight {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 );
my $tiltspeed = 0; # purely moving horizontally
Debug('Move Right');
my $cmd = '/axis-cgi/com/ptz.cgi?move=right';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConUpRight {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 );
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 );
Debug('Move Up/Right');
my $cmd = '/axis-cgi/com/ptz.cgi?move=upright';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConUpLeft {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 ) * -1;
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 );
Debug('Move Up/Left');
my $cmd = '/axis-cgi/com/ptz.cgi?move=upleft';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
sub moveConDownRight {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 );
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 ) * -1;
Debug('Move Down/Right');
my $cmd = '/axis-cgi/com/ptz.cgi?move=downright';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd( $cmd );
}
sub moveConDownLeft {
my $self = shift;
my $params = shift;
my $panspeed = $self->getParam( $params, 'panspeed', 30 ) * -1;
my $tiltspeed = $self->getParam( $params, 'tiltspeed', 30 ) * -1;
Debug('Move Down/Left');
my $cmd = '/axis-cgi/com/ptz.cgi?move=downleft';
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$panspeed,$tiltspeed";
$self->sendCmd($cmd);
}
@@ -248,7 +272,7 @@ sub moveRelDown {
sub moveRelLeft {
my $self = shift;
my $params = shift;
my $step = $self->getParam($params, 'panstep');
my $step = abs($self->getParam($params, 'panstep'));
Debug("Step Left $step");
my $cmd = '/axis-cgi/com/ptz.cgi?rpan=-'.$step;
$self->sendCmd($cmd);
@@ -276,8 +300,8 @@ sub moveRelUpRight {
sub moveRelUpLeft {
my $self = shift;
my $params = shift;
my $panstep = $self->getParam($params, 'panstep');
my $tiltstep = $self->getParam($params, 'tiltstep');
my $panstep = abs($self->getParam($params, 'panstep'));
my $tiltstep = abs($self->getParam($params, 'tiltstep'));
Debug("Step Up/Left $tiltstep/$panstep");
my $cmd = "/axis-cgi/com/ptz.cgi?rpan=-$panstep&rtilt=$tiltstep";
$self->sendCmd($cmd);
@@ -303,6 +327,47 @@ sub moveRelDownLeft {
$self->sendCmd($cmd);
}
sub zoomConTele {
my $self = shift;
my $params = shift;
my $speed = 20;
Debug('Zoom ConTele');
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub zoomConWide {
my $self = shift;
my $params = shift;
#my $step = $self->getParam($params, 'step');
my $speed = -20;
Debug('Zoom ConWide');
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub zoomStop {
my $self = shift;
my $params = shift;
my $speed = 0;
Debug('Zoom Stop');
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub moveStop {
my $self = shift;
my $params = shift;
my $speed = 0;
Debug('Move Stop');
# we have to stop both pans and zooms
my $cmd = "/axis-cgi/com/ptz.cgi?continuouspantiltmove=$speed,$speed";
$self->sendCmd($cmd);
my $cmd = "/axis-cgi/com/ptz.cgi?continuouszoommove=$speed";
$self->sendCmd($cmd);
}
sub zoomRelTele {
my $self = shift;
my $params = shift;
@@ -425,20 +490,15 @@ __END__
=head1 NAME
ZoneMinder::Database - Perl extension for blah blah blah
ZoneMinder::Control::Axis - Zoneminder control for Axis Cameras using the V2 API
=head1 SYNOPSIS
use ZoneMinder::Database;
blah blah blah
use ZoneMinder::Control::AxisV2 ; place this in /usr/share/perl5/ZoneMinder/Control
=head1 DESCRIPTION
Stub documentation for ZoneMinder, created by h2xs. It looks like the
author of the extension was negligent enough to leave the stub
unedited.
Blah blah blah.
This module is an implementation of the Axis V2 API
=head2 EXPORT
@@ -448,14 +508,8 @@ None by default.
=head1 SEE ALSO
Mention other useful documentation such as the documentation of
related modules or operating system documentation (such as man pages
in UNIX), or any relevant external documentation such as RFCs or
standards.
If you have a mailing list set up for your module, mention it here.
If you have a web site set up for your module, mention it here.
AXIS VAPIX Library Documentation; e.g.:
https://www.axis.com/vapix-library/subjects/t10175981/section/t10036011/display
=head1 AUTHOR

View File

@@ -0,0 +1,281 @@
# ==========================================================================
#
# ZoneMinder GrandSteam Control Protocol Module
# Copyright (C) 2021 ZoneMinder Inc
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ==========================================================================
#
# This module contains the implementation of the Vivotek ePTZ camera control
# protocol
#
package ZoneMinder::Control::Grandstream;
use 5.006;
use strict;
use warnings;
require ZoneMinder::Base;
require ZoneMinder::Control;
our @ISA = qw(ZoneMinder::Control);
# ==========================================================================
#
# Vivotek ePTZ Control Protocol
#
# ==========================================================================
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Config qw(:all);
use ZoneMinder::General qw(:all);
use Time::HiRes qw( usleep );
use URI::Encode qw(uri_encode);
use XML::LibXML;
use Digest::MD5 qw(md5 md5_hex md5_base64);
our $REALM = '';
our $PROTOCOL = 'https://';
our $USERNAME = 'admin';
our $PASSWORD = '';
our $ADDRESS = '';
our $BASE_URL = '';
my %config_types = (
upgrade => {
P6767 => { default_value=>1, desc=>'Firmware Upgrade Method http' },
P192 => { desc=>'Firmware Server Path' },
},
date => {
P64 => { desc=>'Timezone' },
P5006 => { default_value=>1, desc=>'Enable NTP' },
P30 => { desc=>'NTP Server', },
},
access => {
P12053 => {default_value=>1, desc=>'Enable UPnP Search' },
},
cmos => {
#P12314=> { value=>0, desc=>'Power Frequency' },
},
video => {
#P12306 => { value=>'26', desc=>'primary codec' },# 26: h264, 96: mjpeg, 98: h265
P12313 => { desc=>'primary profile' },# 0: baseline, 1: main, 2: high
P12307 => { desc=>'primary resolution' }, # 1025: 1920x1080 1023: 1280x960, 1022: 1280x720
P12904 => { desc=>'primary fps', }, # fps 5,10,15,20,25,30
P12311 => { desc=>'Image quality', }, # 0 very high, 4 very low
P12312 => { desc=>'Iframe interval', }, # i-frame interval 5-100
},
osd => {
P10044 => { default_value=> 1, desc=>'Display Time' },
#P10045 => { value=> 1, desc=>'Display Text' },
P10001 => { default_value=> 1, desc=>'OSD Date Format' },
#P10040 => { value=>'', desc=> 'OSD Text' },
},
audio => {
P14000 => { default_value=>1, desc=>'Audio codec' }, # 1,2
P14003 => { default_value=>0, desc=>'Audio out volume' }, # 0-6
P14002 => { default_value=>0, desc=>'Audio in volume' }, # 0-6
},
debug => {
P8042 => { default_value=>0, desc=>'Debug log protocol' }, # 0: UDP 1: SSL/TLS
P207 => { desc=>'Debug Log Server' },
P208 => { desc=>'Debug Log Level' },
},
);
sub open {
my $self = shift;
$self->loadMonitor();
if ($self->{Monitor}{ControlAddress}
and
$self->{Monitor}{ControlAddress} ne 'user:pass@ip'
and
$self->{Monitor}{ControlAddress} ne 'user:port@ip'
) {
Debug("Getting connection details from Path " . $self->{Monitor}->{ControlAddress});
if (($self->{Monitor}->{ControlAddress} =~ /^(?<PROTOCOL>https?:\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>.*)$/)) {
$PROTOCOL = $+{PROTOCOL} if $+{PROTOCOL};
$USERNAME = $+{USERNAME} if $+{USERNAME};
$PASSWORD = $+{PASSWORD} if $+{PASSWORD};
$ADDRESS = $+{ADDRESS} if $+{ADDRESS};
}
} elsif ($self->{Monitor}->{Path}) {
Debug("Getting connection details from Path " . $self->{Monitor}->{Path});
if (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>[^:\/]+)/)) {
$USERNAME = $+{USERNAME} if $+{USERNAME};
$PASSWORD = $+{PASSWORD} if $+{PASSWORD};
$ADDRESS = $+{ADDRESS} if $+{ADDRESS};
}
Debug("username:$USERNAME password:$PASSWORD address:$ADDRESS");
} else {
Error('Failed to parse auth from address ' . $self->{Monitor}->{ControlAddress});
$ADDRESS = $self->{Monitor}->{ControlAddress};
}
$BASE_URL = $PROTOCOL.$ADDRESS;
use LWP::UserAgent;
$self->{ua} = LWP::UserAgent->new;
$self->{ua}->agent('ZoneMinder Control Agent/'.ZoneMinder::Base::ZM_VERSION);
$self->{ua}->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00);
$self->{ua}->cookie_jar( {} );
my $rescode = '';
my $url = $BASE_URL.'/goform/login?cmd=login&type=0&user='.$USERNAME;
my $response = $self->get($url);
if ($response->is_success()) {
my $dom = XML::LibXML->load_xml(string => $response->content);
my $challengeString = $dom->getElementsByTagName('ChallengeCode')->string_value();
Debug('challengstring: '.$challengeString);
my $authcode = md5_hex($challengeString.':GSC36XXlZpRsFzCbM:'.$PASSWORD);
$url .= '&authcode='.$authcode;
$response = $self->get($url);
$dom = XML::LibXML->load_xml(string => $response->content);
$rescode = $dom->getElementsByTagName('ResCode');
} else {
Warning("Falling back to old style");
$PROTOCOL = 'http://';
$BASE_URL = $PROTOCOL.$USERNAME.':'.$PASSWORD.'@'.$ADDRESS;
}
$self->{state} = 'open';
}
sub get {
my $self = shift;
my $url = shift;
Debug("Getting $url");
my $response = $self->{ua}->get($url);
Debug('Response: '. $response->status_line . ' ' . $response->content);
return $response;
}
sub close {
my $self = shift;
$self->{state} = 'closed';
}
sub sendCmd {
my ($self, $cmd, $speedcmd) = @_;
$self->printMsg( $speedcmd, 'Tx' );
$self->printMsg( $cmd, 'Tx' );
my $req = HTTP::Request->new( GET => $BASE_URL."/cgi-bin/camctrl/eCamCtrl.cgi?stream=0&$speedcmd&$cmd");
my $res = $self->{ua}->request($req);
if (!$res->is_success) {
Error('Request failed: '.$res->status_line().' (URI: '.$req->as_string().')');
}
return $res->is_success;
}
sub get_config {
my $self = shift;
my %config;
foreach my $category ( @_ ? @_ : keys %config_types ) {
my $response = $self->get($BASE_URL.'/goform/config?cmd=get&type='.$category);
my $dom = XML::LibXML->load_xml(string => $response->content);
if (!$dom) {
Error("No document from :".$response->content());
return;
}
Debug($dom->toString(1));
$config{$category} = {};
my $Configuration = $dom->getElementsByTagName('Configuration');
my $xml = $Configuration->get_node(0);
if (!$xml) {
Warning("UNable to get Configuration node from ".$response->content());
return \%config;
}
foreach my $node ($xml->childNodes()) {
$config{$category}{$node->nodeName} = {
value=>$node->textContent
};
}
} # end foreach category
return \%config;
} # end sub get_config
sub set_config {
my $self = shift;
my $updates = shift;
my $url = join('&', $BASE_URL.'/goform/config?cmd=set',
map { $_.'='.uri_encode(uri_encode($$updates{$_}{value}, { encode_reserved=>1} )) } keys %$updates );
my $response = $self->get($url);
return 0 if !$response->is_success();
return 0 if ($response->content !~ /Successful/i);
return 1;
}
sub reboot {
my $self = shift;
$self->get($BASE_URL.'/goform/config?cmd=reboot');
}
sub ping {
return -1 if ! $ADDRESS;
require Net::Ping;
my $p = Net::Ping->new();
my $rv = $p->ping($ADDRESS);
$p->close();
return $rv;
}
1;
__END__
=head1 NAME
ZoneMinder::Control::Grandstream - ZoneMinder Perl extension for Grandstream
camera control protocol
=head1 SYNOPSIS
use ZoneMinder::Control::Grandstream;
=head1 DESCRIPTION
This module implements the protocol used in various Grandstream IP cameras.
=head2 EXPORT
None.
=head1 SEE ALSO
I would say, see ZoneMinder::Control documentation. But it is a stub.
=head1 AUTHOR
Isaac Connor E<lt>isaac@zoneminder.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2021 by ZoneMinder Inc
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.3 or,
at your option, any later version of Perl 5 you may have available.
=cut

View File

@@ -25,81 +25,113 @@ our $ADDRESS = '';
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Config qw(:all);
use URI;
use LWP::UserAgent;
sub credentials {
my $self = shift;
($USERNAME, $PASSWORD) = @_;
Debug("Setting credentials to $USERNAME/$PASSWORD");
}
sub open {
my $self = shift;
$self->loadMonitor();
if ( ( $self->{Monitor}->{ControlAddress} =~ /^(?<PROTOCOL>https?:\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>.*)$/ ) ) {
if ($self->{Monitor}{ControlAddress}
and
$self->{Monitor}{ControlAddress} ne 'user:pass@ip'
and
$self->{Monitor}{ControlAddress} ne 'user:port@ip'
and
($self->{Monitor}->{ControlAddress} =~ /^(?<PROTOCOL>https?:\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>.*)$/)
) {
$PROTOCOL = $+{PROTOCOL} if $+{PROTOCOL};
$USERNAME = $+{USERNAME} if $+{USERNAME};
$PASSWORD = $+{PASSWORD} if $+{PASSWORD};
$ADDRESS = $+{ADDRESS} if $+{ADDRESS};
} elsif ($self->{Monitor}{Path}) {
Debug("Using Path for credentials: $self->{Monitor}{Path}");
my $uri = URI->new($self->{Monitor}{Path});
Debug("Using Path for credentials: $self->{Monitor}{Path}" . $uri->userinfo());
( $USERNAME, $PASSWORD ) = split(/:/, $uri->userinfo()) if $uri->userinfo();
$ADDRESS = $uri->host();
} else {
Error('Failed to parse auth from address ' . $self->{Monitor}->{ControlAddress});
$ADDRESS = $self->{Monitor}->{ControlAddress};
}
if ( !($ADDRESS =~ /:/) ) {
Error('You generally need to also specify the port. I will append :80');
Debug('You generally need to also specify the port. I will append :80');
$ADDRESS .= ':80';
}
use LWP::UserAgent;
$self->{ua} = LWP::UserAgent->new;
$self->{ua}->agent('ZoneMinder Control Agent/'.ZoneMinder::Base::ZM_VERSION);
$self->{state} = 'closed';
# credentials: ("ip:port" (no prefix!), realm (string), username (string), password (string)
Debug ( "sendCmd credentials control address:'".$ADDRESS
Debug("sendCmd credentials control address:'".$ADDRESS
."' realm:'" . $REALM
. "' username:'" . $USERNAME
. "' password:'".$PASSWORD
."'"
);
$self->{ua}->credentials($ADDRESS,$REALM,$USERNAME,$PASSWORD);
# Detect REALM
my $res = $self->{ua}->get($PROTOCOL.$ADDRESS.'/cgi/ptdc.cgi');
$REALM = $self->detect_realm($PROTOCOL, $ADDRESS, $REALM, $USERNAME, $PASSWORD, '/');
if (defined($REALM)) {
return !undef;
}
return undef;
} # end sub open
if ( $res->is_success ) {
$self->{state} = 'open';
return;
sub detect_realm {
my ($self, $protocol, $address, $realm, $username, $password, $url) = @_;
$self->{ua}->credentials($address, $realm, $username, $password);
my $res = $self->{ua}->get($protocol.$address.$url);
if ($res->is_success) {
Debug(1, 'Success opening without realm detection for '.$url);
return $realm;
}
if ( $res->status_line() eq '401 Unauthorized' ) {
if ($res->status_line() ne '401 Unauthorized') {
return $realm;
}
my $headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
my $headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ( $$headers{'www-authenticate'} ) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
if ( $tokens =~ /\w+="([^"]+)"/i ) {
if ( $REALM ne $1 ) {
$REALM = $1;
Debug("Changing REALM to $REALM");
$self->{ua}->credentials($ADDRESS,$REALM,$USERNAME,$PASSWORD);
$res = $self->{ua}->get($PROTOCOL.$ADDRESS.'/cgi/ptdc.cgi');
if ( $res->is_success() ) {
$self->{state} = 'open';
return;
}
Error('Authentication still failed after updating REALM' . $res->status_line);
$headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
} # end foreach
} else {
Error('Authentication failed, not a REALM problem');
if ($$headers{'www-authenticate'}) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
if ( $tokens =~ /\w+="([^"]+)"/i ) {
if ($realm ne $1) {
$realm = $1;
Debug("Changing REALM to $realm");
$self->{ua}->credentials($address, $realm, $username, $password);
$res = $self->{ua}->get($protocol.$address.$url);
if ($res->is_success()) {
return $realm;
}
Error('Authentication still failed after updating REALM' . $res->status_line);
$headers = $res->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
} # end foreach
} else {
Error('Failed to match realm in tokens');
} # end if
Error('Authentication failed, not a REALM problem');
}
} else {
Debug('No headers line');
} # end if headers
} # end if $res->status_line() eq '401 Unauthorized'
} # end sub open
Error('Failed to match realm in tokens');
} # end if
} else {
Debug('No headers line');
} # end if headers
return undef;
}
sub sendCmd {
# This routine is used for all moving, which are all GET commands...
@@ -136,17 +168,14 @@ sub sendCmdPost {
my $url = shift;
my $form = shift;
my $result = undef;
if ( $url eq undef ) {
if ($url eq undef) {
Error('url passed to sendCmdPost is undefined.');
return -1;
}
#Debug('sendCmdPost url: ' . $url . ' cmd: ' . $cmd);
Debug('sendCmdPost url: ' . $PROTOCOL.$ADDRESS.$url);
my $res;
$res = $self->{ua}->post(
my $res = $self->{ua}->post(
$PROTOCOL.$ADDRESS.$url,
Referer=>$PROTOCOL.$ADDRESS.$url,
Content=>$form
@@ -154,13 +183,19 @@ sub sendCmdPost {
Debug("sendCmdPost credentials control to: $PROTOCOL$ADDRESS$url realm:'" . $REALM . "' username:'" . $USERNAME . "' password:'".$PASSWORD."'");
if ( $res->is_success ) {
Debug($res->content);
return !undef;
if (!$res->is_success) {
Error("sendCmdPost Error check failed: '".$res->status_line()."' cmd:");
my $new_realm = $self->detect_realm($PROTOCOL, $ADDRESS, $REALM, $USERNAME, $PASSWORD, $url);
if (defined($new_realm) and ($new_realm ne $REALM)) {
Debug("Success after re-detecting realm. New realm is $new_realm");
return !undef;
}
Warning('Failed to reboot');
return undef;
}
Error("sendCmdPost Error check failed: '".$res->status_line()."' cmd:");
Debug($res->content);
return $result;
return !undef;
} # end sub sendCmdPost
sub move {
@@ -377,6 +412,10 @@ sub reset {
sub reboot {
my $self = shift;
Debug('Camera Reboot');
if (!$$self{open}) {
Warning("Not open. opening. Should call ->open() before calling reboot()");
return if !$self->open();
}
$self->sendCmdPost('/eng/admin/reboot.cgi', { reboot => 'true' });
#$referer = 'http://'.$HI->ip().'/eng/admin/tools_default.cgi';
#$initial_url = $HI->ip().'/eng/admin/tools_default.cgi';

View File

@@ -0,0 +1,533 @@
# ==========================================================================
#
# ZoneMinder Uniview Control Protocol Module
# Copyright (C) 2022 ZoneMinder Inc
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ==========================================================================
#
# This module contains an implementation of the Uniview camera control
# protocol. It is incomplete.
#
package ZoneMinder::Control::Uniview;
use 5.006;
use strict;
use warnings;
require ZoneMinder::Base;
require ZoneMinder::Control;
our @ISA = qw(ZoneMinder::Control);
# ==========================================================================
#
# ==========================================================================
use ZoneMinder::Logger qw(:all);
use Time::HiRes qw( usleep );
use LWP::UserAgent;
use HTTP::Cookies;
my $ChannelID = 1; # Usually...
my $DefaultFocusSpeed = 50; # Should be between 1 and 100
my $DefaultIrisSpeed = 50; # Should be between 1 and 100
my ($user,$pass,$host,$port);
sub open {
my $self = shift;
$self->loadMonitor();
$port = 80;
# Create a UserAgent for the requests
$self->{UA} = LWP::UserAgent->new();
$self->{UA}->cookie_jar( {} );
# Extract the username/password host/port from ControlAddress
if ($self->{Monitor}{ControlAddress}
and
$self->{Monitor}{ControlAddress} ne 'user:pass@ip'
and
$self->{Monitor}{ControlAddress} ne 'user:port@ip'
) {
Debug("Using ControlAddress for credentials: $self->{Monitor}{ControlAddress}");
if ($self->{Monitor}{ControlAddress} =~ /^([^:]+):([^@]+)@(.+)/ ) { # user:pass@host...
$user = $1;
$pass = $2;
$host = $3;
} elsif ( $self->{Monitor}{ControlAddress} =~ /^([^@]+)@(.+)/ ) { # user@host...
$user = $1;
$host = $2;
} else { # Just a host
$host = $self->{Monitor}{ControlAddress};
}
# Check if it is a host and port or just a host
if ( $host =~ /([^:]+):(.+)/ ) {
$host = $1;
$port = $2 ? $2 : $port;
}
} elsif ( $self->{Monitor}{Path}) {
Debug("Using Path for credentials: $self->{Monitor}{Path}");
if (($self->{Monitor}->{Path} =~ /^(?<PROTOCOL>(https?|rtsp):\/\/)?(?<USERNAME>[^:@]+)?:?(?<PASSWORD>[^\/@]+)?@?(?<ADDRESS>[^:\/]+)/)) {
$user = $+{USERNAME} if $+{USERNAME};
$pass = $+{PASSWORD} if $+{PASSWORD};
$host = $+{ADDRESS} if $+{ADDRESS};
}
} else {
Debug("Not using credentials");
}
# Save the base url
$self->{BaseURL} = "http://$host:$port";
# Save and test the credentials
if (defined($user)) {
Debug("Credentials: $host:$port, $self->{Monitor}{ControlDevice}, $user, $pass");
$self->{UA}->credentials("$host:$port", $self->{Monitor}{ControlDevice}, $user, $pass);
} # end if defined user
my $url = $self->{BaseURL};
my $response = $self->get($url);
if ($response->status_line() eq '401 Unauthorized' and defined $user) {
my $headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
my $realm = $self->{Monitor}->{ControlDevice};
if ( $$headers{'www-authenticate'} ) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
my %tokens = map { /(\w+)="?([^"]+)"?/i } split(', ', $tokens );
if ( $tokens{realm} ) {
if ( $realm ne $tokens{realm} ) {
$realm = $tokens{realm};
Debug("Changing REALM to $realm");
$self->{UA}->credentials("$host:$port", $realm, $user, $pass);
$response = $self->{UA}->get($url);
if ( !$response->is_success() ) {
Error('Authentication still failed after updating REALM' . $response->status_line);
}
$headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}\n");
} # end foreach
} else {
Error('Authentication failed, not a REALM problem');
}
} else {
Debug('Failed to match realm in tokens');
} # end if
} else {
debug('No headers line');
} # end if headers
} # end if not authen
if ($response->is_success()) {
$self->{state} = 'open';
}
Debug('Response: '. $response->status_line . ' ' . $response->content);
return $response->is_success;
} # end sub open
sub get {
my $self = shift;
my $url = shift;
Debug("Getting $url");
my $response = $self->{UA}->get($url);
#Debug('Response: '. $response->status_line . ' ' . $response->content);
return $response;
}
sub put {
my $self = shift;
my $cmd = shift;
my $content = shift;
if (!$cmd) {
Error("No cmd specified in PutCmd");
return;
}
my $req = HTTP::Request->new(PUT => $self->{BaseURL}.'/'.$cmd);
if ( defined($content) ) {
$req->content_type('application/x-www-form-urlencoded; charset=UTF-8');
$req->content($content);
}
my $res = $self->{UA}->request($req);
unless( $res->is_success ) {
#
# The camera timeouts connections at short intervals. When this
# happens the user agent connects again and uses the same auth tokens.
# The camera rejects this and asks for another token but the UserAgent
# just gives up. Because of this I try the request again and it should
# succeed the second time if the credentials are correct.
#
if ( $res->code == 401 ) {
$res = $self->{UA}->request($req);
unless( $res->is_success ) {
#
# It has failed authentication. The odds are
# that the user has set some parameter incorrectly
# so check the realm against the ControlDevice
# entry and send a message if different
#
my $auth = $res->headers->www_authenticate;
foreach (split(/\s*,\s*/,$auth)) {
if ( $_ =~ /^realm\s*=\s*"([^"]+)"/i ) {
if ( $self->{Monitor}{ControlDevice} ne $1 ) {
Warning("Control Device appears to be incorrect.
Control Device should be set to \"$1\".
Control Device currently set to \"$self->{Monitor}{ControlDevice}\".");
$self->{Monitor}{ControlDevice} = $1;
$self->{UA}->credentials("$host:$port", $self->{Monitor}{ControlDevice}, $user, $pass);
return PutCmd($self,$cmd,$content);
}
}
}
#
# Check for username/password
#
if ( $self->{Monitor}{ControlAddress} =~ /.+:(.+)@.+/ ) {
Info('Check username/password is correct');
} elsif ( $self->{Monitor}{ControlAddress} =~ /^[^:]+@.+/ ) {
Info('No password in Control Address. Should there be one?');
} elsif ( $self->{Monitor}{ControlAddress} =~ /^:.+@.+/ ) {
Info('Password but no username in Control Address.');
} else {
Info('Missing username and password in Control Address.');
}
Error($res->status_line);
}
} else {
Error($res->status_line);
}
} # end unless res->is_success
} # end sub put
#
# The move continuous functions all call moveVector
# with the direction to move in. This includes zoom
#
sub moveVector {
my $self = shift;
my $pandirection = shift;
my $tiltdirection = shift;
my $zoomdirection = shift;
my $params = shift;
my $command; # The ISAPI/PTZ command
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Change from microseconds to milliseconds
$duration = int($duration/1000);
my $momentxml;
if( $duration ) {
$momentxml = "<Momentary><duration>$duration</duration></Momentary>";
$command = "ISAPI/PTZCtrl/channels/$ChannelID/momentary";
}
else {
$momentxml = "";
$command = "ISAPI/PTZCtrl/channels/$ChannelID/continuous";
}
# Calculate movement speeds
my $x = $pandirection * $self->getParam( $params, 'panspeed', 0 );
my $y = $tiltdirection * $self->getParam( $params, 'tiltspeed', 0 );
my $z = $zoomdirection * $self->getParam( $params, 'speed', 0 );
# Create the XML
my $xml = "<PTZData><pan>$x</pan><tilt>$y</tilt><zoom>$z</zoom>$momentxml</PTZData>";
# Send it to the camera
$self->PutCmd($command,$xml);
}
sub zoomStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); }
sub moveStop { $_[0]->moveVector( 0, 0, 0, splice(@_,1)); }
sub moveConUp { $_[0]->moveVector( 0, 1, 0, splice(@_,1)); }
sub moveConUpRight { $_[0]->moveVector( 1, 1, 0, splice(@_,1)); }
sub moveConRight { $_[0]->moveVector( 1, 0, 0, splice(@_,1)); }
sub moveConDownRight { $_[0]->moveVector( 1, -1, 0, splice(@_,1)); }
sub moveConDown { $_[0]->moveVector( 0, -1, 0, splice(@_,1)); }
sub moveConDownLeft { $_[0]->moveVector( -1, -1, 0, splice(@_,1)); }
sub moveConLeft { $_[0]->moveVector( -1, 0, 0, splice(@_,1)); }
sub moveConUpLeft { $_[0]->moveVector( -1, 1, 0, splice(@_,1)); }
sub zoomConTele { $_[0]->moveVector( 0, 0, 1, splice(@_,1)); }
sub zoomConWide { $_[0]->moveVector( 0, 0,-1, splice(@_,1)); }
#
# Presets including Home set and clear
#
sub presetGoto {
my $self = shift;
my $params = shift;
my $preset = $self->getParam($params,'preset');
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/presets/$preset/goto");
}
sub presetSet {
my $self = shift;
my $params = shift;
my $preset = $self->getParam($params,'preset');
my $xml = "<PTZPreset><id>$preset</id></PTZPreset>";
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/presets/$preset",$xml);
}
sub presetHome {
my $self = shift;
my $params = shift;
$self->PutCmd("ISAPI/PTZCtrl/channels/$ChannelID/homeposition/goto");
}
#
# Focus controls all call Focus with a +/- speed
#
sub Focus {
my $self = shift;
my $speed = shift;
my $xml = "<FocusData><focus>$speed</focus></FocusData>";
$self->PutCmd("ISAPI/System/Video/inputs/channels/$ChannelID/focus",$xml);
}
sub focusConNear {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Near {
my $self = shift;
my $params = shift;
$self->Focus(-$DefaultFocusSpeed);
}
sub focusAbsNear {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed);
}
sub focusRelNear {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus(-$speed);
}
sub focusConFar {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Far {
my $self = shift;
my $params = shift;
$self->Focus($DefaultFocusSpeed);
}
sub focusAbsFar {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed);
}
sub focusRelFar {
my $self = shift;
my $params = shift;
# Get the focus speed
my $speed = $self->getParam( $params, 'speed', $DefaultFocusSpeed );
$self->Focus($speed);
}
#
# Iris controls all call Iris with a +/- speed
#
sub Iris {
my $self = shift;
my $speed = shift;
my $xml = "<IrisData><iris>$speed</iris></IrisData>";
$self->PutCmd("ISAPI/System/Video/inputs/channels/$ChannelID/iris",$xml);
}
sub irisConClose {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Close {
my $self = shift;
my $params = shift;
$self->Iris(-$DefaultIrisSpeed);
}
sub irisAbsClose {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed);
}
sub irisRelClose {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris(-$speed);
}
sub irisConOpen {
my $self = shift;
my $params = shift;
# Calculate autostop time
my $duration = $self->getParam( $params, 'autostop', 0 ) * $self->{Monitor}{AutoStopTimeout};
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed);
if($duration) {
usleep($duration);
$self->moveStop($params);
}
}
sub Open {
my $self = shift;
my $params = shift;
$self->Iris($DefaultIrisSpeed);
}
sub irisAbsOpen {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed);
}
sub irisRelOpen {
my $self = shift;
my $params = shift;
# Get the iris speed
my $speed = $self->getParam( $params, 'speed', $DefaultIrisSpeed );
$self->Iris($speed);
}
#
# reset (reboot) the device
#
sub reboot {
my $self = shift;
$self->put('LAPI/V1.0/System/Reboot');
}
sub get_config {
my $self = shift;
return {};
}
sub set_config {
my $self = shift;
my $diff = shift;
return undef;
}
sub ping {
return -1 if ! $host;
require Net::Ping;
my $p = Net::Ping->new();
my $rv = $p->ping($host);
$p->close();
return $rv;
}
sub probe {
my ($ip, $user, $pass) = @_;
my $self = new ZoneMinder::Control::Uniview();
# Create a UserAgent for the requests
$self->{UA} = LWP::UserAgent->new();
$self->{UA}->cookie_jar( {} );
my $realm;
foreach my $port ( '80','443' ) {
my $url = 'http://'.$user.':'.$pass.'@'.$ip.':'.$port.'/ISAPI/Streaming/channels/101';
my $response = $self->get($url);
if ($response->status_line() eq '401 Unauthorized' and defined $user) {
my $headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}");
}
if ( $$headers{'www-authenticate'} ) {
my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/;
my %tokens = map { /(\w+)="?([^"]+)"?/i } split(', ', $tokens );
if ($tokens{realm}) {
$realm = $tokens{realm};
Debug('Changing REALM to '.$tokens{realm});
$self->{UA}->credentials("$ip:$port", $tokens{realm}, $user, $pass);
$response = $self->{UA}->get($url);
if (!$response->is_success()) {
Error('Authentication still failed after updating REALM' . $response->status_line);
}
$headers = $response->headers();
foreach my $k ( keys %$headers ) {
Debug("Initial Header $k => $$headers{$k}\n");
} # end foreach
} else {
Debug('Failed to match realm in tokens');
} # end if
} else {
Debug('No headers line');
} # end if headers
} # end if not authen
Debug('Response: '. $response->status_line . ' ' . $response->content);
if ($response->is_success) {
return {
url => 'http://'.$user.':'.$pass.'@'.$ip.':'.$port.'/h264',
realm => $realm,
};
}
} # end foreach port
return undef;
}
sub profiles {
}
1;
__END__

View File

@@ -356,7 +356,7 @@ sub GenerateVideo {
# commits unless we started the transaction.
sub delete {
my $event = $_[0];
my $event = shift;
if ( !$event->canEdit() ) {
Warning('No permission to delete event.');
@@ -442,37 +442,7 @@ sub delete_files {
( $storage_path ) = ( $storage_path =~ /^(.*)$/ ); # De-taint
( $event_path ) = ( $event_path =~ /^(.*)$/ ); # De-taint
my $deleted = 0;
if ( $$Storage{Type} and ( $$Storage{Type} eq 's3fs' ) ) {
my $url = $$Storage{Url};
$url =~ s/^(s3|s3fs):\/\///ig;
my ( $aws_id, $aws_secret, $aws_host, $aws_bucket, $subpath ) = ( $url =~ /^\s*([^:]+):([^@]+)@([^\/]*)\/([^\/]+)(\/.+)?\s*$/ );
Debug("S3 url parsed to id:$aws_id secret:$aws_secret host:$aws_host, bucket:$aws_bucket, subpath:$subpath\n from $url");
eval {
require Net::Amazon::S3;
my $s3 = Net::Amazon::S3->new( {
aws_access_key_id => $aws_id,
aws_secret_access_key => $aws_secret,
( $aws_host ? ( host => $aws_host ) : () ),
authorization_method => 'Net::Amazon::S3::Signature::V4',
});
my $bucket = $s3->bucket($aws_bucket);
if ( ! $bucket ) {
Error("S3 bucket $bucket not found.");
die;
}
if ( $bucket->delete_key($subpath.$event_path) ) {
$deleted = 1;
} else {
Error('Failed to delete from S3:'.$s3->err . ': ' . $s3->errstr);
}
};
Error($@) if $@;
} # end if s3fs
if ( !$deleted ) {
my $command = "/bin/rm -rf $storage_path/$event_path";
ZoneMinder::General::executeShellCommand($command);
}
$Storage->delete_path($event_path);
} else {
Error('No event path in delete files. ' . $event->to_string());
} # end if event_path
@@ -516,6 +486,46 @@ sub delete_files {
} # end foreach Storage
} # end sub delete_files
sub delete_analysis_jpegs {
my $event = shift;
if ( !$event->canEdit() ) {
Warning('No permission to delete event.');
return 'No permission to delete event.';
}
foreach my $Storage (
@_ ? ($_[0]) : (
new ZoneMinder::Storage($$event{StorageId}),
( $$event{SecondaryStorageId} ? new ZoneMinder::Storage($$event{SecondaryStorageId}) : () ),
) ) {
my $storage_path = $Storage->Path();
if ( !$storage_path ) {
Error("Empty storage path when deleting files for event $$event{Id} with storage id $$event{StorageId}");
return;
}
if ( !$$event{MonitorId} ) {
Error("No monitor id assigned to event $$event{Id}");
return;
}
my $event_path = $event->RelativePath();
Debug("Deleting analysis jpegs for Event $$event{Id} from $storage_path///$event_path, scheme is $$event{Scheme}.");
if ( $event_path ) {
( $storage_path ) = ( $storage_path =~ /^(.*)$/ ); # De-taint
( $event_path ) = ( $event_path =~ /^(.*)$/ ); # De-taint
my @files = glob("$storage_path/$event_path/*analyse.jpg");
Debug(@files . ' analysis jpegs found to delete');
foreach my $file (@files) {
$Storage->delete_path($event_path.'/'.$file);
}
} else {
Error('No event path in delete files. ' . $event->to_string());
} # end if event_path
} # end foreach Storage
} # end sub delete_files
sub StorageId {
my $event = shift;
if ( @_ ) {

View File

@@ -165,12 +165,14 @@ our %mem_data = (
recording => { type=>'uint8', seq=>$mem_seq++ },
signal => { type=>'uint8', seq=>$mem_seq++ },
format => { type=>'uint8', seq=>$mem_seq++ },
reserved1 => { type=>'uint8', seq=>$mem_seq++ },
reserved2 => { type=>'uint8', seq=>$mem_seq++ },
imagesize => { type=>'uint32', seq=>$mem_seq++ },
last_frame_score => { type=>'uint32', seq=>$mem_seq++ },
audio_frequency => { type=>'uint32', seq=>$mem_seq++ },
audio_channels => { type=>'uint32', seq=>$mem_seq++ },
startup_time => { type=>'time_t64', seq=>$mem_seq++ },
zmc_heartbeat_time => { type=>'time_t64', seq=>$mem_seq++ },
heartbeat_time => { type=>'time_t64', seq=>$mem_seq++ },
last_write_time => { type=>'time_t64', seq=>$mem_seq++ },
last_read_time => { type=>'time_t64', seq=>$mem_seq++ },
last_viewed_time => { type=>'time_t64', seq=>$mem_seq++ },
@@ -266,8 +268,15 @@ sub zmMemVerify {
}
my $valid = zmMemRead($monitor, 'shared_data:valid', 1);
if (!$valid) {
Error("Shared data not valid for monitor $$monitor{Id}");
return undef;
zmMemInvalidate($monitor);
if (!zmMemAttach($monitor, $mem_size)) {
return undef;
}
$valid = zmMemRead($monitor, 'shared_data:valid', 1);
if (!$valid) {
Error("Shared data not valid for monitor $$monitor{Id}");
return undef;
}
} else {
Debug(4, "Shared data appears valid for monitor $$monitor{Id}: $valid");
}
@@ -370,7 +379,7 @@ sub zmMemInvalidate {
if ( $mem_key ) {
zmMemDetach($monitor);
} else {
Error("No mem_key for $monitor $$monitor{Name}");
Debug("called zmMemInvalidate when already detached for $$monitor{Id} $$monitor{Name}");
}
} # end sub zmMemInvalidate

View File

@@ -43,6 +43,7 @@ use ZoneMinder::Logger qw(:all);
use parent qw(ZoneMinder::Object);
use vars qw/ $table $primary_key %fields $serial %defaults $debug/;
$debug = 1;
$table = 'Monitors';
$serial = $primary_key = 'Id';
%fields = map { $_ => $_ } qw(
@@ -55,6 +56,7 @@ $serial = $primary_key = 'Id';
Capturing
Analysing
Recording
Decoding
Enabled
LinkedMonitors
Triggers
@@ -152,7 +154,10 @@ $serial = $primary_key = 'Id';
ServerId => 0,
StorageId => 0,
Type => q`'Ffmpeg'`,
Function => q`'Mocord'`,
Capturing => 'Always',
Analysing => 'Always',
Recording => 'Always',
Decoding => 'Always',
Enabled => 1,
LinkedMonitors => undef,
Device => '',
@@ -234,6 +239,12 @@ $serial = $primary_key = 'Id';
Longitude => undef,
);
use constant CAPTURING_NONE => 1;
use constant CAPTURING_ONDEMAND => 2;
use constant CAPTURING_ALWAYS => 3;
use constant ANALYSING_ALWAYS => 2;
use constant ANALYSING_NONE => 1;
sub Server {
return new ZoneMinder::Server( $_[0]{ServerId} );
} # end sub Server
@@ -254,7 +265,7 @@ sub control {
my $command = shift;
my $process = shift;
if ($command eq 'stop' or $command eq 'restart') {
if ($command eq 'stop') {
if ($process) {
ZoneMinder::General::runCommand("zmdc.pl stop $process -m $$monitor{Id}");
} else {
@@ -264,8 +275,7 @@ sub control {
ZoneMinder::General::runCommand('zmdc.pl stop zmc -m '.$monitor->{Id});
}
}
}
if ( $command eq 'start' or $command eq 'restart' ) {
} elsif ($command eq 'start') {
if ( $process ) {
ZoneMinder::General::runCommand("zmdc.pl start $process -m $$monitor{Id}");
} else {
@@ -275,6 +285,16 @@ sub control {
ZoneMinder::General::runCommand('zmdc.pl start zmc -m '.$monitor->{Id});
}
} # end if
} elsif ( $command eq 'restart' ) {
if ( $process ) {
ZoneMinder::General::runCommand("zmdc.pl restart $process -m $$monitor{Id}");
} else {
if ($monitor->{Type} eq 'Local') {
ZoneMinder::General::runCommand('zmdc.pl restart zmc -d '.$monitor->{Device});
} else {
ZoneMinder::General::runCommand('zmdc.pl restart zmc -m '.$monitor->{Id});
}
}
}
} # end sub control
@@ -298,9 +318,9 @@ sub Event_Summary {
sub connect {
my $self = shift;
ZoneMinder::Logger::Debug(4, "Connecting");
if (!ZoneMinder::Memory::zmMemVerify($self)) {
$self->disconnect();
return undef;
}
return !undef;
}
@@ -316,7 +336,9 @@ sub suspendMotionDetection {
return 0 if ! ZoneMinder::Memory::zmMemVerify($self);
return if $$self{Capturing} eq 'None' or $$self{Analysing} eq 'None';
my $count = 50;
while ($count and ZoneMinder::Memory::zmMemRead($self, 'shared_data:analysing', 1)) {
while ($count and
( ZoneMinder::Memory::zmMemRead($self, 'shared_data:analysing', 1) != ANALYSING_NONE)
) {
ZoneMinder::Logger::Debug(1, 'Suspending motion detection');
ZoneMinder::Memory::zmMonitorSuspend($self);
usleep(100000);
@@ -379,6 +401,19 @@ sub Control {
return $$self{Control};
}
sub ImportanceNumber {
my $self = shift;
if ($$self{Importance} eq 'Not') {
return 2;
} elsif ($$self{Importance} eq 'Less') {
return 1;
} elsif ($$self{Importance} eq 'Normal') {
return 0;
}
Warning("Wierd value for Importance $$self{Importance}");
return 0;
}
1;
__END__

View File

@@ -79,7 +79,8 @@ sub new {
no strict 'refs';
my $primary_key = ${$parent.'::primary_key'};
if ( ! $primary_key ) {
Error( 'NO primary_key for type ' . $parent );
my ( $caller, undef, $line ) = caller;
Error( 'NO primary_key for type ' . $parent . ' called from '.$caller.$line);
return;
} # end if
@@ -741,7 +742,8 @@ sub find {
my $fields = \%{$object_type.'::fields'};
my $primary_key = ${$object_type.'::primary_key'};
if ( ! $primary_key ) {
Error( 'NO primary_key for type ' . $object_type );
my ( $caller, undef, $line ) = caller;
Error( 'NO primary_key for type ' . $object_type . ' called from '.$caller.$line);
return;
} # end if
if ( ! ($fields and keys %{$fields}) ) {

View File

@@ -119,6 +119,66 @@ sub CpuLoad {
return (undef, undef, undef);
} # end sub CpuLoad
sub PathToZMS {
my $this = shift;
$this->{PathToZMS} = shift if @_;
if ( $this->Id() and $this->{PathToZMS} ) {
return $this->{PathToZMS};
} else {
return $ZoneMinder::Config{ZM_PATH_ZMS};
}
}
sub UrlToZMS {
my $this = shift;
return $this->Url(@_).$this->PathToZMS();
}
sub Url {
my $this = shift;
my $port = shift if @_;
if (!$this->Id()) {
return '';
}
my $url = $this->Protocol().'://';
$url .= $this->Hostname();
if ( !$port ) {
$port = $this->Port();
}
if ( $this->Protocol() == 'https' and $port == 443 ) {
} elsif ( $this->Protocol() == 'http' and $port == 80 ) {
} else {
$url .= ':'.$port;
}
return $url;
}
sub PathToIndex {
my $this = shift;
$this->{PathToIndex} = shift if @_;
return $this->{PathToIndex} if $this->{PathToIndex};
}
sub UrlToIndex {
my $this = shift;
return $this->Url(@_).$this->PathToIndex();
}
sub UrlToApi {
my $this = shift;
return $this->Url(@_).$this->PathToApi();
}
sub PathToApi {
my $this = shift;
$this->{PathToApi} = shift if @_;
return $this->{'PathToApi'} if $this->{PathToApi};
return '/zm/api';
}
1;
__END__
# Below is stub documentation for your module. You'd better edit it!

View File

@@ -31,6 +31,7 @@ use warnings;
require ZoneMinder::Base;
require ZoneMinder::Object;
require ZoneMinder::Server;
require ZoneMinder::General;
use parent qw(Exporter ZoneMinder::Object);
@@ -59,7 +60,7 @@ sub Path {
$_[0]{Path} = $_[1];
}
if ( ! ( $_[0]{Id} or $_[0]{Path} ) ) {
$_[0]{Path} = ($Config{ZM_DIR_EVENTS}=~/^\//) ? $Config{ZM_DIR_EVENTS} : ($Config{ZM_PATH_WEB}.'/'.$Config{ZM_DIR_EVENTS})
$_[0]{Path} = ($Config{ZM_DIR_EVENTS}=~/^\//) ? $Config{ZM_DIR_EVENTS} : ($Config{ZM_PATH_WEB}.'/'.$Config{ZM_DIR_EVENTS});
}
return $_[0]{Path};
} # end sub Path
@@ -88,18 +89,62 @@ sub Server {
return $$self{Server};
}
sub delete_path {
my $self = shift;
my $path = shift;
my $deleted = 0;
Debug("Delete $path");
if ($$self{Type} and ( $$self{Type} eq 's3fs' )) {
my $url = $$self{Url};
$url =~ s/^(s3|s3fs):\/\///ig;
my ( $aws_id, $aws_secret, $aws_host, $aws_bucket, $subpath ) = ( $url =~ /^\s*([^:]+):([^@]+)@([^\/]*)\/([^\/]+)(\/.+)?\s*$/ );
Debug("S3 url parsed to id:$aws_id secret:$aws_secret host:$aws_host, bucket:$aws_bucket, subpath:$subpath\n from $url");
eval {
require Net::Amazon::S3;
my $s3 = Net::Amazon::S3->new( {
aws_access_key_id => $aws_id,
aws_secret_access_key => $aws_secret,
( $aws_host ? ( host => $aws_host ) : () ),
authorization_method => 'Net::Amazon::S3::Signature::V4',
});
my $bucket = $s3->bucket($aws_bucket);
if ( ! $bucket ) {
Error("S3 bucket $bucket not found.");
die;
}
if ( $bucket->delete_key($subpath.$path) ) {
$deleted = 1;
} else {
Error('Failed to delete from S3:'.$s3->err . ': ' . $s3->errstr);
}
};
Error($@) if $@;
} # end if s3fs
if ( !$deleted ) {
my $storage_path = $self->Path();
( $storage_path ) = ( $storage_path =~ /^(.*)$/ ); # De-taint
( $path ) = ( $path =~ /^(.*)$/ ); # De-taint
my $command = "/bin/rm -rf $storage_path/$path";
ZoneMinder::General::executeShellCommand($command);
}
}
1;
__END__
# Below is stub documentation for your module. You'd better edit it!
=head1 NAME
ZoneMinder::Database - Perl extension for blah blah blah
ZoneMinder::Storage - Perl modules for Storage objects
=head1 SYNOPSIS
use ZoneMinder::Storage;
blah blah blah
my $Storage = ZoneMinder::Storage->find_one(Name=>'Default');
my @S3Areas = ZoneMinder::Stroage->find(Type=>'s3fs');
etc...
=head1 DESCRIPTION
@@ -128,11 +173,11 @@ If you have a web site set up for your module, mention it here.
=head1 AUTHOR
Philip Coombes, E<lt>philip.coombes@zoneminder.comE<gt>
Isaac Connor, E<lt>isaac@zoneminder.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2001-2008 Philip Coombes
Copyright (C) 2022 ZoneMinder Inc
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.3 or,

185
scripts/zmalarm-server.py Executable file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ==========================================================================
#
# ZoneMinder Alarm Server Script for Netsurveillence Software cameras, $Date$, $Revision$
# Copyright (C) 2022
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ==========================================================================
# Adds pyzm support
import pyzm.api as zmapi
import pyzm.ZMLog as zmlog
import pyzm.helpers.utils as utils
import os, sys, struct, json
from time import sleep
#import time
from socket import *
# from datetime import *
# telnet
from telnetlib import Telnet
# multi threading
from threading import Thread
def writezmlog(m,s):
zmlog.init()
zmlog.Info(m+s)
# zmlog.close()
def alarm_thread(m, monid,eventlenght):
import subprocess
print("Monitor " + str(monid)+" entered alarm_thread...")
result = subprocess.run(['zmu','-m', str(monid), '-s'] ,stdout=subprocess.PIPE)
if result.stdout.decode('utf-8') != '3\n':
print('Changing monitor '+ str(monid) + ' status to Alarm...')
m.arm()
sleep(eventlenght)
m.disarm()
else:
print('Monitor '+ str(monid) + ' already in status Alarm...')
print('Finishing thread...')
def event_thread(m_id,eventlenght):
import subprocess
print("Monitor " + str(m_id)+" entered event_thread...")
result = subprocess.run(['zmu','-m', str(m_id), '-x'] ,stdout=subprocess.PIPE)
if result.stdout.decode('utf-8') == '0\n':
print('Firing monitor '+ str(m_id) + ' trigger...')
telbuff = str(m_id) + '|on+'+str(eventlenght)+'|1|Human Motion Detected|'
with Telnet('localhost', 6802) as tn:
tn.write(telbuff.encode('ascii') + alarm_desc.encode('ascii') + b'\n')
tn.read_until(b'off')
else:
print('Monitor '+ str(m_id) + ' already triggered, doing nothing...')
print('Finishing thread...')
def tolog(s):
logfile = open(datetime.now().strftime('%Y_%m_%d_') + log, 'a+')
logfile.write(s)
logfile.close()
def GetIP(s):
return inet_ntoa(struct.pack('<I', int(s, 16)))
# config variables
eventlenght = 60
wrzmlog = 'n'
wrzmevent ='n'
rsealm = 'n'
port = '15002'
if len(sys.argv) > 1:
keys = ["--log=","-l=","--alarm=","-a=","--port=","-p=","--event=","-e="]
for i in range(1,len(sys.argv)):
for key in keys:
if sys.argv[i].find(key) == 0:
if key == "--log=" or key == "-l=":
wrzmlog=sys.argv[i][len(key):]
elif key == "--alarm=" or key == "-a=":
rsealm=sys.argv[i][len(key):]
elif key == "--port=" or key == "-p=":
port=sys.argv[i][len(key):]
elif key == "--event=" or key == "-e=":
wrzmevent=sys.argv[i][len(key):]
break
else:
print('Usage: %s [--port|-p=<value> --log|-l=<y/n> --alarm|-a=<y/n> --event|-e=<y/n>]' % os.path.basename(sys.argv[0]))
sys.exit(1)
print ('Create log entry: ', wrzmlog)
print ('Trigger event: ', wrzmlog)
print ('Raise Alarm: ', rsealm)
server = socket(AF_INET, SOCK_STREAM)
server.bind(('0.0.0.0', int(port)))
# server.settimeout(0.5)
server.listen(1)
log = "AlarmServer.log"
conf = utils.read_config('/etc/zm/secrets.ini')
api_options = {
'apiurl': utils.get(key='ZM_API_PORTAL', section='secrets', conf=conf),
'portalurl':utils.get(key='ZM_PORTAL', section='secrets', conf=conf),
'user': utils.get(key='ZM_USER', section='secrets', conf=conf),
#'disable_ssl_cert_check': True
}
zmapi = zmapi.ZMApi(options=api_options)
# importing the regex to get ip out of path
import re
#define regex pattern for IP addresses
pattern =re.compile('''((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)''')
# store the response of URL
#process monitors create dict of monitors
list_monit = {}
zm_monitors = zmapi.monitors()
for m in zm_monitors.list():
ip_v4=pattern.search(m.get()['Path'])
list_monit[ip_v4.group()]=m.id()
writezmlog('Listening on port: '+port,' AlarmServer.py')
print ('Listening on port: '+port)
#run Alarm Server
while True:
try:
conn, addr = server.accept()
head, version, session, sequence_number, msgid, len_data = struct.unpack(
'BB2xII2xHI', conn.recv(20)
)
sleep(0.1) # Just for recive whole packet
data = conn.recv(len_data)
conn.close()
# make the json a Dictionary
reply = json.loads(data)
# get ip
ip_v4 = GetIP(reply.get('Address'))
# get alarm_event_desc
alarm_desc = reply.get('Event')
# print(datetime.now().strftime('[%Y-%m-%d %H:%M:%S]>>>'))
print ('Ip Address: ',ip_v4)
print ("Alarm Description: ", alarm_desc)
print('<<<')
# tolog(repr(data) + "\r\n")
if alarm_desc == 'HumanDetect':
if wrzmlog == 'y':
writezmlog(alarm_desc+' in monitor ',str(list_monit[ip_v4]))
if rsealm == 'y':
print ("Triggering Alarm...")
mthread = Thread(target=alarm_thread, args=(zm_monitors.find(list_monit[ip_v4]),list_monit[ip_v4],eventlenght))
mthread.start()
elif wrzmevent == 'y':
print ("Triggering Event Rec on zmtrigger...")
mthread = Thread(target=event_thread, args=(list_monit[ip_v4],eventlenght))
mthread.start()
except (KeyboardInterrupt, SystemExit):
break
server.close()
# needs to be closed again... otherwise it will crash on exit.
zmlog.close()
sys.exit(1)

View File

@@ -102,7 +102,8 @@ my @daemons = (
'zmtrack.pl',
'zmcontrol.pl',
'zm_rtsp_server',
'zmtelemetry.pl'
'zmtelemetry.pl',
'zmalarm-server.py'
);
if ( $Config{ZM_OPT_USE_EVENTNOTIFICATION} ) {
@@ -622,7 +623,7 @@ sub restart {
# Start will be handled by the reaper...
# unless it was already pending in which case send_stop will return () so we should start it
if ( !send_stop(0, $process) ) {
dPrint(ZoneMinder::Logger::WARNING, "!send_stop so starting '$command'\n");
dPrint(ZoneMinder::Logger::DEBUG, "!send_stop so starting '$command'\n");
start($daemon, @args);
}
return;

116
scripts/zmeventtool.pl.in Normal file
View File

@@ -0,0 +1,116 @@
#!@PERL_EXECUTABLE@ -wT
#
# ==========================================================================
#
# ZoneMinder Event Tool
# Copyright (C) 2022 ZoneMinder Inc
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ==========================================================================
=head1 NAME
zmeventtool.pl - ZoneMinder tool to perform various actions on events
=head1 SYNOPSIS
zmeventtool.pl [--user=<dbuser> --pass=<dbpass>] command [list of event ids]
=head1 DESCRIPTION
This script performs various actions on an event. It is primarily meant to
be run from a filter but could be run manually.
=head1 OPTIONS
deletejpegs - Deletes all jpegs from the event directory
deleteanalysisjpegs - Deletes the analysis jpegs from the event directory
--help - Print usage information.
--user=<dbuser> - Alternate dB user with privileges to alter dB.
--pass=<dbpass> - Password of alternate dB user with privileges to alter dB.
--version - Print version.
=cut
use strict;
use bytes;
@EXTRA_PERL_LIB@
use ZoneMinder::Config qw(:all);
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Database qw(:all);
use ZoneMinder::Event;
use DBI;
use Getopt::Long;
use autouse 'Pod::Usage'=>qw(pod2usage);
$ENV{PATH} = '/bin:/usr/bin:/usr/local/bin';
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
my $web_uid = (getpwnam( $Config{ZM_WEB_USER} ))[2];
my $use_log = (($> == 0) || ($> == $web_uid));
logInit( toFile=>$use_log?DEBUG:NOLOG );
logSetSignal();
my $help = 0;
my $dbUser = $Config{ZM_DB_USER};
my $dbPass = $Config{ZM_DB_PASS};
my $version = 0;
GetOptions(
'help' =>\$help,
'user:s' =>\$dbUser,
'pass:s' =>\$dbPass,
'version' =>\$version
) or pod2usage(-exitstatus => -1);
$Config{ZM_DB_USER} = $dbUser;
$Config{ZM_DB_PASS} = $dbPass;
if ( $version ) {
print( ZoneMinder::Base::ZM_VERSION . "\n");
exit(0);
}
# Call the appropriate subroutine based on the params given on the commandline
if ($help or (@ARGV < 2) ) {
pod2usage(-exitstatus => -1);
}
my $dbh = zmDbConnect();
my $command = shift @ARGV;
foreach my $event_id (@ARGV) {
if ( $event_id =~ /\D/ ) {
# Assume path to event, strip it
$event_id =~ s/.*\/(\d+)/$1/;
}
if ($command eq 'deleteanalysisjpegs') {
my $Event = ZoneMinder::Event->find_one(Id=>$event_id);
if ($Event) {
$Event->delete_analysis_jpegs();
} else {
Warning("Event not found for $event_id");
}
}
}
zmDbDisconnect();
1;
__END__

View File

@@ -185,12 +185,9 @@ while (!$zm_terminate) {
last if $zm_terminate;
my $elapsed = ($now - ($$filter{last_ran} ? $$filter{last_ran} : 0));
if ($$filter{last_ran} and ($elapsed < $$filter{ExecuteInterval})) {
my $filter_delay = $$filter{ExecuteInterval} - ($now - $$filter{last_ran});
$delay = $filter_delay if $filter_delay < $delay;
Debug("Setting delay to $delay because ExecuteInterval=$$filter{ExecuteInterval} and $elapsed have elapsed");
next;
}
my $filter_delay = $$filter{ExecuteInterval} - $elapsed;
$delay = $filter_delay > 0 ? $filter_delay : 0;
Debug("Setting delay to $delay because ExecuteInterval=$$filter{ExecuteInterval} and $elapsed have elapsed");
if ($$filter{Concurrent} and !($filter_id or $filter_name)) {
my ( $proc ) = $0 =~ /(\S+)/;

View File

@@ -213,11 +213,11 @@ if ( $command =~ /^(?:start|restart)$/ ) {
@monitors = ZoneMinder::Monitor->find(ServerId=>$Config{ZM_SERVER_ID});
} else {
Info('Single server configuration detected. Starting up services.');
@monitors = ZoneMinder::Monitor->find(ServerId=>$Config{ZM_SERVER_ID});
@monitors = ZoneMinder::Monitor->find();
}
foreach my $monitor (@monitors) {
if ( $monitor->{Capturing} ne 'None' && $monitor->{Type} ne 'WebSite' ) {
if (($monitor->{Capturing} ne 'None') and ($monitor->{Type} ne 'WebSite')) {
if ( $monitor->{Type} eq 'Local' ) {
runCommand("zmdc.pl start zmc -d $monitor->{Device}");
} else {
@@ -235,6 +235,8 @@ if ( $command =~ /^(?:start|restart)$/ ) {
} # end if track motion
} # end if controllable
} # end if ZM_OPT_CONTROL
} else {
Debug("Not running process for Monitor $$monitor{Id} Because Capturing($$monitor{Capturing})== None or Type($$monitor{Type})==Website");
} # end if capturing is not none or Website
} # end foreach monitor
@@ -286,6 +288,30 @@ if ( $command =~ /^(?:start|restart)$/ ) {
if ( $Config{ZM_MIN_RTSP_PORT} ) {
runCommand('zmdc.pl start zm_rtsp_server');
}
# run and pass parameters to AlarmServer.py
if ($Config{ZM_OPT_USE_ALARMSERVER} ) {
my $cmd='zmdc.pl start zmalarm-server.py '. $Config{ZM_OPT_ALS_PORT};
if ($Config{ZM_OPT_ALS_LOGENTRY} ) {
$cmd = $cmd . ' --log=y';
}
else {
$cmd = $cmd . ' --log=n';
}
if ($Config{ZM_OPT_ALS_TRIGGEREVENT} ) {
$cmd = $cmd . ' --event=y';
}
else {
$cmd = $cmd . ' --event=n';
}
if ($Config{ZM_OPT_ALS_ALARM} ) {
$cmd = $cmd . ' --alarm=y';
}
else {
$cmd = $cmd . ' --alarm=n';
}
runCommand($cmd);
}
} else {
$retval = 1;
}

View File

@@ -89,7 +89,7 @@ while (!$zm_terminate) {
my $rows;
do {
# Delete any sessions that are more than a week old. Limiting to 100 because mysql sucks
$rows = zmDbDo('DELETE FROM Sessions WHERE access < ? LIMIT 100', time - (60*60*24*7));
$rows = zmDbDo('DELETE FROM Sessions WHERE access < ? LIMIT 100', time - $Config{ZM_COOKIE_LIFETIME});
Debug("Deleted $rows sessions") if $rows;
} while ($rows and ($rows == 100) and !$zm_terminate);

View File

@@ -78,7 +78,7 @@ $SIG{TERM} = \&TermHandler;
$SIG{INT} = \&TermHandler;
Info('Watchdog starting, pausing for '.START_DELAY.' seconds');
sleep(START_DELAY);
#sleep(START_DELAY);
my $dbh = zmDbConnect();
@@ -92,7 +92,7 @@ while (!$zm_terminate) {
foreach my $monitor (ZoneMinder::Monitor->find($Config{ZM_SERVER_ID} ? (ServerId=>$Config{ZM_SERVER_ID}) : ())) {
next if $monitor->{Capturing} eq 'None';
next if $monitor->{Type} eq 'WebSite';
next if $monitor->{Capturing} eq 'Ondemand';
my $now = time();
my $restart = 0;
zmMemInvalidate($monitor);
@@ -102,6 +102,19 @@ while (!$zm_terminate) {
next;
}
my $heartbeat_time = zmMemRead($monitor, 'shared_data:heartbeat_time');
my $heartbeat_elapsed = $now-$heartbeat_time;
if ($heartbeat_elapsed > $Config{ZM_WATCH_MAX_DELAY}) {
Info("Restarting capture daemon for $monitor->{Id} $monitor->{Name}, $now - heartbeat time $heartbeat_time $heartbeat_elapsed > $Config{ZM_WATCH_MAX_DELAY}");
$monitor->control('restart');
next;
} else {
Debug("Monitor $monitor->{Id} $monitor->{Name}, heartbeat time $now - $heartbeat_time $heartbeat_elapsed < $Config{ZM_WATCH_MAX_DELAY}");
}
next if $monitor->{Capturing} eq 'Ondemand';
next if $monitor->{Decoding} eq 'None' or $monitor->{Decoding} eq 'Ondemand';
# Check we have got an image recently
my $capture_time = zmGetLastWriteTime($monitor);
if (!defined($capture_time)) {
@@ -114,7 +127,13 @@ while (!$zm_terminate) {
# We can't get the last capture time so can't be sure it's died, it might just be starting up.
my $startup_time = zmGetStartupTime($monitor);
if (($now - $startup_time) > $Config{ZM_WATCH_MAX_DELAY}) {
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->Importance(),
if ($monitor->ControlId()) {
my $control = $monitor->Control();
if ($control and $control->CanReboot() and $control->open()) {
$control->reboot();
}
}
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),
"Restarting capture daemon for $$monitor{Name}, no image since startup. ".
"Startup time was $startup_time - now $now > $Config{ZM_WATCH_MAX_DELAY}"
);
@@ -133,9 +152,9 @@ while (!$zm_terminate) {
my $image_delay = $now - $capture_time;
Debug("Monitor $monitor->{Id} last captured $image_delay seconds ago, max is $max_image_delay");
if ($image_delay > $max_image_delay) {
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->Importance(),
'Restarting capture daemon for '.$monitor->{Name}.
", time since last capture $image_delay seconds ($now-$capture_time)");
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),
'Restarting capture daemon for '.$monitor->{Name}.
", time since last capture $image_delay seconds ($now-$capture_time)");
$monitor->control('restart');
next;
}
@@ -161,7 +180,7 @@ while (!$zm_terminate) {
my $image_delay = $now-$image_time;
Debug("Monitor $monitor->{Id} last analysed $image_delay seconds ago, max is $max_image_delay");
if ($image_delay > $max_image_delay) {
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->Importance(),
$log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),
"daemon for $$monitor{Id} $$monitor{Name} needs restarting,"
." time since last analysis $image_delay seconds ($now-$image_time)");
$monitor->control('restart');

View File

@@ -36,6 +36,8 @@ set(ZM_BIN_SRC_FILES
zm_monitor_monitorlink.cpp
zm_monitor_janus.cpp
zm_monitor_amcrest.cpp
zm_monitorlink_expression.cpp
#zm_monitorlink_token.cpp
zm_monitorstream.cpp
zm_mqtt.cpp
zm_ffmpeg.cpp
@@ -75,6 +77,7 @@ set(ZM_BIN_SRC_FILES
if(GSOAP_FOUND)
set(ZM_BIN_SRC_FILES
${ZM_BIN_SRC_FILES}
bindings.h
${CMAKE_BINARY_DIR}/generated/soapPullPointSubscriptionBindingProxy.cpp
${CMAKE_BINARY_DIR}/generated/soapC.cpp
${GSOAP_PLUGIN_DIR}/smdevp.c
@@ -104,10 +107,10 @@ if(GSOAP_FOUND)
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/generated/soapC.cpp
OUTPUT ${CMAKE_BINARY_DIR}/generated/soapPullPointSubscriptionBindingProxy.cpp
COMMAND ${GSOAP_WSDL2H} -d -P -O2 -o ${CMAKE_BINARY_DIR}/generated/bindings.h http://www.onvif.org/onvif/ver10/events/wsdl/event.wsdl
COMMAND echo '\#import \"wsse.h\"' >> ${CMAKE_BINARY_DIR}/generated/bindings.h
COMMAND echo '\#import \"struct_timeval.h\"' >> ${CMAKE_BINARY_DIR}/generated/bindings.h
COMMAND ${GSOAP_SOAPCPP2} -n -2 -C -I ${GSOAP_PLUGIN_DIR}/.. -I ${GSOAP_PLUGIN_DIR}/../import/ -I ${GSOAP_PLUGIN_DIR}/../custom/ -d ${CMAKE_BINARY_DIR}/generated -j -x ${CMAKE_BINARY_DIR}/generated/bindings.h
#COMMAND ${GSOAP_WSDL2H} -d -P -O2 -o ${CMAKE_BINARY_DIR}/generated/bindings.h http://www.onvif.org/onvif/ver10/events/wsdl/event.wsdl
#COMMAND echo '\#import \"wsse.h\"' >> ${CMAKE_BINARY_DIR}/generated/bindings.h
#COMMAND echo '\#import \"struct_timeval.h\"' >> ${CMAKE_BINARY_DIR}/generated/bindings.h
COMMAND ${GSOAP_SOAPCPP2} -n -2 -C -I ${GSOAP_PLUGIN_DIR}/.. -I ${GSOAP_PLUGIN_DIR}/../import/ -I ${GSOAP_PLUGIN_DIR}/../custom/ -d ${CMAKE_BINARY_DIR}/generated -j -x ${CMAKE_CURRENT_SOURCE_DIR}/bindings.h
COMMENT "CREATING STUBS AND GLUE CODE"
)
@@ -129,7 +132,9 @@ add_library(zm STATIC ${ZM_BIN_SRC_FILES})
target_include_directories(zm
PUBLIC
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR})
${CMAKE_CURRENT_SOURCE_DIR}
../dep/booleval/include
)
if(GSOAP_FOUND)
target_include_directories(zm
@@ -143,6 +148,7 @@ endif()
target_link_libraries(zm
PUBLIC
FFMPEG::avcodec
FFMPEG::avdevice
FFMPEG::avformat
FFMPEG::avutil
FFMPEG::swresample

7749
src/bindings.h Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ AnalysisThread::AnalysisThread(Monitor *monitor) :
AnalysisThread::~AnalysisThread() {
Stop();
if (thread_.joinable()) thread_.join();
}
void AnalysisThread::Start() {
@@ -21,6 +20,11 @@ void AnalysisThread::Start() {
thread_ = std::thread(&AnalysisThread::Run, this);
}
void AnalysisThread::Stop() {
terminate_ = true;
if (thread_.joinable()) thread_.join();
}
void AnalysisThread::Run() {
while (!(terminate_ or zm_terminate)) {
// Some periodic updates are required for variable capturing framerate

View File

@@ -15,7 +15,7 @@ class AnalysisThread {
AnalysisThread(AnalysisThread &&rhs) = delete;
void Start();
void Stop() { terminate_ = true; }
void Stop();
bool Stopped() const { return terminate_; }
private:

View File

@@ -36,6 +36,8 @@ class Box {
const Vector2 &Lo() const { return lo_; }
const Vector2 &Hi() const { return hi_; }
unsigned int Width() const { return hi_.x_ - lo_.x_; }
unsigned int Height() const { return hi_.y_ - lo_.y_; }
const Vector2 &Size() const { return size_; }
int32 Area() const { return size_.x_ * size_.y_; }

View File

@@ -54,6 +54,8 @@ unsigned int Buffer::expand(unsigned int count) {
if ( mStorage ) {
memcpy(newStorage, mHead, mSize);
delete[] mStorage;
} else {
memset(newStorage, 0, mAllocation);
}
mStorage = newStorage;
mHead = mStorage;

View File

@@ -72,7 +72,9 @@ Camera::Camera(
Camera::~Camera() {
if ( mFormatContext ) {
// Should also free streams
avformat_free_context(mFormatContext);
Debug(1, "Freeing mFormatContext");
//avformat_free_context(mFormatContext);
avformat_close_input(&mFormatContext);
}
if ( mSecondFormatContext ) {
// Should also free streams

View File

@@ -319,6 +319,7 @@ class Socket : public CommsBase {
class InetSocket : virtual public Socket {
public:
InetSocket() : mAddressFamily(AF_UNSPEC) {}
int getDomain() const override { return mAddressFamily; }
socklen_t getAddrSize() const override { return SockAddrInet::addrSize(); }

View File

@@ -14,7 +14,7 @@
// returns username if valid, "" if not
#if HAVE_LIBJWT
std::pair <std::string, unsigned int> verifyToken(std::string jwt_token_str, std::string key) {
std::pair <std::string, unsigned int> verifyToken(const std::string &jwt_token_str, const std::string &key) {
std::string username = "";
unsigned int token_issued_at = 0;
int err = 0;
@@ -74,7 +74,7 @@ std::pair <std::string, unsigned int> verifyToken(std::string jwt_token_str, std
return std::make_pair(username, token_issued_at);
}
#else // HAVE_LIBJWT
std::pair <std::string, unsigned int> verifyToken(std::string jwt_token_str, std::string key) {
std::pair <std::string, unsigned int> verifyToken(const std::string &jwt_token_str, const std::string &key) {
std::string username = "";
unsigned int token_issued_at = 0;
try {

View File

@@ -28,7 +28,7 @@
bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash);
std::pair<std::string, unsigned int> verifyToken(std::string token, std::string key);
std::pair<std::string, unsigned int> verifyToken(const std::string &token, const std::string &key);
namespace zm {
namespace crypto {

View File

@@ -323,7 +323,7 @@ int cURLCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
}
zm_packet->keyframe = 1;
zm_packet->codec_type = AVMEDIA_TYPE_VIDEO;
zm_packet->packet.stream_index = mVideoStreamId;
zm_packet->packet->stream_index = mVideoStreamId;
zm_packet->stream = mVideoStream;
zm_packet->image->DecodeJpeg(databuffer.extract(frame_content_length), frame_content_length, colours, subpixelorder);
frameComplete = true;
@@ -351,7 +351,7 @@ int cURLCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
}
zm_packet->keyframe = 1;
zm_packet->codec_type = AVMEDIA_TYPE_VIDEO;
zm_packet->packet.stream_index = mVideoStreamId;
zm_packet->packet->stream_index = mVideoStreamId;
zm_packet->stream = mVideoStream;
zm_packet->image->DecodeJpeg(databuffer.extract(single_offsets.front()), single_offsets.front(), colours, subpixelorder);
single_offsets.pop_front();

View File

@@ -162,8 +162,6 @@ MYSQL_RES *zmDbRow::fetch(const std::string &query) {
mysql_free_result(result_set);
result_set = nullptr;
Error("Error getting row from query %s. Error is %s", query.c_str(), mysql_error(&dbconn));
} else {
Debug(5, "Success");
}
return result_set;
}
@@ -229,9 +227,10 @@ zmDbRow::~zmDbRow() {
}
zmDbQueue::zmDbQueue() :
mThread(&zmDbQueue::process, this),
mTerminate(false)
{ }
{
mThread = std::thread(&zmDbQueue::process, this);
}
zmDbQueue::~zmDbQueue() {
stop();

View File

@@ -10,7 +10,6 @@ DecoderThread::DecoderThread(Monitor *monitor) :
DecoderThread::~DecoderThread() {
Stop();
if (thread_.joinable()) thread_.join();
}
void DecoderThread::Start() {
@@ -18,10 +17,23 @@ void DecoderThread::Start() {
terminate_ = false;
thread_ = std::thread(&DecoderThread::Run, this);
}
void DecoderThread::Stop() {
terminate_ = true;
if (thread_.joinable()) thread_.join();
}
void DecoderThread::Run() {
Debug(2, "DecoderThread::Run() for %d", monitor_->Id());
while (!(terminate_ or zm_terminate)) {
monitor_->Decode();
if (!monitor_->Decode()) {
if (!(terminate_ or zm_terminate)) {
// We only sleep when Decode returns false because it is an error condition and we will spin like mad if it persists.
Microseconds sleep_for = monitor_->Active() ? Microseconds(ZM_SAMPLE_RATE) : Microseconds(ZM_SUSPENDED_RATE);
Debug(2, "Sleeping for %" PRId64 "us", int64(sleep_for.count()));
std::this_thread::sleep_for(sleep_for);
}
}
}
}

View File

@@ -15,7 +15,7 @@ class DecoderThread {
DecoderThread(DecoderThread &&rhs) = delete;
void Start();
void Stop() { terminate_ = true; }
void Stop();
private:
void Run();

View File

@@ -48,7 +48,7 @@ Event::Event(
id(0),
monitor(p_monitor),
start_time(p_start_time),
end_time(),
end_time(p_start_time),
cause(p_cause),
noteSetMap(p_noteSetMap),
frames(0),
@@ -58,6 +58,7 @@ Event::Event(
max_score(-1),
//path(""),
//snapshit_file(),
snapshot_file_written(false),
//alarm_file(""),
videoStore(nullptr),
//video_file(""),
@@ -107,7 +108,7 @@ Event::Event(
Storage *storage = monitor->getStorage();
if (monitor->GetOptVideoWriter() != 0) {
container = monitor->OutputContainer();
if ( container == "auto" || container == "" ) {
if (container == "auto" || container == "") {
container = "mp4";
}
video_incomplete_file = "incomplete."+container;
@@ -138,14 +139,10 @@ Event::Event(
}
Event::~Event() {
Debug(1, "Deleting event, calling stop");
Stop();
if (thread_.joinable()) {
// Should be. Issuing the stop and then getting the lock
Debug(1, "Joinable");
thread_.join();
} else {
Debug(1, "Not Joinable");
}
/* Close the video file */
@@ -155,9 +152,7 @@ Event::~Event() {
delete videoStore;
videoStore = nullptr;
int result = rename(video_incomplete_path.c_str(), video_path.c_str());
if (result == 0) {
Debug(1, "File successfully renamed");
} else {
if (result != 0) {
Error("Failed renaming %s to %s", video_incomplete_path.c_str(), video_path.c_str());
// So that we don't update the event record
video_file = video_incomplete_file;
@@ -204,12 +199,12 @@ Event::~Event() {
void Event::createNotes(std::string &notes) {
notes.clear();
for ( StringSetMap::const_iterator mapIter = noteSetMap.begin(); mapIter != noteSetMap.end(); ++mapIter ) {
for (StringSetMap::const_iterator mapIter = noteSetMap.begin(); mapIter != noteSetMap.end(); ++mapIter) {
notes += mapIter->first;
notes += ": ";
const StringSet &stringSet = mapIter->second;
for ( StringSet::const_iterator setIter = stringSet.begin(); setIter != stringSet.end(); ++setIter ) {
if ( setIter != stringSet.begin() )
for (StringSet::const_iterator setIter = stringSet.begin(); setIter != stringSet.end(); ++setIter) {
if (setIter != stringSet.begin())
notes += ", ";
notes += *setIter;
}
@@ -302,8 +297,10 @@ void Event::updateNotes(const StringSetMap &newNoteSetMap) {
} // void Event::updateNotes(const StringSetMap &newNoteSetMap)
void Event::AddPacket(ZMLockedPacket *packetlock) {
std::unique_lock<std::mutex> lck(packet_queue_mutex);
packet_queue.push(packetlock);
{
std::unique_lock<std::mutex> lck(packet_queue_mutex);
packet_queue.push(packetlock);
}
packet_queue_condition.notify_one();
}
@@ -325,7 +322,6 @@ void Event::AddPacket_(const std::shared_ptr<ZMPacket>&packet) {
}
if ((packet->codec_type == AVMEDIA_TYPE_VIDEO) or packet->image) {
//AddFrame(packet->image, packet->timestamp, packet->zone_stats, packet->score, packet->analysis_image);
AddFrame(packet);
}
end_time = packet->timestamp;
@@ -372,15 +368,13 @@ void Event::WriteDbFrames() {
} // end while frames
// The -1 is for the extra , added for values above
frame_insert_sql.erase(frame_insert_sql.size()-1);
//zmDbDo(frame_insert_sql);
dbQueue.push(std::move(frame_insert_sql));
if (stats_insert_sql.size() > 208) {
// The -1 is for the extra , added for values above
stats_insert_sql.erase(stats_insert_sql.size()-1);
//zmDbDo(stats_insert_sql);
dbQueue.push(std::move(stats_insert_sql));
}
} // end void Event::WriteDbFrames()
} // end void Event::WriteDbFrames()
void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
if (packet->timestamp.time_since_epoch() == Seconds(0)) {
@@ -419,10 +413,11 @@ void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
Debug(1, "frames %d, score %d max_score %d", frames, score, max_score);
// If this is the first frame, we should add a thumbnail to the event directory
if ((frames == 1) || (score > max_score)) {
if ((frames == 1) || (score > max_score) || (!snapshot_file_written)) {
write_to_db = true; // web ui might show this as thumbnail, so db needs to know about it.
Debug(1, "Writing snapshot to %s", snapshot_file.c_str());
WriteFrameImage(packet->image, packet->timestamp, snapshot_file.c_str());
snapshot_file_written = true;
} else {
Debug(1, "Not Writing snapshot because frames %d score %d > max %d", frames, score, max_score);
}
@@ -454,7 +449,7 @@ void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
((AVPixelFormat)packet->in_frame->format == AV_PIX_FMT_YUVJ420P)
)
) {
std::string event_file = stringtf("%s/%d-y.jpg", path.c_str(), frames);
event_file = stringtf("%s/%d-y.jpg", path.c_str(), frames);
Image y_image(
packet->in_frame->width,
packet->in_frame->height,
@@ -463,9 +458,8 @@ void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
if (!WriteFrameImage(&y_image, packet->timestamp, event_file.c_str(), true)) {
Error("Failed to write y frame image to %s", event_file.c_str());
}
}
}
} // end if write y-channel image
} // end if has analysis images turned on
} // end if is an alarm frame
} else {
Debug(1, "No image");
@@ -499,11 +493,11 @@ void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
double fps = monitor->get_capture_fps();
if (write_to_db
or
(frame_data.size() >= MAX_DB_FRAMES)
(frame_data.size() >= MAX_DB_FRAMES)
or
(frame_type == BULK)
(frame_type == BULK)
or
(fps and (frame_data.size() > 5*fps))) {
(fps and (frame_data.size() > 5*fps))) {
Debug(1, "Adding %zu frames to DB because write_to_db:%d or frames > analysis fps %f or BULK(%d)",
frame_data.size(), write_to_db, fps, (frame_type == BULK));
WriteDbFrames();
@@ -522,10 +516,8 @@ void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet) {
} else {
Debug(1, "Not Adding %zu frames to DB because write_to_db:%d or frames > analysis fps %f or BULK",
frame_data.size(), write_to_db, fps);
} // end if frame_type == BULK
} // end if db_frame
end_time = packet->timestamp;
} // end if frame_type == BULK
} // end if db_frame
} // void Event::AddFrame(const std::shared_ptr<ZMPacket>&packet)
bool Event::SetPath(Storage *storage) {
@@ -543,7 +535,6 @@ bool Event::SetPath(Storage *storage) {
tm stime = {};
localtime_r(&start_time_t, &stime);
if (scheme == Storage::DEEP) {
int dt_parts[6];
dt_parts[0] = stime.tm_year-100;
dt_parts[1] = stime.tm_mon+1;
@@ -638,7 +629,7 @@ void Event::Run() {
result = zmDbFetch(sql);
if (result) {
for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) {
for (int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++) {
storage = new Storage(atoi(dbrow[0]));
if (SetPath(storage))
break;
@@ -665,7 +656,6 @@ void Event::Run() {
if (monitor->GetOptVideoWriter() != 0) {
/* Save as video */
videoStore = new VideoStore(
video_incomplete_path.c_str(),
container.c_str(),
@@ -675,11 +665,11 @@ void Event::Run() {
( monitor->RecordAudio() ? monitor->GetAudioCodecContext() : nullptr ),
monitor );
if ( !videoStore->open() ) {
if (!videoStore->open()) {
Warning("Failed to open videostore, turning on jpegs");
delete videoStore;
videoStore = nullptr;
if ( ! ( save_jpegs & 1 ) ) {
if (!(save_jpegs & 1)) {
save_jpegs |= 1; // Turn on jpeg storage
zmDbDo(stringtf("UPDATE Events SET SaveJpegs=%d WHERE Id=%" PRIu64, save_jpegs, id));
}
@@ -693,28 +683,32 @@ void Event::Run() {
if (storage != monitor->getStorage())
delete storage;
std::unique_lock<std::mutex> lck(packet_queue_mutex);
// The idea is to process the queue no matter what so that all packets get processed.
// We only break if the queue is empty
while (true) {
if (!packet_queue.empty()) {
Debug(1, "adding packet");
const ZMLockedPacket * packet_lock = packet_queue.front();
ZMLockedPacket * packet_lock = nullptr;
{
std::unique_lock<std::mutex> lck(packet_queue_mutex);
if (packet_queue.empty()) {
if (terminate_ or zm_terminate) break;
packet_queue_condition.wait(lck);
// Neccessary because we don't hold the lock in the while condition
}
if (!packet_queue.empty()) {
// Packets on this queue are locked. They are locked by analysis thread
packet_lock = packet_queue.front();
packet_queue.pop();
}
} // end lock scope
if (packet_lock) {
this->AddPacket_(packet_lock->packet_);
delete packet_lock;
packet_queue.pop();
} else {
if (terminate_ or zm_terminate) {
Debug(1, "terminating");
break;
}
Debug(1, "waiting");
packet_queue_condition.wait(lck);
Debug(1, "wakeing");
}
}
}
} // end while
} // end Run()
int Event::MonitorId() {
return monitor->Id();
}

View File

@@ -88,6 +88,7 @@ class Event {
int max_score;
std::string path;
std::string snapshot_file;
bool snapshot_file_written;
std::string alarm_file;
VideoStore *videoStore;
@@ -145,7 +146,10 @@ class Event {
void AddFrame(const std::shared_ptr<ZMPacket>&packet);
void Stop() {
terminate_ = true;
{
std::unique_lock<std::mutex> lck(packet_queue_mutex);
terminate_ = true;
}
packet_queue_condition.notify_all();
}
bool Stopped() const { return terminate_; }

View File

@@ -450,46 +450,18 @@ void EventStream::processCommand(const CmdMsg *msg) {
x = ((unsigned char)msg->msg_data[1]<<8)|(unsigned char)msg->msg_data[2];
y = ((unsigned char)msg->msg_data[3]<<8)|(unsigned char)msg->msg_data[4];
Debug(1, "Got ZOOM IN command, to %d,%d", x, y);
switch ( zoom ) {
case 100:
zoom = 150;
break;
case 150:
zoom = 200;
break;
case 200:
zoom = 300;
break;
case 300:
zoom = 400;
break;
case 400:
default :
zoom = 500;
break;
}
zoom += 10;
send_frame = true;
break;
case CMD_ZOOMOUT :
Debug(1, "Got ZOOM OUT command");
switch ( zoom ) {
case 500:
zoom = 400;
break;
case 400:
zoom = 300;
break;
case 300:
zoom = 200;
break;
case 200:
zoom = 150;
break;
case 150:
default :
zoom = 100;
break;
}
zoom -= 10;
if (zoom < 100) zoom = 100;
send_frame = true;
break;
case CMD_ZOOMSTOP :
Debug(1, "Got ZOOM STOP command");
zoom = 100;
send_frame = true;
break;
case CMD_PAN :
@@ -745,9 +717,8 @@ bool EventStream::sendFrame(Microseconds delta_us) {
} else {
bool send_raw = (type == STREAM_JPEG) && ((scale >= ZM_SCALE_BASE) && (zoom == ZM_SCALE_BASE)) && !filepath.empty();
fprintf(stdout, "--" BOUNDARY "\r\n");
if (send_raw) {
fprintf(stdout, "--" BOUNDARY "\r\n");
if (!send_file(filepath)) {
Error("Can't send %s: %s", filepath.c_str(), strerror(errno));
return false;
@@ -762,9 +733,8 @@ bool EventStream::sendFrame(Microseconds delta_us) {
FrameData *frame_data = &event_data->frames[curr_frame_id-1];
AVFrame *frame =
ffmpeg_input->get_frame(ffmpeg_input->get_video_stream_id(), FPSeconds(frame_data->offset).count());
if ( frame ) {
if (frame) {
image = new Image(frame);
//av_frame_free(&frame);
} else {
Error("Failed getting a frame.");
return false;
@@ -813,6 +783,7 @@ bool EventStream::sendFrame(Microseconds delta_us) {
int img_buffer_size = 0;
uint8_t *img_buffer = temp_img_buffer;
fprintf(stdout, "--" BOUNDARY "\r\n");
switch ( type ) {
case STREAM_JPEG :
send_image->EncodeJpeg(img_buffer, &img_buffer_size);
@@ -871,25 +842,17 @@ void EventStream::runStream() {
SystemTimePoint::duration last_frame_offset = Seconds(0);
SystemTimePoint::duration time_to_event = Seconds(0);
std::thread command_processor;
if (connkey) {
command_processor = std::thread(&EventStream::checkCommandQueue, this);
}
while ( !zm_terminate ) {
now = std::chrono::steady_clock::now();
Microseconds delta = Microseconds(0);
send_frame = false;
if ( connkey ) {
// commands may set send_frame to true
while ( checkCommandQueue() && !zm_terminate ) {
// The idea is to loop here processing all commands before proceeding.
}
// Update modified time of the socket .lock file so that we can tell which ones are stale.
if (now - last_comm_update > Hours(1)) {
touch(sock_path_lock);
last_comm_update = now;
}
}
// Get current frame data
FrameData *frame_data = &event_data->frames[curr_frame_id-1];
@@ -1104,7 +1067,14 @@ void EventStream::runStream() {
delete vid_stream;
}
closeComms();
if (connkey) {
if (command_processor.joinable()) {
Debug(1, "command_processor is joinable");
command_processor.join();
} else {
Debug(1, "command_processor is not joinable");
}
}
} // end void EventStream::runStream()
bool EventStream::send_file(const std::string &filepath) {
@@ -1125,7 +1095,7 @@ bool EventStream::send_file(const std::string &filepath) {
Info("File size is zero. Unable to send raw frame %d: %s", curr_frame_id, strerror(errno));
return false;
}
if (0 > fprintf(stdout, "Content-Length: %jd\r\n\r\n", filestat.st_size)) {
if (0 > fprintf(stdout, "Content-Length: %jd\r\n\r\n", static_cast<intmax_t>(filestat.st_size))) {
fclose(fdj); /* Close the file handle */
Info("Unable to send raw frame %d: %s", curr_frame_id, strerror(errno));
return false;

View File

@@ -58,7 +58,7 @@ void log_libav_callback(void *ptr, int level, const char *fmt, va_list vargs) {
Error("Unknown log level %d", level);
}
if (log) {
if (log and (log->level() >= log_level) ) {
char logString[8192];
int length = vsnprintf(logString, sizeof(logString)-1, fmt, vargs);
if (length > 0) {
@@ -325,7 +325,7 @@ void zm_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output)
//dump_sidedata(NULL, st, " ");
}
int check_sample_fmt(AVCodec *codec, enum AVSampleFormat sample_fmt) {
int check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt) {
const enum AVSampleFormat *p = codec->sample_fmts;
while (*p != AV_SAMPLE_FMT_NONE) {

View File

@@ -23,6 +23,8 @@
#include "zm_config.h"
#include "zm_define.h"
#include <memory>
extern "C" {
#include <libswresample/swresample.h>
@@ -177,34 +179,34 @@ void zm_dump_codecpar(const AVCodecParameters *par);
Debug(2, "%s: pts: %" PRId64 ", dts: %" PRId64 \
", size: %d, stream_index: %d, flags: %04x, keyframe(%d) pos: %" PRId64 ", duration: %" AV_PACKET_DURATION_FMT, \
text,\
pkt.pts,\
pkt.dts,\
pkt.size,\
pkt.stream_index,\
pkt.flags,\
pkt.flags & AV_PKT_FLAG_KEY,\
pkt.pos,\
pkt.duration)
pkt->pts,\
pkt->dts,\
pkt->size,\
pkt->stream_index,\
pkt->flags,\
pkt->flags & AV_PKT_FLAG_KEY,\
pkt->pos,\
pkt->duration)
# define ZM_DUMP_STREAM_PACKET(stream, pkt, text) \
if (logDebugging()) { \
double pts_time = static_cast<double>(av_rescale_q(pkt.pts, stream->time_base, AV_TIME_BASE_Q)) / AV_TIME_BASE; \
double pts_time = static_cast<double>(av_rescale_q(pkt->pts, stream->time_base, AV_TIME_BASE_Q)) / AV_TIME_BASE; \
\
Debug(2, "%s: pts: %" PRId64 " * %u/%u=%f, dts: %" PRId64 \
", size: %d, stream_index: %d, %s flags: %04x, keyframe(%d) pos: %" PRId64", duration: %" AV_PACKET_DURATION_FMT, \
text, \
pkt.pts, \
pkt->pts, \
stream->time_base.num, \
stream->time_base.den, \
pts_time, \
pkt.dts, \
pkt.size, \
pkt.stream_index, \
pkt->dts, \
pkt->size, \
pkt->stream_index, \
av_get_media_type_string(CODEC_TYPE(stream)), \
pkt.flags, \
pkt.flags & AV_PKT_FLAG_KEY, \
pkt.pos, \
pkt.duration); \
pkt->flags, \
pkt->flags & AV_PKT_FLAG_KEY, \
pkt->pos, \
pkt->duration); \
}
#else
@@ -217,7 +219,7 @@ void zm_dump_codecpar(const AVCodecParameters *par);
#define zm_av_frame_alloc() av_frame_alloc()
int check_sample_fmt(AVCodec *codec, enum AVSampleFormat sample_fmt);
int check_sample_fmt(const AVCodec *codec, enum AVSampleFormat sample_fmt);
enum AVPixelFormat fix_deprecated_pix_fmt(enum AVPixelFormat );
bool is_video_stream(const AVStream *);
@@ -238,4 +240,58 @@ int zm_resample_get_delay(SwrContext *resample_ctx, int time_base);
int zm_add_samples_to_fifo(AVAudioFifo *fifo, AVFrame *frame);
int zm_get_samples_from_fifo(AVAudioFifo *fifo, AVFrame *frame);
struct zm_free_av_packet
{
void operator()(AVPacket *pkt) const
{
av_packet_free(&pkt);
}
};
using av_packet_ptr = std::unique_ptr<AVPacket, zm_free_av_packet>;
struct av_packet_guard
{
av_packet_guard() : packet{nullptr}
{
}
explicit av_packet_guard(const av_packet_ptr& p) : packet{p.get()}
{
}
explicit av_packet_guard(AVPacket *p) : packet{p}
{
}
~av_packet_guard()
{
if (packet)
av_packet_unref(packet);
}
void acquire(const av_packet_ptr& p)
{
packet = p.get();
}
void acquire(AVPacket *p)
{
packet = p;
}
void release()
{
packet = nullptr;
}
private:
AVPacket *packet;
};
struct zm_free_av_frame
{
void operator()(AVFrame *frame) const
{
av_frame_free(&frame);
}
};
using av_frame_ptr = std::unique_ptr<AVFrame, zm_free_av_frame>;
#endif // ZM_FFMPEG_H

View File

@@ -27,6 +27,7 @@
extern "C" {
#include <libavutil/time.h>
#include <libavdevice/avdevice.h>
}
TimePoint start_read_time;
@@ -84,6 +85,8 @@ FfmpegCamera::FfmpegCamera(
const Monitor *monitor,
const std::string &p_path,
const std::string &p_second_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,
@@ -113,16 +116,20 @@ FfmpegCamera::FfmpegCamera(
),
mPath(p_path),
mSecondPath(p_second_path),
mUser(UriEncode(p_user)),
mPass(UriEncode(p_pass)),
mMethod(p_method),
mOptions(p_options),
hwaccel_name(p_hwaccel_name),
hwaccel_device(p_hwaccel_device)
hwaccel_device(p_hwaccel_device),
frameCount(0)
{
mMaskedPath = remove_authentication(mPath);
mMaskedSecondPath = remove_authentication(mSecondPath);
if ( capture ) {
FFMPEGInit();
}
frameCount = 0;
mCanCapture = false;
error_count = 0;
use_hwaccel = true;
@@ -149,6 +156,7 @@ FfmpegCamera::FfmpegCamera(
Panic("Unexpected colours: %d", colours);
}
packet = av_packet_ptr{av_packet_alloc()};
} // FfmpegCamera::FfmpegCamera
FfmpegCamera::~FfmpegCamera() {
@@ -160,12 +168,12 @@ FfmpegCamera::~FfmpegCamera() {
int FfmpegCamera::PrimeCapture() {
start_read_time = std::chrono::steady_clock::now();
if ( mCanCapture ) {
Debug(1, "Priming capture from %s, Closing", mPath.c_str());
Debug(1, "Priming capture from %s, Closing", mMaskedPath.c_str());
Close();
}
mVideoStreamId = -1;
mAudioStreamId = -1;
Debug(1, "Priming capture from %s", mPath.c_str());
Debug(1, "Priming capture from %s", mMaskedPath.c_str());
return OpenFfmpeg();
}
@@ -201,7 +209,7 @@ int FfmpegCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
);
}
if ((ret = av_read_frame(formatContextPtr, &packet)) < 0) {
if ((ret = av_read_frame(formatContextPtr, packet.get())) < 0) {
if (
// Check if EOF.
(ret == AVERROR_EOF || (formatContextPtr->pb && formatContextPtr->pb->eof_reached)) ||
@@ -209,37 +217,38 @@ int FfmpegCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
(ret == -110)
) {
Info("Unable to read packet from stream %d: error %d \"%s\".",
packet.stream_index, ret, av_make_error_string(ret).c_str());
packet->stream_index, ret, av_make_error_string(ret).c_str());
} else {
Error("Unable to read packet from stream %d: error %d \"%s\".",
packet.stream_index, ret, av_make_error_string(ret).c_str());
packet->stream_index, ret, av_make_error_string(ret).c_str());
}
return -1;
}
AVStream *stream = formatContextPtr->streams[packet.stream_index];
av_packet_guard pkt_guard{packet};
AVStream *stream = formatContextPtr->streams[packet->stream_index];
ZM_DUMP_STREAM_PACKET(stream, packet, "ffmpeg_camera in");
zm_packet->codec_type = stream->codecpar->codec_type;
bytes += packet.size;
zm_packet->set_packet(&packet);
bytes += packet->size;
zm_packet->set_packet(packet.get());
zm_packet->stream = stream;
zm_packet->pts = av_rescale_q(packet.pts, stream->time_base, AV_TIME_BASE_Q);
if (packet.pts != AV_NOPTS_VALUE) {
zm_packet->pts = av_rescale_q(packet->pts, stream->time_base, AV_TIME_BASE_Q);
if (packet->pts != AV_NOPTS_VALUE) {
if (stream == mVideoStream) {
if (mFirstVideoPTS == AV_NOPTS_VALUE)
mFirstVideoPTS = packet.pts;
mFirstVideoPTS = packet->pts;
mLastVideoPTS = packet.pts - mFirstVideoPTS;
mLastVideoPTS = packet->pts - mFirstVideoPTS;
} else {
if (mFirstAudioPTS == AV_NOPTS_VALUE)
mFirstAudioPTS = packet.pts;
mFirstAudioPTS = packet->pts;
mLastAudioPTS = packet.pts - mFirstAudioPTS;
mLastAudioPTS = packet->pts - mFirstAudioPTS;
}
}
zm_av_packet_unref(&packet);
return 1;
} // FfmpegCamera::Capture
@@ -250,15 +259,21 @@ int FfmpegCamera::PostCapture() {
}
int FfmpegCamera::OpenFfmpeg() {
int ret;
int ret = 0;
error_count = 0;
#if LIBAVFORMAT_VERSION_CHECK(59, 16, 100, 16, 100)
const
#endif
AVInputFormat *input_format = nullptr;
// Handle options
AVDictionary *opts = nullptr;
ret = av_dict_parse_string(&opts, Options().c_str(), "=", ",", 0);
if ( ret < 0 ) {
Warning("Could not parse ffmpeg input options '%s'", Options().c_str());
if (!mOptions.empty()) {
ret = av_dict_parse_string(&opts, mOptions.c_str(), "=", ",", 0);
if (ret < 0) {
Warning("Could not parse ffmpeg input options '%s'", mOptions.c_str());
}
}
// Set transport method as specified by method field, rtpUni is default
@@ -277,46 +292,54 @@ int FfmpegCamera::OpenFfmpeg() {
} else {
Warning("Unknown method (%s)", method.c_str());
}
if ( ret < 0 ) {
if (ret < 0) {
Warning("Could not set rtsp_transport method '%s'", method.c_str());
}
} else if (protocol == "V4L2") {
avdevice_register_all();
input_format = av_find_input_format("video4linux2");
if (!input_format) {
Error("Cannot find v4l2 input format");
return -1;
}
mPath = mPath.substr(7);
} // end if RTSP
Debug(1, "Calling avformat_open_input for %s", mPath.c_str());
Debug(1, "Calling avformat_open_input for %s", mMaskedPath.c_str());
mFormatContext = avformat_alloc_context();
// Speed up find_stream_info
// FIXME can speed up initial analysis but need sensible parameters...
// mFormatContext->probesize = 32;
// mFormatContext->max_analyze_duration = 32;
mFormatContext->interrupt_callback.callback = FfmpegInterruptCallback;
mFormatContext->interrupt_callback.opaque = this;
mFormatContext->flags |= AVFMT_FLAG_NOBUFFER | AVFMT_FLAG_FLUSH_PACKETS;
ret = avformat_open_input(&mFormatContext, mPath.c_str(), nullptr, &opts);
if( mUser.length() > 0 ) {
// build the actual uri string with encoded parameters (from the user and pass fields)
mPath = StringToLower(protocol) + "://" + mUser + ":" + mPass + "@" + mMaskedPath.substr(7, std::string::npos);
Debug(1, "Rebuilt URI with encoded parameters: '%s'", mPath.c_str());
}
ret = avformat_open_input(&mFormatContext, mPath.c_str(), input_format, &opts);
if (ret != 0) {
logPrintf(Logger::ERROR + monitor->Importance(),
"Unable to open input %s due to: %s", mPath.c_str(),
"Unable to open input %s due to: %s", mMaskedPath.c_str(),
av_make_error_string(ret).c_str());
if (mFormatContext) {
avformat_close_input(&mFormatContext);
mFormatContext = nullptr;
}
avformat_close_input(&mFormatContext);
mFormatContext = nullptr;
av_dict_free(&opts);
return -1;
}
AVDictionaryEntry *e = nullptr;
while ( (e = av_dict_get(opts, "", e, AV_DICT_IGNORE_SUFFIX)) != nullptr ) {
while ((e = av_dict_get(opts, "", e, AV_DICT_IGNORE_SUFFIX)) != nullptr) {
Warning("Option %s not recognized by ffmpeg", e->key);
}
av_dict_free(&opts);
Debug(1, "Finding stream info");
ret = avformat_find_stream_info(mFormatContext, nullptr);
if ( ret < 0 ) {
if (ret < 0) {
Error("Unable to find stream info from %s due to: %s",
mPath.c_str(), av_make_error_string(ret).c_str());
mMaskedPath.c_str(), av_make_error_string(ret).c_str());
avformat_close_input(&mFormatContext);
return -1;
}
@@ -325,13 +348,15 @@ int FfmpegCamera::OpenFfmpeg() {
mVideoStreamId = -1;
mAudioStreamId = -1;
for (unsigned int i=0; i < mFormatContext->nb_streams; i++) {
AVStream *stream = mFormatContext->streams[i];
const AVStream *stream = mFormatContext->streams[i];
if (is_video_stream(stream)) {
if (!(stream->codecpar->width && stream->codecpar->height)) {
Warning("No width and height in video stream. Trying again");
continue;
}
if (mVideoStreamId == -1) {
mVideoStreamId = i;
mVideoStream = mFormatContext->streams[i];
// if we break, then we won't find the audio stream
continue;
} else {
Debug(2, "Have another video stream.");
}
@@ -346,14 +371,14 @@ int FfmpegCamera::OpenFfmpeg() {
} // end foreach stream
if (mVideoStreamId == -1) {
Error("Unable to locate video stream in %s", mPath.c_str());
avformat_close_input(&mFormatContext);
return -1;
}
Debug(3, "Found video stream at index %d, audio stream at index %d",
mVideoStreamId, mAudioStreamId);
AVCodec *mVideoCodec = nullptr;
const AVCodec *mVideoCodec = nullptr;
if (mVideoStream->codecpar->codec_id == AV_CODEC_ID_H264) {
if ((mVideoCodec = avcodec_find_decoder_by_name("h264_mmal")) == nullptr) {
Debug(1, "Failed to find decoder (h264_mmal)");
@@ -366,7 +391,7 @@ int FfmpegCamera::OpenFfmpeg() {
mVideoCodec = avcodec_find_decoder(mVideoStream->codecpar->codec_id);
if (!mVideoCodec) {
// Try and get the codec from the codec context
Error("Can't find codec for video stream from %s", mPath.c_str());
Error("Can't find codec for video stream from %s", mMaskedPath.c_str());
return -1;
}
}
@@ -475,7 +500,7 @@ int FfmpegCamera::OpenFfmpeg() {
Warning("Option %s not recognized by ffmpeg", e->key);
}
if (ret < 0) {
Error("Unable to open codec for video stream from %s", mPath.c_str());
Error("Unable to open codec for video stream from %s", mMaskedPath.c_str());
av_dict_free(&opts);
return -1;
}
@@ -494,9 +519,9 @@ int FfmpegCamera::OpenFfmpeg() {
} // end if have audio stream
if ( mAudioStreamId >= 0 ) {
AVCodec *mAudioCodec = nullptr;
const AVCodec *mAudioCodec = nullptr;
if (!(mAudioCodec = avcodec_find_decoder(mAudioStream->codecpar->codec_id))) {
Debug(1, "Can't find codec for audio stream from %s", mPath.c_str());
Debug(1, "Can't find codec for audio stream from %s", mMaskedPath.c_str());
} else {
mAudioCodecContext = avcodec_alloc_context3(mAudioCodec);
avcodec_parameters_to_context(mAudioCodecContext, mAudioStream->codecpar);
@@ -504,7 +529,7 @@ int FfmpegCamera::OpenFfmpeg() {
zm_dump_stream_format((mSecondFormatContext?mSecondFormatContext:mFormatContext), mAudioStreamId, 0, 0);
// Open the codec
if (avcodec_open2(mAudioCodecContext, mAudioCodec, nullptr) < 0) {
Error("Unable to open codec for audio stream from %s", mPath.c_str());
Error("Unable to open codec for audio stream from %s", mMaskedPath.c_str());
return -1;
} // end if opened
} // end if found decoder

View File

@@ -36,7 +36,11 @@ typedef struct DecodeContext {
class FfmpegCamera : public Camera {
protected:
std::string mPath;
std::string mMaskedPath;
std::string mSecondPath;
std::string mUser;
std::string mPass;
std::string mMaskedSecondPath;
std::string mMethod;
std::string mOptions;
@@ -56,7 +60,7 @@ class FfmpegCamera : public Camera {
// Used to store the incoming packet, it will get copied when queued.
// We only ever need one at a time, so instead of constantly allocating
// and freeing this structure, we will just make it a member of the object.
AVPacket packet;
av_packet_ptr packet;
int OpenFfmpeg();
int Close() override;
@@ -69,8 +73,10 @@ class FfmpegCamera : public Camera {
public:
FfmpegCamera(
const Monitor *monitor,
const std::string &path,
const std::string &second_path,
const std::string &p_path,
const std::string &p_second_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,

View File

@@ -9,18 +9,13 @@ FFmpeg_Input::FFmpeg_Input() {
audio_stream_id = -1;
FFMPEGInit();
streams = nullptr;
frame = nullptr;
last_seek_request = -1;
}
FFmpeg_Input::~FFmpeg_Input() {
if ( input_format_context ) {
if (input_format_context) {
Close();
}
if ( frame ) {
av_frame_free(&frame);
frame = nullptr;
}
} // end ~FFmpeg_Input()
/* Takes streams provided from elsewhere. They might not come from the same source
@@ -31,9 +26,9 @@ int FFmpeg_Input::Open(
const AVStream * audio_in_stream,
const AVCodecContext * audio_in_ctx
) {
int max_stream_index = video_stream_id = video_in_stream->index;
int max_stream_index = video_stream_id = video_in_stream->index;
if ( audio_in_stream ) {
if (audio_in_stream) {
max_stream_index = video_in_stream->index > audio_in_stream->index ? video_in_stream->index : audio_in_stream->index;
audio_stream_id = audio_in_stream->index;
}
@@ -42,7 +37,6 @@ int FFmpeg_Input::Open(
}
int FFmpeg_Input::Open(const char *filepath) {
int error;
/** Open the input file to read from it. */
@@ -67,17 +61,17 @@ int FFmpeg_Input::Open(const char *filepath) {
streams = new stream[input_format_context->nb_streams];
Debug(2, "Have %d streams", input_format_context->nb_streams);
for ( unsigned int i = 0; i < input_format_context->nb_streams; i += 1 ) {
if ( is_video_stream(input_format_context->streams[i]) ) {
for (unsigned int i = 0; i < input_format_context->nb_streams; i += 1) {
if (is_video_stream(input_format_context->streams[i])) {
zm_dump_stream_format(input_format_context, i, 0, 0);
if ( video_stream_id == -1 ) {
if (video_stream_id == -1) {
video_stream_id = i;
// if we break, then we won't find the audio stream
} else {
Warning("Have another video stream.");
}
} else if ( is_audio_stream(input_format_context->streams[i]) ) {
if ( audio_stream_id == -1 ) {
} else if (is_audio_stream(input_format_context->streams[i])) {
if (audio_stream_id == -1) {
Debug(2, "Audio stream is %d", i);
audio_stream_id = i;
} else {
@@ -88,10 +82,8 @@ int FFmpeg_Input::Open(const char *filepath) {
}
streams[i].frame_count = 0;
streams[i].context = avcodec_alloc_context3(nullptr);
avcodec_parameters_to_context(streams[i].context, input_format_context->streams[i]->codecpar);
if ( !(streams[i].codec = avcodec_find_decoder(streams[i].context->codec_id)) ) {
if (!(streams[i].codec = avcodec_find_decoder(input_format_context->streams[i]->codecpar->codec_id))) {
Error("Could not find input codec");
avformat_close_input(&input_format_context);
return AVERROR_EXIT;
@@ -99,8 +91,15 @@ int FFmpeg_Input::Open(const char *filepath) {
Debug(1, "Using codec (%s) for stream %d", streams[i].codec->name, i);
}
Debug(1, "Allocating");
streams[i].context = avcodec_alloc_context3(streams[i].codec);
Debug(1, "Parameters");
avcodec_parameters_to_context(streams[i].context, input_format_context->streams[i]->codecpar);
Debug(1, "Dumping codec");
zm_dump_codec(streams[i].context);
error = avcodec_open2(streams[i].context, streams[i].codec, nullptr);
if ( error < 0 ) {
if (error < 0) {
Error("Could not open input codec (error '%s')",
av_make_error_string(error).c_str());
avcodec_free_context(&streams[i].context);
@@ -108,19 +107,25 @@ int FFmpeg_Input::Open(const char *filepath) {
input_format_context = nullptr;
return error;
}
zm_dump_codec(streams[i].context);
if (!(streams[i].context->time_base.num && streams[i].context->time_base.den)) {
Warning("Setting to default time base");
streams[i].context->time_base.num = 1;
streams[i].context->time_base.den = 90000;
}
} // end foreach stream
if ( video_stream_id == -1 )
if (video_stream_id == -1)
Debug(1, "Unable to locate video stream in %s", filepath);
if ( audio_stream_id == -1 )
if (audio_stream_id == -1)
Debug(3, "Unable to locate audio stream in %s", filepath);
return 1;
} // end int FFmpeg_Input::Open( const char * filepath )
int FFmpeg_Input::Close( ) {
if ( streams ) {
for ( unsigned int i = 0; i < input_format_context->nb_streams; i += 1 ) {
if (streams) {
for (unsigned int i = 0; i < input_format_context->nb_streams; i += 1) {
avcodec_close(streams[i].context);
avcodec_free_context(&streams[i].context);
streams[i].context = nullptr;
@@ -129,7 +134,7 @@ int FFmpeg_Input::Close( ) {
streams = nullptr;
}
if ( input_format_context ) {
if (input_format_context) {
avformat_close_input(&input_format_context);
input_format_context = nullptr;
}
@@ -137,13 +142,17 @@ int FFmpeg_Input::Close( ) {
} // end int FFmpeg_Input::Close()
AVFrame *FFmpeg_Input::get_frame(int stream_id) {
int frameComplete = false;
AVPacket packet;
av_init_packet(&packet);
bool frameComplete = false;
av_packet_ptr packet{av_packet_alloc()};
while ( !frameComplete ) {
int ret = av_read_frame(input_format_context, &packet);
if ( ret < 0 ) {
if (!packet) {
Error("Unable to allocate packet.");
return nullptr;
}
while (!frameComplete) {
int ret = av_read_frame(input_format_context, packet.get());
if (ret < 0) {
if (
// Check if EOF.
(ret == AVERROR_EOF || (input_format_context->pb && input_format_context->pb->eof_reached)) ||
@@ -154,45 +163,63 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id) {
return nullptr;
}
Error("Unable to read packet from stream %d: error %d \"%s\".",
packet.stream_index, ret, av_make_error_string(ret).c_str());
packet->stream_index, ret, av_make_error_string(ret).c_str());
return nullptr;
}
ZM_DUMP_STREAM_PACKET(input_format_context->streams[packet.stream_index], packet, "Received packet");
ZM_DUMP_STREAM_PACKET(input_format_context->streams[packet->stream_index], packet, "Received packet");
if ( (stream_id >= 0) && (packet.stream_index != stream_id) ) {
Debug(1,"Packet is not for our stream (%d)", packet.stream_index );
av_packet_guard pkt_guard{packet};
if ((stream_id >= 0) && (packet->stream_index != stream_id)) {
Debug(1,"Packet is not for our stream (%d)", packet->stream_index );
continue;
}
AVCodecContext *context = streams[packet.stream_index].context;
AVCodecContext *context = streams[packet->stream_index].context;
if ( frame ) {
av_frame_free(&frame);
frame = zm_av_frame_alloc();
} else {
frame = zm_av_frame_alloc();
frame = av_frame_ptr{zm_av_frame_alloc()};
if (!frame) {
Error("Unable to allocate frame.");
return nullptr;
}
ret = zm_send_packet_receive_frame(context, frame, packet);
ret = zm_send_packet_receive_frame(context, frame.get(), *packet);
if ( ret < 0 ) {
Error("Unable to decode frame at frame %d: %d %s, continuing",
streams[packet.stream_index].frame_count, ret, av_make_error_string(ret).c_str());
zm_av_packet_unref(&packet);
av_frame_free(&frame);
streams[packet->stream_index].frame_count, ret, av_make_error_string(ret).c_str());
frame = nullptr;
continue;
} else {
if ( is_video_stream(input_format_context->streams[packet.stream_index]) ) {
zm_dump_video_frame(frame, "resulting video frame");
if (is_video_stream(input_format_context->streams[packet->stream_index])) {
zm_dump_video_frame(frame.get(), "resulting video frame");
} else {
zm_dump_frame(frame, "resulting frame");
zm_dump_frame(frame.get(), "resulting frame");
}
}
frameComplete = 1;
frameComplete = true;
zm_av_packet_unref(&packet);
if (context->time_base.num && context->time_base.den) {
// Convert timestamps to stream timebase instead of codec timebase
frame->pts = av_rescale_q(frame->pts,
context->time_base,
input_format_context->streams[stream_id]->time_base
);
} else {
Warning("No timebase set in context!");
}
if (is_video_stream(input_format_context->streams[packet->stream_index])) {
zm_dump_video_frame(frame.get(), "resulting video frame");
} else {
zm_dump_frame(frame.get(), "resulting frame");
}
} // end while !frameComplete
return frame;
if (is_video_stream(input_format_context->streams[packet->stream_index])) {
zm_dump_video_frame(frame.get(), "resulting video frame");
} else {
zm_dump_frame(frame.get(), "resulting frame");
}
return frame.get();
} // end AVFrame *FFmpeg_Input::get_frame
AVFrame *FFmpeg_Input::get_frame(int stream_id, double at) {
@@ -245,26 +272,38 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id, double at) {
}
} else if ( last_seek_request == seek_target ) {
// paused case, sending keepalives
return frame;
return frame.get();
} // end if frame->pts > seek_target
last_seek_request = seek_target;
// Seeking seems to typically seek to a keyframe, so then we have to decode until we get the frame we want.
if ( frame->pts <= seek_target ) {
if ( is_video_stream(input_format_context->streams[stream_id]) ) {
zm_dump_video_frame(frame, "pts <= seek_target");
} else {
zm_dump_frame(frame, "pts <= seek_target");
// Normally it is likely just the next packet. Need a heuristic for seeking, something like duration * keyframe interval
if (frame->pts + 10*frame->pkt_duration < seek_target) {
Debug(1, "Jumping ahead");
if (( ret = av_seek_frame(input_format_context, stream_id, seek_target,
AVSEEK_FLAG_FRAME
) ) < 0) {
Error("Unable to seek in stream %d", ret);
return nullptr;
}
while ( frame && (frame->pts < seek_target) ) {
if ( !get_frame(stream_id) ) {
// Have to grab a frame to update our current frame to know where we are
get_frame(stream_id);
}
// Seeking seems to typically seek to a keyframe, so then we have to decode until we get the frame we want.
if (frame->pts <= seek_target) {
while (frame && (frame->pts + frame->pkt_duration < seek_target)) {
if (is_video_stream(input_format_context->streams[stream_id])) {
zm_dump_video_frame(frame, "pts <= seek_target");
} else {
zm_dump_frame(frame, "pts <= seek_target");
}
if (!get_frame(stream_id)) {
Warning("Got no frame. returning nothing");
return frame;
return frame.get();
}
}
zm_dump_frame(frame, "frame->pts <= seek_target, got");
return frame;
return frame.get();
}
return get_frame(stream_id);

View File

@@ -2,6 +2,7 @@
#define ZM_FFMPEG_INPUT_H
#include "zm_define.h"
#include "zm_ffmpeg.h"
extern "C" {
#include <libavformat/avformat.h>
@@ -41,7 +42,7 @@ class FFmpeg_Input {
private:
typedef struct {
AVCodecContext *context;
AVCodec *codec;
const AVCodec *codec;
int frame_count;
} stream;
@@ -49,7 +50,7 @@ class FFmpeg_Input {
int video_stream_id;
int audio_stream_id;
AVFormatContext *input_format_context;
AVFrame *frame;
av_frame_ptr frame;
int64_t last_seek_request;
};

View File

@@ -84,13 +84,22 @@ AVFrame *FFmpeg_Output::get_frame( int stream_id ) {
Debug(1, "Getting frame from stream %d", stream_id );
int frameComplete = false;
AVPacket packet;
av_init_packet( &packet );
AVFrame *frame = zm_av_frame_alloc();
av_packet_ptr packet{av_packet_alloc()};
char errbuf[AV_ERROR_MAX_STRING_SIZE];
if (!packet) {
Error("Unable to allocate packet.", );
return nullptr;
}
frame = av_frame_ptr{zm_av_frame_alloc()};
if (!frame) {
Error("Unable to allocate frame.");
return nullptr;
}
while ( !frameComplete ) {
int ret = av_read_frame( input_format_context, &packet );
int ret = av_read_frame( input_format_context, packet.get() );
if ( ret < 0 ) {
av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
if (
@@ -102,20 +111,21 @@ AVFrame *FFmpeg_Output::get_frame( int stream_id ) {
Info( "av_read_frame returned %s.", errbuf );
return NULL;
}
Error( "Unable to read packet from stream %d: error %d \"%s\".", packet.stream_index, ret, errbuf );
Error( "Unable to read packet from stream %d: error %d \"%s\".", packet->stream_index, ret, errbuf );
return NULL;
}
if ( (stream_id < 0 ) || ( packet.stream_index == stream_id ) ) {
Debug(1,"Packet is for our stream (%d)", packet.stream_index );
av_packet_guard pkt_guard{packet};
AVCodecContext *context = streams[packet.stream_index].context;
if ( (stream_id < 0 ) || ( packet->stream_index == stream_id ) ) {
Debug(1,"Packet is for our stream (%d)", packet->stream_index );
ret = avcodec_send_packet( context, &packet );
AVCodecContext *context = streams[packet->stream_index].context;
ret = avcodec_send_packet( context, packet.get() );
if ( ret < 0 ) {
av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE );
Error( "Unable to send packet at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf );
zm_av_packet_unref( &packet );
Error( "Unable to send packet at frame %d: %s, continuing", streams[packet->stream_index].frame_count, errbuf );
continue;
} else {
Debug(1, "Success getting a packet");
@@ -126,25 +136,22 @@ AVFrame *FFmpeg_Output::get_frame( int stream_id ) {
ret = avcodec_receive_frame( context, hwFrame );
if ( ret < 0 ) {
av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE );
Error( "Unable to receive frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf );
zm_av_packet_unref( &packet );
Error( "Unable to receive frame %d: %s, continuing", streams[packet->stream_index].frame_count, errbuf );
continue;
}
ret = av_hwframe_transfer_data(frame, hwFrame, 0);
if (ret < 0) {
av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE );
Error( "Unable to transfer frame at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf );
zm_av_packet_unref( &packet );
Error( "Unable to transfer frame at frame %d: %s, continuing", streams[packet->stream_index].frame_count, errbuf );
continue;
}
} else {
#endif
Debug(1,"Getting a frame?");
ret = avcodec_receive_frame( context, frame );
ret = avcodec_receive_frame( context, frame.get() );
if ( ret < 0 ) {
av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE );
Error( "Unable to send packet at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf );
zm_av_packet_unref( &packet );
Error( "Unable to send packet at frame %d: %s, continuing", streams[packet->stream_index].frame_count, errbuf );
continue;
}
@@ -155,9 +162,7 @@ AVFrame *FFmpeg_Output::get_frame( int stream_id ) {
frameComplete = 1;
} // end if it's the right stream
zm_av_packet_unref( &packet );
} // end while ! frameComplete
return frame;
return frame.get();
} // end AVFrame *FFmpeg_Output::get_frame

View File

@@ -35,6 +35,7 @@ class FFmpeg_Output {
int video_stream_id;
int audio_stream_id;
AVFormatContext *input_format_context;
av_frame_ptr frame;
};
#endif

View File

@@ -99,9 +99,9 @@ bool Fifo::close() {
bool Fifo::writePacket(const ZMPacket &packet) {
if (!(outfile or open())) return false;
Debug(2, "Writing header ZM %u %" PRId64, packet.packet.size, packet.pts);
Debug(2, "Writing header ZM %u %" PRId64, packet.packet->size, packet.pts);
// Going to write a brief header
if (fprintf(outfile, "ZM %u %" PRId64 "\n", packet.packet.size, packet.pts) < 0) {
if (fprintf(outfile, "ZM %u %" PRId64 "\n", packet.packet->size, packet.pts) < 0) {
if (errno != EAGAIN) {
Error("Problem during writing: %s", strerror(errno));
} else {
@@ -110,14 +110,14 @@ bool Fifo::writePacket(const ZMPacket &packet) {
return false;
}
if (fwrite(packet.packet.data, packet.packet.size, 1, outfile) != 1) {
if (fwrite(packet.packet->data, packet.packet->size, 1, outfile) != 1) {
Debug(1, "Unable to write to '%s': %s", path.c_str(), strerror(errno));
return false;
}
return true;
}
bool Fifo::writePacket(std::string filename, const ZMPacket &packet) {
bool Fifo::writePacket(const std::string &filename, const ZMPacket &packet) {
FILE *outfile = nullptr;
int raw_fd = ::open(filename.c_str(), O_WRONLY|O_NONBLOCK|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
@@ -129,8 +129,8 @@ bool Fifo::writePacket(std::string filename, const ZMPacket &packet) {
return false;
}
Debug(4, "Writing packet of size %d pts %" PRId64, packet.packet.size, packet.pts);
if (fwrite(packet.packet.data, packet.packet.size, 1, outfile) != 1) {
Debug(4, "Writing packet of size %d pts %" PRId64, packet.packet->size, packet.pts);
if (fwrite(packet.packet->data, packet.packet->size, 1, outfile) != 1) {
Debug(1, "Unable to write to '%s': %s", filename.c_str(), strerror(errno));
fclose(outfile);
return false;
@@ -159,7 +159,7 @@ bool Fifo::write(uint8_t *data, size_t bytes, int64_t pts) {
return true;
}
bool Fifo::write(std::string filename, uint8_t *data, size_t bytes) {
bool Fifo::write(const std::string &filename, uint8_t *data, size_t bytes) {
FILE *outfile = nullptr;
int raw_fd = ::open(filename.c_str(), O_WRONLY|O_NONBLOCK|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);

View File

@@ -48,8 +48,8 @@ class Fifo {
{}
~Fifo();
static bool writePacket(std::string filename, const ZMPacket &packet);
static bool write(std::string filename, uint8_t *data, size_t size);
static bool writePacket(const std::string &filename, const ZMPacket &packet);
static bool write(const std::string &filename, uint8_t *data, size_t size);
bool open();
bool close();

View File

@@ -35,8 +35,6 @@ bool FifoStream::sendRAWFrames() {
return false;
}
while ( (bytes_read = read(fd, buffer, RAW_BUFFER)) ) {
if ( bytes_read == 0 )
continue;
if ( bytes_read < 0 ) {
Error("Problem during reading: %s", strerror(errno));
close(fd);

View File

@@ -21,6 +21,7 @@
#include "zm_font.h"
#include "zm_poly.h"
#include "zm_swscale.h"
#include "zm_utils.h"
#include <algorithm>
#include <fcntl.h>
@@ -288,7 +289,11 @@ bool Image::Assign(const AVFrame *frame) {
// Desired format
AVPixelFormat format = (AVPixelFormat)AVPixFormat();
AVFrame *dest_frame = zm_av_frame_alloc();
av_frame_ptr dest_frame{zm_av_frame_alloc()};
if (!dest_frame) {
Error("Unable to allocate destination frame");
return false;
}
sws_convert_context = sws_getCachedContext(
sws_convert_context,
frame->width, frame->height, (AVPixelFormat)frame->format,
@@ -300,8 +305,7 @@ bool Image::Assign(const AVFrame *frame) {
Error("Unable to create conversion context");
return false;
}
bool result = Assign(frame, sws_convert_context, dest_frame);
av_frame_free(&dest_frame);
bool result = Assign(frame, sws_convert_context, dest_frame.get());
update_function_pointers();
return result;
} // end Image::Assign(const AVFrame *frame)
@@ -312,12 +316,14 @@ bool Image::Assign(const AVFrame *frame, SwsContext *convert_context, AVFrame *t
temp_frame->pts = frame->pts;
AVPixelFormat format = (AVPixelFormat)AVPixFormat();
if (sws_scale(convert_context,
int ret = sws_scale(convert_context,
frame->data, frame->linesize, 0, frame->height,
temp_frame->data, temp_frame->linesize) < 0) {
Error("Unable to convert raw format %u %ux%u to target format %u %ux%u",
frame->format, frame->width, frame->height,
format, width, height);
temp_frame->data, temp_frame->linesize);
if (ret < 0) {
Error("Unable to convert raw format %u %s %ux%u to target format %u %s %ux%u: %s",
frame->format, av_get_pix_fmt_name(static_cast<AVPixelFormat>(frame->format)), frame->width, frame->height,
format, av_get_pix_fmt_name(format), width, height,
av_make_error_string(ret).c_str());
return false;
}
zm_dump_video_frame(temp_frame, "dest frame after convert");
@@ -783,7 +789,7 @@ void Image::Assign(const Image &image) {
return;
}
} else {
if (new_size > allocation || !buffer) {
if ((new_size > allocation) || !buffer) {
// DumpImgBuffer(); This is also done in AllocImgBuffer
AllocImgBuffer(new_size);
}
@@ -796,9 +802,9 @@ void Image::Assign(const Image &image) {
subpixelorder = image.subpixelorder;
size = new_size;
linesize = image.linesize;
update_function_pointers();
}
update_function_pointers();
if ( image.buffer != buffer )
(*fptr_imgbufcpy)(buffer, image.buffer, size);
}
@@ -911,7 +917,7 @@ bool Image::ReadRaw(const char *filename) {
if ( (unsigned int)statbuf.st_size != size ) {
fclose(infile);
Error("Raw file size mismatch, expected %d bytes, found %ld", size, statbuf.st_size);
Error("Raw file size mismatch, expected %d bytes, found %jd", size, static_cast<intmax_t>(statbuf.st_size));
return false;
}
@@ -2474,6 +2480,10 @@ void Image::Fill(Rgb colour, int density, const Polygon &polygon) {
colour = rgb_convert(colour, subpixelorder);
size_t n_coords = polygon.GetVertices().size();
if (n_coords < 3) {
Error("Not enough vertices in polygon!");
return;
}
std::vector<PolygonFill::Edge> global_edges;
global_edges.reserve(n_coords);
@@ -2745,6 +2755,27 @@ void Image::Flip( bool leftright ) {
AssignDirect(width, height, colours, subpixelorder, flip_buffer, size, ZM_BUFTYPE_ZM);
}
void Image::Scale(const unsigned int new_width, const unsigned int new_height) {
if (width == new_width and height == new_height) return;
// Why larger than we need?
size_t scale_buffer_size = (new_width+1) * (new_height+1) * colours;
uint8_t* scale_buffer = AllocBuffer(scale_buffer_size);
AVPixelFormat format = AVPixFormat();
SWScale swscale;
swscale.init();
swscale.Convert( buffer, allocation,
scale_buffer,
scale_buffer_size,
format, format,
width,
height,
new_width,
new_height);
AssignDirect(new_width, new_height, colours, subpixelorder, scale_buffer, scale_buffer_size, ZM_BUFTYPE_ZM);
}
void Image::Scale(const unsigned int factor) {
if ( !factor ) {
Error("Bogus scale factor %d found", factor);
@@ -5254,4 +5285,21 @@ __attribute__((noinline)) void std_deinterlace_4field_abgr(uint8_t* col1, uint8_
}
}
AVPixelFormat Image::AVPixFormat() const {
if ( colours == ZM_COLOUR_RGB32 ) {
return AV_PIX_FMT_RGBA;
} else if ( colours == ZM_COLOUR_RGB24 ) {
if ( subpixelorder == ZM_SUBPIX_ORDER_BGR){
return AV_PIX_FMT_BGR24;
} else {
return AV_PIX_FMT_RGB24;
}
} else if ( colours == ZM_COLOUR_GRAY8 ) {
return AV_PIX_FMT_GRAY8;
} else {
Error("Unknown colours (%d)",colours);
return AV_PIX_FMT_RGBA;
}
}

View File

@@ -164,22 +164,7 @@ class Image {
inline unsigned int SubpixelOrder() const { return subpixelorder; }
inline unsigned int Size() const { return size; }
inline AVPixelFormat AVPixFormat() {
if ( colours == ZM_COLOUR_RGB32 ) {
return AV_PIX_FMT_RGBA;
} else if ( colours == ZM_COLOUR_RGB24 ) {
if ( subpixelorder == ZM_SUBPIX_ORDER_BGR){
return AV_PIX_FMT_BGR24;
} else {
return AV_PIX_FMT_RGB24;
}
} else if ( colours == ZM_COLOUR_GRAY8 ) {
return AV_PIX_FMT_GRAY8;
} else {
Error("Unknown colours (%d)",colours);
return AV_PIX_FMT_RGBA;
}
}
AVPixelFormat AVPixFormat() const;
inline uint8_t* Buffer() { return buffer; }
inline const uint8_t* Buffer() const { return buffer; }
@@ -292,6 +277,7 @@ class Image {
void Rotate( int angle );
void Flip( bool leftright );
void Scale( unsigned int factor );
void Scale(unsigned int x, unsigned int y);
void Deinterlace_Discard();
void Deinterlace_Linear();

View File

@@ -21,7 +21,6 @@
#include "zm_packet.h"
#include "zm_signal.h"
#include "zm_utils.h"
#include <dlfcn.h>
#if HAVE_LIBVLC
@@ -105,6 +104,8 @@ void LibvlcUnlockBuffer(void* opaque, void* picture, void *const *planes) {
LibvlcCamera::LibvlcCamera(
const Monitor *monitor,
const std::string &p_path,
const std::string &p_user,
const std::string &p_pass,
const std::string &p_method,
const std::string &p_options,
int p_width,
@@ -132,6 +133,8 @@ LibvlcCamera::LibvlcCamera(
p_record_audio
),
mPath(p_path),
mUser(UriEncode(p_user)),
mPass(UriEncode(p_pass)),
mMethod(p_method),
mOptions(p_options)
{
@@ -213,7 +216,9 @@ void LibvlcCamera::Terminate() {
int LibvlcCamera::PrimeCapture() {
Debug(1, "Priming capture from %s, libvlc version %s", mPath.c_str(), (*libvlc_get_version_f)());
StringVector opVect = Split(Options(), ",");
opVect = Split(Options(), ",");
Debug(1, "Method: '%s'", Method().c_str());
// Set transport method as specified by method field, rtpUni is default
if ( Method() == "rtpMulti" )
@@ -242,6 +247,17 @@ int LibvlcCamera::PrimeCapture() {
}
(*libvlc_log_set_f)(mLibvlcInstance, LibvlcCamera::log_callback, nullptr);
// recreate the path with encoded authentication info
if( mUser.length() > 0 ) {
std::string mMaskedPath = remove_authentication(mPath);
std::string protocol = StringToUpper(mPath.substr(0, 4));
if ( protocol == "RTSP" ) {
// build the actual uri string with encoded parameters (from the user and pass fields)
mPath = StringToLower(protocol) + "://" + mUser + ":" + mPass + "@" + mMaskedPath.substr(7, std::string::npos);
Debug(1, "Rebuilt URI with encoded parameters: '%s'", mPath.c_str());
}
}
mLibvlcMedia = (*libvlc_media_new_location_f)(mLibvlcInstance, mPath.c_str());
if ( mLibvlcMedia == nullptr ) {
@@ -289,7 +305,7 @@ int LibvlcCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
mLibvlcData.mutex.lock();
zm_packet->image->Assign(width, height, colours, subpixelorder, mLibvlcData.buffer, width * height * mBpp);
zm_packet->packet.stream_index = mVideoStreamId;
zm_packet->packet->stream_index = mVideoStreamId;
zm_packet->stream = mVideoStream;
mLibvlcData.mutex.unlock();

View File

@@ -21,6 +21,7 @@
#define ZM_LIBVLC_CAMERA_H
#include "zm_camera.h"
#include "zm_utils.h"
#include <condition_variable>
#include <mutex>
@@ -48,8 +49,11 @@ class LibvlcCamera : public Camera {
static void log_callback( void *ptr, int level, const libvlc_log_t *ctx, const char *format, va_list vargs );
protected:
std::string mPath;
std::string mUser;
std::string mPass;
std::string mMethod;
std::string mOptions;
StringVector opVect; // mOptArgV will point into opVect so it needs to hang around
char **mOptArgV;
LibvlcPrivateData mLibvlcData;
std::string mTargetChroma;
@@ -60,7 +64,7 @@ protected:
libvlc_media_player_t *mLibvlcMediaPlayer;
public:
LibvlcCamera( const Monitor *monitor, const std::string &path, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture, bool p_record_audio );
LibvlcCamera( const Monitor *monitor, const std::string &path, const std::string &user,const std::string &pass, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture, bool p_record_audio );
~LibvlcCamera();
const std::string &Path() const { return mPath; }

View File

@@ -214,7 +214,7 @@ int VncCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
}
zm_packet->keyframe = 1;
zm_packet->codec_type = AVMEDIA_TYPE_VIDEO;
zm_packet->packet.stream_index = mVideoStreamId;
zm_packet->packet->stream_index = mVideoStreamId;
zm_packet->stream = mVideoStream;
uint8_t *directbuffer = zm_packet->image->WriteBuffer(width, height, colours, subpixelorder);

View File

@@ -219,7 +219,7 @@ int LocalCamera::vid_fd = -1;
int LocalCamera::v4l_version = 0;
LocalCamera::V4L2Data LocalCamera::v4l2_data;
AVFrame **LocalCamera::capturePictures = nullptr;
av_frame_ptr *LocalCamera::capturePictures;
LocalCamera *LocalCamera::last_camera = nullptr;
@@ -436,7 +436,7 @@ LocalCamera::LocalCamera(
/* Initialize swscale stuff */
if (capture and (conversion_type == 1)) {
tmpPicture = av_frame_alloc();
tmpPicture = av_frame_ptr{zm_av_frame_alloc()};
if (!tmpPicture)
Fatal("Could not allocate temporary picture");
@@ -456,7 +456,6 @@ LocalCamera::LocalCamera(
Fatal("Unable to initialise image scaling context");
}
} else {
tmpPicture = nullptr;
imgConversionContext = nullptr;
} // end if capture and conversion_tye == swscale
if (capture and device_prime)
@@ -471,8 +470,6 @@ LocalCamera::~LocalCamera() {
if (capture && (conversion_type == 1)) {
sws_freeContext(imgConversionContext);
imgConversionContext = nullptr;
av_frame_free(&tmpPicture);
}
} // end LocalCamera::~LocalCamera
@@ -658,7 +655,7 @@ void LocalCamera::Initialise() {
channel_count, v4l_multi_buffer, v4l2_data.reqbufs.count);
v4l2_data.buffers = new V4L2MappedBuffer[v4l2_data.reqbufs.count];
capturePictures = new AVFrame *[v4l2_data.reqbufs.count];
capturePictures = new av_frame_ptr[v4l2_data.reqbufs.count];
for (unsigned int i = 0; i < v4l2_data.reqbufs.count; i++) {
struct v4l2_buffer vid_buf;
@@ -681,7 +678,7 @@ void LocalCamera::Initialise() {
Fatal("Can't map video buffer %u (%u bytes) to memory: %s(%d)",
i, vid_buf.length, strerror(errno), errno);
capturePictures[i] = av_frame_alloc();
capturePictures[i] = av_frame_ptr{zm_av_frame_alloc()};
if (!capturePictures[i])
Fatal("Could not allocate picture");
@@ -738,7 +735,7 @@ void LocalCamera::Terminate() {
Debug(3, "Unmapping video buffers");
for ( unsigned int i = 0; i < v4l2_data.reqbufs.count; i++ ) {
av_frame_free(&capturePictures[i]);
capturePictures[i] = nullptr;
if ( munmap(v4l2_data.buffers[i].start, v4l2_data.buffers[i].length) < 0 )
Error("Failed to munmap buffer %d: %s", i, strerror(errno));
@@ -1023,7 +1020,8 @@ bool LocalCamera::GetCurrentSettings(
if ( vidioctl(vid_fd, VIDIOC_CROPCAP, &cropcap) < 0 ) {
if ( errno != EINVAL ) {
/* Failed querying crop capability, write error to the log and continue as if crop is not supported */
Error("Failed to query crop capabilities: %s", strerror(errno));
Error("Failed to query crop capabilities for %s: %d, %s",
device.c_str(), errno, strerror(errno));
}
if ( verbose ) {
@@ -1075,7 +1073,8 @@ bool LocalCamera::GetCurrentSettings(
if ( errno == EINVAL ) {
break;
}
Error("Failed to enumerate input %d: %s", input.index, strerror(errno));
Error("Failed to enumerate input for %s %d: %d %s",
device.c_str(), input.index, errno, strerror(errno));
if ( verbose )
output_ptr += sprintf(output_ptr, "Error, failed to enumerate input %d: %s\n", input.index, strerror(errno));
else
@@ -1374,7 +1373,7 @@ int LocalCamera::Capture(std::shared_ptr<ZMPacket> &zm_packet) {
zm_packet->image->Assign(width, height, colours, subpixelorder, buffer, imagesize);
} // end if doing conversion or not
zm_packet->packet.stream_index = mVideoStreamId;
zm_packet->packet->stream_index = mVideoStreamId;
zm_packet->stream = mVideoStream;
zm_packet->codec_type = AVMEDIA_TYPE_VIDEO;
zm_packet->keyframe = 1;

View File

@@ -73,11 +73,11 @@ protected:
static V4L2Data v4l2_data;
static AVFrame **capturePictures;
static av_frame_ptr *capturePictures;
_AVPIXELFORMAT imagePixFormat;
_AVPIXELFORMAT capturePixFormat;
struct SwsContext *imgConversionContext;
AVFrame *tmpPicture;
av_frame_ptr tmpPicture;
static LocalCamera *last_camera;

View File

@@ -82,7 +82,8 @@ Logger::Logger() :
smSyslogPriorities[PANIC] = LOG_ERR;
char code[4] = "";
for (int i = DEBUG1; i <= DEBUG9; i++) {
// Extra comparison against DEBUG1 to ensure GCC knows we are printing a single byte.
for (int i = DEBUG1; i>=DEBUG1 && i <= DEBUG9; i++) {
snprintf(code, sizeof(code), "DB%d", i);
smCodes[i] = code;
smSyslogPriorities[i] = LOG_DEBUG;

View File

@@ -1,5 +1,5 @@
/*
* ZoneMinder Logger Interface, $Date$, $Revision$
* ZoneMinder Logger Interface
* Copyright (C) 2001-2008 Philip Coombes
*
* This program is free software; you can redistribute it and/or
@@ -29,18 +29,18 @@
#ifdef HAVE_SYS_SYSCALL_H
#include <sys/syscall.h>
#endif // HAVE_SYS_SYSCALL_H
#endif
class Logger {
public:
enum {
NOOPT=-6,
NOLOG, // -5
PANIC, // -4
FATAL, // -3
ERROR, // -2
WARNING, // -1
INFO, // 0
public:
enum {
NOOPT = -6,
NOLOG, // -5
PANIC, // -4
FATAL, // -3
ERROR, // -2
WARNING, // -1
INFO, // 0
DEBUG1,
DEBUG2,
DEBUG3,
@@ -54,11 +54,11 @@ public:
typedef int Level;
typedef std::map<Level,std::string> StringMap;
typedef std::map<Level,int> IntMap;
typedef std::map<Level, std::string> StringMap;
typedef std::map<Level, int> IntMap;
class Options {
public:
public:
int mTerminalLevel;
int mDatabaseLevel;
int mFileLevel;
@@ -68,24 +68,23 @@ public:
std::string mLogFile;
Options(
Level terminalLevel=NOOPT,
Level databaseLevel=NOOPT,
Level fileLevel=NOOPT,
Level syslogLevel=NOOPT,
const std::string &logPath=".",
const std::string &logFile=""
Level terminalLevel = NOOPT,
Level databaseLevel = NOOPT,
Level fileLevel = NOOPT,
Level syslogLevel = NOOPT,
const std::string &logPath = ".",
const std::string &logFile = ""
) :
mTerminalLevel(terminalLevel),
mDatabaseLevel(databaseLevel),
mFileLevel(fileLevel),
mSyslogLevel(syslogLevel),
mLogPath(logPath),
mLogFile(logFile)
{
mLogFile(logFile) {
}
};
private:
private:
static bool smInitialised;
static Logger *smInstance;
@@ -116,7 +115,7 @@ private:
bool mHasTerminal;
bool mFlush;
private:
private:
Logger();
~Logger();
@@ -128,15 +127,17 @@ private:
return level;
}
bool boolEnv(const std::string &name, bool defaultValue=false);
int intEnv(const std::string &name, bool defaultValue=0);
std::string strEnv(const std::string &name, const std::string &defaultValue="");
bool boolEnv(const std::string &name, bool defaultValue = false);
int intEnv(const std::string &name, bool defaultValue = 0);
std::string strEnv(
const std::string &name,
const std::string &defaultValue = "");
char *getTargettedEnv(const std::string &name);
void loadEnv();
static void usrHandler(int sig);
public:
public:
friend void logInit(const char *name, const Options &options);
friend void logTerm();
@@ -156,16 +157,16 @@ public:
const std::string &id() const { return mId; }
Level level() const { return mLevel; }
Level level(Level=NOOPT);
Level level(Level = NOOPT);
bool debugOn() const { return mEffectiveLevel >= DEBUG1; }
Level terminalLevel(Level=NOOPT);
Level databaseLevel(Level=NOOPT);
Level fileLevel(Level=NOOPT);
Level syslogLevel(Level=NOOPT);
Level terminalLevel(Level = NOOPT);
Level databaseLevel(Level = NOOPT);
Level fileLevel(Level = NOOPT);
Level syslogLevel(Level = NOOPT);
private:
private:
void logFile(const std::string &logFile);
void openFile();
void closeFile();
@@ -182,7 +183,10 @@ private:
...) __attribute__((format(printf, 6, 7)));
};
void logInit(const char *name, const Logger::Options &options=Logger::Options());
void logInit(
const char *name,
const Logger::Options &options = Logger::Options()
);
void logTerm();
inline const std::string &logId() {
return Logger::fetch()->id();
@@ -194,43 +198,45 @@ inline Logger::Level logDebugging() {
return Logger::fetch()->debugOn();
}
#define logPrintf(logLevel, params...) \
do { \
if (logLevel <= Logger::fetch()->level()) { \
Logger::fetch()->logPrint(false, __FILE__, __LINE__, logLevel, ##params); \
} \
#define logPrintf(logLevel, params...) \
do { \
Logger *log = Logger::fetch(); \
if (logLevel <= log->level()) { \
log->logPrint(false, __FILE__, __LINE__, logLevel, ##params); \
} \
} while (0)
#define logHexdump(logLevel, data, len) \
do { \
if (logLevel <= Logger::fetch()->level()) { \
Logger::fetch()->logPrint(true, __FILE__, __LINE__, logLevel, "%p (%d)", data, len); \
} \
#define logHexdump(logLevel, data, len) \
do { \
Logger *log = Logger::fetch(); \
if (logLevel <= log->level()) { \
log->logPrint(true, __FILE__, __LINE__, logLevel, "%p (%d)", data, len);\
} \
} while (0)
/* Debug compiled out */
#ifndef DBG_OFF
#define Debug(level,params...) logPrintf(level,##params)
#define Hexdump(level,data,len) logHexdump(level,data,len)
#define Debug(level, params...) logPrintf(level, ##params)
#define Hexdump(level, data, len) logHexdump(level, data, len)
#else
#define Debug(level,params...)
#define Hexdump(level,data,len)
#define Debug(level, params...)
#define Hexdump(level, data, len)
#endif
/* Standard debug calls */
#define Info(params...) logPrintf(Logger::INFO,##params)
#define Warning(params...) logPrintf(Logger::WARNING,##params)
#define Error(params...) logPrintf(Logger::ERROR,##params)
#define Fatal(params...) logPrintf(Logger::FATAL,##params)
#define Panic(params...) logPrintf(Logger::PANIC,##params)
#define Mark() Info("Mark/%s/%d",__FILE__,__LINE__)
#define Info(params...) logPrintf(Logger::INFO, ##params)
#define Warning(params...) logPrintf(Logger::WARNING, ##params)
#define Error(params...) logPrintf(Logger::ERROR, ##params)
#define Fatal(params...) logPrintf(Logger::FATAL, ##params)
#define Panic(params...) logPrintf(Logger::PANIC, ##params)
#define Mark() Info("Mark/%s/%d", __FILE__, __LINE__)
#define Log() Info("Log")
#ifdef __GNUC__
#define Enter(level) logPrintf(level,("Entering %s",__PRETTY_FUNCTION__))
#define Exit(level) logPrintf(level,("Exiting %s",__PRETTY_FUNCTION__))
#define Enter(level) logPrintf(level, ("Entering %s", __PRETTY_FUNCTION__))
#define Exit(level) logPrintf(level, ("Exiting %s", __PRETTY_FUNCTION__))
#else
#define Enter(level)
#define Exit(level)
#define Enter(level)
#define Exit(level)
#endif
#endif // ZM_LOGGER_H
#endif // ZM_LOGGER_H

View File

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,9 @@
#include "zm_packet.h"
#include "zm_packetqueue.h"
#include "zm_utils.h"
#include "zm_zone.h"
#include <list>
#include <memory>
#include <sys/time.h>
#include <vector>
@@ -44,6 +47,7 @@
#endif
class Group;
class MonitorLinkExpression;
#define SIGNAL_CAUSE "Signal"
#define MOTION_CAUSE "Motion"
@@ -54,8 +58,9 @@ class Group;
// This is the main class for monitors. Each monitor is associated
// with a camera and is effectively a collector for events.
//
class Monitor {
class Monitor : public std::enable_shared_from_this<Monitor> {
friend class MonitorStream;
friend class MonitorLinkExpression;
public:
typedef enum {
@@ -80,6 +85,11 @@ public:
ANALYSIS_SECONDARY
} AnalysisSourceOption;
typedef enum {
ANALYSISIMAGE_FULLCOLOUR=1,
ANALYSISIMAGE_YCHANNEL
} AnalysisImageOption;
typedef enum {
RECORDING_NONE=1,
RECORDING_ONMOTION,
@@ -92,6 +102,14 @@ public:
RECORDING_BOTH
} RecordingSourceOption;
typedef enum {
DECODING_NONE=1,
DECODING_ONDEMAND,
DECODING_KEYFRAMES,
DECODING_KEYFRAMESONDEMAND,
DECODING_ALWAYS
} DecodingOption;
typedef enum {
LOCAL=1,
REMOTE,
@@ -172,37 +190,40 @@ protected:
uint8_t recording; /* +55 */
uint8_t signal; /* +56 */
uint8_t format; /* +57 */
uint32_t imagesize; /* +58 */
uint32_t last_frame_score; /* +62 */
uint32_t audio_frequency; /* +66 */
uint32_t audio_channels; /* +70 */
uint8_t reserved1; /* +58 */
uint8_t reserved2; /* +59 */
uint32_t imagesize; /* +60 */
uint32_t last_frame_score; /* +64 */
uint32_t audio_frequency; /* +68 */
uint32_t audio_channels; /* +72 */
uint32_t reserved3; /* +76 */
/*
** This keeps 32bit time_t and 64bit time_t identical and compatible as long as time is before 2038.
** Shared memory layout should be identical for both 32bit and 64bit and is multiples of 16.
** Because startup_time is 64bit it may be aligned to a 64bit boundary. So it's offset SHOULD be a multiple
** of 8. Add or delete epadding's to achieve this.
*/
union { /* +72 */
union { /* +80 */
time_t startup_time; /* When the zmc process started. zmwatch uses this to see how long the process has been running without getting any images */
uint64_t extrapad1;
};
union { /* +80 */
time_t zmc_heartbeat_time; /* Constantly updated by zmc. Used to determine if the process is alive or hung or dead */
union { /* +88 */
time_t heartbeat_time; /* Constantly updated by zmc. Used to determine if the process is alive or hung or dead */
uint64_t extrapad2;
};
union { /* +88 */
union { /* +96 */
time_t last_write_time;
uint64_t extrapad3;
};
union { /* +96 */
union { /* +104 */
time_t last_read_time;
uint64_t extrapad4;
};
union { /* +104 */
union { /* +112 */
time_t last_viewed_time;
uint64_t extrapad5;
};
uint8_t control_state[256]; /* +112 */
uint8_t control_state[256]; /* +120 */
char alarm_cause[256];
char video_fifo_path[64];
@@ -236,10 +257,15 @@ protected:
timeval recording; // used as both bool and a pointer to the timestamp when recording should begin
} VideoStoreData;
public:
class MonitorLink {
protected:
unsigned int id;
char name[64];
std::shared_ptr<Monitor> monitor;
unsigned int zone_id;
const Zone *zone;
int zone_index; // index into zone_scores for our zone
std::string name;
bool connected;
time_t last_connect_time;
@@ -256,16 +282,18 @@ protected:
volatile SharedData *shared_data;
volatile TriggerData *trigger_data;
volatile VideoStoreData *video_store_data;
volatile int * zone_scores;
int last_state;
uint64_t last_event_id;
std::vector<Zone> zones;
public:
MonitorLink(unsigned int p_id, const char *p_name);
MonitorLink(std::shared_ptr<Monitor> p_monitor, unsigned int p_zone_id);
~MonitorLink();
inline unsigned int Id() const { return id; }
inline const char *Name() const { return name; }
inline unsigned int Id() const { return monitor->Id(); }
inline const char *Name() const { return name.c_str(); }
inline bool isConnected() const { return connected && shared_data->valid; }
inline time_t getLastConnectTime() const { return last_connect_time; }
@@ -280,7 +308,9 @@ protected:
bool isAlarmed();
bool inAlarm();
bool hasAlarmed();
int score();
};
protected:
class AmcrestAPI {
protected:
@@ -291,7 +321,7 @@ protected:
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp);
public:
AmcrestAPI( Monitor *parent_);
explicit AmcrestAPI(Monitor *parent_);
~AmcrestAPI();
int API_Connect();
void WaitForMessage();
@@ -306,6 +336,7 @@ protected:
//helper class for CURL
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp);
bool Janus_Healthy;
bool Use_RTSP_Restream;
std::string janus_session;
std::string janus_handle;
std::string janus_endpoint;
@@ -313,9 +344,10 @@ protected:
std::string rtsp_username;
std::string rtsp_password;
std::string rtsp_path;
std::string profile_override;
public:
JanusManager(Monitor *parent_);
explicit JanusManager(Monitor *parent_);
~JanusManager();
int add_to_janus();
int check_janus();
@@ -336,12 +368,15 @@ protected:
CapturingOption capturing; // None, OnDemand, Always
AnalysingOption analysing; // None, Always
AnalysisSourceOption analysis_source; // Primary, Secondary
AnalysisImageOption analysis_image; // FullColour, YChannel
RecordingOption recording; // None, OnMotion, Always
RecordingSourceOption recording_source; // Primary, Secondary, Both
bool decoding_enabled; // Whether the monitor will decode h264/h265 packets
DecodingOption decoding; // Whether the monitor will decode h264/h265 packets
bool janus_enabled; // Whether we set the h264/h265 stream up on janus
bool janus_audio_enabled; // Whether we tell Janus to try to include audio.
std::string janus_profile_override; // The Profile-ID to force the stream to use.
bool janus_use_rtsp_restream; // Point Janus at the ZM RTSP output, rather than the camera directly.
std::string protocol;
std::string method;
@@ -406,8 +441,8 @@ protected:
int pre_event_count; // How many images to hold and prepend to an alarm event
int post_event_count; // How many unalarmed images must occur before the alarm state is reset
int stream_replay_buffer; // How many frames to store to support DVR functions, IGNORED from this object, passed directly into zms now
Seconds section_length; // How long events should last in continuous modes
Seconds min_section_length; // Minimum event length when using event_close_mode == ALARM
Seconds section_length; // How long events should last in continuous modes
Seconds min_section_length; // Minimum event length when using event_close_mode == ALARM
bool adaptive_skip; // Whether to use the newer adaptive algorithm for this monitor
int frame_skip; // How many frames to skip in continuous modes
int motion_frame_skip; // How many frames to skip in motion detection
@@ -427,7 +462,9 @@ protected:
bool embed_exif; // Whether to embed Exif data into each image frame or not
bool rtsp_server; // Whether to include this monitor as an rtsp server stream
std::string rtsp_streamname; // path in the rtsp url for this monitor
std::string onvif_alarm_txt; // def onvif_alarm_txt
int importance; // Importance of this monitor, affects Connection logging errors.
unsigned int zone_count;
int capture_max_fps;
@@ -465,6 +502,7 @@ protected:
SharedData *shared_data;
TriggerData *trigger_data;
VideoStoreData *video_store_data;
int *zone_scores;
struct timeval *shared_timestamps;
unsigned char *shared_images;
@@ -487,7 +525,7 @@ protected:
std::unique_ptr<AnalysisThread> analysis_thread;
packetqueue_iterator *decoder_it;
std::unique_ptr<DecoderThread> decoder;
AVFrame *dest_frame; // Used by decoding thread doing colorspace conversions
av_frame_ptr dest_frame; // Used by decoding thread doing colorspace conversions
SwsContext *convert_context;
std::thread close_event_thread;
@@ -499,8 +537,11 @@ protected:
const unsigned char *privacy_bitmask;
std::string linked_monitors_string;
int n_linked_monitors;
MonitorLink **linked_monitors;
MonitorLinkExpression *linked_monitors;
//MonitorLink **linked_monitors;
std::string event_start_command;
std::string event_end_command;
@@ -558,11 +599,11 @@ public:
gettimeofday(&now, nullptr);
Debug(3, "Shared data is valid, checking heartbeat %" PRIi64 " - %" PRIi64 " = %" PRIi64" < %f",
static_cast<int64>(now.tv_sec),
static_cast<int64>(shared_data->zmc_heartbeat_time),
static_cast<int64>(now.tv_sec - shared_data->zmc_heartbeat_time),
static_cast<int64>(shared_data->heartbeat_time),
static_cast<int64>(now.tv_sec - shared_data->heartbeat_time),
config.watch_max_delay);
if ((now.tv_sec - shared_data->zmc_heartbeat_time) < config.watch_max_delay)
if ((now.tv_sec - shared_data->heartbeat_time) < config.watch_max_delay)
return true;
}
return false;
@@ -587,8 +628,8 @@ public:
inline bool Enabled() const {
return shared_data->capturing;
}
inline bool DecodingEnabled() const {
return decoding_enabled;
DecodingOption Decoding() const {
return decoding;
}
bool JanusEnabled() {
return janus_enabled;
@@ -721,7 +762,7 @@ public:
SystemTimePoint GetStartupTime() const { return std::chrono::system_clock::from_time_t(shared_data->startup_time); }
void SetStartupTime(SystemTimePoint time) { shared_data->startup_time = std::chrono::system_clock::to_time_t(time); }
void SetHeartbeatTime(SystemTimePoint time) {
shared_data->zmc_heartbeat_time = std::chrono::system_clock::to_time_t(time);
shared_data->heartbeat_time = std::chrono::system_clock::to_time_t(time);
}
void get_ref_image();
@@ -755,6 +796,7 @@ public:
//unsigned int DetectBlack( const Image &comp_image, Event::StringSet &zoneSet );
bool CheckSignal( const Image *image );
bool Analyse();
bool setupConvertContext(const AVFrame *input_frame, const Image *image);
bool Decode();
bool Poll();
void DumpImage( Image *dump_image ) const;
@@ -767,7 +809,7 @@ public:
void Reload();
void ReloadZones();
void ReloadLinkedMonitors( const char * );
void ReloadLinkedMonitors();
bool DumpSettings( char *output, bool verbose );
void DumpZoneImage( const char *zone_string=0 );

View File

@@ -20,8 +20,10 @@
#include "zm_monitor.h"
Monitor::AmcrestAPI::AmcrestAPI(Monitor *parent_) {
parent = parent_;
Monitor::AmcrestAPI::AmcrestAPI(Monitor *parent_) :
parent(parent_),
Amcrest_Alarmed(false)
{
curl_multi = curl_multi_init();
start_Amcrest();
}
@@ -38,7 +40,6 @@ int Monitor::AmcrestAPI::start_Amcrest() {
//init the transfer and start it in multi-handle
int running_handles;
long response_code;
struct CURLMsg *m;
CURLMcode curl_error;
if (Amcrest_handle != nullptr) { //potentially clean up the old handle
curl_multi_remove_handle(curl_multi, Amcrest_handle);
@@ -67,7 +68,7 @@ int Monitor::AmcrestAPI::start_Amcrest() {
if (curl_error == CURLM_OK) {
curl_easy_getinfo(Amcrest_handle, CURLINFO_RESPONSE_CODE, &response_code);
int msgq = 0;
m = curl_multi_info_read(curl_multi, &msgq);
struct CURLMsg *m = curl_multi_info_read(curl_multi, &msgq);
if (m && (m->msg == CURLMSG_DONE)) {
Warning("Libcurl exited Early: %i", m->data.result);
}

View File

@@ -18,27 +18,37 @@
//
#include "zm_monitor.h"
#include <regex>
std::string escape_json_string( std::string input );
Monitor::JanusManager::JanusManager(Monitor *parent_) { //constructor takes care of init and calls add_to
parent = parent_;
Monitor::JanusManager::JanusManager(Monitor *parent_) :
parent(parent_),
Janus_Healthy(false)
{
//constructor takes care of init and calls add_to
//parent = parent_;
Use_RTSP_Restream = parent->janus_use_rtsp_restream;
profile_override = parent->janus_profile_override;
if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) {
janus_endpoint = config.janus_path; //TODO: strip trailing /
janus_endpoint = config.janus_path;
//remove the trailing slash if present
if (janus_endpoint.back() == '/') janus_endpoint.pop_back();
} else {
janus_endpoint = "127.0.0.1:8088/janus";
}
if (janus_endpoint.back() == '/') janus_endpoint.pop_back(); //remove the trailing slash if present
std::size_t at_pos = parent->path.find("@", 7);
if (at_pos != std::string::npos) { //If we find an @ symbol, we have a username/password. Otherwise, passwordless login.
std::size_t colon_pos = parent->path.find(":", 7); //Search for the colon, but only after the RTSP:// text.
if (colon_pos == std::string::npos) throw std::runtime_error("Cannot Parse URL for Janus."); //Looks like an invalid url
rtsp_username = parent->path.substr(7, colon_pos-7);
rtsp_password = parent->path.substr(colon_pos+1, at_pos - colon_pos - 1);
rtsp_path = "RTSP://";
rtsp_path += parent->path.substr(at_pos + 1);
rtsp_username = "";
rtsp_password = "";
if( parent->user.length() > 0 ) {
rtsp_username = escape_json_string(parent->user);
rtsp_password = escape_json_string(parent->pass);
}
if (Use_RTSP_Restream) {
int restream_port = config.min_rtsp_port;
rtsp_path = "rtsp://127.0.0.1:" + std::to_string(restream_port) + "/" + parent->rtsp_streamname;
} else {
rtsp_username = "";
rtsp_password = "";
rtsp_path = parent->path;
}
}
@@ -99,7 +109,7 @@ int Monitor::JanusManager::check_janus() {
curl_easy_cleanup(curl);
if (res != CURLE_OK) { //may mean an error code thrown by Janus, because of a bad session
Warning("Attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res));
Warning("Attempted to send %s to %s and got %s", postData.c_str(), endpoint.c_str(), curl_easy_strerror(res));
janus_session = "";
janus_handle = "";
return -1;
@@ -142,7 +152,11 @@ int Monitor::JanusManager::add_to_janus() {
postData += "\", \"type\" : \"rtsp\", \"rtsp_quirk\" : true, ";
postData += "\"url\" : \"";
postData += rtsp_path;
if (rtsp_username != "") {
if (profile_override[0] != '\0') {
postData += "\", \"videofmtp\" : \"";
postData += profile_override;
}
if (rtsp_username.length() > 0) {
postData += "\", \"rtsp_user\" : \"";
postData += rtsp_username;
postData += "\", \"rtsp_pwd\" : \"";
@@ -157,12 +171,12 @@ int Monitor::JanusManager::add_to_janus() {
CURLcode res;
std::string response;
curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str());
curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
Error("Failed to curl_easy_perform adding rtsp stream");
@@ -188,8 +202,8 @@ int Monitor::JanusManager::add_to_janus() {
size_t Monitor::JanusManager::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
((std::string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;
((std::string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;
}
/*
@@ -212,11 +226,10 @@ void Monitor::JanusManager::generateKey()
}
*/
int Monitor::JanusManager::get_janus_session() {
janus_session = "";
curl = curl_easy_init();
if(!curl) return -1;
if (!curl) return -1;
std::string endpoint = janus_endpoint;
std::string response;
@@ -245,15 +258,14 @@ int Monitor::JanusManager::get_janus_session() {
int Monitor::JanusManager::get_janus_handle() {
curl = curl_easy_init();
if(!curl) return -1;
if (!curl) return -1;
CURLcode res;
std::string response = "";
std::string endpoint = janus_endpoint+"/"+janus_session;
std::string postData = "{\"janus\" : \"attach\", \"plugin\" : \"janus.plugin.streaming\", \"transaction\" : \"randomString\"}";
curl_easy_setopt(curl, CURLOPT_URL,endpoint.c_str());
curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());
@@ -272,3 +284,15 @@ int Monitor::JanusManager::get_janus_handle() {
janus_handle = response.substr(pos + 6, 16);
return 1;
} //get_janus_handle
std::string escape_json_string( std::string input ) {
std::string tmp;
tmp = regex_replace(input, std::regex("\n"), "\\n");
tmp = regex_replace(tmp, std::regex("\b"), "\\b");
tmp = regex_replace(tmp, std::regex("\f"), "\\f");
tmp = regex_replace(tmp, std::regex("\r"), "\\r");
tmp = regex_replace(tmp, std::regex("\t"), "\\t");
tmp = regex_replace(tmp, std::regex("\""), "\\\"");
tmp = regex_replace(tmp, std::regex("[\\\\]"), "\\\\");
return tmp;
}

View File

@@ -30,17 +30,39 @@
#include <sys/shm.h>
#endif // ZM_MEM_MAPPED
Monitor::MonitorLink::MonitorLink(unsigned int p_id, const char *p_name) :
id(p_id),
Monitor::MonitorLink::MonitorLink(std::shared_ptr<Monitor>p_monitor, unsigned int p_zone_id) :
monitor(p_monitor),
zone_id(p_zone_id),
zone(nullptr),
zone_index(-1),
shared_data(nullptr),
trigger_data(nullptr),
video_store_data(nullptr)
video_store_data(nullptr),
zone_scores(nullptr)
{
strncpy(name, p_name, sizeof(name)-1);
name = monitor->Name();
if (zone_id) {
zones = Zone::Load(monitor);
int zone_i = 0;
for (const Zone &z : zones) {
if (z.Id() == zone_id) {
zone = &z;
zone_index = zone_i;
break;
}
++zone_i;
}
if (zone_index == -1) {
Error("Unable to find zone %u", zone_id);
zone_index = -1;
}
}
if (zone) name += " : " + zone->Name();
#if ZM_MEM_MAPPED
map_fd = -1;
mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), id);
mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), monitor->Id());
#else // ZM_MEM_MAPPED
shm_id = 0;
#endif // ZM_MEM_MAPPED
@@ -55,6 +77,7 @@ Monitor::MonitorLink::MonitorLink(unsigned int p_id, const char *p_name) :
}
Monitor::MonitorLink::~MonitorLink() {
zones.clear();
disconnect();
}
@@ -92,7 +115,7 @@ bool Monitor::MonitorLink::connect() {
disconnect();
return false;
} else if (map_stat.st_size < mem_size) {
Error("Got unexpected memory map file size %ld, expected %jd", map_stat.st_size, static_cast<intmax_t>(mem_size));
Error("Got unexpected memory map file size %jd, expected %jd", static_cast<intmax_t>(map_stat.st_size), static_cast<intmax_t>(mem_size));
disconnect();
return false;
}
@@ -120,6 +143,7 @@ bool Monitor::MonitorLink::connect() {
shared_data = (SharedData *)mem_ptr;
trigger_data = (TriggerData *)((char *)shared_data + sizeof(SharedData));
zone_scores = (int *)((unsigned long)trigger_data + sizeof(TriggerData));
if (!shared_data->valid) {
Debug(3, "Linked memory not initialised by capture daemon");
@@ -133,7 +157,7 @@ bool Monitor::MonitorLink::connect() {
return true;
}
return false;
return connected;
} // end bool Monitor::MonitorLink::connect()
bool Monitor::MonitorLink::disconnect() {
@@ -190,10 +214,21 @@ bool Monitor::MonitorLink::inAlarm() {
return( shared_data->state == ALARM || shared_data->state == ALERT );
}
bool Monitor::MonitorLink::hasAlarmed() {
if (shared_data->state == ALARM) {
return true;
int Monitor::MonitorLink::score() {
if (zone_id and (zone_index >= 0)) {
Debug(1, "Checking zone %u, zone_index is %d, score is %d", zone_id, zone_index, zone_scores[zone_index]);
return zone_scores[zone_index];
}
if (shared_data->state == ALARM) {
Debug(1, "Checking all zones score is %d", shared_data->last_frame_score);
return shared_data->last_frame_score;
}
Debug(1, "not alarmed. %d", shared_data->state);
return 0;
}
bool Monitor::MonitorLink::hasAlarmed() {
return this->score() > 0;
last_event_id = shared_data->last_event_id;
return false;
}

View File

@@ -0,0 +1,254 @@
#include "zm_monitorlink_expression.h"
bool MonitorLinkExpression::parse() {
Tokens tokens;
auto first = std::begin(expression_);
Debug(1, "Parsing %s", std::string(expression_).c_str());
// First we tokenize
while (first != std::end(expression_)) {
auto const second = std::find_first_of(
first, std::cend(expression_),
std::cbegin(delimiters_), std::cend(delimiters_)
);
if (first != second) {
std::string_view t = expression_.substr(
std::distance(std::begin(expression_), first),
std::distance(first, second)
);
Debug(1, "Have a token %s", std::string(t).c_str());
tokens.emplace_back(t);
}
if (std::end(expression_) == second) {
Debug(1, "Breaking");
break;
}
std::string_view delim { second, 1 };
if (!delim.empty()) {
Debug(1, "Have delim %s", std::string(delim).c_str());
tokens.emplace_back(delim);
}
first = std::next(second);
}
if (tokens.empty()) {
Debug(1, "No tokens?");
return false;
}
std::size_t current{ 0u };
// Then we parse the tokens into a tree
tree_ = parse_expression(tokens, current);
return true;
}
bool MonitorLinkExpression::evaluate() {
if (!tree_) {
Debug(1, "No tree");
return false;
}
MonitorLinkExpression::result result = this->visit(*tree_);
if (!result.success) {
Warning("%s", std::string(result.message).c_str());
return false;
}
return result.score > 0;
}
MonitorLinkExpression::result
MonitorLinkExpression::visit(Node const & node) {
Debug(1, "visit: Node: %p Token: %d value %s",
&node,
static_cast<int>(node.token.type()),
std::string(node.token.value()).c_str()
);
if (node.token.type() == Token::TokenType::monitorlink) {
Debug(1, "Have monitorlink, return true, value");
return { true, "", node.token.score() };
} else if (nullptr == node.left || nullptr == node.right) {
return { false, "Missing operand", 0 };
}
switch (node.token.type()) {
case Token::TokenType::logical_and:
Debug(1, "and");
return visit_logical_and(node);
case Token::TokenType::logical_or :
Debug(1, "or");
return visit_logical_or(node);
case Token::TokenType::logical_comma :
Debug(1, "comma");
return visit_logical_or(node);
default:
Debug(1, "unknown");
return { false, "Unknown token type" };
}
}
MonitorLinkExpression::result
MonitorLinkExpression::visit_logical_and(MonitorLinkExpression::Node const & node)
{
auto const left { visit(*node.left) };
auto const right { visit(*node.right) };
// always pick the error message closer to the beginning of the expression
auto const message {
left.message.empty() ? right.message : left.message
};
Debug(1, "aND left score %d right score %d", left.score, right.score);
return { left.success && right.success, message,
((left.score and right.score) ? left.score + right.score : 0)
};
}
MonitorLinkExpression::result
MonitorLinkExpression::visit_logical_or(MonitorLinkExpression::Node const & node)
{
auto const left { visit(*node.left) };
auto const right { visit(*node.right) };
// always pick the error message closer to the beginning of the expression
auto const message {
left.message.empty() ? right.message : left.message
};
Debug(1, "Or left score %d right score %d", left.score, right.score);
return {
left.success || right.success,
message,
((left.score or right.score) ? left.score + right.score : 0)
};
}
std::unique_ptr<MonitorLinkExpression::Node>
MonitorLinkExpression::parse_expression( Tokens const & tokens, std::size_t & current ) {
if (tokens.size() == 1) {
Debug(1, "Special case monitorlink");
// Special case, must be a monitorlink
return std::make_unique<Node>(tokens[0]);
}
// First token could me a parenthesis or monitorlink. Otherwise invalid.
auto left{ parse_and_operation(tokens, current) };
if (
has_unused(tokens, current)
and
tokens[current].is_not(Token::TokenType::logical_or)
and
tokens[current].is_not(Token::TokenType::logical_comma)
) {
Debug(1, "parse_expression: not or, Returning left %s", std::string(left->token.value()).c_str());
return left;
}
/*
if (tokens[current].is(Token::TokenType::monitorlink)) {
Debug(1, "Left is a monitorlink");
left = std::make_unique<Node>(tokens[current]);
current++;
} else {
Debug(1, "Left is not a monitorlink, parsing and");
left = parse_and_operation(tokens, current);
// invalid
//return nullptr;
}
*/
while (has_unused(tokens, current) and
(
tokens[current].is(Token::TokenType::logical_or)
or
tokens[current].is(Token::TokenType::logical_comma)
)
) {
Debug(1, "Have or adding it");
auto logical_or{ std::make_unique<Node>( Token::TokenType::logical_or ) };
current++;
auto right{ parse_and_operation( tokens, current ) };
if (right == nullptr) {
Debug(1, "null from right side");
return nullptr;
}
logical_or->left = std::move( left );
logical_or->right = std::move( right );
left = std::move( logical_or );
}
return left;
}
std::unique_ptr<MonitorLinkExpression::Node>
MonitorLinkExpression::parse_and_operation( Tokens const & tokens, std::size_t & current ) {
auto left{ parse_parentheses( tokens, current ) };
if (left == nullptr) {
Debug(1, "null from parse_parenteses, adding a left.");
//left = std::make_unique< Node >(Token::TokenType::lp);
return nullptr;
}
while (
has_unused(tokens, current)
and
tokens[current].is(Token::TokenType::logical_and)
) {
++current;
auto logical_and{ std::make_unique< Node >(Token::TokenType::logical_and) };
auto right{parse_parentheses(tokens, current) };
if (right == nullptr) {
// No right parentheses. Add it?
Debug(1, "No right parenthesis, adding it");
//right = std::make_unique< Node >(Token::TokenType::rp);
return nullptr;
}
logical_and->left = std::move(left);
logical_and->right = std::move(right);
left = std::move(logical_and);
}
return left;
}
std::unique_ptr<MonitorLinkExpression::Node>
MonitorLinkExpression::parse_parentheses(Tokens const &tokens, std::size_t &current) {
if (!has_unused(tokens, current)) {
Debug(1, "No unused...");
return nullptr;
}
if (tokens[current].is(Token::TokenType::lp)) {
++current;
auto expression{ parse_expression(tokens, current) };
// Because we are parsing a left, there SHOULD be a remaining right. If not, invalid.
if (!has_unused(tokens, current)) return nullptr;
if (tokens[ current++ ].is(Token::TokenType::rp)) {
return expression;
}
} else if (tokens[current].is(Token::TokenType::monitorlink)) {
Debug(1, "Have monitorlink, returning it");
auto link {std::make_unique<Node>(tokens[current])};
current++;
return link;
}
return nullptr;
}

View File

@@ -0,0 +1,104 @@
//
// ZoneMinder Monitor Class Interfaces
// Copyright (C) 2022 ZoneMinder Inc
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#ifndef ZM_MONITORLINK_EVAL_H
#define ZM_MONITORLINK_EVAL_H
#include "zm_utils.h"
#include "zm_monitorlink_token.h"
#include <istream>
#include <string>
class MonitorLinkExpression {
public:
/**
* struct node
*
* Represents the tree node containing references to left and right child nodes
* as well as the token that the node represents in the actual expression tree.
*/
class Node {
public:
Token token{ Token::TokenType::unknown };
std::unique_ptr< Node > left { nullptr };
std::unique_ptr< Node > right{ nullptr };
constexpr Node() noexcept = default;
Node( Node && rhs ) noexcept = default;
Node( Node const & rhs ) noexcept = delete;
constexpr Node(Token::TokenType const type) noexcept : token(type) {}
constexpr Node(Token const &token) noexcept : token(token) {}
Node & operator=( Node && rhs ) noexcept = default;
Node & operator=( Node const & rhs ) noexcept = delete;
~Node() noexcept = default;
};
struct result {
/**
* True if evaluation process is successful. Otherwise, false.
*/
bool success{ false };
/**
* Message in case of the fault.
*/
std::string_view message{};
int score {-1};
};
public:
using Tokens = std::vector< Token >;
private:
std::unique_ptr<Node> tree_;
int score_;
static std::unique_ptr< Node > parse_and_operation ( Tokens const & tokens, std::size_t & current );
static std::unique_ptr< Node > parse_or_operation ( Tokens const & tokens, std::size_t & current );
static std::unique_ptr< Node > parse_parentheses ( Tokens const & tokens, std::size_t & current );
static std::unique_ptr< Node > parse_terminal ( Tokens const & tokens, std::size_t & current );
static std::unique_ptr<Node> parse_expression( Tokens const & tokens, std::size_t & current );
static inline bool has_unused( Tokens const & tokens, std::size_t const current ) noexcept {
return current < std::size( tokens );
}
static result visit(Node const &node);
static result visit_logical_and(Node const &node);
static result visit_logical_or(Node const &node);
public:
MonitorLinkExpression();
MonitorLinkExpression(const std::string &expression) : expression_(expression) {
};
int score() { return score_; }
bool evaluate();
bool parse();
private:
const std::string_view delimiters_ = "|&(),";
std::string_view expression_;
};
#endif

226
src/zm_monitorlink_token.h Normal file
View File

@@ -0,0 +1,226 @@
//
// ZoneMinder Monitor Class Interfaces
// Copyright (C) 2022 ZoneMinder Inc
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#ifndef ZM_MONITORLINK_TOKEN_H
#define ZM_MONITORLINK_TOKEN_H
#include "zm_monitor.h"
class Token {
public:
/**
* enum class token_type
*
* Represents a token type. Supported types are logical operators, relational operators, parentheses and monitorlink
*/
enum class [[ nodiscard ]] TokenType : std::uint8_t {
unknown,
monitorlink,
logical_and,
logical_or,
logical_comma,
lp,
rp
};
/**
* @class token
*
* Represents all tokens ('and', 'or', 'eq', ...).
*/
private:
using token_type_pair = std::pair<std::string_view, TokenType>;
constexpr static std::array symbols {
token_type_pair{ "&", TokenType::logical_and },
token_type_pair{ "|", TokenType::logical_or },
token_type_pair{ ",", TokenType::logical_comma }, // or
token_type_pair{ "(", TokenType::lp },
token_type_pair{ ")", TokenType::rp }
};
//constexpr TokenType to_token_type( std::string_view const value ) noexcept;
constexpr TokenType to_token_type( std::string_view const value ) noexcept {
auto find_matching {
[ value ]( auto const & collection ) noexcept {
return utils::find_if
(
std::cbegin( collection ),
std::cend ( collection ),
[ value ]( auto && item )
{
return item.first == value;
}
);
}
};
auto const symbol{ find_matching(symbols) };
if (symbol != std::cend(symbols)) {
return symbol->second;
}
return TokenType::monitorlink;
} // end constexpr TokenType to_token_type( std::string_view const value )
public:
Token(TokenType const type, std::string_view const value)
: type_(type)
, value_(value)
, monitor_link_(nullptr)
{
if (type_ == TokenType::monitorlink) {
auto colon_position = value_.find(':');
unsigned int monitor_id = 0;
unsigned int zone_id = 0;
std::string monitor_name;
std::string zone_name;
if (colon_position != std::string::npos) {
// Has a zone specification
monitor_id = std::stoul(std::string(value_.substr(0, colon_position)));
zone_id = std::stoul(std::string(value_.substr(colon_position+1, std::string::npos)));
} else {
monitor_id = std::stoul(std::string(value_));
}
Debug(1, "Have linked monitor %d zone %d", monitor_id, zone_id);
std::shared_ptr<Monitor> monitor = Monitor::Load(monitor_id, false, Monitor::QUERY);
monitor_link_ = new Monitor::MonitorLink(monitor, zone_id);
} else {
Debug( 1, "Not a monitor link value is %s", std::string(value_).c_str());
}
}
constexpr Token() noexcept :
type_(TokenType::unknown),
value_(""),
monitor_link_(nullptr)
{ }
//Token( TokenType const type, std::string_view const value );
constexpr Token( Token && rhs ) noexcept = default;
constexpr Token( Token const & rhs ) noexcept = default;
constexpr Token( TokenType const type ) noexcept
: type_ ( type )
, value_("")
, monitor_link_(nullptr)
{}
Token( std::string_view const value ) noexcept
: type_ (to_token_type(value))
, value_(value)
, monitor_link_(nullptr)
{
if (type_ == TokenType::monitorlink) {
auto colon_position = value_.find(':');
unsigned int monitor_id = 0;
unsigned int zone_id = 0;
std::string monitor_name;
std::string zone_name;
if (colon_position != std::string::npos) {
// Has a zone specification
monitor_id = std::stoul(std::string(value_.substr(0, colon_position)));
zone_id = std::stoul(std::string(value_.substr(colon_position+1, std::string::npos)));
} else {
monitor_id = std::stoul(std::string(value_));
}
Debug(1, "Have linked monitor %d zone %d", monitor_id, zone_id);
std::shared_ptr<Monitor> monitor = Monitor::Load(monitor_id, false, Monitor::QUERY);
monitor_link_ = new Monitor::MonitorLink(monitor, zone_id);
} else {
Debug( 1, "Not a monitor link value is %s", std::string(value_).c_str());
}
}
Token & operator=( Token && rhs ) noexcept = default;
Token & operator=( Token const & rhs ) noexcept = default;
[[ nodiscard ]] constexpr bool operator==( Token const & rhs ) const noexcept {
return type_ == rhs.type_ && value_ == rhs.value_;
}
~Token() noexcept = default;
constexpr void type( TokenType const type ) noexcept {
if ( type != type_ ) {
type_ = type;
value_ = "";//to_token_keyword( type );
}
}
constexpr TokenType type() const noexcept { return type_; }
constexpr void value( std::string_view const value ) noexcept {
type_ = to_token_type( value );
value_ = value;
}
[[ nodiscard ]] constexpr std::string_view value() const noexcept {
return value_;
}
[[ nodiscard ]] constexpr bool is( TokenType const type ) const noexcept {
return type_ == type;
}
[[ nodiscard ]] constexpr bool is_not( TokenType const type ) const noexcept {
return type_ != type;
}
[[ nodiscard ]] constexpr bool is_one_of(
TokenType const first,
TokenType const second
) const noexcept
{
return is(first) || is(second);
}
[[ nodiscard ]] constexpr bool hasAlarmed() const {
return (monitor_link_ && monitor_link_->connect() && monitor_link_->hasAlarmed());
}
[[ nodiscard ]] constexpr int score() const {
if ( monitor_link_ ) {
if (!monitor_link_->isConnected() ) {
Debug(1, "connecting");
if (!monitor_link_->connect()) {
Debug(1, "failed");
return 0;
}
}
int s = monitor_link_->score();
Debug(1, "Score from monitor %s is %d", monitor_link_->Name(), s);
return s;
}
return 0;
}
private:
TokenType type_;
std::string_view value_;
Monitor::MonitorLink *monitor_link_;
};
#endif

Some files were not shown because too many files have changed in this diff Show More