mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2025-12-23 22:37:53 -05:00
Merge branch 'master' into add_mqtt
This commit is contained in:
@@ -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
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
11
.github/docker/centos7-gcc8-zm/Dockerfile
vendored
Normal file
11
.github/docker/centos7-gcc8-zm/Dockerfile
vendored
Normal 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
|
||||
7
.github/workflows/ci-bionic.yml
vendored
7
.github/workflows/ci-bionic.yml
vendored
@@ -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
|
||||
|
||||
7
.github/workflows/ci-bullseye.yml
vendored
7
.github/workflows/ci-bullseye.yml
vendored
@@ -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
|
||||
|
||||
7
.github/workflows/ci-buster.yml
vendored
7
.github/workflows/ci-buster.yml
vendored
@@ -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
|
||||
|
||||
18
.github/workflows/ci-centos-7.yml
vendored
18
.github/workflows/ci-centos-7.yml
vendored
@@ -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]})
|
||||
|
||||
5
.github/workflows/ci-centos-8.yml
vendored
5
.github/workflows/ci-centos-8.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/ci-eslint.yml
vendored
5
.github/workflows/ci-eslint.yml
vendored
@@ -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
|
||||
|
||||
41
.github/workflows/ci-stretch.yml
vendored
41
.github/workflows/ci-stretch.yml
vendored
@@ -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]})
|
||||
17
.github/workflows/codeql-analysis.yml
vendored
17
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/create-packages.yml
vendored
8
.github/workflows/create-packages.yml
vendored
@@ -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
14
.github/workflows/depsreview.yaml
vendored
Normal 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
39
.github/workflows/release-packages.yml
vendored
Normal 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/
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
1
db/monitors_dbupdate.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE zm.Monitors ADD Onvif_Alarm_Txt varchar(30) DEFAULT 'MotionAlarm' NULL;
|
||||
@@ -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
18
db/zm_update-1.36.20.sql
Normal 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;
|
||||
@@ -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
14
db/zm_update-1.37.13.sql
Normal 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
1
db/zm_update-1.37.14.sql
Normal 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
20
db/zm_update-1.37.15.sql
Normal 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
2
db/zm_update-1.37.16.sql
Normal 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
11
db/zm_update-1.37.17.sql
Normal 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
18
db/zm_update-1.37.18.sql
Normal 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
18
db/zm_update-1.37.19.sql
Normal 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
37
db/zm_update-1.37.20.sql
Normal 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;
|
||||
Submodule dep/RtspServer updated: 1b40f1661f...eab3285142
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
281
scripts/ZoneMinder/lib/ZoneMinder/Control/Grandstream.pm
Normal file
281
scripts/ZoneMinder/lib/ZoneMinder/Control/Grandstream.pm
Normal 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
|
||||
@@ -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';
|
||||
|
||||
533
scripts/ZoneMinder/lib/ZoneMinder/Control/Uniview.pm
Normal file
533
scripts/ZoneMinder/lib/ZoneMinder/Control/Uniview.pm
Normal 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__
|
||||
@@ -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 ( @_ ) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
|
||||
@@ -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}) ) {
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
185
scripts/zmalarm-server.py
Executable 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)
|
||||
@@ -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
116
scripts/zmeventtool.pl.in
Normal 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__
|
||||
@@ -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+)/;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
7749
src/bindings.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ class AnalysisThread {
|
||||
AnalysisThread(AnalysisThread &&rhs) = delete;
|
||||
|
||||
void Start();
|
||||
void Stop() { terminate_ = true; }
|
||||
void Stop();
|
||||
bool Stopped() const { return terminate_; }
|
||||
|
||||
private:
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class DecoderThread {
|
||||
DecoderThread(DecoderThread &&rhs) = delete;
|
||||
|
||||
void Start();
|
||||
void Stop() { terminate_ = true; }
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void Run();
|
||||
|
||||
@@ -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 ¬es) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,7 @@ class FFmpeg_Output {
|
||||
int video_stream_id;
|
||||
int audio_stream_id;
|
||||
AVFormatContext *input_format_context;
|
||||
av_frame_ptr frame;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
126
src/zm_logger.h
126
src/zm_logger.h
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
104
src/zm_monitor.h
104
src/zm_monitor.h
@@ -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 );
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
254
src/zm_monitorlink_expression.cpp
Normal file
254
src/zm_monitorlink_expression.cpp
Normal 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 ¤t) {
|
||||
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;
|
||||
}
|
||||
104
src/zm_monitorlink_expression.h
Normal file
104
src/zm_monitorlink_expression.h
Normal 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
226
src/zm_monitorlink_token.h
Normal 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
Reference in New Issue
Block a user