diff --git a/.eslintignore b/.eslintignore index 91b0fd196..55fe41a49 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ web/api/lib web/includes/csrf/ web/js/videojs.zoomrotate.js web/skins/classic/js/bootstrap-4.5.0.js +web/skins/classic/js/bootstrap.bundle.min.js web/skins/classic/js/chosen web/skins/classic/js/dateTimePicker web/skins/classic/js/jquery-*.js @@ -13,6 +14,8 @@ web/skins/classic/js/jquery.js web/skins/classic/js/moment.js web/skins/classic/js/video.js web/tools/mootools +web/js/janus.js +web/js/ajaxQueue.js # Cannot be parsed as JS web/skins/classic/includes/export_functions.php diff --git a/.eslintrc.js b/.eslintrc.js index 0bb050ad1..16cbb37ba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { "env": { "browser": true, - "es6": true, + "es2017": true, }, "extends": ["google"], "overrides": [{ diff --git a/.github/workflows/ci-centos-8.yml b/.github/workflows/ci-centos-8.yml index f01f9929b..8ad222793 100644 --- a/.github/workflows/ci-centos-8.yml +++ b/.github/workflows/ci-centos-8.yml @@ -19,7 +19,7 @@ jobs: - crypto_backend: gnutls jwt_backend: libjwt runs-on: ubuntu-latest - container: centos:8 + container: rockylinux:8 steps: - name: Enable RPMFusion, EPEL and PowerTools diff --git a/.github/workflows/ci-eslint.yml b/.github/workflows/ci-eslint.yml index 7a5c4337f..dce24c367 100644 --- a/.github/workflows/ci-eslint.yml +++ b/.github/workflows/ci-eslint.yml @@ -16,6 +16,6 @@ jobs: with: submodules: recursive - name: Install ESLint - run: npm install eslint@5.12.0 eslint-config-google@0.11.0 eslint-plugin-html@5.0.0 eslint-plugin-php-markup@0.2.5 + run: npm install eslint@8.7.0 eslint-config-google@0.14.0 eslint-plugin-html@6.2.0 eslint-plugin-php-markup@6.0.0 - name: Run ESLint run: npx eslint --ext .php,.js . diff --git a/.github/workflows/create-packages.yml b/.github/workflows/create-packages.yml index 6a76da322..e62c5aec1 100644 --- a/.github/workflows/create-packages.yml +++ b/.github/workflows/create-packages.yml @@ -2,8 +2,7 @@ name: Create packages on: push: - branches: - - '*' + branches: [ master ] pull_request: branches: [ master ] @@ -21,6 +20,7 @@ jobs: steps: - uses: actions/checkout@v2 with: + fetch-depth: '0' submodules: recursive - name: Run packpack env: @@ -29,3 +29,13 @@ jobs: 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/master/mini-dinstall/incoming/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..38593578d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,9 @@ +version: 2 + +build: + os: "ubuntu-20.04" + tools: + python: "3.8" + +sphinx: + fail_on_warning: true diff --git a/CMakeLists.txt b/CMakeLists.txt index ca9c7384b..c78c4ee3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ mark_as_advanced( ZM_TARGET_DISTRO ZM_PATH_MAP ZM_PATH_ARP + ZM_PATH_ARP_SCAN ZM_CONFIG_DIR ZM_CONFIG_SUBDIR ZM_SYSTEMD @@ -145,6 +146,8 @@ set(ZM_PATH_MAP "/dev/shm" CACHE PATH "Location to save mapped memory files, default: /dev/shm") set(ZM_PATH_ARP "" CACHE PATH "Full path to compatible arp binary. Leave empty for automatic detection.") +set(ZM_PATH_ARP_SCAN "" CACHE PATH + "Full path to compatible scan_arp binary. Leave empty for automatic detection.") set(ZM_CONFIG_DIR "/${CMAKE_INSTALL_SYSCONFDIR}" CACHE PATH "Location of ZoneMinder configuration, default system config directory") set(ZM_CONFIG_SUBDIR "${ZM_CONFIG_DIR}/conf.d" CACHE PATH @@ -167,6 +170,8 @@ set(ZM_NO_X10 "OFF" CACHE BOOL set(ZM_ONVIF "ON" CACHE BOOL "Set to ON to enable basic ONVIF support. This is EXPERIMENTAL and may not work with all cameras claiming to be ONVIF compliant. default: ON") +set(ZM_NO_PCRE "OFF" CACHE BOOL + "Set to ON to skip libpcre3 checks and force building ZM without libpcre3. default: OFF") set(ZM_NO_RTSPSERVER "OFF" CACHE BOOL "Set to ON to skip building ZM with rtsp server support. default: OFF") set(ZM_PERL_MM_PARMS INSTALLDIRS=vendor NO_PACKLIST=1 NO_PERLLOCAL=1 CACHE STRING @@ -316,10 +321,10 @@ endif() # Do not check for cURL if ZM_NO_CURL is on if(NOT ZM_NO_CURL) # cURL - find_package(CURL) + find_package(CURL REQUIRED) if(CURL_FOUND) set(HAVE_LIBCURL 1) - #list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) + list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) include_directories(${CURL_INCLUDE_DIRS}) set(CMAKE_REQUIRED_INCLUDES ${CURL_INCLUDE_DIRS}) check_include_file("curl/curl.h" HAVE_CURL_CURL_H) @@ -407,21 +412,24 @@ else() message(FATAL_ERROR "ZoneMinder requires pthread but it was not found on your system") endif() -# pcre (using find_library and find_path) -find_library(PCRE_LIBRARIES pcre) -if(PCRE_LIBRARIES) - set(HAVE_LIBPCRE 1) - list(APPEND ZM_BIN_LIBS "${PCRE_LIBRARIES}") - find_path(PCRE_INCLUDE_DIR pcre.h) - if(PCRE_INCLUDE_DIR) - include_directories("${PCRE_INCLUDE_DIR}") - set(CMAKE_REQUIRED_INCLUDES "${PCRE_INCLUDE_DIR}") +# Do not check for cURL if ZM_NO_CURL is on +if(NOT ZM_NO_PRCE) + # pcre (using find_library and find_path) + find_library(PCRE_LIBRARIES pcre) + if(PCRE_LIBRARIES) + set(HAVE_LIBPCRE 1) + list(APPEND ZM_BIN_LIBS "${PCRE_LIBRARIES}") + find_path(PCRE_INCLUDE_DIR pcre.h) + if(PCRE_INCLUDE_DIR) + include_directories("${PCRE_INCLUDE_DIR}") + set(CMAKE_REQUIRED_INCLUDES "${PCRE_INCLUDE_DIR}") + endif() + mark_as_advanced(FORCE PCRE_LIBRARIES PCRE_INCLUDE_DIR) + check_include_file("pcre.h" HAVE_PCRE_H) + set(optlibsfound "${optlibsfound} PCRE") + else() + set(optlibsnotfound "${optlibsnotfound} PCRE") endif() - mark_as_advanced(FORCE PCRE_LIBRARIES PCRE_INCLUDE_DIR) - check_include_file("pcre.h" HAVE_PCRE_H) - set(optlibsfound "${optlibsfound} PCRE") -else() - set(optlibsnotfound "${optlibsnotfound} PCRE") endif() # mysqlclient (using find_library and find_path) @@ -513,6 +521,15 @@ endif() #list(APPEND ZM_BIN_LIBS "${Boost_LIBRARIES}") #endif() + +find_package(GSOAP 2.0.0) +if (GSOAP_FOUND) + set(optlibsfound "${optlibsfound} gsoap") + add_compile_definitions(WITH_GSOAP) +else() + set(optlibsnotfound "${optlibsnotfound} gsoap") +endif() + if(NOT ZM_NO_RTSPSERVER) set(HAVE_RTSP_SERVER 1) else() @@ -542,6 +559,7 @@ set(ZM_PCRE 0) if(HAVE_LIBPCRE AND HAVE_PCRE_H) set(ZM_PCRE 1) endif() + # Check for mmap and enable in all components set(ZM_MEM_MAPPED 0) set(ENABLE_MMAP no) @@ -628,6 +646,18 @@ if(ZM_PATH_ARP STREQUAL "") endif() endif() +# Find the path to an arp-scan compatible executable +if(ZM_PATH_ARP_SCAN STREQUAL "") + find_program(ARP_SCAN_EXECUTABLE arp-scan) + if(ARP_SCAN_EXECUTABLE) + set(ZM_PATH_ARP_SCAN "${ARP_SCAN_EXECUTABLE}") + mark_as_advanced(ARP_SCAN_EXECUTABLE) + endif() + if(ARP_SCAN_EXECUTABLE-NOTFOUND) + message(WARNING "Unable to find a compatible arp-scan binary. Monitor probe will be less powerful.") + endif() +endif() + # Some variables that zm expects set(ZM_PID "${ZM_RUNDIR}/zm.pid") set(ZM_CONFIG "${ZM_CONFIG_DIR}/zm.conf") diff --git a/README.md b/README.md index 2249f1752..bcb53ba8a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ ZoneMinder ========== -[![Build Status](https://travis-ci.org/ZoneMinder/zoneminder.png)](https://travis-ci.org/ZoneMinder/zoneminder) [![Bounty Source](https://api.bountysource.com/badge/team?team_id=204&style=bounties_received)](https://www.bountysource.com/teams/zoneminder/issues?utm_source=ZoneMinder&utm_medium=shield&utm_campaign=bounties_received) [![Join Slack](https://github.com/ozonesecurity/ozonebase/blob/master/img/slacksm.png?raw=true)](https://join.slack.com/t/zoneminder-chat/shared_invite/enQtNTU0NDkxMDM5NDQwLTdhZmQ5Y2M2NWQyN2JkYTBiN2ZkMzIzZGQ0MDliMTRmM2FjZWRlYzUwYTQ2MjMwMTVjMzQ1NjYxOTdmMjE2MTE) -[![IRC Network](https://img.shields.io/badge/irc-%23zoneminder-blue.svg "IRC Freenode")](https://webchat.freenode.net/?channels=zoneminder) All documentation for ZoneMinder is now online at https://zoneminder.readthedocs.org @@ -26,7 +24,7 @@ https://github.com/ZoneMinder/zmdockerfiles This is the recommended method to install ZoneMinder onto your system. ZoneMinder packages are maintained for the following distros: -- Ubuntu via [Iconnor's PPA](https://launchpad.net/~iconnor) +- Ubuntu via [Isaac Connor's PPA](https://launchpad.net/~iconnor) - Debian from their [default repository](https://packages.debian.org/search?searchon=names&keywords=zoneminder) - RHEL/CentOS and clones via [RPM Fusion](http://rpmfusion.org) - Fedora via [RPM Fusion](http://rpmfusion.org) diff --git a/cmake/Modules/FindFmt.cmake b/cmake/Modules/FindFmt.cmake new file mode 100644 index 000000000..b426d8c77 --- /dev/null +++ b/cmake/Modules/FindFmt.cmake @@ -0,0 +1,100 @@ +# FindFmt +# ------- +# Finds the Fmt library +# +# This will define the following variables:: +# +# FMT_FOUND - system has Fmt +# FMT_INCLUDE_DIRS - the Fmt include directory +# FMT_LIBRARIES - the Fmt libraries +# +# and the following imported targets:: +# +# Fmt::Fmt - The Fmt library + +if(ENABLE_INTERNAL_FMT) + include(ExternalProject) + file(STRINGS ${CMAKE_SOURCE_DIR}/tools/depends/target/libfmt/Makefile VER REGEX "^[ ]*VERSION[ ]*=.+$") + string(REGEX REPLACE "^[ ]*VERSION[ ]*=[ ]*" "" FMT_VERSION "${VER}") + + # allow user to override the download URL with a local tarball + # needed for offline build envs + if(FMT_URL) + get_filename_component(FMT_URL "${FMT_URL}" ABSOLUTE) + else() + set(FMT_URL http://mirrors.kodi.tv/build-deps/sources/fmt-${FMT_VERSION}.tar.gz) + endif() + if(VERBOSE) + message(STATUS "FMT_URL: ${FMT_URL}") + endif() + + if(APPLE) + set(EXTRA_ARGS "-DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}") + endif() + + set(FMT_LIBRARY ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/lib/libfmt.a) + set(FMT_INCLUDE_DIR ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/include) + externalproject_add(fmt + URL ${FMT_URL} + DOWNLOAD_DIR ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/download + PREFIX ${CORE_BUILD_DIR}/fmt + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR} + -DCMAKE_CXX_EXTENSIONS=${CMAKE_CXX_EXTENSIONS} + -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_INSTALL_LIBDIR=lib + -DFMT_DOC=OFF + -DFMT_TEST=OFF + "${EXTRA_ARGS}" + BUILD_BYPRODUCTS ${FMT_LIBRARY}) + set_target_properties(fmt PROPERTIES FOLDER "External Projects") + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Fmt + REQUIRED_VARS FMT_LIBRARY FMT_INCLUDE_DIR + VERSION_VAR FMT_VERSION) + + set(FMT_LIBRARIES ${FMT_LIBRARY}) + set(FMT_INCLUDE_DIRS ${FMT_INCLUDE_DIR}) + +else() + +find_package(FMT 6.1.2 CONFIG REQUIRED QUIET) + +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_FMT libfmt QUIET) + if(PC_FMT_VERSION AND NOT FMT_VERSION) + set(FMT_VERSION ${PC_FMT_VERSION}) + endif() +endif() + +find_path(FMT_INCLUDE_DIR NAMES fmt/format.h + PATHS ${PC_FMT_INCLUDEDIR}) + +find_library(FMT_LIBRARY_RELEASE NAMES fmt + PATHS ${PC_FMT_LIBDIR}) +find_library(FMT_LIBRARY_DEBUG NAMES fmtd + PATHS ${PC_FMT_LIBDIR}) + +include(SelectLibraryConfigurations) +select_library_configurations(FMT) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Fmt + REQUIRED_VARS FMT_LIBRARY FMT_INCLUDE_DIR FMT_VERSION + VERSION_VAR FMT_VERSION) + +if(FMT_FOUND) + set(FMT_LIBRARIES ${FMT_LIBRARY}) + set(FMT_INCLUDE_DIRS ${FMT_INCLUDE_DIR}) + + if(NOT TARGET fmt) + add_library(fmt UNKNOWN IMPORTED) + set_target_properties(fmt PROPERTIES + IMPORTED_LOCATION "${FMT_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${FMT_INCLUDE_DIR}") + endif() +endif() + +endif() +mark_as_advanced(FMT_INCLUDE_DIR FMT_LIBRARY) diff --git a/cmake/Modules/FindGSOAP.cmake b/cmake/Modules/FindGSOAP.cmake new file mode 100644 index 000000000..c7f181bec --- /dev/null +++ b/cmake/Modules/FindGSOAP.cmake @@ -0,0 +1,113 @@ +# +# This module detects if gsoap is installed and determines where the +# include files and libraries are. +# +# This code sets the following variables: +# +# GSOAP_IMPORT_DIR = full path to the gsoap import directory +# GSOAP_LIBRARIES = full path to the gsoap libraries +# GSOAP_SSL_LIBRARIES = full path to the gsoap ssl libraries +# GSOAP_INCLUDE_DIR = include dir to be used when using the gsoap library +# GSOAP_PLUGIN_DIR = gsoap plugins directory +# GSOAP_WSDL2H = wsdl2h binary +# GSOAP_SOAPCPP2 = soapcpp2 binary +# GSOAP_FOUND = set to true if gsoap was found successfully +# +# GSOAP_ROOT +# setting this enables search for gsoap libraries / headers in this location + +# ----------------------------------------------------- +# GSOAP Import Directories +# ----------------------------------------------------- +find_path(GSOAP_IMPORT_DIR + NAMES wsa.h + PATHS ${GSOAP_ROOT}/import ${GSOAP_ROOT}/share/gsoap/import +) + +# ----------------------------------------------------- +# GSOAP Libraries +# ----------------------------------------------------- +find_library(GSOAP_CXX_LIBRARIES + NAMES gsoap++ + HINTS ${GSOAP_ROOT}/lib ${GSOAP_ROOT}/lib64 + ${GSOAP_ROOT}/lib32 + DOC "The main gsoap library" +) +find_library(GSOAP_SSL_CXX_LIBRARIES + NAMES gsoapssl++ + HINTS ${GSOAP_ROOT}/lib ${GSOAP_ROOT}/lib64 + ${GSOAP_ROOT}/lib32 + DOC "The ssl gsoap library" +) + + +# ----------------------------------------------------- +# GSOAP Include Directories +# ----------------------------------------------------- +find_path(GSOAP_INCLUDE_DIR + NAMES stdsoap2.h + HINTS ${GSOAP_ROOT} ${GSOAP_ROOT}/include ${GSOAP_ROOT}/include/* + DOC "The gsoap include directory" +) + +# ----------------------------------------------------- +# GSOAP plugin Directories +# ----------------------------------------------------- +find_path(GSOAP_PLUGIN_DIR + NAMES wsseapi.c + HINTS ${GSOAP_ROOT} /usr/share/gsoap/plugin + DOC "The gsoap plugin directory" +) + +# ----------------------------------------------------- +# GSOAP Binaries +# ---------------------------------------------------- +if(NOT GSOAP_TOOL_DIR) + set(GSOAP_TOOL_DIR GSOAP_ROOT) +endif() + +find_program(GSOAP_WSDL2H + NAMES wsdl2h + HINTS ${GSOAP_TOOL_DIR}/bin + DOC "The gsoap bin directory" +) +find_program(GSOAP_SOAPCPP2 + NAMES soapcpp2 + HINTS ${GSOAP_TOOL_DIR}/bin + DOC "The gsoap bin directory" +) +# ----------------------------------------------------- +# GSOAP version +# try to determine the flagfor the 2.7.6 compatiblity, break with 2.7.13 and re-break with 2.7.16 +# ---------------------------------------------------- +if(GSOAP_SOAPCPP2) + execute_process(COMMAND ${GSOAP_SOAPCPP2} "-V" OUTPUT_VARIABLE GSOAP_STRING_VERSION ERROR_VARIABLE GSOAP_STRING_VERSION ) + string(REGEX MATCH "[0-9]*\\.[0-9]*\\.[0-9]*" GSOAP_VERSION ${GSOAP_STRING_VERSION}) +endif() +# ----------------------------------------------------- +# GSOAP_276_COMPAT_FLAGS and GSOAPVERSION +# try to determine the flagfor the 2.7.6 compatiblity, break with 2.7.13 and re-break with 2.7.16 +# ---------------------------------------------------- +if( "${GSOAP_VERSION}" VERSION_LESS "2.7.6") + set(GSOAP_276_COMPAT_FLAGS "") +elseif ( "${GSOAP_VERSION}" VERSION_LESS "2.7.14") + set(GSOAP_276_COMPAT_FLAGS "-z") +else ( "${GSOAP_VERSION}" VERSION_LESS "2.7.14") + set(GSOAP_276_COMPAT_FLAGS "-z1 -z2") +endif ( "${GSOAP_VERSION}" VERSION_LESS "2.7.6") + +# ----------------------------------------------------- +# handle the QUIETLY and REQUIRED arguments and set GSOAP_FOUND to TRUE if +# all listed variables are TRUE +# ----------------------------------------------------- +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GSOAP DEFAULT_MSG GSOAP_CXX_LIBRARIES + GSOAP_INCLUDE_DIR GSOAP_WSDL2H GSOAP_SOAPCPP2) +mark_as_advanced(GSOAP_INCLUDE_DIR GSOAP_LIBRARIES GSOAP_WSDL2H GSOAP_SOAPCPP2) + +if(GSOAP_FOUND) + if(GSOAP_FIND_REQUIRED AND GSOAP_FIND_VERSION AND ${GSOAP_VERSION} VERSION_LESS ${GSOAP_FIND_VERSION}) + message(SEND_ERROR "Found GSOAP version ${GSOAP_VERSION} less then required ${GSOAP_FIND_VERSION}.") + endif() +endif() + diff --git a/conf.d/01-system-paths.conf.in b/conf.d/01-system-paths.conf.in index 277f63b70..523a8c0c8 100644 --- a/conf.d/01-system-paths.conf.in +++ b/conf.d/01-system-paths.conf.in @@ -47,5 +47,9 @@ ZM_PATH_SWAP=@ZM_TMPDIR@ # ZoneMinder will find the arp binary automatically on most systems ZM_PATH_ARP="@ZM_PATH_ARP@" +# Full path to optional arp-scan binary +# ZoneMinder will find the arp-scan binary automatically on most systems +ZM_PATH_ARP_SCAN="@ZM_PATH_ARP_SCAN@" + #Full path to shutdown binary ZM_PATH_SHUTDOWN="@ZM_PATH_SHUTDOWN@" diff --git a/db/CMakeLists.txt b/db/CMakeLists.txt index 18f440fc8..00cc7d8a4 100644 --- a/db/CMakeLists.txt +++ b/db/CMakeLists.txt @@ -4,6 +4,7 @@ configure_file(zm_create.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_create.sql" @ONLY) configure_file(zm_update-1.31.30.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.31.30.sql" @ONLY) configure_file(zm_update-1.35.24.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.35.24.sql" @ONLY) +configure_file(zm_update-1.37.4.sql.in "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.4.sql" @ONLY) # Glob all database upgrade scripts file(GLOB dbfileslist RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "zm_update-*.sql") @@ -15,6 +16,8 @@ install(FILES ${dbfileslist} DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.31.30.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") # install zm_update-1.35.24.sql install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.35.24.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") +# install zm_update-1.37.4.sql +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_update-1.37.4.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") # install zm_create.sql install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_create.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") @@ -22,3 +25,8 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zm_create.sql" DESTINATION "${CMAKE_I # install triggers.sql install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/triggers.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") +# install manufacturers.sql +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/manufacturers.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") + +# install models.sql +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/models.sql" DESTINATION "${CMAKE_INSTALL_DATADIR}/zoneminder/db") diff --git a/db/manufacturers.sql b/db/manufacturers.sql new file mode 100644 index 000000000..3761f5cbe --- /dev/null +++ b/db/manufacturers.sql @@ -0,0 +1,24 @@ +INSERT IGNORE INTO Manufacturers VALUES (1, 'Acti'); +INSERT IGNORE INTO Manufacturers VALUES (2, 'Amcrest'); +INSERT IGNORE INTO Manufacturers VALUES (3, 'Airlink101'); +INSERT IGNORE INTO Manufacturers VALUES (4, 'Arecont Vision'); +INSERT IGNORE INTO Manufacturers VALUES (5, 'Axis'); +INSERT IGNORE INTO Manufacturers VALUES (6, 'Dahua'); +INSERT IGNORE INTO Manufacturers VALUES (7, 'D-Link'); +INSERT IGNORE INTO Manufacturers VALUES (8, 'Edimax'); +INSERT IGNORE INTO Manufacturers VALUES (9, 'Foscam'); +INSERT IGNORE INTO Manufacturers VALUES (10, 'Gadspot'); +INSERT IGNORE INTO Manufacturers VALUES (11, 'GrandStream'); +INSERT IGNORE INTO Manufacturers VALUES (12, 'HikVision'); +INSERT IGNORE INTO Manufacturers VALUES (13, 'JVC'); +INSERT IGNORE INTO Manufacturers VALUES (14, 'Maginon'); +INSERT IGNORE INTO Manufacturers VALUES (15, 'Mobotix'); +INSERT IGNORE INTO Manufacturers VALUES (16, 'Oncam Grandeye'); +INSERT IGNORE INTO Manufacturers VALUES (17, 'Panasonic'); +INSERT IGNORE INTO Manufacturers VALUES (18, 'Pelco'); +INSERT IGNORE INTO Manufacturers VALUES (19, 'Sony'); +INSERT IGNORE INTO Manufacturers VALUES (20, 'TP-Link'); +INSERT IGNORE INTO Manufacturers VALUES (21, 'Trendnet'); +INSERT IGNORE INTO Manufacturers VALUES (22, 'VisionTek'); +INSERT IGNORE INTO Manufacturers VALUES (23, 'Vivotek'); +INSERT IGNORE INTO Manufacturers VALUES (24, 'Wansview'); diff --git a/db/models.sql b/db/models.sql new file mode 100644 index 000000000..ffafb76a8 --- /dev/null +++ b/db/models.sql @@ -0,0 +1,56 @@ +/* INSERT INTO Manufacturers VALUES (1, 'Acti'); */ +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A21'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A23'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A24'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A28'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A31'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A310'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A311'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A32'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A41'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A415'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A416'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A418'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A42'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A421'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A43'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A45'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A46'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A48'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (1, 'A74'); +/* +INSERT INTO Manufacturers VALUES (2, 'Amcrest'); +*/ +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (2, 'IP8M-T2499EW'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (2, 'ASH42-B'); +/* +INSERT INTO Manufacturers VALUES (3, 'Airlink101'); +INSERT INTO Manufacturers VALUES (4, 'Arecont Vision'); +INSERT INTO Manufacturers VALUES (5, 'Axis'); +INSERT INTO Manufacturers VALUES (6, 'Dahua'); +INSERT INTO Manufacturers VALUES (7, 'D-Link'); +*/ +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (7, 'DCS-930L'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (7, 'DCS-932L'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (7, 'DCS-933L'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (7, 'DCS-942L'); +INSERT IGNORE INTO Models (ManufacturerId,Name) VALUES (7, 'DCS-5020L'); +/* +INSERT INTO Manufacturers VALUES (8, 'Edimax'); +INSERT INTO Manufacturers VALUES (9, 'Foscam'); +INSERT INTO Manufacturers VALUES (10, 'Gadspot'); +INSERT INTO Manufacturers VALUES (11, 'GrandStream'); +INSERT INTO Manufacturers VALUES (12, 'HikVision'); +INSERT INTO Manufacturers VALUES (13, 'JVC'); +INSERT INTO Manufacturers VALUES (14, 'Maginon'); +INSERT INTO Manufacturers VALUES (15, 'Mobotix'); +INSERT INTO Manufacturers VALUES (16, 'Oncam Grandeye'); +INSERT INTO Manufacturers VALUES (17, 'Panasonic'); +INSERT INTO Manufacturers VALUES (18, 'Pelco'); +INSERT INTO Manufacturers VALUES (19, 'Sony'); +INSERT INTO Manufacturers VALUES (20, 'TP-Link'); +INSERT INTO Manufacturers VALUES (21, 'Trendnet'); +INSERT INTO Manufacturers VALUES (22, 'VisionTek'); +INSERT INTO Manufacturers VALUES (23, 'Vivotek'); +INSERT INTO Manufacturers VALUES (24, 'Wansview'); +*/ diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 5cf6d3573..040bc593c 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -39,6 +39,7 @@ CREATE TABLE `Config` ( `Help` text, `Category` varchar(32) NOT NULL default '', `Readonly` tinyint(3) unsigned NOT NULL default '0', + `Private` BOOLEAN NOT NULL DEFAULT FALSE, `Requires` text, PRIMARY KEY (`Name`) ) ENGINE=@ZM_MYSQL_ENGINE@; @@ -188,7 +189,7 @@ CREATE TABLE `Events` ( `StorageId` smallint(5) unsigned default 0, `SecondaryStorageId` smallint(5) unsigned default 0, `Name` varchar(64) NOT NULL default '', - `Cause` varchar(32) NOT NULL default '', + `Cause` TEXT, `StartDateTime` datetime default NULL, `EndDateTime` datetime default NULL, `Width` smallint(5) unsigned NOT NULL default '0', @@ -283,6 +284,7 @@ CREATE TABLE `Filters` ( `Id` int(10) unsigned NOT NULL auto_increment, `Name` varchar(64) NOT NULL default '', `UserId` int(10) unsigned, + `ExecuteInterval` int(10) unsigned NOT NULL default '60', `Query_json` text NOT NULL, `AutoArchive` tinyint(3) unsigned NOT NULL default '0', `AutoUnarchive` tinyint(3) unsigned NOT NULL default '0', @@ -412,6 +414,7 @@ CREATE TABLE `Models` ( DROP TABLE IF EXISTS `MonitorPresets`; CREATE TABLE `MonitorPresets` ( `Id` int(10) unsigned NOT NULL auto_increment, + `ModelId` int unsigned, FOREIGN KEY (`ModelId`) REFERENCES `Models` (Id), `Name` varchar(64) NOT NULL default '', `Type` enum('Local','Remote','File','Ffmpeg','Libvlc','cURL','WebSite','NVSocket','VNC') NOT NULL default 'Local', `Device` tinytext, @@ -447,16 +450,24 @@ CREATE TABLE `Monitors` ( `Notes` TEXT, `ServerId` int(10) unsigned, `StorageId` smallint(5) unsigned default 0, + `ManufacturerId` int unsigned, FOREIGN KEY (`ManufacturerId`) REFERENCES `Manufacturers` (Id), + `ModelId` int unsigned, FOREIGN KEY (`ModelId`) REFERENCES `Models` (Id), `Type` enum('Local','Remote','File','Ffmpeg','Libvlc','cURL','WebSite','NVSocket','VNC') NOT NULL default 'Local', `Function` enum('None','Monitor','Modect','Record','Mocord','Nodect') NOT NULL default 'Monitor', `Enabled` tinyint(3) unsigned NOT NULL default '1', `DecodingEnabled` tinyint(3) unsigned NOT NULL default '1', + `JanusEnabled` BOOLEAN NOT NULL default false, + `JanusAudioEnabled` BOOLEAN NOT NULL default false, `LinkedMonitors` varchar(255), `Triggers` set('X10') NOT NULL default '', + `EventStartCommand` VARCHAR(255) NOT NULL DEFAULT '', + `EventEndCommand` VARCHAR(255) NOT NULL DEFAULT '', `ONVIF_URL` VARCHAR(255) NOT NULL DEFAULT '', `ONVIF_Username` VARCHAR(64) NOT NULL DEFAULT '', `ONVIF_Password` VARCHAR(64) NOT NULL DEFAULT '', `ONVIF_Options` VARCHAR(64) NOT NULL DEFAULT '', + `ONVIF_Event_Listener` BOOLEAN NOT NULL DEFAULT FALSE, + `use_Amcrest_API` BOOLEAN NOT NULL DEFAULT FALSE, `Device` tinytext NOT NULL default '', `Channel` tinyint(3) unsigned NOT NULL default '0', `Format` int(10) unsigned NOT NULL default '0', @@ -971,81 +982,81 @@ INSERT INTO `Controls` VALUES (NULL,'Amcrest HTTP API','Ffmpeg','Amcrest_HTTP',0 -- Add some monitor preset values -- -INSERT into MonitorPresets VALUES (NULL,'Amcrest, IP8M-T2499EW 640x480, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp','NULL',554,'rtsp://:@/cam/realmonitor?channel=1&subtype=1',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT into MonitorPresets VALUES (NULL,'Amcrest, IP8M-T2499EW 3840x2160, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp','NULL',554,'rtsp://:@/cam/realmonitor?channel=1&subtype=0',NULL,3840,2160,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 320x240, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240&req_fps=5',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 640x480, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480&req_fps=5',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 320x240, mpjpeg, B&W','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240&color=0',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP, 640x480, mpjpeg, B&W','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480&color=0',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240',NULL,320,240,3,NULL,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 320x240, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240&req_fps=5',NULL,320,240,3,NULL,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,NULL,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,5.0,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480',NULL,640,480,3,NULL,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 640x480, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480&req_fps=5',NULL,640,480,3,NULL,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,NULL,1,4,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis IP PTZ, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,5.0,1,4,NULL,':',100,100); -INSERT into MonitorPresets VALUES (NULL,'Axis IP, mpeg4, unicast','Remote','rtsp',0,255,'rtsp','rtpUni','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT into MonitorPresets VALUES (NULL,'Axis IP, mpeg4, multicast','Remote','rtsp',0,255,'rtsp','rtpMulti','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT into MonitorPresets VALUES (NULL,'Axis IP, mpeg4, RTP/RTSP','Remote','rtsp',0,255,'rtsp','rtpRtsp','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT into MonitorPresets VALUES (NULL,'Axis IP, mpeg4, RTP/RTSP/HTTP','Remote',NULL,NULL,NULL,'rtsp','rtpRtspHttp','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'D-link DCS-930L, 640x480, mjpeg','Remote','http',0,0,'http','simple','',80,'/mjpeg.cgi',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'D-Link DCS-5020L, 640x480, mjpeg','Remote','http',0,0,'http','simple',':@','80','/video.cgi',NULL,640,480,0,NULL,1,'34',NULL,':@',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP PTZ, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,1,5,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP PTZ, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,1,5,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP PTZ, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,5.0,1,5,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP PTZ, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,1,5,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP PTZ, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,1,5,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Panasonic IP PTZ, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,5.0,1,5,NULL,':',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Gadspot IP, jpeg','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Gadspot IP, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Gadspot IP, mpjpeg','Remote','http',0,0,'http','simple','',80,'/GetData.cgi',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Gadspot IP, mpjpeg','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'IP Webcam by Pavel Khlebovich 1920x1080','Remote','/dev/video','0',255,'http','simple','','8080','/video','',1920,1080,0,NULL,0,'0','','',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'VEO Observer, jpeg','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Blue Net Video Server, jpeg','Remote','http',0,0,'http','simple','',80,'/cgi-bin/image.cgi?control=0&id=admin&passwd=admin',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT into MonitorPresets VALUES (NULL,'ACTi IP, mpeg4, unicast','Remote',NULL,NULL,NULL,'rtsp','rtpUni','',7070,'','/track',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp:///axis-media/media.amp?videocodec=h264',NULL,NULL,NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Vivotek FFMPEG','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp://:554/live.sdp',NULL,NULL,NULL,352,240,NULL,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Axis FFMPEG','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp:///axis-media/media.amp',NULL,NULL,NULL,640,480,NULL,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'ACTi TCM FFMPEG','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp://admin:123456@:7070',NULL,NULL,NULL,320,240,NULL,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), PAL, 320x240','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), PAL, 320x240, max 5 FPS','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), PAL, 640x480','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), PAL, 640x480, max 5 FPS','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), NTSC, 320x240','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), NTSC, 320x240, max 5 FPS','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), NTSC, 640x480','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L2), NTSC, 640x480, max 5 FPS','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), PAL, 320x240','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), PAL, 320x240, max 5 FPS','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), PAL, 640x480','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), PAL, 640x480, max 5 FPS','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), NTSC, 320x240','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), NTSC, 320x240, max 5 FPS','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), NTSC, 640x480','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'BTTV Video (V4L1), NTSC, 640x480, max 5 FPS','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,5.0,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Remote ZoneMinder','Remote',NULL,NULL,NULL,'http','simple','',80,'/cgi-bin/nph-zms?mode=jpeg&monitor=&scale=100&maxfps=5&buffer=0',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Foscam FI8620 FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,'','','','rtsp://:@:554/11',NULL,704,576,0,NULL,1,'10','','',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Foscam FI8608W FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,'','','','rtsp://:@:554/11',NULL,640,480,0,NULL,1,'11','','',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Foscam FI9821W FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,'','','','rtsp://:@:88/videoMain',NULL,1280,720,0,NULL,1,'12','','',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Loftek Sentinel PTZ, 640x480, mjpeg','Remote','http',0,0,NULL,NULL,'','80','/videostream.cgi?user=&pwd=&resolution=32&rate=11',NULL,640,480,4,NULL,1,'13','',':@',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Airlink 777W PTZ, 640x480, mjpeg','Remote','http',0,0,NULL,NULL,':@','80','/cgi/mjpg/mjpg.cgi',NULL,640,480,4,NULL,1,'7','',':@',100,100); -INSERT INTO MonitorPresets VALUES (NULL,'SunEyes SP-P1802SWPTZ','Libvlc','/dev/video','0',255,'','rtpMulti','','80','rtsp://:554/11','',1920,1080,0,0.00,1,'16','-speed=64',':',100,33); -INSERT INTO MonitorPresets VALUES (NULL,'Qihan IP, 1280x720, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp',NULL,554,'rtsp:///tcp_live/ch0_0',NULL,1280,720,3,NULL,0,NULL,NULL,NULL,100,100); -INSERT INTO MonitorPresets VALUES (NULL,'Qihan IP, 1920x1080, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp',NULL,554,'rtsp:///tcp_live/ch0_0',NULL,1920,1080,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'Amcrest, IP8M-T2499EW 640x480, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp','NULL',554,'rtsp://:@/cam/realmonitor?channel=1&subtype=1',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'Amcrest, IP8M-T2499EW 3840x2160, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp','NULL',554,'rtsp://:@/cam/realmonitor?channel=1&subtype=0',NULL,3840,2160,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 320x240, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240&req_fps=5',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 640x480, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480&req_fps=5',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 320x240, mpjpeg, B&W','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240&color=0',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP, 640x480, mpjpeg, B&W','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480&color=0',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240',NULL,320,240,3,NULL,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 320x240, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=320x240&req_fps=5',NULL,320,240,3,NULL,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,NULL,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=320x240',NULL,320,240,3,5.0,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480',NULL,640,480,3,NULL,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 640x480, mpjpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/mjpg/video.cgi?resolution=640x480&req_fps=5',NULL,640,480,3,NULL,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,NULL,1,4,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis IP PTZ, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/axis-cgi/jpg/image.cgi?resolution=640x480',NULL,640,480,3,5.0,1,4,NULL,':',100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'Axis IP, mpeg4, unicast','Remote','rtsp',0,255,'rtsp','rtpUni','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'Axis IP, mpeg4, multicast','Remote','rtsp',0,255,'rtsp','rtpMulti','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'Axis IP, mpeg4, RTP/RTSP','Remote','rtsp',0,255,'rtsp','rtpRtsp','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'Axis IP, mpeg4, RTP/RTSP/HTTP','Remote',NULL,NULL,NULL,'rtsp','rtpRtspHttp','',554,'/mpeg4/media.amp','/trackID=',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'D-link DCS-930L, 640x480, mjpeg','Remote','http',0,0,'http','simple','',80,'/mjpeg.cgi',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'D-Link DCS-5020L, 640x480, mjpeg','Remote','http',0,0,'http','simple',':@','80','/video.cgi',NULL,640,480,0,NULL,1,'34',NULL,':@',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP PTZ, 320x240, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,1,5,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP PTZ, 320x240, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,NULL,1,5,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP PTZ, 320x240, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=320x240&Quality=Standard',NULL,320,240,3,5.0,1,5,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP PTZ, 640x480, mpjpeg','Remote','http',0,0,'http','simple','',80,'/nphMotionJpeg?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,1,5,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP PTZ, 640x480, jpeg','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,NULL,1,5,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Panasonic IP PTZ, 640x480, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/SnapshotJPEG?Resolution=640x480&Quality=Standard',NULL,640,480,3,5.0,1,5,NULL,':',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Gadspot IP, jpeg','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Gadspot IP, jpeg, max 5 FPS','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Gadspot IP, mpjpeg','Remote','http',0,0,'http','simple','',80,'/GetData.cgi',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Gadspot IP, mpjpeg','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'IP Webcam by Pavel Khlebovich 1920x1080','Remote','/dev/video','0',255,'http','simple','','8080','/video','',1920,1080,0,NULL,0,'0','','',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'VEO Observer, jpeg','Remote','http',0,0,'http','simple','',80,'/Jpeg/CamImg.jpg',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Blue Net Video Server, jpeg','Remote','http',0,0,'http','simple','',80,'/cgi-bin/image.cgi?control=0&id=admin&passwd=admin',NULL,320,240,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT into MonitorPresets VALUES (NULL,NULL,'ACTi IP, mpeg4, unicast','Remote',NULL,NULL,NULL,'rtsp','rtpUni','',7070,'','/track',NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp:///axis-media/media.amp?videocodec=h264',NULL,NULL,NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Vivotek FFMPEG','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp://:554/live.sdp',NULL,NULL,NULL,352,240,NULL,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Axis FFMPEG','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp:///axis-media/media.amp',NULL,NULL,NULL,640,480,NULL,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'ACTi TCM FFMPEG','Ffmpeg',NULL,NULL,NULL,NULL,NULL,'rtsp://admin:123456@:7070',NULL,NULL,NULL,320,240,NULL,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), PAL, 320x240','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), PAL, 320x240, max 5 FPS','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), PAL, 640x480','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), PAL, 640x480, max 5 FPS','Local','/dev/video',0,255,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), NTSC, 320x240','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), NTSC, 320x240, max 5 FPS','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,320,240,1345466932,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), NTSC, 640x480','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L2), NTSC, 640x480, max 5 FPS','Local','/dev/video',0,45056,NULL,'v4l2',NULL,NULL,NULL,NULL,640,480,1345466932,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), PAL, 320x240','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), PAL, 320x240, max 5 FPS','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), PAL, 640x480','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), PAL, 640x480, max 5 FPS','Local','/dev/video',0,0,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), NTSC, 320x240','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), NTSC, 320x240, max 5 FPS','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,320,240,13,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), NTSC, 640x480','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'BTTV Video (V4L1), NTSC, 640x480, max 5 FPS','Local','/dev/video',0,1,NULL,'v4l1',NULL,NULL,NULL,NULL,640,480,13,5.0,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Remote ZoneMinder','Remote',NULL,NULL,NULL,'http','simple','',80,'/cgi-bin/nph-zms?mode=jpeg&monitor=&scale=100&maxfps=5&buffer=0',NULL,NULL,NULL,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Foscam FI8620 FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,'','','','rtsp://:@:554/11',NULL,704,576,0,NULL,1,'10','','',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Foscam FI8608W FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,'','','','rtsp://:@:554/11',NULL,640,480,0,NULL,1,'11','','',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Foscam FI9821W FFMPEG H.264','Ffmpeg',NULL,NULL,NULL,NULL,'','','','rtsp://:@:88/videoMain',NULL,1280,720,0,NULL,1,'12','','',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Loftek Sentinel PTZ, 640x480, mjpeg','Remote','http',0,0,NULL,NULL,'','80','/videostream.cgi?user=&pwd=&resolution=32&rate=11',NULL,640,480,4,NULL,1,'13','',':@',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Airlink 777W PTZ, 640x480, mjpeg','Remote','http',0,0,NULL,NULL,':@','80','/cgi/mjpg/mjpg.cgi',NULL,640,480,4,NULL,1,'7','',':@',100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'SunEyes SP-P1802SWPTZ','Libvlc','/dev/video','0',255,'','rtpMulti','','80','rtsp://:554/11','',1920,1080,0,0.00,1,'16','-speed=64',':',100,33); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Qihan IP, 1280x720, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp',NULL,554,'rtsp:///tcp_live/ch0_0',NULL,1280,720,3,NULL,0,NULL,NULL,NULL,100,100); +INSERT INTO MonitorPresets VALUES (NULL,NULL,'Qihan IP, 1920x1080, RTP/RTSP','Ffmpeg','rtsp',0,255,'rtsp','rtpRtsp',NULL,554,'rtsp:///tcp_live/ch0_0',NULL,1920,1080,3,NULL,0,NULL,NULL,NULL,100,100); -- -- Add some zone preset values @@ -1115,6 +1126,9 @@ CREATE TABLE Snapshot_Events ( -- We generally don't alter triggers, we drop and re-create them, so let's keep them in a separate file that we can just source in update scripts. source @PKGDATADIR@/db/triggers.sql + +source @PKGDATADIR@/db/manufacturers.sql +source @PKGDATADIR@/db/models.sql -- -- Apply the initial configuration -- diff --git a/db/zm_update-1.35.25.sql b/db/zm_update-1.35.25.sql index 387ef09b0..16df91d74 100644 --- a/db/zm_update-1.35.25.sql +++ b/db/zm_update-1.35.25.sql @@ -56,7 +56,7 @@ EXECUTE stmt; SET @s = (SELECT IF( (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() AND table_name = 'Monitor_Status' - AND column_name = 'DayEvents' + AND column_name = 'DayEventDiskSpace' ) > 0, "ALTER TABLE `Monitor_Status` DROP `DayEventDiskSpace`", "SELECT 'Column DayEventDiskSpace already removed from Monitor_Status'" diff --git a/db/zm_update-1.35.29.sql b/db/zm_update-1.35.29.sql new file mode 100644 index 000000000..341c5e162 --- /dev/null +++ b/db/zm_update-1.35.29.sql @@ -0,0 +1,47 @@ +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ManufacturerId' + ) > 0, +"SELECT 'Column ManufacturerId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `ManufacturerId` int(10) unsigned AFTER `StorageId`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ManufacturerId' + ) > 0, +"SELECT 'FOREIGN KEY for ManufacturerId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD FOREIGN KEY (`ManufacturerId`) REFERENCES `Manufacturers` (Id)" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ModelId' + ) > 0, +"SELECT 'Column ModelId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `ModelId` int(10) unsigned AFTER `ManufacturerId`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ModelId' + ) > 0, +"SELECT 'FOREIGN KEY for ModelId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD FOREIGN KEY (`ModelId`) REFERENCES `Models` (Id)" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.37.10.sql b/db/zm_update-1.37.10.sql new file mode 100644 index 000000000..30c2d8766 --- /dev/null +++ b/db/zm_update-1.37.10.sql @@ -0,0 +1,20 @@ +-- +-- Update Config table to have Private +-- + +SELECT 'Checking for Private in Config'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Config' + AND table_schema = DATABASE() + AND column_name = 'Private' + ) > 0, +"SELECT 'Column Private already exists in Config'", +"ALTER TABLE `Config` ADD COLUMN `Private` BOOLEAN NOT NULL DEFAULT FALSE AFTER `Readonly`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + + diff --git a/db/zm_update-1.37.11.sql b/db/zm_update-1.37.11.sql new file mode 100644 index 000000000..58a3d0cf1 --- /dev/null +++ b/db/zm_update-1.37.11.sql @@ -0,0 +1,18 @@ +-- +-- Update Monitors table to have use_Amcrest_API +-- + +SELECT 'Checking for use_Amcrest_API in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'use_Amcrest_API' + ) > 0, +"SELECT 'Column use_Amcrest_API already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `use_Amcrest_API` BOOLEAN NOT NULL default false AFTER `ONVIF_Event_Listener`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.37.3.sql b/db/zm_update-1.37.3.sql new file mode 100644 index 000000000..5ae4aa1b1 --- /dev/null +++ b/db/zm_update-1.37.3.sql @@ -0,0 +1,73 @@ +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ManufacturerId' + ) > 0, +"SELECT 'Column ManufacturerId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `ManufacturerId` int(10) unsigned AFTER `StorageId`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ManufacturerId' + ) > 0, +"SELECT 'FOREIGN KEY for ManufacturerId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD FOREIGN KEY (`ManufacturerId`) REFERENCES `Manufacturers` (Id)" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ModelId' + ) > 0, +"SELECT 'Column ModelId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `ModelId` int(10) unsigned AFTER `ManufacturerId`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'ModelId' + ) > 0, +"SELECT 'FOREIGN KEY for ModelId already exists in Monitors'", +"ALTER TABLE `Monitors` ADD FOREIGN KEY (`ModelId`) REFERENCES `Models` (Id)" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'MonitorPresets' + AND column_name = 'ModelId' + ) > 0, +"SELECT 'Column ModelId already exists in MonitorPresets'", +"ALTER TABLE `MonitorPresets` ADD `ModelId` int(10) unsigned AFTER `Id`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() + AND table_name = 'MonitorPresets' + AND column_name = 'ModelId' + ) > 0, +"SELECT 'FOREIGN KEY for ModelId already exists in MonitorPresets'", +"ALTER TABLE `MonitorPresets` ADD FOREIGN KEY (`ModelId`) REFERENCES `Models` (Id)" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +UPDATE `MonitorPresets` SET `ModelId`=(SELECT `Id` FROM `Models` WHERE `Name`='IP8M-T2499EW') WHERE `Name` like 'Amcrest, IP8M-T2499EW +%'; diff --git a/db/zm_update-1.37.4.sql.in b/db/zm_update-1.37.4.sql.in new file mode 100644 index 000000000..d0ab7b36c --- /dev/null +++ b/db/zm_update-1.37.4.sql.in @@ -0,0 +1,2 @@ +source @PKGDATADIR@/db/manufacturers.sql +source @PKGDATADIR@/db/models.sql diff --git a/db/zm_update-1.37.5.sql b/db/zm_update-1.37.5.sql new file mode 100644 index 000000000..035a73a1a --- /dev/null +++ b/db/zm_update-1.37.5.sql @@ -0,0 +1,31 @@ +-- +-- This update adds EventStartCommand and EventEndCommand +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'EventEndCommand' + ) > 0, +"SELECT 'Column EventEndCommand already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `EventEndCommand` VARCHAR(255) NOT NULL DEFAULT '' AFTER `Triggers`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'EventStartCommand' + ) > 0, +"SELECT 'Column EventStartCommand already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `EventStartCommand` VARCHAR(255) NOT NULL DEFAULT '' AFTER `Triggers`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.37.6.sql b/db/zm_update-1.37.6.sql new file mode 100644 index 000000000..7c79886ad --- /dev/null +++ b/db/zm_update-1.37.6.sql @@ -0,0 +1,18 @@ +-- +-- Update Filters table to have a ExecuteInterval Column +-- + +SELECT 'Checking for ExecuteInterval in Filters'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Filters' + AND table_schema = DATABASE() + AND column_name = 'ExecuteInterval' + ) > 0, +"SELECT 'Column ExecuteInterval already exists in Filters'", +"ALTER TABLE Filters ADD COLUMN `ExecuteInterval` int(10) unsigned NOT NULL default '60' AFTER `UserId`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.37.7.sql b/db/zm_update-1.37.7.sql new file mode 100644 index 000000000..d7d592974 --- /dev/null +++ b/db/zm_update-1.37.7.sql @@ -0,0 +1,21 @@ +/* Change Cause from varchar(32) to TEXT. We now include alarmed zone name */ +ALTER TABLE `Events` MODIFY `Cause` TEXT; + +-- +-- Update Monitors table to have a ONVIF_Event_Listener Column +-- + +SELECT 'Checking for ONVIF_Event_Listener in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'ONVIF_Event_Listener' + ) > 0, +"SELECT 'Column ONVIF_Event_Listener already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `ONVIF_Event_Listener` BOOLEAN NOT NULL default false AFTER `ONVIF_Options`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.37.8.sql b/db/zm_update-1.37.8.sql new file mode 100644 index 000000000..650489e7b --- /dev/null +++ b/db/zm_update-1.37.8.sql @@ -0,0 +1,18 @@ +-- +-- Update Monitors table to have JanusEnabled +-- + +SELECT 'Checking for JanusEnabled in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'JanusEnabled' + ) > 0, +"SELECT 'Column JanusEnabled already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `JanusEnabled` BOOLEAN NOT NULL default false AFTER `DecodingEnabled`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.37.9.sql b/db/zm_update-1.37.9.sql new file mode 100644 index 000000000..be7d2a9ec --- /dev/null +++ b/db/zm_update-1.37.9.sql @@ -0,0 +1,18 @@ +-- +-- Update Monitors table to have JanusEnabled +-- + +SELECT 'Checking for JanusAudioEnabled in Monitors'; +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'JanusAudioEnabled' + ) > 0, +"SELECT 'Column JanusAudioEnabled already exists in Monitors'", +"ALTER TABLE `Monitors` ADD COLUMN `JanusAudioEnabled` BOOLEAN NOT NULL default false AFTER `JanusEnabled`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/dep/RtspServer b/dep/RtspServer index cd7fd49be..eab328514 160000 --- a/dep/RtspServer +++ b/dep/RtspServer @@ -1 +1 @@ -Subproject commit cd7fd49becad6010a1b8466bfebbd93999a39878 +Subproject commit eab32851421ffe54fec0229c3efc44c642bc8d46 diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 649c27dbf..3cd8aceaf 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -9,7 +9,7 @@ %global ceb_version 1.0-zm # RtspServer is configured as a git submodule -%global rtspserver_commit cd7fd49becad6010a1b8466bfebbd93999a39878 +%global rtspserver_commit eab32851421ffe54fec0229c3efc44c642bc8d46 %global sslcert %{_sysconfdir}/pki/tls/certs/localhost.crt %global sslkey %{_sysconfdir}/pki/tls/private/localhost.key @@ -36,7 +36,7 @@ %global _hardened_build 1 Name: zoneminder -Version: 1.37.1 +Version: 1.37.11 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons @@ -354,7 +354,8 @@ ln -sf %{_sysconfdir}/zm/www/zoneminder.nginx.conf %{_sysconfdir}/zm/www/zonemin %config(noreplace) %{_sysconfdir}/logrotate.d/zoneminder %{_unitdir}/zoneminder.service -%{_datadir}/polkit-1/actions/com.zoneminder.systemctl.policy +%{_datadir}/polkit-1/actions/com.zoneminder.* +%{_datadir}/polkit-1/rules.d/com.zoneminder.arp-scan.rules %{_bindir}/zmsystemctl.pl %{_bindir}/zmaudit.pl diff --git a/distros/ubuntu2004/control b/distros/ubuntu2004/control index 43c8853f1..76f8f43e0 100644 --- a/distros/ubuntu2004/control +++ b/distros/ubuntu2004/control @@ -16,7 +16,6 @@ Build-Depends: debhelper (>= 11), sphinx-doc, python3-sphinx, dh-linktree, dh-ap ,libjpeg-turbo8-dev | libjpeg62-turbo-dev | libjpeg8-dev | libjpeg9-dev ,libturbojpeg0-dev ,default-libmysqlclient-dev | libmysqlclient-dev | libmariadbclient-dev-compat - ,libpcre3-dev ,libpolkit-gobject-1-dev ,libv4l-dev [!hurd-any] ,libvlc-dev @@ -32,6 +31,7 @@ Build-Depends: debhelper (>= 11), sphinx-doc, python3-sphinx, dh-linktree, dh-ap ,libvncserver-dev ,libfmt-dev ,libjwt-gnutls-dev|libjwt-dev + ,libgsoap-dev Standards-Version: 4.5.0 Homepage: https://www.zoneminder.com/ @@ -44,7 +44,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,libswscale5|libswscale4 ,libswresample3|libswresample2 ,ffmpeg - ,libdate-manip-perl, libmime-lite-perl, libmime-tools-perl + ,libcurl4, libcurl4-gnutls-dev + ,libdatetime-perl, libdate-manip-perl, libmime-lite-perl, libmime-tools-perl ,libdbd-mysql-perl ,libphp-serialization-perl ,libmodule-load-conditional-perl @@ -72,12 +73,12 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,policykit-1 ,rsyslog | system-log-daemon ,zip - ,libpcre3 ,libcrypt-eksblowfish-perl ,libdata-entropy-perl ,libvncclient1|libvncclient0 ,libfmt ,libjwt-gnutls0|libjwt0 + ,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 diff --git a/distros/ubuntu2004/rules b/distros/ubuntu2004/rules index c137a9da2..af75a409a 100755 --- a/distros/ubuntu2004/rules +++ b/distros/ubuntu2004/rules @@ -19,6 +19,7 @@ override_dh_auto_configure: -DCMAKE_VERBOSE_MAKEFILE=ON \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_MAN=0 \ + -DZM_NO_PCRE=ON \ -DZM_CONFIG_DIR="/etc/zm" \ -DZM_CONFIG_SUBDIR="/etc/zm/conf.d" \ -DZM_RUNDIR="/run/zm" \ diff --git a/distros/ubuntu2004/zoneminder.service b/distros/ubuntu2004/zoneminder.service index cb2d6791e..6645fea04 100644 --- a/distros/ubuntu2004/zoneminder.service +++ b/distros/ubuntu2004/zoneminder.service @@ -5,7 +5,8 @@ Description=ZoneMinder CCTV recording and surveillance system After=network.target mysql.service # Remarked out so that it will start ZM on machines that don't have mysql installed -#Requires=mysql.service +# Override it by placing an override.conf in /etc/systemd/system/zoneminder.service.d +#BindsTo=mysql.service [Service] #User=www-data diff --git a/docs/installationguide/debian.rst b/docs/installationguide/debian.rst index f7325fe1f..a5162d185 100644 --- a/docs/installationguide/debian.rst +++ b/docs/installationguide/debian.rst @@ -4,7 +4,7 @@ Debian .. contents:: Easy Way: Debian 11 (Bullseye) ------------------------- +------------------------------ This procedure will guide you through the installation of ZoneMinder on Debian 11 (Bullseye). @@ -35,7 +35,7 @@ Run the following commands. sudo apt install mariadb-server sudo apt install zoneminder -When mariadb is installed for the first time, it doesn't add a password to the root user. Therefore, for security, it is recommended to run ``mysql secure installation``. +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 @@ -104,7 +104,7 @@ Add the following to the /etc/apt/sources.list.d/zoneminder.list file You can do this using: -.. code-block:: +:: echo "deb https://zmrepo.zoneminder.com/debian/release-1.36 buster/" | sudo tee /etc/apt/sources.list.d/zoneminder.list @@ -337,3 +337,6 @@ Reload Apache to enable your changes and then start ZoneMinder. sudo systemctl start zoneminder You are now ready to go with ZoneMinder. Open a browser and type either ``localhost/zm`` one the local machine or ``{IP-OF-ZM-SERVER}/zm`` if you connect from a remote computer. + +.. _unix socket authentication: https://mariadb.com/kb/en/authentication-plugin-unix-socket/ +.. _mariadb-secure-installation: https://mariadb.com/kb/en/mysql_secure_installation/ diff --git a/misc/CMakeLists.txt b/misc/CMakeLists.txt index e51636771..c705b63bb 100644 --- a/misc/CMakeLists.txt +++ b/misc/CMakeLists.txt @@ -7,6 +7,8 @@ configure_file(logrotate.conf.in "${CMAKE_CURRENT_BINARY_DIR}/logrotate.conf" @O configure_file(syslog.conf.in "${CMAKE_CURRENT_BINARY_DIR}/syslog.conf" @ONLY) configure_file(com.zoneminder.systemctl.policy.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.policy" @ONLY) configure_file(com.zoneminder.systemctl.rules.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.rules" @ONLY) +configure_file(com.zoneminder.arp-scan.policy.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.policy" @ONLY) +configure_file(com.zoneminder.arp-scan.rules.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.rules" @ONLY) configure_file(zoneminder.service.in "${CMAKE_CURRENT_BINARY_DIR}/zoneminder.service" @ONLY) configure_file(zoneminder-tmpfiles.conf.in "${CMAKE_CURRENT_BINARY_DIR}/zoneminder-tmpfiles.conf" @ONLY) configure_file(zoneminder.desktop.in "${CMAKE_CURRENT_BINARY_DIR}/zoneminder.desktop" @ONLY) @@ -19,6 +21,8 @@ configure_file(zm-sudo.in "${CMAKE_CURRENT_BINARY_DIR}/zm-sudo" @ONLY) if(WITH_SYSTEMD) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.policy" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/actions") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.rules" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/rules.d") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.policy" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/actions") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.arp-scan.rules" DESTINATION "${PC_POLKIT_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/polkit-1/rules.d") endif() install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zoneminder.desktop" DESTINATION "${CMAKE_INSTALL_DATADIR}/applications") diff --git a/misc/com.zoneminder.arp-scan.policy.in b/misc/com.zoneminder.arp-scan.policy.in new file mode 100644 index 000000000..7b5b6043e --- /dev/null +++ b/misc/com.zoneminder.arp-scan.policy.in @@ -0,0 +1,21 @@ + + + + + The ZoneMinder Project + http://www.zoneminder.com/ + + + Allow the ZoneMinder webuser to run arp-scan + The ZoneMinder webuser is trusted to run arp-scan + + yes + yes + yes + + /usr/sbin/arp-scan + + + diff --git a/misc/com.zoneminder.arp-scan.rules.in b/misc/com.zoneminder.arp-scan.rules.in new file mode 100644 index 000000000..74ac25af0 --- /dev/null +++ b/misc/com.zoneminder.arp-scan.rules.in @@ -0,0 +1,7 @@ +polkit.addRule(function(action, subject) { + if (action.id == "com.zoneminder.policykit.pkexec.run-arp-scan" && + subject.user != "@WEB_USER@") { + return polkit.Result.NO; + } + +}); diff --git a/misc/janus.jcfg b/misc/janus.jcfg new file mode 100644 index 000000000..a0adffb88 --- /dev/null +++ b/misc/janus.jcfg @@ -0,0 +1,50 @@ +general: { + configs_folder = "/usr/local/etc/janus" # Configuration files folder + plugins_folder = "/usr/local/lib/janus/plugins" # Plugins folder + transports_folder = "/usr/local/lib/janus/transports" # Transports folder + events_folder = "/usr/local/lib/janus/events" # Event handlers folder + loggers_folder = "/usr/local/lib/janus/loggers" # External loggers folder + debug_level = 4 # Debug/logging level, valid values are 0-7 + admin_secret = "janusoverlord" # String that all Janus requests must contain + protected_folders = [ + "/bin", + "/boot", + "/dev", + "/etc", + "/initrd", + "/lib", + "/lib32", + "/lib64", + "/proc", + "/sbin", + "/sys", + "/usr", + "/var", + "/opt/janus/bin", + "/opt/janus/etc", + "/opt/janus/include", + "/opt/janus/lib", + "/opt/janus/lib32", + "/opt/janus/lib64", + "/opt/janus/sbin" + ] +} +media: { + #ipv6 = true + #ipv6_linklocal = true + rtp_port_range = "20000-40000" +} +nat: { + nice_debug = false + ignore_mdns = true + keep_private_host = true + ice_ignore_list = "vmnet" +} + +plugins: { + disable = "libjanus_audiobridge.so,libjanus_echotest.so,libjanus_recordplay.so,libjanus_sip.so,libjanus_textroom.so,libjanus_videocall.so,libjanus_videoroom.so,libjanus_voicemail.so, + libjanus_nosip.so" +} +transports: { + disable = "libjanus_rabbitmq.so, libjanus_pfunix.so,libjanus_websockets.so" +} diff --git a/misc/janus.plugin.streaming.jcfg b/misc/janus.plugin.streaming.jcfg new file mode 100644 index 000000000..f93b15a3e --- /dev/null +++ b/misc/janus.plugin.streaming.jcfg @@ -0,0 +1,4 @@ +general: { + admin_key = "supersecret" + rtp_port_range = "20000-40000" +} diff --git a/misc/janus.transport.http.jcfg b/misc/janus.transport.http.jcfg new file mode 100644 index 000000000..8ae9171ad --- /dev/null +++ b/misc/janus.transport.http.jcfg @@ -0,0 +1,25 @@ +general: { + json = "indented" # Whether the JSON messages should be indented (default), + base_path = "/janus" # Base path to bind to in the web server (plain HTTP only) + http = true # Whether to enable the plain HTTP interface + port = 8088 # Web server HTTP port + https = false # Whether to enable HTTPS (default=false) +} + +# Janus can also expose an admin/monitor endpoint, to allow you to check +# which sessions are up, which handles they're managing, their current +# status and so on. This provides a useful aid when debugging potential +# issues in Janus. The configuration is pretty much the same as the one +# already presented above for the webserver stuff, as the API is very +# similar: choose the base bath for the admin/monitor endpoint (/admin +# by default), ports, etc. Besides, you can specify +# a secret that must be provided in all requests as a crude form of +# authorization mechanism, and partial or full source IPs if you want to +# limit access basing on IP addresses. For security reasons, this +# endpoint is disabled by default, enable it by setting admin_http=true. +admin: { + admin_base_path = "/admin" # Base path to bind to in the admin/monitor web server (plain HTTP only) + admin_http = false # Whether to enable the plain HTTP interface + admin_port = 7088 # Admin/monitor web server HTTP port + admin_https = false # Whether to enable HTTPS (default=false) +} diff --git a/misc/zoneminder.service.in b/misc/zoneminder.service.in index d1cfb36a0..21be2e433 100644 --- a/misc/zoneminder.service.in +++ b/misc/zoneminder.service.in @@ -3,8 +3,8 @@ [Unit] Description=ZoneMinder CCTV recording and security system -After=network.target mysqld.service httpd.service -Requires=mysqld.service httpd.service +After=network.target mysqld.service httpd.service janus.service +Requires=mysqld.service httpd.service janus.service [Service] User=@WEB_USER@ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in index 21f9a2d02..849ec5450 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in @@ -156,7 +156,7 @@ sub loadConfigFromDB { print("Error: unable to load options from database: $DBI::errstr\n"); return(0); } - my $sql = "select * from Config"; + my $sql = 'SELECT * FROM Config'; my $sth = $dbh->prepare_cached( $sql ) or croak( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute() @@ -203,7 +203,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 = ?, Requires = ?"; + $sql = "replace into Config set Id = ?, Name = ?, Value = ?, Type = ?, DefaultValue = ?, Hint = ?, Pattern = ?, Format = ?, Prompt = ?, Help = ?, Category = ?, Readonly = ?, Private = ?, Requires = ?"; my $sth = $dbh->prepare_cached( $sql ) or croak( "Can't prepare '$sql': ".$dbh->errstr() ); foreach my $option ( @options ) { @@ -240,6 +240,7 @@ sub saveConfigToDB { $option->{help}, $option->{category}, $option->{readonly} ? 1 : 0, + $option->{private} ? 1 : 0, $option->{db_requires} ) or croak("Can't execute when updating config entry $$option{name}: ".$sth->errstr() ); } # end foreach option diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index 48d494614..dc50e8d64 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -304,6 +304,7 @@ our @options = ( { name=>'ZM_AUTH_RELAY', value=>'hashed' } ], type => $types{string}, + private => 1, category => 'system', }, { @@ -370,6 +371,28 @@ our @options = ( type => $types{boolean}, category => 'system', }, + { + name => 'ZM_JANUS_SECRET', + default => '', + description => 'Password for Janus streaming administration.', + help => q`This value should be set to a secure password, + and match the admin_key value in janus.plugin.streaming.config. + `, + type => $types{string}, + category => 'system', + }, + { + name => 'ZM_JANUS_PATH', + default => '', + description => 'URL for Janus HTTP/S port', + help => q`Janus requires HTTP/S communication to administer + and initiate h.264 streams. If left blank, this will default to + the ZM hostname, port 8088/janus. This setting is particularly + useful for putting janus behind a reverse proxy. + `, + type => $types{string}, + category => 'system', + }, { name => 'ZM_ENABLE_CSRF_MAGIC', default => 'yes', @@ -462,6 +485,7 @@ our @options = ( {name=>'ZM_OPT_USE_GOOG_RECAPTCHA', value=>'yes'} ], type => $types{string}, + private => 1, category => 'system', }, { @@ -477,6 +501,7 @@ our @options = ( {name=>'ZM_OPT_USE_GOOG_RECAPTCHA', value=>'yes'} ], type => $types{string}, + private => 1, category => 'system', }, { diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/AxisV2.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/AxisV2.pm index 64e284278..e50449927 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/AxisV2.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/AxisV2.pm @@ -51,11 +51,21 @@ sub open { my $self = shift; $self->loadMonitor(); - if ( $self->{Monitor}->{ControlAddress} !~ /^\w+:\/\// ) { - # Has no scheme at the beginning, so won't parse as a URI - $self->{Monitor}->{ControlAddress} = 'http://'.$self->{Monitor}->{ControlAddress}; - } - $uri = URI->new($self->{Monitor}->{ControlAddress}); + + if ($self->{Monitor}->{ControlAddress} and ($self->{Monitor}->{ControlAddress} ne 'user:pass@ip')) { + Debug("Getting connection details from Control Address " . $self->{Monitor}->{ControlAddress}); + if ( $self->{Monitor}->{ControlAddress} !~ /^\w+:\/\// ) { + # Has no scheme at the beginning, so won't parse as a URI + $self->{Monitor}->{ControlAddress} = 'http://'.$self->{Monitor}->{ControlAddress}; + } + $uri = URI->new($self->{Monitor}->{ControlAddress}); + } elsif ($self->{Monitor}->{Path}) { + Debug("Getting connection details from Path " . $self->{Monitor}->{Path}); + $uri = URI->new($self->{Monitor}->{Path}); + $uri->scheme('http'); + $uri->port(80); + $uri->path(''); + } use LWP::UserAgent; $self->{ua} = LWP::UserAgent->new; @@ -64,6 +74,7 @@ sub open { $self->{state} = 'closed'; my ( $username, $password, $host ) = ( $uri->authority() =~ /^([^:]+):([^@]*)@(.+)$/ ); + Debug("Have username: $username password: $password host: $host from authority:" . $uri->authority()); $uri->userinfo(undef); @@ -75,40 +86,47 @@ sub open { # test auth my $res = $self->{ua}->get($uri->canonical().$url); - if ( $res->is_success ) { - if ( $res->content() ne "Properties.PTZ.PTZ=yes\n" ) { + if ($res->is_success) { + if ($res->content() ne "Properties.PTZ.PTZ=yes\n") { Warning('Response suggests that camera doesn\'t support PTZ. Content:('.$res->content().')'); } $self->{state} = 'open'; return; } + if ($res->status_line() eq '404 Not Found') { + #older style + $url = 'axis-cgi/com/ptz.cgi'; + $res = $self->{ua}->get($uri->canonical().$url); + Debug("Result from getting ".$uri->canonical().$url . ':' . $res->status_line()); + } - if ( $res->status_line() eq '401 Unauthorized' ) { - + if ($res->status_line() eq '401 Unauthorized') { 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; - $self->{ua}->credentials($uri->host_port(), $realm, $username, $password); - $res = $self->{ua}->get($uri->canonical().$url); - if ( $res->is_success() ) { - Info("Auth succeeded after setting realm to $realm. You can set this value in the Control Device field to speed up connections and remove these log entries."); - $self->{state} = 'open'; - return; + foreach my $auth_header ( ref $$headers{'www-authenticate'} eq 'ARRAY' ? @{$$headers{'www-authenticate'}} : ($$headers{'www-authenticate'})) { + my ( $auth, $tokens ) = $auth_header =~ /^(\w+)\s+(.*)$/; + if ( $tokens =~ /\w+="([^"]+)"/i ) { + if ( $realm ne $1 ) { + $realm = $1; + $self->{ua}->credentials($uri->host_port(), $realm, $username, $password); + $res = $self->{ua}->get($uri->canonical().$url); + if ( $res->is_success() ) { + Info("Auth succeeded after setting realm to $realm. You can set this value in the Control Device field to speed up connections and remove these log entries."); + $self->{state} = 'open'; + return; + } + Error('Authentication still failed after updating REALM status: '.$res->status_line); + } else { + Error('Authentication failed, not a REALM problem'); } - Error('Authentication still failed after updating REALM status: '.$res->status_line); } else { - Error('Authentication failed, not a REALM problem'); - } - } else { - Error('Failed to match realm in tokens'); - } # end if + Error('Failed to match realm in tokens'); + } # end if + } # end foreach auth header } else { Debug('No headers line'); } # end if headers diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm index f10b7aabb..8043eedad 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/Netcat.pm @@ -194,7 +194,6 @@ sub getCamParams { } } -#autoStop #This makes use of the ZoneMinder Auto Stop Timeout on the Control Tab sub autoStop { my $self = shift; @@ -202,13 +201,19 @@ sub autoStop { if ( $autostop ) { Debug('Auto Stop'); - my $cmd = 'onvif/PTZ'; - my $msg = '' . ((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '') . '' . $profileToken . 'truefalse'; - my $content_type = 'application/soap+xml; charset=utf-8; action="http://www.onvif.org/ver20/ptz/wsdl/ContinuousMove"'; usleep($autostop); + + my $cmd = 'onvif/PTZ'; + my $content_type = 'application/soap+xml; charset=utf-8; action="http://www.onvif.org/ver20/ptz/wsdl/ContinuousMove"'; + + my $msg =''.((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '').'' . $profileToken . ''; + $self->sendCmd($cmd, $msg, $content_type); + + # Reported to not work, so superceded by the cmd above + $msg = '' . ((%identity) ? authentificationHeader($identity{username}, $identity{password}) : '') . '' . $profileToken . 'truefalse'; $self->sendCmd($cmd, $msg, $content_type); } -} +} # end sub autoStop # Reset the Camera sub reset { diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/Trendnet.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/Trendnet.pm index 8bdf1db42..cc437974f 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/Trendnet.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/Trendnet.pm @@ -374,6 +374,25 @@ sub reset { $self->sendCmdPost($url,$cmd); } +sub reboot { + my $self = shift; + Debug('Camera Reboot'); + $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'; +} + +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__ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Database.pm b/scripts/ZoneMinder/lib/ZoneMinder/Database.pm index 35d2dc6fa..ae1259814 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Database.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Database.pm @@ -273,6 +273,9 @@ sub zmDbDo { if ( ! defined $rows ) { $sql =~ s/\?/'%s'/; Error(sprintf("Failed $sql :", @_).$dbh->errstr()); + } elsif ( ZoneMinder::Logger::logLevel() > INFO ) { + $sql =~ s/\?/'%s'/; + Debug(sprintf("Succeeded $sql : $rows rows affected", @_)); } return $rows; } diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm index 0276af097..026ff6cea 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm @@ -43,6 +43,7 @@ require Date::Parse; require POSIX; use Date::Format qw(time2str); use Time::HiRes qw(gettimeofday tv_interval stat); +use Scalar::Util qw(looks_like_number); #our @ISA = qw(ZoneMinder::Object); use parent qw(ZoneMinder::Object); @@ -601,7 +602,7 @@ sub CopyTo { # First determine if we can move it to the dest. # We do this before bothering to lock the event my ( $NewPath ) = ( $NewStorage->Path() =~ /^(.*)$/ ); # De-taint - if ( ! $$NewStorage{Id} ) { + if ( ! looks_like_number($$NewStorage{Id}) ) { return 'New storage does not have an id. Moving will not happen.'; } elsif ( $$NewStorage{Id} == $$self{StorageId} ) { return 'Event is already located at ' . $NewPath; @@ -733,19 +734,22 @@ sub MoveTo { my $was_in_transaction = !$ZoneMinder::Database::dbh->{AutoCommit}; $ZoneMinder::Database::dbh->begin_work() if !$was_in_transaction; - $self->lock_and_load(); # The fact that we are in a transaction might not imply locking + if (!$self->lock_and_load()) { + Warning('Unable to lock event record '.$$self{Id}); # The fact that we are in a transaction might not imply locking + $ZoneMinder::Database::dbh->commit() if !$was_in_transaction; + return 'Unable to lock event record'; + } my $OldStorage = $self->Storage(undef); - my $error = $self->CopyTo($NewStorage); - return $error if $error; + if (!$error) { + # Succeeded in copying all files, so we may now update the Event. + $$self{StorageId} = $$NewStorage{Id}; + $self->Storage($NewStorage); + $error .= $self->save(); - # Succeeded in copying all files, so we may now update the Event. - $$self{StorageId} = $$NewStorage{Id}; - $self->Storage($NewStorage); - $error .= $self->save(); - - # Going to leave it to upper layer as to whether we rollback or not + # Going to leave it to upper layer as to whether we rollback or not + } $ZoneMinder::Database::dbh->commit() if !$was_in_transaction; return $error if $error; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index 3a3308938..0a10baa3b 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm @@ -56,6 +56,7 @@ $primary_key = 'Id'; %fields = map { $_ => $_ } qw( Id Name +ExecuteInterval Query_json AutoArchive AutoUnarchive @@ -106,7 +107,6 @@ sub Execute { $sql =~ s/zmSystemLoad/$load/g; } - $sql .= ' FOR UPDATE' if $$self{LockRows}; Debug("Filter::Execute SQL ($sql)"); my $sth = $ZoneMinder::Database::dbh->prepare_cached($sql) @@ -230,8 +230,8 @@ sub Sql { # PostCondition, so no further SQL } else { ( my $stripped_value = $value ) =~ s/^["\']+?(.+)["\']+?$/$1/; - foreach my $temp_value ( split( /["'\s]*?,["'\s]*?/, $stripped_value ) ) { - + # Empty value will result in () from split + foreach my $temp_value ( $stripped_value ne '' ? split( /["'\s]*?,["'\s]*?/, $stripped_value ) : $stripped_value ) { if ( $term->{attr} eq 'AlarmedZoneId' ) { $value = '(SELECT * FROM Stats WHERE EventId=E.Id AND Score > 0 AND ZoneId='.$value.')'; } elsif ( $term->{attr} =~ /^MonitorName/ ) { @@ -250,7 +250,8 @@ sub Sql { $$self{Server} = new ZoneMinder::Server($temp_value); } } elsif ( $term->{attr} eq 'StorageId' ) { - $value = "'$temp_value'"; + # Empty means NULL, otherwise must be an integer + $value = $temp_value ne '' ? int($temp_value) : 'NULL'; $$self{Storage} = new ZoneMinder::Storage($temp_value); } elsif ( $term->{attr} eq 'Name' || $term->{attr} eq 'Cause' @@ -322,7 +323,7 @@ sub Sql { $self->{Sql} .= ' IS NOT '.$value; } elsif ( $term->{op} eq '=[]' or $term->{op} eq 'IN' ) { $self->{Sql} .= ' IN ('.join(',', @value_list).")"; - } elsif ( $term->{op} eq '![]' ) { + } elsif ( $term->{op} eq '![]' or $term->{op} eq 'NOT IN') { $self->{Sql} .= ' NOT IN ('.join(',', @value_list).')'; } elsif ( $term->{op} eq 'LIKE' ) { $self->{Sql} .= ' LIKE '.$value; @@ -370,10 +371,7 @@ sub Sql { if ( @auto_terms ) { $sql .= ' AND ( '.join(' or ', @auto_terms).' )'; } - if ( !$filter_expr->{sort_field} ) { - $filter_expr->{sort_field} = 'StartDateTime'; - $filter_expr->{sort_asc} = 0; - } + my $sort_column = ''; if ( $filter_expr->{sort_field} eq 'Id' ) { $sort_column = 'E.Id'; @@ -405,14 +403,21 @@ sub Sql { $sort_column = 'E.MaxScore'; } elsif ( $filter_expr->{sort_field} eq 'DiskSpace' ) { $sort_column = 'E.DiskSpace'; - } else { - $sort_column = 'E.StartDateTime'; + } elsif ( $filter_expr->{sort_field} ne '' ) { + $sort_column = 'E.'.$filter_expr->{sort_field}; } - my $sort_order = $filter_expr->{sort_asc} ? 'ASC' : 'DESC'; - $sql .= ' ORDER BY '.$sort_column.' '.$sort_order; - if ( $filter_expr->{limit} ) { + if ( $sort_column ne '' ) { + $sql .= ' ORDER BY '.$sort_column.' '.($filter_expr->{sort_asc} ? 'ASC' : 'DESC'); + } + if ($filter_expr->{limit}) { $sql .= ' LIMIT 0,'.$filter_expr->{limit}; } + if ($$self{LockRows}) { + $sql .= ' FOR UPDATE'; + if ($filter_expr->{skip_locked}) { + $sql .= ' SKIP LOCKED'; + } + } $self->{Sql} = $sql; } # end if has Sql return $self->{Sql}; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/General.pm b/scripts/ZoneMinder/lib/ZoneMinder/General.pm index b14b08aae..90e7399ce 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/General.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/General.pm @@ -28,6 +28,7 @@ our %EXPORT_TAGS = ( makePath jsonEncode jsonDecode + jsonLoad systemStatus packageControl daemonControl @@ -536,6 +537,23 @@ sub jsonDecode { return $result; } +sub jsonLoad { + my $file = shift; + my $json = undef; + eval { + require File::Slurp; + my $contents = File::Slurp::read_file($file); + if (!$contents) { + Error("No contents for $file"); + return $json; + } + require JSON; + $json = JSON::decode_json($contents); + }; + Error($@) if $@; + return $json; +} + sub parseNameEqualsValueToHash { my %settings; foreach my $line ( split ( /\r?\n/, $_[0] ) ) { diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm index 43ae38e72..03bc84cd3 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm @@ -638,6 +638,7 @@ sub logInit( ;@ ) { $logger = ZoneMinder::Logger->new() if !$logger; $logger->initialise(%options); logSetSignal(); + return $logger; } sub logReinit { diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in index e7fdcd35f..e0c9bf971 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in @@ -42,6 +42,7 @@ our @ISA = qw(Exporter ZoneMinder::Base); # will save memory. our %EXPORT_TAGS = ( constants => [ qw( + STATE_UNKNOWN STATE_IDLE STATE_PREALARM STATE_ALARM @@ -98,11 +99,12 @@ our $VERSION = $ZoneMinder::Base::VERSION; use ZoneMinder::Config qw(:all); use ZoneMinder::Logger qw(:all); -use constant STATE_IDLE => 0; -use constant STATE_PREALARM => 1; -use constant STATE_ALARM => 2; -use constant STATE_ALERT => 3; -use constant STATE_TAPE => 4; +use constant STATE_UNKNOWN => 0; +use constant STATE_IDLE => 1; +use constant STATE_PREALARM => 2; +use constant STATE_ALARM => 3; +use constant STATE_ALERT => 4; +use constant STATE_TAPE => 5; use constant ACTION_GET => 1; use constant ACTION_SET => 2; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index c5e09c137..10cfffe8b 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -56,6 +56,14 @@ $serial = $primary_key = 'Id'; Enabled LinkedMonitors Triggers + EventStartCommand + EventEndCommand + ONVIF_URL + ONVIF_Username + ONVIF_Password + ONVIF_Options + ONVIF_Event_Listener + use_Amcrest_API Device Channel Format @@ -133,6 +141,9 @@ $serial = $primary_key = 'Id'; DefaultCodec Latitude Longitude + RTSPServer + RTSPStreamName + Importance ); %defaults = ( @@ -241,20 +252,26 @@ sub control { my $command = shift; my $process = shift; - if ( $command eq 'stop' or $command eq 'restart' ) { - if ( $process ) { - `/usr/bin/zmdc.pl stop $process -m $$monitor{Id}`; + if ($command eq 'stop' or $command eq 'restart') { + if ($process) { + ZoneMinder::General::runCommand("zmdc.pl stop $process -m $$monitor{Id}"); } else { - `/usr/bin/zmdc.pl stop zma -m $$monitor{Id}`; - `/usr/bin/zmdc.pl stop zmc -m $$monitor{Id}`; + if ($monitor->{Type} eq 'Local') { + ZoneMinder::General::runCommand('zmdc.pl stop zmc -d '.$monitor->{Device}); + } else { + ZoneMinder::General::runCommand('zmdc.pl stop zmc -m '.$monitor->{Id}); + } } } if ( $command eq 'start' or $command eq 'restart' ) { if ( $process ) { - `/usr/bin/zmdc.pl start $process -m $$monitor{Id}`; + ZoneMinder::General::runCommand("zmdc.pl start $process -m $$monitor{Id}"); } else { - `/usr/bin/zmdc.pl start zmc -m $$monitor{Id}`; - `/usr/bin/zmdc.pl start zma -m $$monitor{Id}`; + if ($monitor->{Type} eq 'Local') { + ZoneMinder::General::runCommand('zmdc.pl start zmc -d '.$monitor->{Device}); + } else { + ZoneMinder::General::runCommand('zmdc.pl start zmc -m '.$monitor->{Id}); + } } # end if } } # end sub control @@ -326,18 +343,30 @@ sub resumeMotionDetection { sub Control { my $self = shift; - if ( ! exists $$self{Control}) { - require ZoneMinder::Control; - my $Control = ZoneMinder::Control->find_one(Id=>$$self{ControlId}); - if ($Control) { - require Module::Load::Conditional; - if (!Module::Load::Conditional::can_load(modules => {'ZoneMinder::Control::'.$$Control{Protocol} => undef})) { - Error("Can't load ZoneMinder::Control::$$Control{Protocol}\n$Module::Load::Conditional::ERROR"); - return undef; + if (!exists $$self{Control}) { + if ($$self{ControlId}) { + require ZoneMinder::Control; + my $Control = ZoneMinder::Control->find_one(Id=>$$self{ControlId}); + if ($Control) { + my $Protocol = $$Control{Protocol}; + + if (!$Protocol) { + Error("No protocol set in control $$Control{Id}, trying Name $$Control{Name}"); + $Protocol = $$Control{Name}; + } + require Module::Load::Conditional; + if (!Module::Load::Conditional::can_load(modules => {'ZoneMinder::Control::'.$Protocol => undef})) { + Error("Can't load ZoneMinder::Control::$Protocol\n$Module::Load::Conditional::ERROR"); + return undef; + } + bless $Control, 'ZoneMinder::Control::'.$Protocol; + $$Control{MonitorId} = $$self{Id}; + $$self{Control} = $Control; + } else { + Error("Unable to load control for control $$self{ControlId} for monitor $$self{Id}"); } - bless $Control, 'ZoneMinder::Control::'.$$Control{Protocol}; - $$Control{MonitorId} = $$self{Id}; - $$self{Control} = $Control; + } else { + Info("No ControlId set in monitor $$self{Id}") } } return $$self{Control}; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm index e54bb15ce..d64ee7e8a 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm @@ -639,9 +639,9 @@ $log->debug("Have array for $k $$search{$k}") if DEBUG_ALL; if ( ! ( $db_field =~ /\?/ ) ) { if ( @{$$search{$k}} != 1 ) { - push @where, $db_field .' IN ('.join(',', map {'?'} @{$$search{$k}} ) . ')'; + push @where, '`'.$db_field .'` IN ('.join(',', map {'?'} @{$$search{$k}} ) . ')'; } else { - push @where, $db_field.'=?'; + push @where, '`'.$db_field.'`=?'; } # end if } else { $log->debug("Have question ? for $k $$search{$k} $db_field") if DEBUG_ALL; @@ -656,10 +656,10 @@ $log->debug("Have question ? for $k $$search{$k} $db_field") if DEBUG_ALL; foreach my $p_k ( keys %{$$search{$k}} ) { my $v = $$search{$k}{$p_k}; if ( ref $v eq 'ARRAY' ) { - push @where, $db_field.' IN ('.join(',', map {'?'} @{$v} ) . ')'; + push @where, '`'.$db_field.'` IN ('.join(',', map {'?'} @{$v} ) . ')'; push @values, $p_k, @{$v}; } else { - push @where, $db_field.'=?'; + push @where, '`'.$db_field.'`=?'; push @values, $p_k, $v; } # end if } # end foreach p_k @@ -667,7 +667,7 @@ $log->debug("Have question ? for $k $$search{$k} $db_field") if DEBUG_ALL; push @where, $db_field.' IS NULL'; } else { if ( ! ( $db_field =~ /\?/ ) ) { - push @where, $db_field .'=?'; + push @where, '`'.$db_field .'`=?'; } else { push @where, $db_field; } diff --git a/scripts/zmdc.pl.in b/scripts/zmdc.pl.in index 5cf866e56..793049479 100644 --- a/scripts/zmdc.pl.in +++ b/scripts/zmdc.pl.in @@ -429,10 +429,20 @@ sub start { # It's not running, or at least it's not been started by us $process = { daemon=>$daemon, args=>\@args, command=>$command, keepalive=>!undef }; } elsif ( $process->{pid} && $pid_hash{$process->{pid}} ) { - dPrint(ZoneMinder::Logger::INFO, "'$process->{command}' already running at " + if ($process->{term_sent_at}) { + dPrint(ZoneMinder::Logger::INFO, "'$process->{command}' was told to term at " + .strftime('%y/%m/%d %H:%M:%S', localtime($process->{term_sent_at})) + .", pid = $process->{pid}\n" + ); + $process->{keepalive} = !undef; + $process->{delay} = 0; + delete $terminating_processes{$command}; + } else { + dPrint(ZoneMinder::Logger::INFO, "'$process->{command}' already running at " .strftime('%y/%m/%d %H:%M:%S', localtime($process->{started})) .", pid = $process->{pid}\n" - ); + ); + } return; } @@ -523,7 +533,7 @@ sub send_stop { ."\n" ); sigprocmask(SIG_UNBLOCK, $blockset) or die "dying at unblock...\n"; - return(); + return (); } my $pid = $process->{pid}; @@ -586,7 +596,7 @@ sub check_for_processes_to_kill { sub stop { my ( $daemon, @args ) = @_; - my $command = join(' ', $daemon, @args ); + my $command = join(' ', $daemon, @args); my $process = $cmd_hash{$command}; if ( !$process ) { dPrint(ZoneMinder::Logger::WARNING, "Can't find process with command of '$command'"); diff --git a/scripts/zmfilter.pl.in b/scripts/zmfilter.pl.in index 61922771b..3fb286039 100644 --- a/scripts/zmfilter.pl.in +++ b/scripts/zmfilter.pl.in @@ -21,28 +21,6 @@ # # ========================================================================== -=head1 NAME - -zmfilter.pl - ZoneMinder tool to filter events - -=head1 SYNOPSIS - -zmfilter.pl [-f ,--filter=] [--filter_id=] | -v, --version - -=head1 DESCRIPTION - -This script continuously monitors the recorded events for the given -monitor and applies any filters which would delete and/or upload -matching events. - -=head1 OPTIONS - - --f{filter name}, --filter={filter name} - The name of a specific filter to run ---filter_id={filter id} - The id of a specific filter to run --v, --version - Print ZoneMinder version - -=cut use strict; use bytes; @@ -160,10 +138,9 @@ $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 $delay = $Config{ZM_FILTER_EXECUTE_INTERVAL}; my $event_id = 0; -if ( !EVENT_PATH ) { +if (!EVENT_PATH) { Error('No event path defined. Config was '.$Config{ZM_DIR_EVENTS}); die; } @@ -195,26 +172,37 @@ if ( ! ( $filter_name or $filter_id ) ) { my @filters; my $last_action = 0; -while( !$zm_terminate ) { +while (!$zm_terminate) { + my $delay = $Config{ZM_FILTER_EXECUTE_INTERVAL}; my $now = time; - if ( ($now - $last_action) > $Config{ZM_FILTER_RELOAD_DELAY} ) { + if (($now - $last_action) > $Config{ZM_FILTER_RELOAD_DELAY}) { Debug('Reloading filters'); $last_action = $now; @filters = getFilters({ Name=>$filter_name, Id=>$filter_id }); } - foreach my $filter ( @filters ) { + foreach my $filter (@filters) { last if $zm_terminate; - if ( $$filter{Concurrent} and ! ( $filter_id or $filter_name ) ) { + + 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; + } + + if ($$filter{Concurrent} and !($filter_id or $filter_name)) { my ( $proc ) = $0 =~ /(\S+)/; my ( $id ) = $$filter{Id} =~ /(\d+)/; Debug("Running concurrent filter process $proc --filter_id $$filter{Id} => $id for $$filter{Name}"); - system(qq`$proc --filter "$$filter{Name}" &`); + system(qq`$proc --filter_id $id &`); } else { checkFilter($filter); + $$filter{last_ran} = $now; } - } + } # end foreach filter last if (!$daemon and ($filter_name or $filter_id)) or $zm_terminate; @@ -384,11 +372,6 @@ sub checkFilter { } # end if AutoCopy if ( $filter->{UpdateDiskSpace} ) { - if ( $$filter{LockRows} ) { - $ZoneMinder::Database::dbh->begin_work(); - $Event->lock_and_load(); - } - my $old_diskspace = $$Event{DiskSpace}; my $new_diskspace = $Event->DiskSpace(undef); @@ -481,15 +464,18 @@ sub generateImage { my $event_path = $Event->Path(); my $capture_image_path = sprintf('%s/%0'.$Config{ZM_EVENT_IMAGE_DIGITS}.'d-capture.jpg', $event_path, $frame->{FrameId}); my $analyse_image_path = sprintf('%s/%0'.$Config{ZM_EVENT_IMAGE_DIGITS}.'d-analyse.jpg', $event_path, $frame->{FrameId}) if $analyse; - my $video_path = sprintf('%s/%d-video.mp4', $event_path, $Event->{Id}); + my $video_path = sprintf('%s/%s', $event_path, $Event->{DefaultVideo}); my $image_path = ''; # check if the image file exists. If the file doesn't exist and we use H264 try to extract it from .mp4 video if ( $analyse && -r $analyse_image_path ) { + Debug("Using analysis and jpeg exists $analyse_image_path"); $image_path = $analyse_image_path; } elsif ( -r $capture_image_path ) { + Debug("Using captures and jpeg exists $capture_image_path"); $image_path = $capture_image_path; } elsif ( -r $video_path ) { + Debug("mp4 exists $video_path"); my $command ="ffmpeg -nostdin -ss $$frame{Delta} -i '$video_path' -frames:v 1 '$capture_image_path'"; #$command = "ffmpeg -y -v 0 -i $video_path -vf 'select=gte(n\\,$$frame{FrameId}),setpts=PTS-STARTPTS' -vframes 1 -f image2 $capture_image_path"; my $output = qx($command); @@ -503,6 +489,8 @@ sub generateImage { } else { $image_path = $capture_image_path; } + } else { + Debug("No files found at $analyse_image_path, $capture_image_path or $video_path"); } return $image_path; } @@ -740,7 +728,7 @@ sub substituteTags { if ( -e $path ) { push @$attachments_ref, { type=>'image/jpeg', path=>$path }; } else { - Warning("Path to first image does not exist at $path"); + Warning("Path to first image does not exist at $path for image $first_alarm_frame"); } } @@ -1051,9 +1039,7 @@ sub executeCommand { my $filter = shift; my $Event = shift; - my $event_path = $Event->Path(); - - my $command = $filter->{AutoExecuteCmd}.' '.$event_path; + my $command = $filter->{AutoExecuteCmd}.' '.$Event->Path(); $command = substituteTags($command, $filter, $Event); Info("Executing '$command'"); @@ -1063,15 +1049,37 @@ sub executeCommand { chomp($output); Debug("Output: $output"); } - if ( $status ) { + if ($status) { Error("Command '$command' exited with status: $status"); return 0; } else { - my $sql = 'UPDATE `Events` SET `Executed` = 1 WHERE `Id` = ?'; - my $sth = $dbh->prepare_cached($sql) - or Fatal("Unable to prepare '$sql': ".$dbh->errstr()); - my $res = $sth->execute( $Event->{Id} ) - or Fatal("Unable to execute '$sql': ".$dbh->errstr()); + ZoneMinder::Database::zmSQLExecute('UPDATE `Events` SET `Executed` = 1 WHERE `Id` = ?', $Event->{Id}); } - return( 1 ); + return 1; } + +1; +__END__ + +=head1 NAME + +zmfilter.pl - ZoneMinder tool to select events and perform actions on them + +=head1 SYNOPSIS + +zmfilter.pl [-f ,--filter=] [--filter_id=] [--daemon] | -v, --version + +=head1 DESCRIPTION + +This script performs a specified database query to select recorded events and performs specified actions on them +such as email reporting, deleting, moving, etc. If the --daemon option is given it will remain resident, repeating +the query and applying actions. This is normally managed by zmdc.pl however it can be used manually as well. + +=head1 OPTIONS + + -f{filter name}, --filter={filter name} - The name of a specific filter to run + --filter_id={filter id} - The id of a specific filter to run + --daemon - Causes zmfilter.pl to stay running endlessly repeating the filter(s). + -v, --version - Print ZoneMinder version + +=cut diff --git a/scripts/zmtelemetry.pl.in b/scripts/zmtelemetry.pl.in index a3debabdb..c164df7e4 100644 --- a/scripts/zmtelemetry.pl.in +++ b/scripts/zmtelemetry.pl.in @@ -263,7 +263,10 @@ sub countQuery { sub getMonitorRef { my $dbh = shift; - my $sql = 'SELECT `Id`,`Name`,`Type`,`Function`,`Width`,`Height`,`Colours`,`MaxFPS`,`AlarmMaxFPS` FROM `Monitors`'; + my $sql = 'SELECT `Id`,`Name`,`Type`,`Function`,`Width`,`Height`,`Colours`,`MaxFPS`,`AlarmMaxFPS`, + (SELECT Name FROM Manufacturers WHERE Manufacturers.Id = ManufacturerId), + (SELECT Name FROM Models WHERE Models.Id = ModelId) + FROM `Monitors`'; my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute() or die( "Can't execute: ".$sth->errstr() ); my $arrayref = $sth->fetchall_arrayref({}); diff --git a/scripts/zmtrigger.pl.in b/scripts/zmtrigger.pl.in index 041d1c449..94f59e001 100644 --- a/scripts/zmtrigger.pl.in +++ b/scripts/zmtrigger.pl.in @@ -42,6 +42,7 @@ use constant SELECT_TIMEOUT => 0.25; @EXTRA_PERL_LIB@ use ZoneMinder; +use ZoneMinder::Monitor; use ZoneMinder::Trigger::Channel::Inet; use ZoneMinder::Trigger::Channel::Unix; use ZoneMinder::Trigger::Channel::Serial; @@ -166,13 +167,9 @@ while (!$zm_terminate) { foreach my $connection ( values(%spawned_connections) ) { if ( vec($rout, $connection->fileno(), 1) ) { Debug('Got input from spawned connection ' - .$connection->name() - .' (' - .$connection->fileno() - .')' - ); + .$connection->name().' ('.$connection->fileno().')'); my $messages = $connection->getMessages(); - if ( defined($messages) ) { + if (defined($messages)) { foreach my $message ( @$messages ) { handleMessage($connection, $message); } @@ -199,34 +196,28 @@ while (!$zm_terminate) { # Check polled connections foreach my $connection ( @in_poll_connections ) { my $messages = $connection->getMessages(); - if ( defined($messages) ) { - foreach my $message ( @$messages ) { - handleMessage($connection, $message); - } + if (defined($messages)) { + foreach my $message (@$messages) { handleMessage($connection, $message) }; } } # Check for alarms that might have happened my @out_messages; foreach my $monitor ( values %monitors ) { - - if ( ! zmMemVerify($monitor) ) { + if (!$monitor->connect()) { # Our attempt to verify the memory handle failed. We should reload the monitors. # Don't need to zmMemInvalidate because the monitor reload will do it. + Debug("Failed connect, putting on reloads"); push @needsReload, $monitor; next; } - my ( $state, $last_event ) = zmMemRead( $monitor, - [ + my ($state, $last_event) = zmMemRead($monitor, [ 'shared_data:state', 'shared_data:last_event' - ] - ); + ]); -#print( "$monitor->{Id}: S:$state, LE:$last_event" ); -#print( "$monitor->{Id}: mS:$monitor->{LastState}, mLE:$monitor->{LastEvent}" ); - if ( $state == STATE_ALARM or $state == STATE_ALERT ) { + if ($state == STATE_ALARM or $state == STATE_ALERT) { # In alarm state if ( !defined($monitor->{LastEvent}) or ($last_event != $monitor->{LastEvent}) @@ -255,6 +246,7 @@ while (!$zm_terminate) { } $monitor->{LastState} = $state; $monitor->{LastEvent} = $last_event; + $monitor->disconnect(); } # end foreach monitor foreach my $connection ( @out_connections ) { @@ -304,15 +296,22 @@ while (!$zm_terminate) { # Reload all monitors from the dB every MONITOR_RELOAD_INTERVAL if ( (time() - $monitor_reload_time) > MONITOR_RELOAD_INTERVAL ) { - foreach my $monitor ( values(%monitors) ) { - zmMemInvalidate( $monitor ); # Free up any used memory handle - } loadMonitors(); @needsReload = (); # We just reloaded all monitors so no need reload a specific monitor # If we have NOT just reloaded all monitors, reload a specific monitor if its shared mem changed - } elsif ( @needsReload ) { - foreach my $monitor ( @needsReload ) { - loadMonitor($monitor); + } elsif (@needsReload) { + foreach my $monitor (@needsReload) { + $monitor = $monitors{$monitor->Id()} = ZoneMinder::Monitor->find_one(Id=>$monitor->Id()); + if ( $$monitor{Function} eq 'None' ) { + delete $monitors{$monitor->Id()}; + } elsif ( $Config{ZM_SERVER_ID} and ($$monitor{ServerId} != $Config{ZM_SERVER_ID})) { + delete $monitors{$monitor->Id()}; + } else { + if ($monitor->connect()) { + $monitor->{LastState} = zmGetMonitorState($monitor); + $monitor->{LastEvent} = zmGetLastEvent($monitor); + } + } } @needsReload = (); } @@ -323,40 +322,21 @@ while (!$zm_terminate) { Info('Trigger daemon exiting'); exit; -sub loadMonitor { - my $monitor = shift; - - Debug('Loading monitor '.$monitor); - zmMemInvalidate($monitor); - - if ( zmMemVerify($monitor) ) { # This will re-init shared memory - $monitor->{LastState} = zmGetMonitorState($monitor); - $monitor->{LastEvent} = zmGetLastEvent($monitor); - } -} # end sub loadMonitor - sub loadMonitors { $monitor_reload_time = time(); - my %new_monitors = (); + %monitors = (); - my $sql = 'SELECT * FROM `Monitors` - WHERE find_in_set( `Function`, \'Modect,Mocord,Nodect,Record\' )'. - ( $Config{ZM_SERVER_ID} ? ' AND `ServerId`=?' : '' ) - ; - my $sth = $dbh->prepare_cached( $sql ) - or Fatal( "Can't prepare '$sql': ".$dbh->errstr() ); - my $res = $sth->execute( $Config{ZM_SERVER_ID} ? $Config{ZM_SERVER_ID} : () ) - or Fatal( "Can't execute: ".$sth->errstr() ); - - while ( my $monitor = $sth->fetchrow_hashref() ) { - if ( zmMemVerify($monitor) ) { # This will re-init shared memory + foreach my $monitor ( ZoneMinder::Monitor->find( + Function=>['Modect','Mocord','Nodect','Record'], + ($Config{ZM_SERVER_ID} ? (ServerId=>$Config{ZM_SERVER_ID}) : ()), + )) { + if ($monitor->connect()) { # This will re-init shared memory $monitor->{LastState} = zmGetMonitorState($monitor); $monitor->{LastEvent} = zmGetLastEvent($monitor); } - $new_monitors{$monitor->{Id}} = $monitor; + $monitors{$monitor->{Id}} = $monitor; } # end while fetchrow - %monitors = %new_monitors; } # end sub loadMonitors sub handleMessage { diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index 0253fadc2..f62b14316 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -2,7 +2,7 @@ # # ========================================================================== # -# ZoneMinder Update Script, $Date$, $Revision$ +# ZoneMinder Update Script # Copyright (C) 2001-2008 Philip Coombes # # This program is free software; you can redistribute it and/or @@ -31,29 +31,30 @@ zmupdate.pl -c,--check | -f,--freshen | -v,--version= [-u , --version= - Force upgrade to the current version from --u , --user= - Alternate DB user with privileges to alter DB --p , --pass= - Password of alternate DB user with privileges to alter DB --s, --super - Use system maintenance account on debian based systems instead of unprivileged account --d , --dir= - Directory containing update files if not in default build location --interactive - interact with the user --nointeractive - do not interact with the user + -c, --check - Check for updated versions of ZoneMinder. + If not interactive zmupdate.pl will stay running, checking every hour. + If interactive will try once, print out result and quit. + -f, --freshen - Freshen the configuration in the database. Equivalent of old zmconfig.pl -noi + --migrate-events - Update database structures as per USE_DEEP_STORAGE setting. + -v , --version= - Force upgrade to the current version from + -u , --user= - Alternate DB user with privileges to alter DB + -p , --pass= - Password of alternate DB user with privileges to alter DB + -s, --super - Use system maintenance account on debian based systems instead of unprivileged account + -d , --dir= - Directory containing update files if not in default build location + -interactive - interact with the user + -nointeractive - do not interact with the user =cut use strict; +use warnings; use bytes; use version; -use Crypt::Eksblowfish::Bcrypt; -use Data::Entropy::Algorithms qw(rand_bits); # ========================================================================== # @@ -95,7 +96,7 @@ my $use_log = (($> == 0) || ($> == $web_uid)); logInit( toFile=>$use_log?DEBUG:NOLOG ); logSetSignal(); -my $interactive = 1; +my $interactive = -t STDERR; # interactive if we have IO my $check = 0; my $freshen = 0; my $rename = 0; @@ -122,9 +123,8 @@ GetOptions( ) or pod2usage(-exitstatus => -1); my $dbh = zmDbConnect(undef, { mysql_multi_statements=>1 } ); -if ( !$dbh ) { - die "Unable to connect to db\n"; -} +die "Unable to connect to db\n" if !$dbh; + $Config{ZM_DB_USER} = $dbUser; $Config{ZM_DB_PASS} = $dbPass; # we escape dbpass with single quotes so that $ in the password has no effect, but dbpass could have a ' in it. @@ -144,8 +144,10 @@ if ( ($check + $freshen + $rename + $zoneFix + $migrateEvents + ($version?1:0)) pod2usage(-exitstatus => -1); } -if ( $check && $Config{ZM_CHECK_FOR_UPDATES} ) { - print('Update agent starting at '.strftime('%y/%m/%d %H:%M:%S', localtime() )."\n"); +if ($check) { + if (!$interactive) { + Info('Update agent starting at '.strftime('%y/%m/%d %H:%M:%S', localtime() )."\n"); + } my $currVersion = $Config{ZM_DYN_CURR_VERSION}; my $lastVersion = $Config{ZM_DYN_LAST_VERSION}; @@ -153,16 +155,14 @@ if ( $check && $Config{ZM_CHECK_FOR_UPDATES} ) { if ( !$currVersion ) { $currVersion = $Config{ZM_VERSION}; - - my $sql = "update Config set Value = ? where Name = 'ZM_DYN_CURR_VERSION'"; - my $sth = $dbh->prepare_cached($sql) or die("Can't prepare '$sql': ".$dbh->errstr()); - my $res = $sth->execute($currVersion) or die("Can't execute: ".$sth->errstr()); - $sth->finish(); + zmDbDo("UPDATE `Config` SET `Value` = ? WHERE `Name` = 'ZM_DYN_CURR_VERSION'", $currVersion); } while ( 1 ) { my $now = time(); - if ( !$lastVersion || !$lastCheck || (($now-$lastCheck) > CHECK_INTERVAL) ) { + if ( !$interactive and $lastVersion and $lastCheck and (($now-$lastCheck) <= CHECK_INTERVAL) ) { + Debug("Not checking for updates since we already have less than " . CHECK_INTERVAL . " seconds ago."); + } else { Info('Checking for updates'); use LWP::UserAgent; @@ -175,21 +175,18 @@ if ( $check && $Config{ZM_CHECK_FOR_UPDATES} ) { my $res = $ua->request($req); if ( $res->is_success ) { - $lastVersion = $res->content; - chomp($lastVersion); + my $latestVersion = $res->content; + chomp($latestVersion); $lastCheck = $now; - Info('Got version: '.$lastVersion); + Info('Got version: '.$latestVersion); - my $lv_sql = 'UPDATE Config SET Value = ? WHERE Name = \'ZM_DYN_LAST_VERSION\''; - my $lv_sth = $dbh->prepare_cached($lv_sql) or die("Can't prepare '$lv_sql': ".$dbh->errstr()); - my $lv_res = $lv_sth->execute($lastVersion) or die("Can't execute: ".$lv_sth->errstr()); - $lv_sth->finish(); - - my $lc_sql = 'UPDATE Config SET Value = ? WHERE Name = \'ZM_DYN_LAST_CHECK\''; - my $lc_sth = $dbh->prepare_cached($lc_sql) or die("Can't prepare '$lc_sql': ".$dbh->errstr()); - my $lc_res = $lc_sth->execute($lastCheck) or die("Can't execute: ".$lc_sth->errstr()); - $lc_sth->finish(); + zmDbDo('UPDATE Config SET Value = ? WHERE Name = \'ZM_DYN_LAST_VERSION\'', $latestVersion); + zmDbDo('UPDATE Config SET Value = ? WHERE Name = \'ZM_DYN_LAST_CHECK\'', $lastCheck); + if ($interactive) { + print("Last version $lastVersion, Latest version $latestVersion, our version " . ZM_VERSION."\n"); + exit(0); + } } else { Error('Error check failed: \''.$res->status_line().'\''); } @@ -447,11 +444,6 @@ if ( $version ) { print( "\nUpgrading database to version ".ZM_VERSION."\n" ); -# Update config first of all - migratePaths(); - ZoneMinder::Config::loadConfigFromDB(); - ZoneMinder::Config::saveConfigToDB(); - my $cascade = undef; if ( $cascade || $version eq "1.19.0" ) { # Patch the database @@ -1044,14 +1036,16 @@ sub patchDB { } # end sub patchDB sub migratePasswords { - print ("Migratings passwords, if any...\n"); + use Crypt::Eksblowfish::Bcrypt; + use Data::Entropy::Algorithms qw(rand_bits); + print("Migratings passwords, if any...\n"); my $sql = 'SELECT * FROM `Users`'; my $sth = $dbh->prepare_cached($sql) or die( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute() or die("Can't execute: ".$sth->errstr()); - while( my $user = $sth->fetchrow_hashref() ) { + while ( my $user = $sth->fetchrow_hashref() ) { my $scheme = substr($user->{Password}, 0, 1); if ($scheme eq '*') { - print ('-->'.$user->{Username}." password will be migrated\n"); + print('-->'.$user->{Username}." password will be migrated\n"); my $salt = Crypt::Eksblowfish::Bcrypt::en_base64(rand_bits(16*8)); my $settings = '$2a$10$'.$salt; my $pass_hash = Crypt::Eksblowfish::Bcrypt::bcrypt($user->{Password},$settings); diff --git a/scripts/zmwatch.pl.in b/scripts/zmwatch.pl.in index 96235ccd3..dd5cfc252 100644 --- a/scripts/zmwatch.pl.in +++ b/scripts/zmwatch.pl.in @@ -56,6 +56,7 @@ use constant START_DELAY => 30; # To give everything else time to start @EXTRA_PERL_LIB@ use ZoneMinder; use ZoneMinder::Storage; +use ZoneMinder::Monitor; use POSIX; use DBI; use autouse 'Data::Dumper'=>qw(Dumper); @@ -66,7 +67,7 @@ $ENV{PATH} = '/bin:/usr/bin:/usr/local/bin'; $ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; -logInit(); +my $log = logInit(); logSetSignal(); my $zm_terminate = 0; sub TermHandler { @@ -80,91 +81,76 @@ Info('Watchdog starting, pausing for '.START_DELAY.' seconds'); sleep(START_DELAY); my $dbh = zmDbConnect(); -my $sql = $Config{ZM_SERVER_ID} ? 'SELECT * FROM Monitors WHERE ServerId=?' : 'SELECT * FROM Monitors'; -my $sth = $dbh->prepare_cached($sql) - or Fatal("Can't prepare '$sql': ".$dbh->errstr()); while (!$zm_terminate) { - while ( ! ( $dbh and $dbh->ping() ) ) { - if ( ! ( $dbh = zmDbConnect() ) ) { + while (!($dbh and $dbh->ping())) { + if (!($dbh = zmDbConnect())) { sleep($Config{ZM_WATCH_CHECK_INTERVAL}); } } - my $res = $sth->execute( $Config{ZM_SERVER_ID} ? $Config{ZM_SERVER_ID} : () ) - or Fatal('Can\'t execute: '.$sth->errstr()); - while( my $monitor = $sth->fetchrow_hashref() ) { + foreach my $monitor (ZoneMinder::Monitor->find($Config{ZM_SERVER_ID} ? (ServerId=>$Config{ZM_SERVER_ID}) : ())) { next if $monitor->{Function} eq 'None'; next if $monitor->{Type} eq 'WebSite'; my $now = time(); my $restart = 0; - if (zmMemVerify($monitor)) { -# Check we have got an image recently - my $capture_time = zmGetLastWriteTime($monitor); - if (!defined($capture_time)) { -# Can't read from shared data - Debug('LastWriteTime is not defined.'); - zmMemInvalidate($monitor); - next; - } - Debug("Monitor $$monitor{Id} LastWriteTime is $capture_time."); - if (!$capture_time) { - my $startup_time = zmGetStartupTime($monitor); - if (($now - $startup_time) > $Config{ZM_WATCH_MAX_DELAY}) { - Warning( - "Restarting capture daemon for $$monitor{Name}, no image since startup. ". - "Startup time was $startup_time - now $now > $Config{ZM_WATCH_MAX_DELAY}" - ); - $restart = 1; - } else { - # We can't get the last capture time so can't be sure it's died, it might just be starting up. - zmMemInvalidate($monitor); - next; - } - } - if (!$restart) { - my $max_image_delay = ( - $monitor->{MaxFPS} - &&($monitor->{MaxFPS}>0) - &&($monitor->{MaxFPS}<1) - ) ? (3/$monitor->{MaxFPS}) - : $Config{ZM_WATCH_MAX_DELAY} - ; - 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 ) { - Warning("Restarting capture daemon for " - .$monitor->{Name}.", time since last capture $image_delay seconds ($now-$capture_time)" - ); - $restart = 1; - } - } # end if ! restart - } else { + + zmMemInvalidate($monitor); + if (!zmMemVerify($monitor)) { Info("Restarting capture daemon for $monitor->{Name}, shared data not valid"); - $restart = 1; + $monitor->control('restart'); + next; } - if ($restart) { - my $command; - if ($monitor->{Type} eq 'Local') { - $command = 'zmdc.pl restart zmc -d '.$monitor->{Device}; - } else { - $command = 'zmdc.pl restart zmc -m '.$monitor->{Id}; + # Check we have got an image recently + my $capture_time = zmGetLastWriteTime($monitor); + if (!defined($capture_time)) { + # Can't read from shared data + Warning('LastWriteTime is not defined.'); + next; + } + Debug("Monitor $$monitor{Id} LastWriteTime is $capture_time."); + if (!$capture_time) { + # 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(), + "Restarting capture daemon for $$monitor{Name}, no image since startup. ". + "Startup time was $startup_time - now $now > $Config{ZM_WATCH_MAX_DELAY}" + ); + $monitor->control('restart'); } - runCommand($command); - } elsif ($monitor->{Function} ne 'Monitor') { -# Now check analysis daemon - $restart = 0; + next; + } + + my $max_image_delay = ( + $monitor->{MaxFPS} + &&($monitor->{MaxFPS}>0) + &&($monitor->{MaxFPS}<1) + ) ? (3/$monitor->{MaxFPS}) + : $Config{ZM_WATCH_MAX_DELAY} + ; + 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)"); + $monitor->control('restart'); + next; + } + + if ($monitor->{Function} ne 'Monitor') { + # Now check analysis thread # Check we have got an image recently my $image_time = zmGetLastReadTime($monitor); if (!defined($image_time)) { -# Can't read from shared data - $restart = 1; + # Can't read from shared data Error("Error reading shared data for $$monitor{Id} $$monitor{Name}"); + $monitor->control('restart'); + next; } elsif (!$image_time) { -# We can't get the last capture time so can't be sure it's died. - #$restart = 1; - Error("Last analyse time for $$monitor{Id} $$monitor{Name} was zero."); + Debug("Last analyse time for $$monitor{Id} $$monitor{Name} was zero."); } else { my $max_image_delay = ( $monitor->{MaxFPS} &&($monitor->{MaxFPS}>0) @@ -175,26 +161,16 @@ 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) { - Warning("Analysis daemon for $$monitor{Id} $$monitor{Name} needs restarting," - ." time since last analysis $image_delay seconds ($now-$image_time)" - ); - $restart = 1; + $log->logPrint(ZoneMinder::Logger::WARNING+$monitor->Importance(), + "daemon for $$monitor{Id} $$monitor{Name} needs restarting," + ." time since last analysis $image_delay seconds ($now-$image_time)"); + $monitor->control('restart'); + next; } } - if ($restart) { - Info("Restarting analysis daemon for $$monitor{Id} $$monitor{Name}"); - my $command; - if ( $monitor->{Type} eq 'Local' ) { - $command = 'zmdc.pl restart zmc -d '.$monitor->{Device}; - } else { - $command = 'zmdc.pl restart zmc -m '.$monitor->{Id}; - } - runCommand($command); - } # end if restart } # end if check analysis daemon - # Prevent open handles building up if we have connect to shared memory - zmMemInvalidate($monitor); # Close our file handle to the zmc process we are about to end + } # end foreach monitor sleep($Config{ZM_WATCH_CHECK_INTERVAL}); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d4bc54e24..274ed07dd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,7 @@ configure_file(zm_config_data.h.in "${CMAKE_BINARY_DIR}/zm_config_data.h" @ONLY) # Group together all the source files that are used by all the binaries (zmc, zmu, zms etc) set(ZM_BIN_SRC_FILES zm_analysis_thread.cpp + zm_poll_thread.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp @@ -32,6 +33,9 @@ set(ZM_BIN_SRC_FILES zm_libvnc_camera.cpp zm_local_camera.cpp zm_monitor.cpp + zm_monitor_monitorlink.cpp + zm_monitor_janus.cpp + zm_monitor_amcrest.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_ffmpeg_camera.cpp @@ -60,12 +64,64 @@ set(ZM_BIN_SRC_FILES zm_signal.cpp zm_stream.cpp zm_swscale.cpp + zm_time.cpp zm_user.cpp zm_utils.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp) +if(GSOAP_FOUND) + set(ZM_BIN_SRC_FILES + ${ZM_BIN_SRC_FILES} + ${CMAKE_BINARY_DIR}/generated/soapPullPointSubscriptionBindingProxy.cpp + ${CMAKE_BINARY_DIR}/generated/soapC.cpp + ${GSOAP_PLUGIN_DIR}/smdevp.c + ${GSOAP_PLUGIN_DIR}/mecevp.c + ${GSOAP_PLUGIN_DIR}/wsaapi.c + ${GSOAP_PLUGIN_DIR}/wsseapi.c + ${GSOAP_PLUGIN_DIR}/../custom/struct_timeval.c + ) + + SET(GCC_COMPILE_FLAGS "-DWITH_OPENSSL -DWITH_DOM") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GCC_COMPILE_FLAGS}") + + #Create the directory that will host files generated by GSOAP + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated) + + #some files are generated by gsoap + set_source_files_properties( ${CMAKE_BINARY_DIR}/generated/soapClientLib.c PROPERTIES GENERATED TRUE ) + set_source_files_properties( ${CMAKE_BINARY_DIR}/generated/soapC.c PROPERTIES GENERATED TRUE ) + set_source_files_properties( ${CMAKE_BINARY_DIR}/generated/soapPullPointSubscriptionBindingProxy.cpp PROPERTIES GENERATED TRUE ) + set_source_files_properties( ${GSOAP_PLUGIN_DIR}/smdevp.c PROPERTIES LANGUAGE CXX) + set_source_files_properties( ${GSOAP_PLUGIN_DIR}/mecevp.c PROPERTIES LANGUAGE CXX) + set_source_files_properties( ${GSOAP_PLUGIN_DIR}/wsaapi.c PROPERTIES LANGUAGE CXX) + set_source_files_properties( ${GSOAP_PLUGIN_DIR}/wsseapi.c PROPERTIES LANGUAGE CXX) + set_source_files_properties( ${GSOAP_PLUGIN_DIR}/../custom/struct_timeval.c PROPERTIES LANGUAGE CXX) + + #Create a cmake target that generate gsoap files + 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 + COMMENT "CREATING STUBS AND GLUE CODE" + ) + + add_custom_target(GSOAP_GENERATION_TARGET + DEPENDS ${CMAKE_BINARY_DIR}/generated/soapC.cpp + DEPENDS ${CMAKE_BINARY_DIR}/generated/soapPullPointSubscriptionBindingProxy.cpp + DEPENDS ${GSOAP_PLUGIN_DIR}/smdevp.c + DEPENDS ${GSOAP_PLUGIN_DIR}/mecevp.c + DEPENDS ${GSOAP_PLUGIN_DIR}/wsaapi.c + DEPENDS ${GSOAP_PLUGIN_DIR}/wsseapi.c + DEPENDS ${GSOAP_PLUGIN_DIR}/../custom/struct_timeval.c + ) + +endif() + # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES}) @@ -74,6 +130,15 @@ target_include_directories(zm ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) +if(GSOAP_FOUND) +target_include_directories(zm + PUBLIC + ${CMAKE_BINARY_DIR}/generated + ${GSOAP_PLUGIN_DIR}/.. + ${GSOAP_INCLUDE_DIR}) + +endif() + target_link_libraries(zm PUBLIC FFMPEG::avcodec @@ -88,6 +153,15 @@ target_link_libraries(zm PRIVATE zm-core-interface) +if(GSOAP_FOUND) + target_link_libraries(zm + PUBLIC + ${GSOAP_CXX_LIBRARIES} + ${GSOAP_SSL_CXX_LIBRARIES} + ${OPENSSL_SSL_LIBRARY} + ${OPENSSL_CRYPTO_LIBRARY}) +endif() + if(${ZM_JWT_BACKEND} STREQUAL "jwt_cpp") target_link_libraries(zm PUBLIC @@ -109,6 +183,11 @@ add_executable(zms zms.cpp) add_executable(zmu zmu.cpp) add_executable(zmbenchmark zmbenchmark.cpp) +if(GSOAP_FOUND) + #Make sure that the client is compiled only after gsoap has been processed + add_dependencies(zmc GSOAP_GENERATION_TARGET) +endif() + target_link_libraries(zmc PRIVATE zm-core-interface diff --git a/src/zm_analysis_thread.cpp b/src/zm_analysis_thread.cpp index 325d8de59..a6f1afcc2 100644 --- a/src/zm_analysis_thread.cpp +++ b/src/zm_analysis_thread.cpp @@ -23,6 +23,14 @@ void AnalysisThread::Start() { void AnalysisThread::Run() { while (!(terminate_ or zm_terminate)) { - monitor_->Analyse(); + // Some periodic updates are required for variable capturing framerate + if (!monitor_->Analyse()) { + if (!(terminate_ or zm_terminate)) { + // We only sleep when Analyse 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); + } + } } } diff --git a/src/zm_buffer.cpp b/src/zm_buffer.cpp index 4cb580aa4..c65d8b24a 100644 --- a/src/zm_buffer.cpp +++ b/src/zm_buffer.cpp @@ -65,7 +65,7 @@ unsigned int Buffer::expand(unsigned int count) { int Buffer::read_into(int sd, unsigned int bytes) { // Make sure there is enough space this->expand(bytes); - Debug(3, "Reading %u btes", bytes); + Debug(3, "Reading %u bytes", bytes); int bytes_read = ::read(sd, mTail, bytes); if (bytes_read > 0) { mTail += bytes_read; diff --git a/src/zm_comms.h b/src/zm_comms.h index 7e7329d5d..ac772ae26 100644 --- a/src/zm_comms.h +++ b/src/zm_comms.h @@ -245,6 +245,7 @@ class Socket : public CommsBase { } virtual ssize_t recv(std::string &msg) const { + msg.reserve(ZM_NETWORK_BUFSIZ); std::vector buffer(msg.capacity()); ssize_t nBytes; if ((nBytes = ::recv(mSd, buffer.data(), buffer.size(), 0)) < 0) { diff --git a/src/zm_db.cpp b/src/zm_db.cpp index e3b737c07..7ddf60c48 100644 --- a/src/zm_db.cpp +++ b/src/zm_db.cpp @@ -108,8 +108,8 @@ bool zmDbConnect() { } void zmDbClose() { + std::lock_guard lck(db_mutex); if (zmDbConnected) { - std::lock_guard lck(db_mutex); mysql_close(&dbconn); // mysql_init() call implicitly mysql_library_init() but // mysql_close() does not call mysql_library_end() @@ -238,8 +238,13 @@ zmDbQueue::~zmDbQueue() { } void zmDbQueue::stop() { - mTerminate = true; + { + std::unique_lock lock(mMutex); + + mTerminate = true; + } mCondition.notify_all(); + if (mThread.joinable()) mThread.join(); } @@ -251,11 +256,11 @@ void zmDbQueue::process() { mCondition.wait(lock); } while (!mQueue.empty()) { - if (mQueue.size() > 10) { + if (mQueue.size() > 30) { Logger *log = Logger::fetch(); Logger::Level db_level = log->databaseLevel(); log->databaseLevel(Logger::NOLOG); - Warning("db queue size has grown larger %zu than 10 entries", mQueue.size()); + Warning("db queue size has grown larger %zu than 20 entries", mQueue.size()); log->databaseLevel(db_level); } std::string sql = mQueue.front(); @@ -271,8 +276,10 @@ void zmDbQueue::process() { void zmDbQueue::push(std::string &&sql) { if (mTerminate) return; - std::unique_lock lock(mMutex); - mQueue.push(std::move(sql)); + { + std::unique_lock lock(mMutex); + mQueue.push(std::move(sql)); + } mCondition.notify_all(); } diff --git a/src/zm_event.cpp b/src/zm_event.cpp index 9f1124e7b..b9a3ccbf2 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -55,7 +55,7 @@ Event::Event( alarm_frames(0), alarm_frame_written(false), tot_score(0), - max_score(0), + max_score(-1), //path(""), //snapshit_file(), //alarm_file(""), @@ -65,7 +65,8 @@ Event::Event( last_db_frame(0), have_video_keyframe(false), //scheme - save_jpegs(0) + save_jpegs(0), + terminate_(false) { std::string notes; createNotes(notes); @@ -133,98 +134,22 @@ Event::Event( ); id = zmDbDoInsert(sql); - if (!SetPath(storage)) { - // Try another - Warning("Failed creating event dir at %s", storage->Path()); - - sql = stringtf("SELECT `Id` FROM `Storage` WHERE `Id` != %u", storage->Id()); - if (monitor->ServerId()) - sql += stringtf(" AND ServerId=%u", monitor->ServerId()); - - storage = nullptr; - - MYSQL_RES *result = zmDbFetch(sql); - if (result) { - for (int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++) { - storage = new Storage(atoi(dbrow[0])); - if (SetPath(storage)) - break; - delete storage; - storage = nullptr; - } // end foreach row of Storage - mysql_free_result(result); - result = nullptr; - } - if (!storage) { - Info("No valid local storage area found. Trying all other areas."); - // Try remote - sql = "SELECT `Id` FROM `Storage` WHERE ServerId IS NULL"; - if (monitor->ServerId()) - sql += stringtf(" OR ServerId != %u", monitor->ServerId()); - - result = zmDbFetch(sql); - if (result) { - for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) { - storage = new Storage(atoi(dbrow[0])); - if (SetPath(storage)) - break; - delete storage; - storage = nullptr; - } // end foreach row of Storage - mysql_free_result(result); - result = nullptr; - } - } - if (!storage) { - storage = new Storage(); - Warning("Failed to find a storage area to save events."); - } - sql = stringtf("UPDATE Events SET StorageId = '%d' WHERE Id=%" PRIu64, storage->Id(), id); - zmDbDo(sql); - } // end if ! setPath(Storage) - Debug(1, "Using storage area at %s", path.c_str()); - - snapshot_file = path + "/snapshot.jpg"; - alarm_file = path + "/alarm.jpg"; - - video_incomplete_path = path + "/" + video_incomplete_file; - - if (monitor->GetOptVideoWriter() != 0) { - /* Save as video */ - - videoStore = new VideoStore( - video_incomplete_path.c_str(), - container.c_str(), - monitor->GetVideoStream(), - monitor->GetVideoCodecContext(), - ( monitor->RecordAudio() ? monitor->GetAudioStream() : nullptr ), - ( monitor->RecordAudio() ? monitor->GetAudioCodecContext() : nullptr ), - monitor ); - - if ( !videoStore->open() ) { - Warning("Failed to open videostore, turning on jpegs"); - delete videoStore; - videoStore = nullptr; - if ( ! ( save_jpegs & 1 ) ) { - save_jpegs |= 1; // Turn on jpeg storage - sql = stringtf("UPDATE Events SET SaveJpegs=%d WHERE Id=%" PRIu64, save_jpegs, id); - zmDbDo(sql); - } - } else { - std::string codec = videoStore->get_codec(); - video_file = stringtf("%" PRIu64 "-%s.%s.%s", id, "video", codec.c_str(), container.c_str()); - video_path = path + "/" + video_file; - Debug(1, "Video file is %s", video_file.c_str()); - } - } // end if GetOptVideoWriter - if (storage != monitor->getStorage()) - delete storage; + thread_ = std::thread(&Event::Run, this); } Event::~Event() { - // We close the videowriter first, because if we finish the event, we might try to view the file, but we aren't done writing it yet. + 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 */ + // We close the videowriter first, because if we finish the event, we might try to view the file, but we aren't done writing it yet. if (videoStore != nullptr) { Debug(4, "Deleting video store"); delete videoStore; @@ -291,6 +216,10 @@ void Event::createNotes(std::string ¬es) { } } // void Event::createNotes(std::string ¬es) +void Event::addNote(const char *cause, const std::string ¬e) { + noteSetMap[cause].insert(note); +} + bool Event::WriteFrameImage(Image *image, SystemTimePoint timestamp, const char *event_file, bool alarm_frame) const { int thisquality = (alarm_frame && (config.jpeg_alarm_file_quality > config.jpeg_file_quality)) ? @@ -372,7 +301,13 @@ void Event::updateNotes(const StringSetMap &newNoteSetMap) { } // end if update } // void Event::updateNotes(const StringSetMap &newNoteSetMap) -void Event::AddPacket(const std::shared_ptr&packet) { +void Event::AddPacket(ZMLockedPacket *packetlock) { + std::unique_lock lck(packet_queue_mutex); + packet_queue.push(packetlock); + packet_queue_condition.notify_one(); +} + +void Event::AddPacket_(const std::shared_ptr&packet) { have_video_keyframe = have_video_keyframe || ( ( packet->codec_type == AVMEDIA_TYPE_VIDEO ) && ( packet->keyframe || monitor->GetOptVideoWriter() == Monitor::ENCODE) ); @@ -486,12 +421,12 @@ void Event::AddFrame(Image *image, 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 > (int)max_score)) { + if ((frames == 1) || (score > max_score)) { write_to_db = true; // web ui might show this as thumbnail, so db needs to know about it. - Debug(1, "Writing snapshot"); + Debug(1, "Writing snapshot to %s", snapshot_file.c_str()); WriteFrameImage(image, timestamp, snapshot_file.c_str()); } else { - Debug(1, "Not Writing snapshot because score %d > max %d", score, max_score); + Debug(1, "Not Writing snapshot because frames %d score %d > max %d", frames, score, max_score); } // We are writing an Alarm frame @@ -500,17 +435,19 @@ void Event::AddFrame(Image *image, if (!alarm_frame_written) { write_to_db = true; // OD processing will need it, so the db needs to know about it alarm_frame_written = true; - Debug(1, "Writing alarm image"); - WriteFrameImage(image, timestamp, alarm_file.c_str()); + Debug(1, "Writing alarm image to %s", alarm_file.c_str()); + if (!WriteFrameImage(image, timestamp, alarm_file.c_str())) { + Error("Failed to write alarm frame image to %s", alarm_file.c_str()); + } } else { Debug(3, "Not Writing alarm image because alarm frame already written"); } if (alarm_image and (save_jpegs & 2)) { std::string event_file = stringtf(staticConfig.analyse_file_format.c_str(), path.c_str(), frames); - Debug(1, "Writing analysis frame %d", frames); + Debug(1, "Writing analysis frame %d to %s", frames, event_file.c_str()); if (!WriteFrameImage(alarm_image, timestamp, event_file.c_str(), true)) { - Error("Failed to write analysis frame image"); + Error("Failed to write analysis frame image to %s", event_file.c_str()); } } } // end if is an alarm frame @@ -523,11 +460,15 @@ void Event::AddFrame(Image *image, bool db_frame = ( frame_type == BULK ) or ( frame_type == ALARM ) or ( frames == 1 ) - or ( score > (int)max_score ) + or ( score > max_score ) or ( monitor_state == Monitor::ALERT ) or ( monitor_state == Monitor::ALARM ) or ( monitor_state == Monitor::PREALARM ); + if (score > max_score) { + max_score = score; + } + if (db_frame) { Microseconds delta_time = std::chrono::duration_cast(timestamp - start_time); Debug(1, "Frame delta is %.2f s - %.2f s = %.2f s, score %u zone_stats.size %zu", @@ -546,7 +487,7 @@ void Event::AddFrame(Image *image, or (frame_type == BULK) or - (fps and (frame_data.size() > 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(); @@ -568,9 +509,6 @@ void Event::AddFrame(Image *image, } // end if frame_type == BULK } // end if db_frame - if (score > (int) max_score) { - max_score = score; - } end_time = timestamp; } @@ -650,3 +588,117 @@ bool Event::SetPath(Storage *storage) { } // deep storage or not return true; } // end bool Event::SetPath + +void Event::Run() { + Storage *storage = monitor->getStorage(); + if (!SetPath(storage)) { + // Try another + Warning("Failed creating event dir at %s", storage->Path()); + + std::string sql = stringtf("SELECT `Id` FROM `Storage` WHERE `Id` != %u", storage->Id()); + if (monitor->ServerId()) + sql += stringtf(" AND ServerId=%u", monitor->ServerId()); + + storage = nullptr; + + MYSQL_RES *result = zmDbFetch(sql); + if (result) { + for (int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++) { + storage = new Storage(atoi(dbrow[0])); + if (SetPath(storage)) + break; + delete storage; + storage = nullptr; + } // end foreach row of Storage + mysql_free_result(result); + result = nullptr; + } + if (!storage) { + Info("No valid local storage area found. Trying all other areas."); + // Try remote + sql = "SELECT `Id` FROM `Storage` WHERE ServerId IS NULL"; + if (monitor->ServerId()) + sql += stringtf(" OR ServerId != %u", monitor->ServerId()); + + result = zmDbFetch(sql); + if (result) { + for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) { + storage = new Storage(atoi(dbrow[0])); + if (SetPath(storage)) + break; + delete storage; + storage = nullptr; + } // end foreach row of Storage + mysql_free_result(result); + result = nullptr; + } + } + if (!storage) { + storage = new Storage(); + Warning("Failed to find a storage area to save events."); + } + sql = stringtf("UPDATE Events SET StorageId = '%d' WHERE Id=%" PRIu64, storage->Id(), id); + zmDbDo(sql); + } // end if ! setPath(Storage) + Debug(1, "Using storage area at %s", path.c_str()); + + snapshot_file = path + "/snapshot.jpg"; + alarm_file = path + "/alarm.jpg"; + + video_incomplete_path = path + "/" + video_incomplete_file; + + if (monitor->GetOptVideoWriter() != 0) { + /* Save as video */ + + videoStore = new VideoStore( + video_incomplete_path.c_str(), + container.c_str(), + monitor->GetVideoStream(), + monitor->GetVideoCodecContext(), + ( monitor->RecordAudio() ? monitor->GetAudioStream() : nullptr ), + ( monitor->RecordAudio() ? monitor->GetAudioCodecContext() : nullptr ), + monitor ); + + if ( !videoStore->open() ) { + Warning("Failed to open videostore, turning on jpegs"); + delete videoStore; + videoStore = nullptr; + if ( ! ( save_jpegs & 1 ) ) { + save_jpegs |= 1; // Turn on jpeg storage + zmDbDo(stringtf("UPDATE Events SET SaveJpegs=%d WHERE Id=%" PRIu64, save_jpegs, id)); + } + } else { + std::string codec = videoStore->get_codec(); + video_file = stringtf("%" PRIu64 "-%s.%s.%s", id, "video", codec.c_str(), container.c_str()); + video_path = path + "/" + video_file; + Debug(1, "Video file is %s", video_file.c_str()); + } + } // end if GetOptVideoWriter + if (storage != monitor->getStorage()) + delete storage; + + std::unique_lock 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(); + 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"); + } + } +} +int Event::MonitorId() { + return monitor->Id(); +} diff --git a/src/zm_event.h b/src/zm_event.h index 611b2f716..1cbf794ef 100644 --- a/src/zm_event.h +++ b/src/zm_event.h @@ -22,14 +22,21 @@ #include "zm_config.h" #include "zm_define.h" +#include "zm_packet.h" #include "zm_storage.h" #include "zm_time.h" #include "zm_utils.h" #include "zm_zone.h" +#include +#include #include +#include +#include #include #include +#include + class EventStream; class Frame; @@ -77,8 +84,8 @@ class Event { int frames; int alarm_frames; bool alarm_frame_written; - unsigned int tot_score; - unsigned int max_score; + int tot_score; + int max_score; std::string path; std::string snapshot_file; std::string alarm_file; @@ -98,6 +105,15 @@ class Event { void createNotes(std::string ¬es); + std::queue packet_queue; + std::mutex packet_queue_mutex; + std::condition_variable packet_queue_condition; + + void Run(); + + std::atomic terminate_; + std::thread thread_; + public: static bool OpenFrameSocket(int); static bool ValidateFrameSocket(int); @@ -110,41 +126,50 @@ class Event { uint64_t Id() const { return id; } const std::string &Cause() const { return cause; } + void addNote(const char *cause, const std::string ¬e); int Frames() const { return frames; } int AlarmFrames() const { return alarm_frames; } SystemTimePoint StartTime() const { return start_time; } SystemTimePoint EndTime() const { return end_time; } + TimePoint::duration Duration() const { return end_time - start_time; }; - void AddPacket(const std::shared_ptr &p); + void AddPacket(ZMLockedPacket *); + void AddPacket_(const std::shared_ptr &p); bool WritePacket(const std::shared_ptr &p); bool SendFrameImage(const Image *image, bool alarm_frame=false); bool WriteFrameImage(Image *image, SystemTimePoint timestamp, const char *event_file, bool alarm_frame = false) const; void updateNotes(const StringSetMap &stringSetMap); - void AddFrame(Image *image, + void AddFrame(Image *image, SystemTimePoint timestamp, const std::vector &stats, int score = 0, Image *alarm_image = nullptr); + void Stop() { + terminate_ = true; + packet_queue_condition.notify_all(); + } + bool Stopped() const { return terminate_; } + private: void WriteDbFrames(); bool SetPath(Storage *storage); public: - static std::string getSubPath(tm time) { - std::string subpath = stringtf("%02d/%02d/%02d/%02d/%02d/%02d", - time.tm_year - 100, time.tm_mon + 1, time.tm_mday, - time.tm_hour, time.tm_min, time.tm_sec); - return subpath; - } - static std::string getSubPath(time_t *time) { - tm time_tm = {}; - localtime_r(time, &time_tm); - return Event::getSubPath(time_tm); - } + static std::string getSubPath(tm time) { + std::string subpath = stringtf("%02d/%02d/%02d/%02d/%02d/%02d", + time.tm_year - 100, time.tm_mon + 1, time.tm_mday, + time.tm_hour, time.tm_min, time.tm_sec); + return subpath; + } + static std::string getSubPath(time_t *time) { + tm time_tm = {}; + localtime_r(time, &time_tm); + return Event::getSubPath(time_tm); + } const char* getEventFile() const { return video_file.c_str(); @@ -154,18 +179,6 @@ class Event { return pre_alarm_count; } static void EmptyPreAlarmFrames() { -#if 0 - while ( pre_alarm_count > 0 ) { - int i = pre_alarm_count - 1; - delete pre_alarm_data[i].image; - pre_alarm_data[i].image = nullptr; - if ( pre_alarm_data[i].alarm_frame ) { - delete pre_alarm_data[i].alarm_frame; - pre_alarm_data[i].alarm_frame = nullptr; - } - pre_alarm_count--; - } -#endif pre_alarm_count = 0; } static void AddPreAlarmFrame( @@ -174,28 +187,11 @@ class Event { int score=0, Image *alarm_frame=nullptr ) { -#if 0 - pre_alarm_data[pre_alarm_count].image = new Image(*image); - pre_alarm_data[pre_alarm_count].timestamp = timestamp; - pre_alarm_data[pre_alarm_count].score = score; - if ( alarm_frame ) { - pre_alarm_data[pre_alarm_count].alarm_frame = new Image(*alarm_frame); - } -#endif pre_alarm_count++; } void SavePreAlarmFrames() { -#if 0 - for ( int i = 0; i < pre_alarm_count; i++ ) { - AddFrame( - pre_alarm_data[i].image, - pre_alarm_data[i].timestamp, - pre_alarm_data[i].score, - pre_alarm_data[i].alarm_frame); - } -#endif EmptyPreAlarmFrames(); } + int MonitorId(); }; - #endif // ZM_EVENT_H diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index af9dd8639..4521aaf89 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -68,17 +68,17 @@ bool EventStream::loadInitialEventData(int monitor_id, SystemTimePoint event_tim curr_frame_id = 1; // curr_frame_id is 1-based if (event_time >= event_data->start_time) { Debug(2, "event time is after event start"); - for (unsigned int i = 0; i < event_data->frame_count; i++) { + for (int i = 0; i < event_data->frame_count; i++) { //Info( "eft %d > et %d", event_data->frames[i].timestamp, event_time ); if (event_data->frames[i].timestamp >= event_time) { curr_frame_id = i + 1; - Debug(3, "Set curr_stream_time: %.2f, curr_frame_id: %ld", + Debug(3, "Set curr_stream_time: %.2f, curr_frame_id: %d", FPSeconds(curr_stream_time.time_since_epoch()).count(), curr_frame_id); break; } } // end foreach frame - Debug(3, "Skipping %ld frames", event_data->frame_count); + Debug(3, "Skipping %d frames", event_data->frame_count); } else { Warning("Requested an event time less than the start of the event. event_time %" PRIi64 " < start_time %" PRIi64, static_cast(std::chrono::duration_cast(event_time.time_since_epoch()).count()), @@ -90,13 +90,13 @@ bool EventStream::loadInitialEventData(int monitor_id, SystemTimePoint event_tim bool EventStream::loadInitialEventData( uint64_t init_event_id, - unsigned int init_frame_id + int init_frame_id ) { loadEventData(init_event_id); if ( init_frame_id ) { if ( init_frame_id >= event_data->frame_count ) { - Error("Invalid frame id specified. %d > %lu", init_frame_id, event_data->frame_count); + Error("Invalid frame id specified. %d > %d", init_frame_id, event_data->frame_count); curr_stream_time = event_data->start_time; curr_frame_id = 1; } else { @@ -225,20 +225,24 @@ bool EventStream::loadEventData(uint64_t event_id) { sql = stringtf("SELECT `FrameId`, unix_timestamp(`TimeStamp`), `Delta` " "FROM `Frames` WHERE `EventId` = %" PRIu64 " ORDER BY `FrameId` ASC", event_id); - result = zmDbFetch(sql); if (!result) { exit(-1); } event_data->n_frames = mysql_num_rows(result); - event_data->frames = new FrameData[event_data->frame_count]; + if (event_data->frame_count < event_data->n_frames) { + event_data->frame_count = event_data->n_frames; + Warning("Event %" PRId64 " has more frames in the Frames table (%d) than in the Event record (%d)", + event_data->event_id, event_data->n_frames, event_data->frame_count); + } + int last_id = 0; SystemTimePoint last_timestamp = event_data->start_time; Microseconds last_delta = Seconds(0); - while ( ( dbrow = mysql_fetch_row(result) ) ) { + while ((dbrow = mysql_fetch_row(result))) { int id = atoi(dbrow[0]); //timestamp = atof(dbrow[1]); Microseconds delta = std::chrono::duration_cast(FPSeconds(atof(dbrow[2]))); @@ -280,7 +284,7 @@ bool EventStream::loadEventData(uint64_t event_id) { // Incomplete events might not have any frame data event_data->last_frame_id = last_id; - if ( mysql_errno(&dbconn) ) { + if (mysql_errno(&dbconn)) { Error("Can't fetch row: %s", mysql_error(&dbconn)); exit(mysql_errno(&dbconn)); } @@ -310,7 +314,7 @@ bool EventStream::loadEventData(uint64_t event_id) { else curr_stream_time = event_data->frames[event_data->last_frame_id-1].timestamp; } - Debug(2, "Event: %" PRIu64 ", Frames: %ld, Last Frame ID (%ld, Duration: %.2f s Frames Duration: %.2f s", + Debug(2, "Event: %" PRIu64 ", Frames: %d, Last Frame ID (%d, Duration: %.2f s Frames Duration: %.2f s", event_data->event_id, event_data->frame_count, event_data->last_frame_id, @@ -342,12 +346,12 @@ void EventStream::processCommand(const CmdMsg *msg) { if ( (mode == MODE_SINGLE || mode == MODE_NONE) && - ((unsigned int)curr_frame_id == event_data->last_frame_id) + (curr_frame_id == event_data->last_frame_id) ) { Debug(1, "Was in single or no replay mode, and at last frame, so jumping to 1st frame"); curr_frame_id = 1; } else { - Debug(1, "mode is %s, current frame is %ld, frame count is %ld, last frame id is %ld", + Debug(1, "mode is %s, current frame is %d, frame count is %d, last frame id is %d", StreamMode_Strings[(int) mode].c_str(), curr_frame_id, event_data->frame_count, @@ -404,9 +408,9 @@ void EventStream::processCommand(const CmdMsg *msg) { paused = true; replay_rate = ZM_RATE_BASE; step = 1; - if ( (unsigned int)curr_frame_id < event_data->last_frame_id ) + if (curr_frame_id < event_data->last_frame_id) curr_frame_id += 1; - Debug(1, "Got SLOWFWD command new frame id %ld", curr_frame_id); + Debug(1, "Got SLOWFWD command new frame id %d", curr_frame_id); break; case CMD_SLOWREV : paused = true; @@ -414,7 +418,7 @@ void EventStream::processCommand(const CmdMsg *msg) { step = -1; curr_frame_id -= 1; if ( curr_frame_id < 1 ) curr_frame_id = 1; - Debug(1, "Got SLOWREV command new frame id %ld", curr_frame_id); + Debug(1, "Got SLOWREV command new frame id %d", curr_frame_id); break; case CMD_FASTREV : Debug(1, "Got FAST REV command"); @@ -538,12 +542,12 @@ void EventStream::processCommand(const CmdMsg *msg) { if ( curr_frame_id < 1 ) { curr_frame_id = 1; - } else if ( (unsigned long)curr_frame_id > event_data->last_frame_id ) { + } else if (curr_frame_id > event_data->last_frame_id) { curr_frame_id = event_data->last_frame_id; } curr_stream_time = event_data->frames[curr_frame_id-1].timestamp; - Debug(1, "Got SEEK command, to %f s (new current frame id: %ld offset %f s)", + Debug(1, "Got SEEK command, to %f s (new current frame id: %d offset %f s)", FPSeconds(offset).count(), curr_frame_id, FPSeconds(event_data->frames[curr_frame_id - 1].offset).count()); @@ -615,11 +619,11 @@ bool EventStream::checkEventLoaded() { sql = stringtf( "SELECT `Id` FROM `Events` WHERE `MonitorId` = %d AND `Id` < %" PRIu64 " ORDER BY `Id` DESC LIMIT 1", event_data->monitor_id, event_data->event_id); - } else if ( (unsigned int)curr_frame_id > event_data->last_frame_id ) { + } else if (curr_frame_id > event_data->last_frame_id) { if (event_data->end_time.time_since_epoch() == Seconds(0)) { // We are viewing an in-process event, so just reload it. loadEventData(event_data->event_id); - if ( (unsigned int)curr_frame_id > event_data->last_frame_id ) + if (curr_frame_id > event_data->last_frame_id) curr_frame_id = event_data->last_frame_id; return false; } @@ -628,7 +632,7 @@ bool EventStream::checkEventLoaded() { event_data->monitor_id, event_data->event_id); } else { // No event change required - Debug(3, "No event change required, as curr frame %ld <=> event frames %lu", + Debug(3, "No event change required, as curr frame %d <=> event frames %d", curr_frame_id, event_data->frame_count); return false; } @@ -662,8 +666,8 @@ bool EventStream::checkEventLoaded() { curr_frame_id = event_data->last_frame_id; else curr_frame_id = 1; - Debug(2, "New frame id = %ld", curr_frame_id); - start = std::chrono::system_clock::now(); + Debug(2, "New frame id = %d", curr_frame_id); + start = std::chrono::steady_clock::now(); return true; } else { Debug(2, "No next event loaded using %s. Pausing", sql.c_str()); @@ -689,22 +693,20 @@ bool EventStream::checkEventLoaded() { Image * EventStream::getImage( ) { std::string path = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id); - Debug(2, "EventStream::getImage path(%s) from %s frame(%ld) ", path.c_str(), event_data->path.c_str(), curr_frame_id); + Debug(2, "EventStream::getImage path(%s) from %s frame(%d) ", path.c_str(), event_data->path.c_str(), curr_frame_id); Image *image = new Image(path.c_str()); return image; } bool EventStream::sendFrame(Microseconds delta_us) { - Debug(2, "Sending frame %ld", curr_frame_id); + Debug(2, "Sending frame %d", curr_frame_id); std::string filepath; struct stat filestat = {}; // This needs to be abstracted. If we are saving jpgs, then load the capture file. // If we are only saving analysis frames, then send that. - if (event_data->SaveJPEGs & 1) { - filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id); - } else if (event_data->SaveJPEGs & 2) { + if ((frame_type == FRAME_ANALYSIS) && (event_data->SaveJPEGs & 2)) { filepath = stringtf(staticConfig.analyse_file_format.c_str(), event_data->path.c_str(), curr_frame_id); if (stat(filepath.c_str(), &filestat) < 0) { Debug(1, "analyze file %s not found will try to stream from other", filepath.c_str()); @@ -714,7 +716,9 @@ bool EventStream::sendFrame(Microseconds delta_us) { filepath = ""; } } - } else if ( !ffmpeg_input ) { + } else if (event_data->SaveJPEGs & 1) { + filepath = stringtf(staticConfig.capture_file_format.c_str(), event_data->path.c_str(), curr_frame_id); + } else if (!ffmpeg_input) { Fatal("JPEGS not saved. zms is not capable of streaming jpegs from mp4 yet"); return false; } @@ -795,7 +799,13 @@ bool EventStream::sendFrame(Microseconds delta_us) { } Image *send_image = prepareImage(image); - static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; + if (temp_img_buffer_size < send_image->Size()) { + Debug(1, "Resizing image buffer from %zu to %u", + temp_img_buffer_size, send_image->Size()); + delete[] temp_img_buffer; + temp_img_buffer = new uint8_t[send_image->Size()]; + temp_img_buffer_size = send_image->Size(); + } int img_buffer_size = 0; uint8_t *img_buffer = temp_img_buffer; @@ -837,12 +847,13 @@ void EventStream::runStream() { //checkInitialised(); - if ( type == STREAM_JPEG ) + if (type == STREAM_JPEG) fputs("Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n\r\n", stdout); - if ( !event_data ) { + if (!event_data) { sendTextFrame("No event data found"); - exit(0); + zm_terminate = true; + return; } double fps = 1.0; @@ -851,13 +862,13 @@ void EventStream::runStream() { } updateFrameRate(fps); - start = std::chrono::system_clock::now(); + start = std::chrono::steady_clock::now(); SystemTimePoint::duration last_frame_offset = Seconds(0); SystemTimePoint::duration time_to_event = Seconds(0); while ( !zm_terminate ) { - now = std::chrono::system_clock::now(); + now = std::chrono::steady_clock::now(); Microseconds delta = Microseconds(0); send_frame = false; @@ -880,7 +891,7 @@ void EventStream::runStream() { if ( !paused ) { // Figure out if we should send this frame - Debug(3, "not paused at cur_frame_id (%ld-1) mod frame_mod(%d)", curr_frame_id, frame_mod); + Debug(3, "not paused at cur_frame_id (%d-1) mod frame_mod(%d)", curr_frame_id, frame_mod); // If we are streaming and this frame is due to be sent // frame mod defaults to 1 and if we are going faster than max_fps will get multiplied by 2 // so if it is 2, then we send every other frame, if is it 4 then every fourth frame, etc. @@ -904,7 +915,7 @@ void EventStream::runStream() { // time_to_event > 0 means that we are not in the event if (time_to_event > Seconds(0) and mode == MODE_ALL) { - SystemTimePoint::duration time_since_last_send = now - last_frame_sent; + TimePoint::duration time_since_last_send = now - last_frame_sent; Debug(1, "Time since last send = %.2f s", FPSeconds(time_since_last_send).count()); if (time_since_last_send > Seconds(1)) { char frame_text[64]; @@ -976,13 +987,13 @@ void EventStream::runStream() { // +/- 1? What if we are skipping frames? curr_frame_id += (replay_rate>0) ? frame_mod : -1*frame_mod; // sending the frame may have taken some time, so reload now - now = std::chrono::system_clock::now(); + now = std::chrono::steady_clock::now(); // we incremented by replay_rate, so might have jumped past frame_count if ( (mode == MODE_SINGLE) && ( (curr_frame_id < 1 ) || - ((unsigned int)curr_frame_id >= event_data->frame_count) + (curr_frame_id >= event_data->frame_count) ) ) { Debug(2, "Have mode==MODE_SINGLE and at end of event, looping back to start"); @@ -990,45 +1001,51 @@ void EventStream::runStream() { // Have to reset start to now when replaying start = now; } - frame_data = &event_data->frames[curr_frame_id-1]; - // frame_data->delta is the time since last frame as a float in seconds - // but what if we are skipping frames? We need the distance from the last frame sent - // Also, what about reverse? needs to be absolute value + if (curr_frame_id <= event_data->frame_count) { + frame_data = &event_data->frames[curr_frame_id-1]; - // There are two ways to go about this, not sure which is correct. - // you can calculate the relationship between now and the start - // or calc the relationship from the last frame. I think from the start is better as it self-corrects - // - if (last_frame_offset != Seconds(0)) { - // We assume that we are going forward and the next frame is in the future. - delta = std::chrono::duration_cast(frame_data->offset - (now - start)); + // frame_data->delta is the time since last frame as a float in seconds + // but what if we are skipping frames? We need the distance from the last frame sent + // Also, what about reverse? needs to be absolute value - Debug(2, "New delta: now - start = %" PRIu64 " us offset %" PRIi64 " us- elapsed = %" PRIu64 " us", - static_cast(std::chrono::duration_cast(now - start).count()), - static_cast(std::chrono::duration_cast(frame_data->offset).count()), - static_cast(std::chrono::duration_cast(delta).count())); - } else { - Debug(2, "No last frame_offset, no sleep"); - delta = Seconds(0); - } - last_frame_offset = frame_data->offset; + // There are two ways to go about this, not sure which is correct. + // you can calculate the relationship between now and the start + // or calc the relationship from the last frame. I think from the start is better as it self-corrects + // + if (last_frame_offset != Seconds(0)) { + // We assume that we are going forward and the next frame is in the future. + delta = std::chrono::duration_cast(frame_data->offset - (now - start)); - if (send_frame && type != STREAM_MPEG) { - if (delta != Seconds(0)) { - if (delta > MAX_SLEEP) { - Debug(1, "Limiting sleep to %" PRIi64 " ms because calculated sleep is too long: %" PRIi64" us", + Debug(2, "New delta: now - start = %" PRIu64 " us offset %" PRIi64 " us- elapsed = %" PRIu64 " us", + static_cast(std::chrono::duration_cast(now - start).count()), + static_cast(std::chrono::duration_cast(frame_data->offset).count()), + static_cast(std::chrono::duration_cast(delta).count())); + } else { + Debug(2, "No last frame_offset, no sleep"); + delta = Seconds(0); + } + last_frame_offset = frame_data->offset; + + if (send_frame && type != STREAM_MPEG) { + if (delta != Seconds(0)) { + if (delta > MAX_SLEEP) { + Debug(1, "Limiting sleep to %" PRIi64 " ms because calculated sleep is too long: %" PRIi64" us", static_cast(std::chrono::duration_cast(MAX_SLEEP).count()), static_cast(std::chrono::duration_cast(delta).count())); - delta = MAX_SLEEP; - } + delta = MAX_SLEEP; + } - std::this_thread::sleep_for(delta); - Debug(3, "Done sleeping: %" PRIi64 " us", + std::this_thread::sleep_for(delta); + Debug(3, "Done sleeping: %" PRIi64 " us", static_cast(std::chrono::duration_cast(delta).count())); - } - } + } + } // end if need to sleep + } else { + Debug(1, "invalid curr_frame_id %d !< %d", curr_frame_id, event_data->frame_count); + } // end if not at end of event } else { + // Paused delta = std::chrono::duration_cast(FPSeconds( ZM_RATE_BASE / ((base_fps ? base_fps : 1) * (replay_rate ? abs(replay_rate * 2) : 2)))); @@ -1087,62 +1104,57 @@ void EventStream::runStream() { } // end void EventStream::runStream() bool EventStream::send_file(const std::string &filepath) { - static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; - - int img_buffer_size = 0; - uint8_t *img_buffer = temp_img_buffer; - - FILE *fdj = nullptr; - fdj = fopen(filepath.c_str(), "rb"); - if ( !fdj ) { + FILE *fdj = fopen(filepath.c_str(), "rb"); + if (!fdj) { Error("Can't open %s: %s", filepath.c_str(), strerror(errno)); std::string error_message = stringtf("Can't open %s: %s", filepath.c_str(), strerror(errno)); return sendTextFrame(error_message.c_str()); } -#if HAVE_SENDFILE static struct stat filestat; - if ( fstat(fileno(fdj), &filestat) < 0 ) { + if (fstat(fileno(fdj), &filestat) < 0) { fclose(fdj); /* Close the file handle */ Error("Failed getting information about file %s: %s", filepath.c_str(), strerror(errno)); return false; } - if ( !filestat.st_size ) { + if (!filestat.st_size) { fclose(fdj); /* Close the file handle */ - Info("File size is zero. Unable to send raw frame %ld: %s", curr_frame_id, strerror(errno)); + 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: %d\r\n\r\n", (int)filestat.st_size) ) { + if (0 > fprintf(stdout, "Content-Length: %jd\r\n\r\n", filestat.st_size)) { fclose(fdj); /* Close the file handle */ - Info("Unable to send raw frame %ld: %s", curr_frame_id, strerror(errno)); + Info("Unable to send raw frame %d: %s", curr_frame_id, strerror(errno)); return false; } - int rc = zm_sendfile(fileno(stdout), fileno(fdj), 0, (int)filestat.st_size); - if ( rc == (int)filestat.st_size ) { + ssize_t remaining = filestat.st_size; + + while (remaining > 0) { + ssize_t rc = zm_sendfile(fileno(stdout), fileno(fdj), nullptr, remaining); + if (rc < 0) break; + if (rc > 0) { + remaining -= rc; + } + } // end while remaining + + if (!remaining) { // Success fclose(fdj); /* Close the file handle */ return true; } - Warning("Unable to send raw frame %ld: %s rc %d", curr_frame_id, strerror(errno), rc); -#endif - img_buffer_size = fread(img_buffer, 1, sizeof(temp_img_buffer), fdj); - fclose(fdj); /* Close the file handle */ - if ( !img_buffer_size ) { - Info("Unable to read raw frame %ld: %s", curr_frame_id, strerror(errno)); - return false; - } - - return send_buffer(img_buffer, img_buffer_size); -} + Warning("Unable to send raw frame %d: %s %zu remaining", + curr_frame_id, strerror(errno), remaining); + return false; +} // end bool EventStream::send_file(const std::string &filepath) bool EventStream::send_buffer(uint8_t* buffer, int size) { if ( 0 > fprintf(stdout, "Content-Length: %d\r\n\r\n", size) ) { - Info("Unable to send raw frame %ld: %s", curr_frame_id, strerror(errno)); + Info("Unable to send raw frame %d: %s", curr_frame_id, strerror(errno)); return false; } int rc = fwrite(buffer, size, 1, stdout); if ( 1 != rc ) { - Error("Unable to send raw frame %ld: %s %d", curr_frame_id, strerror(errno), rc); + Error("Unable to send raw frame %d: %s %d", curr_frame_id, strerror(errno), rc); return false; } return true; @@ -1150,7 +1162,7 @@ bool EventStream::send_buffer(uint8_t* buffer, int size) { void EventStream::setStreamStart( uint64_t init_event_id, - unsigned int init_frame_id=0) { + int init_frame_id=0) { loadInitialEventData(init_event_id, init_frame_id); } // end void EventStream::setStreamStart(init_event_id,init_frame_id=0) diff --git a/src/zm_eventstream.h b/src/zm_eventstream.h index 387748b08..093bcd0eb 100644 --- a/src/zm_eventstream.h +++ b/src/zm_eventstream.h @@ -49,9 +49,9 @@ class EventStream : public StreamBase { struct EventData { uint64_t event_id; unsigned int monitor_id; - unsigned long storage_id; - unsigned long frame_count; // Value of Frames column in Event - unsigned long last_frame_id; // Highest frame id known about. Can be < frame_count in incomplete events + unsigned int storage_id; + int frame_count; // Value of Frames column in Event + int last_frame_id; // Highest frame id known about. Can be < frame_count in incomplete events SystemTimePoint start_time; SystemTimePoint end_time; Microseconds duration; @@ -73,16 +73,16 @@ class EventStream : public StreamBase { StreamMode mode; bool forceEventChange; - long curr_frame_id; + int curr_frame_id; SystemTimePoint curr_stream_time; bool send_frame; - SystemTimePoint start; // clock time when started the event + TimePoint start; // clock time when started the event EventData *event_data; protected: bool loadEventData(uint64_t event_id); - bool loadInitialEventData(uint64_t init_event_id, unsigned int init_frame_id); + bool loadInitialEventData(uint64_t init_event_id, int init_frame_id); bool loadInitialEventData(int monitor_id, SystemTimePoint event_time); bool checkEventLoaded(); @@ -118,7 +118,7 @@ class EventStream : public StreamBase { ffmpeg_input = nullptr; } } - void setStreamStart(uint64_t init_event_id, unsigned int init_frame_id); + void setStreamStart(uint64_t init_event_id, int init_frame_id); void setStreamStart(int monitor_id, time_t event_time); void setStreamMode(StreamMode p_mode) { mode = p_mode; } void runStream() override; diff --git a/src/zm_ffmpeg.cpp b/src/zm_ffmpeg.cpp index 3986b38b8..4fe14051f 100644 --- a/src/zm_ffmpeg.cpp +++ b/src/zm_ffmpeg.cpp @@ -257,8 +257,8 @@ void zm_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output) Debug(1, "ids [0x%x]", st->id); if (lang) Debug(1, "language (%s)", lang->value); - Debug(1, "frames:%d, frame_size:%d stream timebase: %d/%d", - st->codec_info_nb_frames, codec->frame_size, + Debug(1, "frame_size:%d stream timebase: %d/%d", + codec->frame_size, st->time_base.num, st->time_base.den ); diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index b122e0f2b..4a0c94ec9 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -293,17 +293,16 @@ int FfmpegCamera::OpenFfmpeg() { mFormatContext->interrupt_callback.opaque = this; ret = avformat_open_input(&mFormatContext, mPath.c_str(), nullptr, &opts); - if ( ret != 0 ) - { - Error("Unable to open input %s due to: %s", mPath.c_str(), + if (ret != 0) { + logPrintf(Logger::ERROR + monitor->Importance(), + "Unable to open input %s due to: %s", mPath.c_str(), av_make_error_string(ret).c_str()); - if ( mFormatContext ) { + if (mFormatContext) { avformat_close_input(&mFormatContext); mFormatContext = nullptr; } av_dict_free(&opts); - return -1; } AVDictionaryEntry *e = nullptr; @@ -458,6 +457,17 @@ int FfmpegCamera::OpenFfmpeg() { #endif } // end if hwaccel_name + // set codec to automatically determine how many threads suits best for the decoding job + mVideoCodecContext->thread_count = 0; + + if (mVideoCodec->capabilities | AV_CODEC_CAP_FRAME_THREADS) { + mVideoCodecContext->thread_type = FF_THREAD_FRAME; + } else if (mVideoCodec->capabilities | AV_CODEC_CAP_SLICE_THREADS) { + mVideoCodecContext->thread_type = FF_THREAD_SLICE; + } else { + mVideoCodecContext->thread_count = 1; //don't use multithreading + } + ret = avcodec_open2(mVideoCodecContext, mVideoCodec, &opts); e = nullptr; diff --git a/src/zm_ffmpeg_input.cpp b/src/zm_ffmpeg_input.cpp index 17ce88eab..8e0a5b0e8 100644 --- a/src/zm_ffmpeg_input.cpp +++ b/src/zm_ffmpeg_input.cpp @@ -31,8 +31,7 @@ int FFmpeg_Input::Open( const AVStream * audio_in_stream, const AVCodecContext * audio_in_ctx ) { - video_stream_id = video_in_stream->index; - int max_stream_index = video_in_stream->index; + int max_stream_index = video_stream_id = video_in_stream->index; if ( audio_in_stream ) { max_stream_index = video_in_stream->index > audio_in_stream->index ? video_in_stream->index : audio_in_stream->index; diff --git a/src/zm_fifo_stream.cpp b/src/zm_fifo_stream.cpp index 51d88b663..0acb720d2 100644 --- a/src/zm_fifo_stream.cpp +++ b/src/zm_fifo_stream.cpp @@ -155,7 +155,7 @@ void FifoStream::runStream() { } while (!zm_terminate) { - now = std::chrono::system_clock::now(); + now = std::chrono::steady_clock::now(); checkCommandQueue(); if (stream_type == MJPEG) { diff --git a/src/zm_fifo_stream.h b/src/zm_fifo_stream.h index 911980d48..332275771 100644 --- a/src/zm_fifo_stream.h +++ b/src/zm_fifo_stream.h @@ -28,7 +28,6 @@ class FifoStream : public StreamBase { std::string stream_path; int total_read; int bytes_read; - unsigned int frame_count; protected: typedef enum { UNKNOWN, MJPEG, RAW } StreamType; @@ -39,9 +38,9 @@ class FifoStream : public StreamBase { public: FifoStream() : + StreamBase(), total_read(0), bytes_read(0), - frame_count(0), stream_type(UNKNOWN) {} diff --git a/src/zm_file_camera.cpp b/src/zm_file_camera.cpp index fd07515ad..feaad3fe0 100644 --- a/src/zm_file_camera.cpp +++ b/src/zm_file_camera.cpp @@ -46,9 +46,9 @@ FileCamera::FileCamera( p_hue, p_colour, p_capture, - p_record_audio) + p_record_audio), + path(p_path) { - path = std::string(p_path); if (capture) { Initialise(); } diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 295426254..a3662c654 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -24,6 +24,7 @@ #include "zm_utils.h" #include #include +#include #include #include @@ -80,9 +81,14 @@ imgbufcpy_fptr_t fptr_imgbufcpy; /* Font */ static ZmFont font; +std::mutex jpeg_mutex; + void Image::update_function_pointers() { - /* Because many loops are unrolled and work on 16 colours/time or 4 pixels/time, we have to meet requirements */ - if ( pixels % 16 || pixels % 12 ) { + /* Because many loops are unrolled and work on 16 colours/time or 4 pixels/time, we have to meet requirements + * previous tests were %16 or %12 but that is incorrect. Should just be %4 + */ + + if (pixels %4) { // have to use non-loop unrolled functions delta8_rgb = &std_delta8_rgb; delta8_bgr = &std_delta8_bgr; @@ -92,6 +98,7 @@ void Image::update_function_pointers() { delta8_abgr = &std_delta8_abgr; delta8_gray8 = &std_delta8_gray8; blend = &std_blend; + Debug(1, "Using slow std functions because pixels %d mod 4=%d", pixels, pixels%4); } else { // Use either sse or neon, or loop unrolled version delta8_rgb = fptr_delta8_rgb; @@ -114,22 +121,23 @@ Image::Image() : delta8_argb(&std_delta8_argb), delta8_abgr(&std_delta8_abgr), delta8_gray8(&std_delta8_gray8), - blend(&std_blend) + blend(&std_blend), + width(0), + linesize(0), + height(0), + pixels(0), + colours(0), + padding(0), + size(0), + subpixelorder(0), + allocation(0), + buffer(nullptr), + buffertype(ZM_BUFTYPE_DONTFREE), + holdbuffer(0) { - if ( !initialised ) + if (!initialised) Initialise(); - width = 0; - linesize = 0; - height = 0; - padding = 0; - pixels = 0; - colours = 0; - subpixelorder = 0; - size = 0; - allocation = 0; - buffer = 0; - buffertype = ZM_BUFTYPE_DONTFREE; - holdbuffer = 0; + // Update blend to fast function determined by Initialise, I'm sure this can be improve. blend = fptr_blend; } @@ -158,15 +166,15 @@ Image::Image(int p_width, int p_height, int p_colours, int p_subpixelorder, uint colours(p_colours), padding(p_padding), subpixelorder(p_subpixelorder), - buffer(p_buffer) { + buffer(p_buffer), + holdbuffer(0) +{ if (!initialised) Initialise(); pixels = width * height; linesize = p_width * p_colours; size = linesize * height + padding; - buffer = nullptr; - holdbuffer = 0; if (p_buffer) { allocation = size; buffertype = ZM_BUFTYPE_DONTFREE; @@ -174,7 +182,7 @@ Image::Image(int p_width, int p_height, int p_colours, int p_subpixelorder, uint } else { AllocImgBuffer(size); } - if (!subpixelorder and colours>1) { + if (!subpixelorder and (colours>1)) { // Default to RGBA when no subpixelorder is specified. subpixelorder = ZM_SUBPIX_ORDER_RGBA; } @@ -214,25 +222,26 @@ Image::Image(int p_width, int p_linesize, int p_height, int p_colours, int p_sub update_function_pointers(); } -Image::Image(const AVFrame *frame) { +Image::Image(const AVFrame *frame) : + colours(ZM_COLOUR_RGB32), + padding(0), + subpixelorder(ZM_SUBPIX_ORDER_RGBA), + imagePixFormat(AV_PIX_FMT_RGBA), + buffer(0), + holdbuffer(0) +{ width = frame->width; height = frame->height; pixels = width*height; zm_dump_video_frame(frame, "Image.Assign(frame)"); // FIXME - colours = ZM_COLOUR_RGB32; - subpixelorder = ZM_SUBPIX_ORDER_RGBA; - imagePixFormat = AV_PIX_FMT_RGBA; - //(AVPixelFormat)frame->format; + //(AVPixelFormat)frame->format; size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, width, height, 32); // av_image_get_linesize isn't aligned, so we have to do that. linesize = FFALIGN(av_image_get_linesize(AV_PIX_FMT_RGBA, width, 0), 32); - padding = 0; - buffer = nullptr; - holdbuffer = 0; AllocImgBuffer(size); this->Assign(frame); } @@ -1081,11 +1090,16 @@ bool Image::WriteJpeg(const std::string &filename, const int &quality_override, SystemTimePoint timestamp, bool on_blocking_abort) const { + if (config.colour_jpeg_files && (colours == ZM_COLOUR_GRAY8)) { Image temp_image(*this); temp_image.Colourise(ZM_COLOUR_RGB24, ZM_SUBPIX_ORDER_RGB); return temp_image.WriteJpeg(filename, quality_override, timestamp, on_blocking_abort); } + + // jpeg libs are not thread safe + std::unique_lock lck(jpeg_mutex); + int quality = quality_override ? quality_override : config.jpeg_file_quality; jpeg_compress_struct *cinfo = writejpg_ccinfo[quality]; @@ -1155,7 +1169,7 @@ bool Image::WriteJpeg(const std::string &filename, } else if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) { cinfo->in_color_space = JCS_EXT_XBGR; } else { - Warning("Unknwon subpixelorder %d", subpixelorder); + Warning("Unknown subpixelorder %d", subpixelorder); /* Assume RGBA */ cinfo->in_color_space = JCS_EXT_RGBX; } @@ -1364,6 +1378,8 @@ bool Image::EncodeJpeg(JOCTET *outbuffer, int *outbuffer_size, int quality_overr return temp_image.EncodeJpeg(outbuffer, outbuffer_size, quality_override); } + std::unique_lock lck(jpeg_mutex); + int quality = quality_override ? quality_override : config.jpeg_stream_quality; struct jpeg_compress_struct *cinfo = encodejpg_ccinfo[quality]; @@ -1677,15 +1693,15 @@ void Image::Overlay( const Image &image ) { } /* RGB32 compatible: complete */ -void Image::Overlay( const Image &image, unsigned int x, unsigned int y ) { +void Image::Overlay( const Image &image, const unsigned int lo_x, const unsigned int lo_y ) { if ( !(width < image.width || height < image.height) ) { Panic("Attempt to overlay image too big for destination, %dx%d > %dx%d", image.width, image.height, width, height ); } - if ( !(width < (x+image.width) || height < (y+image.height)) ) { + if ( !(width < (lo_x+image.width) || height < (lo_y+image.height)) ) { Panic("Attempt to overlay image outside of destination bounds, %dx%d @ %dx%d > %dx%d", - image.width, image.height, x, y, width, height ); + image.width, image.height, lo_x, lo_y, width, height ); } if ( !(colours == image.colours) ) { @@ -1693,10 +1709,8 @@ void Image::Overlay( const Image &image, unsigned int x, unsigned int y ) { colours, image.colours); } - unsigned int lo_x = x; - unsigned int lo_y = y; - unsigned int hi_x = (x+image.width)-1; - unsigned int hi_y = (y+image.height-1); + unsigned int hi_x = (lo_x+image.width)-1; + unsigned int hi_y = (lo_y+image.height-1); if ( colours == ZM_COLOUR_GRAY8 ) { const uint8_t *psrc = image.buffer; for ( unsigned int y = lo_y; y <= hi_y; y++ ) { @@ -1729,7 +1743,6 @@ void Image::Overlay( const Image &image, unsigned int x, unsigned int y ) { } // end void Image::Overlay( const Image &image, unsigned int x, unsigned int y ) void Image::Blend( const Image &image, int transparency ) { - uint8_t* new_buffer; if ( !( width == image.width && height == image.height @@ -1743,7 +1756,7 @@ void Image::Blend( const Image &image, int transparency ) { if ( transparency <= 0 ) return; - new_buffer = AllocBuffer(size); + uint8_t* new_buffer = AllocBuffer(size); #ifdef ZM_IMAGE_PROFILING TimePoint start = std::chrono::steady_clock::now(); @@ -2732,7 +2745,7 @@ void Image::Flip( bool leftright ) { AssignDirect(width, height, colours, subpixelorder, flip_buffer, size, ZM_BUFTYPE_ZM); } -void Image::Scale(unsigned int factor) { +void Image::Scale(const unsigned int factor) { if ( !factor ) { Error("Bogus scale factor %d found", factor); return; @@ -2756,15 +2769,13 @@ void Image::Scale(unsigned int factor) { unsigned int h_count = ZM_SCALE_BASE/2; unsigned int last_h_index = 0; unsigned int last_w_index = 0; - unsigned int h_index; for ( unsigned int y = 0; y < height; y++ ) { unsigned char *ps = &buffer[y*wc]; unsigned int w_count = ZM_SCALE_BASE/2; - unsigned int w_index; last_w_index = 0; for ( unsigned int x = 0; x < width; x++ ) { w_count += factor; - w_index = w_count/ZM_SCALE_BASE; + unsigned int w_index = w_count/ZM_SCALE_BASE; for (unsigned int f = last_w_index; f < w_index; f++ ) { for ( unsigned int c = 0; c < colours; c++ ) { *pd++ = *(ps+c); @@ -2774,7 +2785,7 @@ void Image::Scale(unsigned int factor) { last_w_index = w_index; } h_count += factor; - h_index = h_count/ZM_SCALE_BASE; + unsigned int h_index = h_count/ZM_SCALE_BASE; for ( unsigned int f = last_h_index+1; f < h_index; f++ ) { memcpy(pd, pd-nwc, nwc); pd += nwc; @@ -2786,17 +2797,14 @@ void Image::Scale(unsigned int factor) { } else { unsigned char *pd = scale_buffer; unsigned int wc = width*colours; - unsigned int xstart = factor/2; - unsigned int ystart = factor/2; - unsigned int h_count = ystart; + unsigned int h_count = factor/2; unsigned int last_h_index = 0; unsigned int last_w_index = 0; - unsigned int h_index; for ( unsigned int y = 0; y < height; y++ ) { h_count += factor; - h_index = h_count/ZM_SCALE_BASE; + unsigned int h_index = h_count/ZM_SCALE_BASE; if ( h_index > last_h_index ) { - unsigned int w_count = xstart; + unsigned int w_count = factor/2; unsigned int w_index; last_w_index = 0; @@ -2825,6 +2833,7 @@ void Image::Scale(unsigned int factor) { void Image::Deinterlace_Discard() { /* Simple deinterlacing. Copy the even lines into the odd lines */ + // ICON: These can be drastically improved. But who cares? if ( colours == ZM_COLOUR_GRAY8 ) { const uint8_t *psrc; @@ -3107,9 +3116,9 @@ __attribute__((noinline,__target__("sse2"))) #endif void sse2_fastblend(const uint8_t* col1, const uint8_t* col2, uint8_t* result, unsigned long count, double blendpercent) { #if ((defined(__i386__) || defined(__x86_64__) || defined(ZM_KEEP_SSE)) && !defined(ZM_STRIP_SSE)) - static uint32_t divider = 0; - static uint32_t clearmask = 0; static double current_blendpercent = 0.0; + static uint32_t clearmask = 0; + static uint32_t divider = 0; if ( current_blendpercent != blendpercent ) { /* Attempt to match the blending percent to one of the possible values */ @@ -3310,10 +3319,10 @@ void neon32_armv7_fastblend(const uint8_t* col1, const uint8_t* col2, uint8_t* r __attribute__((noinline)) void neon64_armv8_fastblend(const uint8_t* col1, const uint8_t* col2, uint8_t* result, unsigned long count, double blendpercent) { #if (defined(__aarch64__) && !defined(ZM_STRIP_NEON)) - static int8_t divider = 0; static double current_blendpercent = 0.0; + static int8_t divider = 0; - if(current_blendpercent != blendpercent) { + if (current_blendpercent != blendpercent) { /* Attempt to match the blending percent to one of the possible values */ if(blendpercent < 2.34375) { // 1.5625% blending @@ -3393,6 +3402,7 @@ __attribute__((noinline)) void neon64_armv8_fastblend(const uint8_t* col1, const } __attribute__((noinline)) void std_blend(const uint8_t* col1, const uint8_t* col2, uint8_t* result, unsigned long count, double blendpercent) { + Warning("Using slow std_blend"); double divide = blendpercent / 100.0; double opacity = 1.0 - divide; const uint8_t* const max_ptr = result + count; diff --git a/src/zm_image.h b/src/zm_image.h index 74e5931eb..8bcb92c4c 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -145,11 +145,13 @@ class Image { explicit Image(const AVFrame *frame); ~Image(); + static void Initialise(); static void Deinitialise(); inline void DumpImgBuffer() { - DumpBuffer(buffer, buffertype); + if (buffertype != ZM_BUFTYPE_DONTFREE) + DumpBuffer(buffer, buffertype); buffertype = ZM_BUFTYPE_DONTFREE; buffer = nullptr; allocation = 0; diff --git a/src/zm_libvnc_camera.cpp b/src/zm_libvnc_camera.cpp index 6fb414686..3ff7804b6 100644 --- a/src/zm_libvnc_camera.cpp +++ b/src/zm_libvnc_camera.cpp @@ -23,7 +23,7 @@ void bind_libvnc_symbols() { libvnc_lib = dlopen("libvncclient.so", RTLD_LAZY | RTLD_GLOBAL); if (!libvnc_lib) { - Error("Error loading libvncclient: %s", dlerror()); + Error("Error loading libvncclient.so: %s", dlerror()); return; } @@ -135,11 +135,6 @@ VncCamera::VncCamera( } VncCamera::~VncCamera() { - if (capture and mRfb) { - if (mRfb->frameBuffer) - free(mRfb->frameBuffer); - (*rfbClientCleanup_f)(mRfb); - } if (libvnc_lib) { dlclose(libvnc_lib); libvnc_lib = nullptr; @@ -253,6 +248,12 @@ int VncCamera::PostCapture() { } int VncCamera::Close() { + if (capture and mRfb) { + if (mRfb->frameBuffer) + free(mRfb->frameBuffer); + (*rfbClientCleanup_f)(mRfb); + mRfb = nullptr; + } return 1; } #endif diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index d808ddba0..853504a31 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #if ZM_MEM_MAPPED #include @@ -66,11 +67,19 @@ #define MAP_LOCKED 0 #endif +#ifdef WITH_GSOAP +//Workaround for the gsoap library on RHEL +struct Namespace namespaces[] = +{ + {NULL, NULL} // end of table +}; +#endif + // This is the official SQL (and ordering of the fields) to load a Monitor. // It will be used whereever a Monitor dbrow is needed. WHERE conditions can be appended std::string load_monitor_sql = -"SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Function`+0, `Enabled`, `DecodingEnabled`, " -"`LinkedMonitors`, `AnalysisFPSLimit`, `AnalysisUpdateDelay`, `MaxFPS`, `AlarmMaxFPS`," +"SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Function`+0, `Enabled`, `DecodingEnabled`, `JanusEnabled`, `JanusAudioEnabled`," +"`LinkedMonitors`, `EventStartCommand`, `EventEndCommand`, `AnalysisFPSLimit`, `AnalysisUpdateDelay`, `MaxFPS`, `AlarmMaxFPS`," "`Device`, `Channel`, `Format`, `V4LMultiBuffer`, `V4LCapturesPerFrame`, " // V4L Settings "`Protocol`, `Method`, `Options`, `User`, `Pass`, `Host`, `Port`, `Path`, `SecondPath`, `Width`, `Height`, `Colours`, `Palette`, `Orientation`+0, `Deinterlacing`, " "`DecoderHWAccelName`, `DecoderHWAccelDevice`, `RTSPDescribe`, " @@ -83,6 +92,7 @@ std::string load_monitor_sql = "`SectionLength`, `MinSectionLength`, `FrameSkip`, `MotionFrameSkip`, " "`FPSReportInterval`, `RefBlendPerc`, `AlarmRefBlendPerc`, `TrackMotion`, `Exif`," "`RTSPServer`, `RTSPStreamName`," +"`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, " "`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1 FROM `Monitors`"; std::string CameraType_Strings[] = { @@ -120,175 +130,7 @@ std::string TriggerState_Strings[] = { "Cancel", "On", "Off" }; -Monitor::MonitorLink::MonitorLink(unsigned int p_id, const char *p_name) : - id(p_id), - shared_data(nullptr), - trigger_data(nullptr), - video_store_data(nullptr) -{ - strncpy(name, p_name, sizeof(name)-1); - -#if ZM_MEM_MAPPED - map_fd = -1; - mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), id); -#else // ZM_MEM_MAPPED - shm_id = 0; -#endif // ZM_MEM_MAPPED - mem_size = 0; - mem_ptr = nullptr; - - last_event_id = 0; - last_state = IDLE; - - last_connect_time = 0; - connected = false; -} - -Monitor::MonitorLink::~MonitorLink() { - disconnect(); -} - -bool Monitor::MonitorLink::connect() { - SystemTimePoint now = std::chrono::system_clock::now(); - if (!last_connect_time || (now - std::chrono::system_clock::from_time_t(last_connect_time)) > Seconds(60)) { - last_connect_time = std::chrono::system_clock::to_time_t(now); - - mem_size = sizeof(SharedData) + sizeof(TriggerData); - - Debug(1, "link.mem.size=%jd", static_cast(mem_size)); -#if ZM_MEM_MAPPED - map_fd = open(mem_file.c_str(), O_RDWR, (mode_t)0600); - if (map_fd < 0) { - Debug(3, "Can't open linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); - disconnect(); - return false; - } - while (map_fd <= 2) { - int new_map_fd = dup(map_fd); - Warning("Got one of the stdio fds for our mmap handle. map_fd was %d, new one is %d", map_fd, new_map_fd); - close(map_fd); - map_fd = new_map_fd; - } - - struct stat map_stat; - if (fstat(map_fd, &map_stat) < 0) { - Error("Can't stat linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); - disconnect(); - return false; - } - - if (map_stat.st_size == 0) { - Error("Linked memory map file %s is empty: %s", mem_file.c_str(), strerror(errno)); - 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(mem_size)); - disconnect(); - return false; - } - - mem_ptr = (unsigned char *)mmap(nullptr, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, 0); - if (mem_ptr == MAP_FAILED) { - Error("Can't map file %s (%jd bytes) to memory: %s", mem_file.c_str(), static_cast(mem_size), strerror(errno)); - disconnect(); - return false; - } -#else // ZM_MEM_MAPPED - shm_id = shmget((config.shm_key&0xffff0000)|id, mem_size, 0700); - if (shm_id < 0) { - Debug(3, "Can't shmget link memory: %s", strerror(errno)); - connected = false; - return false; - } - mem_ptr = (unsigned char *)shmat(shm_id, 0, 0); - if ((int)mem_ptr == -1) { - Debug(3, "Can't shmat link memory: %s", strerror(errno)); - connected = false; - return false; - } -#endif // ZM_MEM_MAPPED - - shared_data = (SharedData *)mem_ptr; - trigger_data = (TriggerData *)((char *)shared_data + sizeof(SharedData)); - - if (!shared_data->valid) { - Debug(3, "Linked memory not initialised by capture daemon"); - disconnect(); - return false; - } - - last_state = shared_data->state; - last_event_id = shared_data->last_event_id; - connected = true; - - return true; - } - return false; -} // end bool Monitor::MonitorLink::connect() - -bool Monitor::MonitorLink::disconnect() { - if (connected) { - connected = false; - -#if ZM_MEM_MAPPED - if (mem_ptr > (void *)0) { - msync(mem_ptr, mem_size, MS_ASYNC); - munmap(mem_ptr, mem_size); - } - if (map_fd >= 0) - close(map_fd); - - map_fd = -1; -#else // ZM_MEM_MAPPED - struct shmid_ds shm_data; - if (shmctl(shm_id, IPC_STAT, &shm_data) < 0) { - Debug(3, "Can't shmctl: %s", strerror(errno)); - return false; - } - - shm_id = 0; - - if (shm_data.shm_nattch <= 1) { - if (shmctl(shm_id, IPC_RMID, 0) < 0) { - Debug(3, "Can't shmctl: %s", strerror(errno)); - return false; - } - } - - if (shmdt(mem_ptr) < 0) { - Debug(3, "Can't shmdt: %s", strerror(errno)); - return false; - } -#endif // ZM_MEM_MAPPED - mem_size = 0; - mem_ptr = nullptr; - } - return true; -} - -bool Monitor::MonitorLink::isAlarmed() { - if (!connected) { - return false; - } - return( shared_data->state == ALARM ); -} - -bool Monitor::MonitorLink::inAlarm() { - if (!connected) { - return false; - } - return( shared_data->state == ALARM || shared_data->state == ALERT ); -} - -bool Monitor::MonitorLink::hasAlarmed() { - if (shared_data->state == ALARM) { - return true; - } - last_event_id = shared_data->last_event_id; - return false; -} - -Monitor::Monitor() +Monitor::Monitor() : id(0), name(""), server_id(0), @@ -297,6 +139,8 @@ Monitor::Monitor() function(NONE), enabled(false), decoding_enabled(false), + janus_enabled(false), + janus_audio_enabled(false), //protocol //method //options @@ -305,7 +149,7 @@ Monitor::Monitor() //user //pass //path - //device + //device palette(0), channel(0), format(0), @@ -375,7 +219,6 @@ Monitor::Monitor() first_alarm_count(0), last_alarm_count(0), last_signal(false), - last_section_mod(0), buffer_count(0), state(IDLE), last_motion_score(0), @@ -411,6 +254,12 @@ Monitor::Monitor() privacy_bitmask(nullptr), n_linked_monitors(0), linked_monitors(nullptr), + Event_Poller_Closes_Event(FALSE), + Janus_Manager(nullptr), + Amcrest_Manager(nullptr), +#ifdef WITH_GSOAP + soap(nullptr), +#endif red_val(0), green_val(0), blue_val(0), @@ -426,7 +275,6 @@ Monitor::Monitor() event_close_mode = CLOSE_IDLE; event = nullptr; - last_section_mod = 0; adaptive_skip = true; @@ -435,7 +283,7 @@ Monitor::Monitor() /* std::string load_monitor_sql = - "SELECT Id, Name, ServerId, StorageId, Type, Function+0, Enabled, DecodingEnabled, LinkedMonitors, " + "SELECT Id, Name, ServerId, StorageId, Type, Function+0, Enabled, DecodingEnabled, JanusEnabled, JanusAudioEnabled, LinkedMonitors, `EventStartCommand`, `EventEndCommand`, " "AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS," "Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, " // V4L Settings "Protocol, Method, Options, User, Pass, Host, Port, Path, SecondPath, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, " @@ -447,6 +295,7 @@ Monitor::Monitor() "SectionLength, MinSectionLength, FrameSkip, MotionFrameSkip, " "FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif," "`RTSPServer`,`RTSPStreamName`, + "`ONVIF_URL`, `ONVIF_Username`, `ONVIF_Password`, `ONVIF_Options`, `ONVIF_Event_Listener`, `use_Amcrest_API`, " "SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */ @@ -475,7 +324,7 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { } else if ( ! strcmp(dbrow[col], "Libvlc") ) { type = LIBVLC; } else if ( ! strcmp(dbrow[col], "cURL") ) { - type = CURL; + type = LIBCURL; } else if ( ! strcmp(dbrow[col], "VNC") ) { type = VNC; } else { @@ -487,8 +336,12 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; decoding_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; // See below after save_jpegs for a recalculation of decoding_enabled + janus_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; + janus_audio_enabled = dbrow[col] ? atoi(dbrow[col]) : false; col++; ReloadLinkedMonitors(dbrow[col]); col++; + event_start_command = dbrow[col] ? dbrow[col] : ""; col++; + event_end_command = dbrow[col] ? dbrow[col] : ""; col++; /* "AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS," */ analysis_fps_limit = dbrow[col] ? strtod(dbrow[col], nullptr) : 0.0; col++; @@ -615,6 +468,13 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { rtsp_server = (*dbrow[col] != '0'); col++; rtsp_streamname = dbrow[col]; col++; + onvif_url = std::string(dbrow[col] ? dbrow[col] : ""); col++; + onvif_username = std::string(dbrow[col] ? dbrow[col] : ""); col++; + onvif_password = std::string(dbrow[col] ? dbrow[col] : ""); col++; + onvif_options = std::string(dbrow[col] ? dbrow[col] : ""); col++; + onvif_event_listener = (*dbrow[col] != '0'); col++; + use_Amcrest_API = (*dbrow[col] != '0'); col++; + /*"SignalCheckPoints, SignalCheckColour, Importance-1 FROM Monitors"; */ signal_check_points = atoi(dbrow[col]); col++; signal_check_colour = strtol(dbrow[col][0] == '#' ? dbrow[col]+1 : dbrow[col], 0, 16); col++; @@ -648,8 +508,9 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { mem_size = sizeof(SharedData) + sizeof(TriggerData) + sizeof(VideoStoreData) //Information to pass back to the capture process - + (image_buffer_count * sizeof(struct timeval)) - + (image_buffer_count * image_size) + + (image_buffer_count*sizeof(struct timeval)) + + (image_buffer_count*image_size) + + image_size // alarm_image + 64; /* Padding used to permit aligning the images buffer to 64 byte boundary */ Debug(1, @@ -838,7 +699,7 @@ void Monitor::LoadCamera() { #endif // HAVE_LIBVLC break; } - case CURL: { + case LIBCURL: { #if HAVE_LIBCURL camera = zm::make_unique(this, path.c_str(), @@ -1005,9 +866,16 @@ bool Monitor::connect() { image_buffer[i] = new Image(width, height, camera->Colours(), camera->SubpixelOrder(), &(shared_images[i*camera->ImageSize()])); image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ } + alarm_image.AssignDirect(width, height, camera->Colours(), camera->SubpixelOrder(), + &(shared_images[image_buffer_count*camera->ImageSize()]), + camera->ImageSize(), + ZM_BUFTYPE_DONTFREE + ); + alarm_image.HoldBuffer(true); /* Don't release the internal buffer or replace it with another */ Debug(3, "Allocated %zu %zu image buffers", image_buffer.capacity(), image_buffer.size()); if (purpose == CAPTURE) { + curl_global_init(CURL_GLOBAL_DEFAULT); //May not be the appropriate place. Need to do this before any other curl calls, and any other threads start. memset(mem_ptr, 0, mem_size); shared_data->size = sizeof(SharedData); shared_data->active = enabled; @@ -1046,6 +914,60 @@ bool Monitor::connect() { video_store_data->size = sizeof(VideoStoreData); usedsubpixorder = camera->SubpixelOrder(); // Used in CheckSignal shared_data->valid = true; + + + //ONVIF and Amcrest Setup + //For now, only support one event type per camera, so share some state. + Poll_Trigger_State = FALSE; + if (onvif_event_listener) { // + Debug(1, "Starting ONVIF"); + Event_Poller_Healthy = FALSE; + if (onvif_options.find("closes_event") != std::string::npos) { //Option to indicate that ONVIF will send a close event message + Event_Poller_Closes_Event = TRUE; + } + if (use_Amcrest_API) { + Amcrest_Manager = new AmcrestAPI(this); + } else { //using GSOAP +#ifdef WITH_GSOAP + tev__PullMessages.Timeout = "PT600S"; + tev__PullMessages.MessageLimit = 100; + soap = soap_new(); + soap->connect_timeout = 5; + soap->recv_timeout = 5; + soap->send_timeout = 5; + soap_register_plugin(soap, soap_wsse); + proxyEvent = PullPointSubscriptionBindingProxy(soap); + std::string full_url = onvif_url + "/Events"; + proxyEvent.soap_endpoint = full_url.c_str(); + + set_credentials(soap); + Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); + if (proxyEvent.CreatePullPointSubscription(&request, response) != SOAP_OK) { + Error("Couldn't create subscription! %s, %s", soap_fault_string(soap), soap_fault_detail(soap)); + } else { + //Empty the stored messages + set_credentials(soap); + if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) && + ( soap->error != SOAP_EOF)) { //SOAP_EOF could indicate no messages to pull. + Error("Couldn't do initial event pull! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); + } else { + Debug(1, "Good Initial ONVIF Pull"); + Event_Poller_Healthy = TRUE; + } + } +#else + Error("zmc not compiled with GSOAP. ONVIF support not built in!"); +#endif + } + } else { + Debug(1, "Not Starting ONVIF"); + } + //End ONVIF Setup + + if (janus_enabled) { + Janus_Manager = new JanusManager(this); + } + } else if (!shared_data->valid) { Error("Shared data not initialised by capture daemon for monitor %s", name.c_str()); return false; @@ -1066,6 +988,7 @@ bool Monitor::disconnect() { } if (purpose == CAPTURE) { + alarm_image.HoldBuffer(false); /* Allow to reset buffer */ if (unlink(mem_file.c_str()) < 0) { Warning("Can't unlink '%s': %s", mem_file.c_str(), strerror(errno)); } @@ -1140,6 +1063,12 @@ Monitor::~Monitor() { sws_freeContext(convert_context); convert_context = nullptr; } + if (Amcrest_Manager != nullptr) { + delete Amcrest_Manager; + } + if (purpose == CAPTURE) { + curl_global_cleanup(); //not sure about this location. + } } // end Monitor::~Monitor() void Monitor::AddPrivacyBitmask() { @@ -1164,6 +1093,10 @@ void Monitor::AddPrivacyBitmask() { privacy_bitmask = privacy_image->Buffer(); } +Image *Monitor::GetAlarmImage() { + return &alarm_image; +} + int Monitor::GetImage(int32_t index, int scale) { if (index < 0 || index > image_buffer_count) { Debug(1, "Invalid index %d passed. image_buffer_count = %d", index, image_buffer_count); @@ -1178,26 +1111,23 @@ int Monitor::GetImage(int32_t index, int scale) { return 0; } - Image *image; + std::string filename = stringtf("Monitor%u.jpg", id); // If we are going to be modifying the snapshot before writing, then we need to copy it if ((scale != ZM_SCALE_BASE) || (!config.timestamp_on_capture)) { - alarm_image.Assign(*image_buffer[index]); + Image image; + image.Assign(*image_buffer[index]); if (scale != ZM_SCALE_BASE) { - alarm_image.Scale(scale); + image.Scale(scale); } if (!config.timestamp_on_capture) { - TimestampImage(&alarm_image, SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); + TimestampImage(&image, SystemTimePoint(zm::chrono::duration_cast(shared_timestamps[index]))); } - image = &alarm_image; + return image.WriteJpeg(filename); } else { - image = image_buffer[index]; + return image_buffer[index]->WriteJpeg(filename); } - - std::string filename = stringtf("Monitor%u.jpg", id); - image->WriteJpeg(filename); - return 1; } ZMPacket *Monitor::getSnapshot(int index) const { @@ -1705,6 +1635,70 @@ void Monitor::UpdateFPS() { } // end if report fps } // void Monitor::UpdateFPS() +//Thread where ONVIF polling, and other similar status polling can happen. +//Since these can be blocking, run here to avoid intefering with other processing +bool Monitor::Poll() { + + //We want to trigger every 5 seconds or so. so grab the time at the beginning of the loop, and sleep at the end. + std::chrono::system_clock::time_point loop_start_time = std::chrono::system_clock::now(); + + if (Event_Poller_Healthy) { + if(use_Amcrest_API) { + Amcrest_Manager->WaitForMessage(); + } else { + +#ifdef WITH_GSOAP + set_credentials(soap); + int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, NULL, &tev__PullMessages, tev__PullMessagesResponse); + if (result != SOAP_OK) { + if (result != SOAP_EOF) { //Ignore the timeout error + Error("Failed to get ONVIF messages! %s", soap_fault_string(soap)); + Event_Poller_Healthy = FALSE; + } + } else { + Debug(1, "Got Good Response! %i", result); + for (auto msg : tev__PullMessagesResponse.wsnt__NotificationMessage) { + if (msg->Topic->__any.text != NULL && + std::strstr(msg->Topic->__any.text, "MotionAlarm") && + msg->Message.__any.elts != NULL && + msg->Message.__any.elts->next != NULL && + msg->Message.__any.elts->next->elts != NULL && + msg->Message.__any.elts->next->elts->atts != NULL && + msg->Message.__any.elts->next->elts->atts->next != NULL && + msg->Message.__any.elts->next->elts->atts->next->text != NULL) { + Debug(1,"Got Motion Alarm!"); + if (strcmp(msg->Message.__any.elts->next->elts->atts->next->text, "true") == 0) { + //Event Start + Debug(1,"Triggered on ONVIF"); + if (!Poll_Trigger_State) { + Debug(1,"Triggered Event"); + Poll_Trigger_State = TRUE; + std::this_thread::sleep_for (std::chrono::seconds(1)); //thread sleep + } + } else { + Debug(1, "Triggered off ONVIF"); + Poll_Trigger_State = FALSE; + if (!Event_Poller_Closes_Event) { //If we get a close event, then we know to expect them. + Event_Poller_Closes_Event = TRUE; + Debug(1,"Setting ClosesEvent"); + } + } + } + } + } +#endif + } + } + if (janus_enabled) { + + if (Janus_Manager->check_janus() == 0) { + Janus_Manager->add_to_janus(); + } + } + std::this_thread::sleep_until(loop_start_time + std::chrono::seconds(5)); + return TRUE; +} //end Poll + // Would be nice if this JUST did analysis // This idea is that we should be analysing as close to the capture frame as possible. // This function should process as much as possible before returning @@ -1726,6 +1720,12 @@ bool Monitor::Analyse() { packetqueue.increment_it(analysis_it); return false; } + // Ready means that we have captured the warmup # of frames + if (!Ready()) { + Debug(3, "Not ready?"); + delete packet_lock; + return false; + } // signal is set by capture bool signal = shared_data->signal; @@ -1734,525 +1734,476 @@ bool Monitor::Analyse() { Debug(3, "Motion detection is enabled signal(%d) signal_change(%d) trigger state(%s) image index %d", signal, signal_change, TriggerState_Strings[trigger_data->trigger_state].c_str(), snap->image_index); - // Need to guard around event creation/deletion from Reload() - std::lock_guard lck(event_mutex); - Debug(3, "Have event lock"); + { // scope for event lock + // Need to guard around event creation/deletion from Reload() + std::lock_guard lck(event_mutex); - // if we have been told to be OFF, then we are off and don't do any processing. - if (trigger_data->trigger_state != TriggerState::TRIGGER_OFF) { - Debug(4, "Trigger not OFF state is (%d)", int(trigger_data->trigger_state)); - int score = 0; - // Ready means that we have captured the warmup # of frames - if (!Ready()) { - Debug(3, "Not ready?"); - delete packet_lock; - return false; - } + // if we have been told to be OFF, then we are off and don't do any processing. + if (trigger_data->trigger_state != TriggerState::TRIGGER_OFF) { + Debug(4, "Trigger not OFF state is (%d)", int(trigger_data->trigger_state)); - std::string cause; - Event::StringSetMap noteSetMap; + int score = 0; + std::string cause; + Event::StringSetMap noteSetMap; - // Specifically told to be on. Setting the score here will trigger the alarm. - if (trigger_data->trigger_state == TriggerState::TRIGGER_ON) { - score += trigger_data->trigger_score; - Debug(1, "Triggered on score += %d => %d", trigger_data->trigger_score, score); - if (!event) { - cause += trigger_data->trigger_cause; - } - Event::StringSet noteSet; - noteSet.insert(trigger_data->trigger_text); - noteSetMap[trigger_data->trigger_cause] = noteSet; - } // end if trigger_on - - // FIXME this snap might not be the one that caused the signal change. Need to store that in the packet. - if (signal_change) { - Debug(2, "Signal change, new signal is %d", signal); - const char *signalText = "Unknown"; - if (!signal) { - signalText = "Lost"; - if (event) { - Info("%s: %03d - Closing event %" PRIu64 ", signal loss", name.c_str(), analysis_image_count, event->Id()); - closeEvent(); - last_section_mod = 0; - } - } else { - signalText = "Reacquired"; - score += 100; - } - if (!event) { - if (cause.length()) cause += ", "; - cause += SIGNAL_CAUSE; - } - Event::StringSet noteSet; - noteSet.insert(signalText); - noteSetMap[SIGNAL_CAUSE] = noteSet; - shared_data->state = state = IDLE; - shared_data->active = signal; - if ((function == MODECT or function == MOCORD) and snap->image) - ref_image.Assign(*(snap->image)); - } // end if signal change - - if (signal) { - if (snap->codec_type == AVMEDIA_TYPE_VIDEO) { - // Check to see if linked monitors are triggering. - if (n_linked_monitors > 0) { - Debug(1, "Checking linked monitors"); - // FIXME improve logic here - bool first_link = true; +#ifdef WITH_GSOAP + if (onvif_event_listener && Event_Poller_Healthy) { + if (Poll_Trigger_State) { + score += 9; + Debug(1, "Triggered on ONVIF"); Event::StringSet noteSet; - for (int i = 0; i < n_linked_monitors; i++) { - // TODO: Shouldn't we try to connect? - if (linked_monitors[i]->isConnected()) { - Debug(1, "Linked monitor %d %s is connected", - linked_monitors[i]->Id(), linked_monitors[i]->Name()); - if (linked_monitors[i]->hasAlarmed()) { - Debug(1, "Linked monitor %d %s is alarmed", + noteSet.insert("ONVIF2"); + noteSetMap[MOTION_CAUSE] = noteSet; + cause += "ONVIF"; + //If the camera isn't going to send an event close, we need to close it here, but only after it has actually triggered an alarm. + if (!Event_Poller_Closes_Event && state == ALARM) + Poll_Trigger_State = FALSE; + } // end ONVIF_Trigger + } // end if (onvif_event_listener && Event_Poller_Healthy) +#endif + + // Specifically told to be on. Setting the score here is not enough to trigger the alarm. Must jump directly to ALARM + if (trigger_data->trigger_state == TriggerState::TRIGGER_ON) { + score += trigger_data->trigger_score; + Debug(1, "Triggered on score += %d => %d", trigger_data->trigger_score, score); + if (!cause.empty()) cause += ", "; + cause += trigger_data->trigger_cause; + Event::StringSet noteSet; + noteSet.insert(trigger_data->trigger_text); + noteSetMap[trigger_data->trigger_cause] = noteSet; + shared_data->state = state = ALARM; + } // end if trigger_on + + // FIXME this snap might not be the one that caused the signal change. Need to store that in the packet. + if (signal_change) { + Debug(2, "Signal change, new signal is %d", signal); + if (!signal) { + if (event) { + event->addNote(SIGNAL_CAUSE, "Lost"); + Info("%s: %03d - Closing event %" PRIu64 ", signal loss", name.c_str(), analysis_image_count, event->Id()); + closeEvent(); + } + } else if (function == MOCORD or function == RECORD) { + if (!event) { + if (!cause.empty()) cause += ", "; + cause += SIGNAL_CAUSE + std::string(": Reacquired"); + } else { + event->addNote(SIGNAL_CAUSE, "Reacquired"); + } + if (0 and snap->in_frame && ( + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUV420P) + || + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUVJ420P) + ) ) { + Debug(1, "assigning refimage from v-channel"); + Image v_image(snap->in_frame->width, + snap->in_frame->height, 1, ZM_SUBPIX_ORDER_NONE, snap->in_frame->data[0], 0); + ref_image.Assign(v_image); + } else if (snap->image) { + Debug(1, "assigning refimage from snap->image"); + ref_image.Assign(*(snap->image)); + } + } + shared_data->state = state = IDLE; + shared_data->active = signal; + } // end if signal change + + if (signal) { + if (snap->codec_type == AVMEDIA_TYPE_VIDEO) { + // Check to see if linked monitors are triggering. + if (n_linked_monitors > 0) { + Debug(1, "Checking linked monitors"); + // FIXME improve logic here + bool first_link = true; + Event::StringSet noteSet; + for (int i = 0; i < n_linked_monitors; i++) { + // TODO: Shouldn't we try to connect? + if (linked_monitors[i]->isConnected()) { + Debug(1, "Linked monitor %d %s is connected", linked_monitors[i]->Id(), linked_monitors[i]->Name()); - if (!event) { - if (first_link) { - if (cause.length()) - cause += ", "; - cause += LINKED_CAUSE; - first_link = false; + if (linked_monitors[i]->hasAlarmed()) { + Debug(1, "Linked monitor %d %s is alarmed score will be %d", + linked_monitors[i]->Id(), linked_monitors[i]->Name(), linked_monitors[i]->lastFrameScore()); + if (!event) { + if (first_link) { + if (cause.length()) + cause += ", "; + cause += LINKED_CAUSE; + first_link = false; + } } + noteSet.insert(linked_monitors[i]->Name()); + score += linked_monitors[i]->lastFrameScore(); // 50; + } else { + Debug(1, "Linked monitor %d %s is not alarmed", + linked_monitors[i]->Id(), linked_monitors[i]->Name()); } - noteSet.insert(linked_monitors[i]->Name()); - score += linked_monitors[i]->lastFrameScore(); // 50; } else { - Debug(1, "Linked monitor %d %s is not alarmed", - linked_monitors[i]->Id(), linked_monitors[i]->Name()); + Debug(1, "Linked monitor %d %d is not connected. Connecting.", i, linked_monitors[i]->Id()); + linked_monitors[i]->connect(); } - } else { - Debug(1, "Linked monitor %d %d is not connected. Connecting.", i, linked_monitors[i]->Id()); - linked_monitors[i]->connect(); - } - } // end foreach linked_monitor - if (noteSet.size() > 0) - noteSetMap[LINKED_CAUSE] = noteSet; - } // end if linked_monitors + } // end foreach linked_monitor + if (noteSet.size() > 0) + noteSetMap[LINKED_CAUSE] = noteSet; + } // end if linked_monitors - /* try to stay behind the decoder. */ - if (decoding_enabled) { - while (!snap->decoded and !zm_terminate and !analysis_thread->Stopped()) { - // Need to wait for the decoder thread. - Debug(1, "Waiting for decode"); - packetqueue.unlock(packet_lock); // This will delete packet_lock and notify_all - packetqueue.wait(); - - // Another thread may have moved our it. Unlikely but possible - packet_lock = packetqueue.get_packet(analysis_it); - if (!packet_lock) return false; - snap = packet_lock->packet_; - - if (!snap->image and snap->decoded) { - Debug(1, "No image but was decoded, giving up"); + /* try to stay behind the decoder. */ + if (decoding_enabled) { + while (!snap->decoded and !zm_terminate and !analysis_thread->Stopped()) { + // Need to wait for the decoder thread. + // decoder thread might be waiting on the lock for this packet. + // So we need to relinquish the lock and wait. Waiting automatically relinquishes the lock + // So... + Debug(1, "Waiting for decode"); + packet_lock->wait(); + } // end while ! decoded + if (zm_terminate or analysis_thread->Stopped()) { delete packet_lock; return false; } - } // end while ! decoded - if (zm_terminate) { - delete packet_lock; - return false; - } - } // end if decoding enabled + } // end if decoding enabled - SystemTimePoint timestamp = snap->timestamp; + if (Active() and (function == MODECT or function == MOCORD)) { + Debug(3, "signal and active and modect"); + Event::StringSet zoneSet; - if (Active() and (function == MODECT or function == MOCORD)) { - Debug(3, "signal and active and modect"); - Event::StringSet zoneSet; + if (analysis_fps_limit) { + double capture_fps = get_capture_fps(); + motion_frame_skip = capture_fps / analysis_fps_limit; + Debug(1, "Recalculating motion_frame_skip (%d) = capture_fps(%f) / analysis_fps(%f)", + motion_frame_skip, capture_fps, analysis_fps_limit); + } - if (analysis_fps_limit) { - double capture_fps = get_capture_fps(); - motion_frame_skip = capture_fps / analysis_fps_limit; - Debug(1, "Recalculating motion_frame_skip (%d) = capture_fps(%f) / analysis_fps(%f)", - motion_frame_skip, capture_fps, analysis_fps_limit); - } - - if (!(analysis_image_count % (motion_frame_skip+1))) { if (snap->image) { // decoder may not have been able to provide an image if (!ref_image.Buffer()) { - Debug(1, "Assigning instead of Dectecting"); - ref_image.Assign(*(snap->image)); - } else { + Debug(1, "Assigning instead of Detecting"); + if (0 and snap->in_frame && ( + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUV420P) + || + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUVJ420P) + ) ) { + Debug(1, "assigning refimage from v-channel"); + Image v_image(snap->in_frame->width, snap->in_frame->height, 1, ZM_SUBPIX_ORDER_NONE, snap->in_frame->data[0], 0); + ref_image.Assign(v_image); + } else { + Debug(1, "assigning refimage from snap->image"); + ref_image.Assign(*(snap->image)); + } + alarm_image.Assign(*(snap->image)); + } else if (!(analysis_image_count % (motion_frame_skip+1))) { Debug(1, "Detecting motion on image %d, image %p", snap->image_index, snap->image); // Get new score. - int motion_score = DetectMotion(*(snap->image), zoneSet); + if (0 and snap->in_frame && ( + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUV420P) + || + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUVJ420P) + ) ) { + Image v_image(snap->in_frame->width, snap->in_frame->height, 1, ZM_SUBPIX_ORDER_NONE, snap->in_frame->data[0], 0); + snap->score = DetectMotion(v_image, zoneSet); + } else { + snap->score = DetectMotion(*(snap->image), zoneSet); + } + if (!snap->analysis_image) + snap->analysis_image = new Image(*(snap->image)); + // lets construct alarm cause. It will contain cause + names of zones alarmed snap->zone_stats.reserve(zones.size()); for (const Zone &zone : zones) { const ZoneStats &stats = zone.GetStats(); stats.DumpToLog("After detect motion"); snap->zone_stats.push_back(stats); + if (zone.Alarmed()) { + if (!snap->alarm_cause.empty()) snap->alarm_cause += ","; + snap->alarm_cause += std::string(zone.Label()); + if (zone.AlarmImage()) + snap->analysis_image->Overlay(*(zone.AlarmImage())); + } } - + alarm_image.Assign(*(snap->analysis_image)); Debug(3, "After motion detection, score:%d last_motion_score(%d), new motion score(%d)", - score, last_motion_score, motion_score); + score, last_motion_score, snap->score); motion_frame_count += 1; - last_motion_score = motion_score; - if (motion_score) { + last_motion_score = snap->score; + + if (snap->score) { if (cause.length()) cause += ", "; - cause += MOTION_CAUSE; + cause += MOTION_CAUSE + std::string(":") + snap->alarm_cause; noteSetMap[MOTION_CAUSE] = zoneSet; + score += snap->score; } // end if motion_score + } else { + Debug(1, "Skipped motion detection last motion score was %d", last_motion_score); + alarm_image.Assign(*(snap->image)); } } else { Debug(1, "no image so skipping motion detection"); } // end if has image + //score += last_motion_score; } else { - Debug(1, "Skipped motion detection last motion score was %d", last_motion_score); - } - score += last_motion_score; - } else { - Debug(1, "Not Active(%d) enabled %d active %d doing motion detection: %d", - Active(), enabled, shared_data->active, - (function == MODECT or function == MOCORD) - ); - } // end if active and doing motion detection + Debug(1, "Not Active(%d) enabled %d shared->active %d doing motion detection: %d", + Active(), enabled, shared_data->active, + (function == MODECT or function == MOCORD) + ); + } // end if active and doing motion detection - if (function == RECORD or function == MOCORD) { - // If doing record, check to see if we need to close the event or not. - if (event) { - Debug(2, "Have event %" PRIu64 " in record", event->Id()); - if (section_length != Seconds(0) && (timestamp - event->StartTime() >= section_length) - && ((function == MOCORD && event_close_mode != CLOSE_TIME) - || (function == RECORD && event_close_mode == CLOSE_TIME) - || std::chrono::duration_cast(timestamp.time_since_epoch()) % section_length == Seconds(0))) { - Info("%s: %03d - Closing event %" PRIu64 ", section end forced %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64 , - name.c_str(), - image_count, - event->Id(), - static_cast(std::chrono::duration_cast(timestamp.time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(event->StartTime().time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(timestamp - event->StartTime()).count()), - static_cast(Seconds(section_length).count())); - closeEvent(); - } // end if section_length - } // end if event + // Set this before any state changes so that it's value is picked up immediately by linked monitors + shared_data->last_frame_score = score; - if (!event) { - Debug(2, "Creating continuous event"); - if (!snap->keyframe and (videowriter == PASSTHROUGH)) { - // Must start on a keyframe so rewind. Only for passthrough though I guess. - // FIXME this iterator is not protected from invalidation - packetqueue_iterator *start_it = packetqueue.get_event_start_packet_it( - *analysis_it, 0 /* pre_event_count */ - ); - - // This gets a lock on the starting packet - - ZMLockedPacket *starting_packet_lock = nullptr; - std::shared_ptr starting_packet = nullptr; - if (*start_it != *analysis_it) { - starting_packet_lock = packetqueue.get_packet(start_it); - if (!starting_packet_lock) { - Warning("Unable to get starting packet lock"); - delete packet_lock; - return false; - } - starting_packet = starting_packet_lock->packet_; - } else { - starting_packet = snap; + // If motion detecting, score will be > 0 on motion, but if skipping frames, might not be. So also test snap->score + if ((score > 0) or ((snap->score > 0) and (function != MONITOR))) { + if ((state == IDLE) || (state == TAPE) || (state == PREALARM)) { + // If we should end then previous continuous event and start a new non-continuous event + if (event && event->Frames() + && (event->AlarmFrames() < alarm_frame_count) + && (event_close_mode == CLOSE_ALARM) + // FIXME since we won't be including this snap in the event if we close it, we should be looking at event->duration() instead + && (event->Duration() >= min_section_length) + && ((!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count - 1))) { + Info("%s: %03d - Closing event %" PRIu64 ", continuous end, alarm begins", + name.c_str(), snap->image_index, event->Id()); + closeEvent(); + } else if (event) { + // This is so if we need more than 1 alarm frame before going into alarm, so it is basically if we have enough alarm frames + Debug(3, + "pre_alarm_count in event %d of %d, event frames %d, alarm frames %d event length %" PRIi64 " >=? %" PRIi64 " min close mode is ALARM? %d", + Event::PreAlarmCount(), pre_event_count, + event->Frames(), + event->AlarmFrames(), + static_cast(std::chrono::duration_cast(event->Duration()).count()), + static_cast(Seconds(min_section_length).count()), + (event_close_mode == CLOSE_ALARM)); } + if ((!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count-1)) { + Info("%s: %03d - Gone into alarm state PreAlarmCount: %u > AlarmFrameCount:%u Cause:%s", + name.c_str(), snap->image_index, Event::PreAlarmCount(), alarm_frame_count, cause.c_str()); + shared_data->state = state = ALARM; - event = new Event(this, starting_packet->timestamp, "Continuous", noteSetMap); - // Write out starting packets, do not modify packetqueue it will garbage collect itself - while (starting_packet and ((*start_it) != *analysis_it)) { - event->AddPacket(starting_packet); - // Have added the packet, don't want to unlock it until we have locked the next - - packetqueue.increment_it(start_it); - if ((*start_it) == *analysis_it) { - if (starting_packet_lock) delete starting_packet_lock; - break; - } - ZMLockedPacket *lp = packetqueue.get_packet(start_it); - delete starting_packet_lock; - if (!lp) return false; - starting_packet_lock = lp; - starting_packet = lp->packet_; + } else if (state != PREALARM) { + Info("%s: %03d - Gone into prealarm state", name.c_str(), analysis_image_count); + shared_data->state = state = PREALARM; } - packetqueue.free_it(start_it); - delete start_it; - start_it = nullptr; + } else if (state == ALERT) { + alert_to_alarm_frame_count--; + Info("%s: %03d - Alarmed frame while in alert state. Consecutive alarmed frames left to return to alarm state: %03d", + name.c_str(), analysis_image_count, alert_to_alarm_frame_count); + if (alert_to_alarm_frame_count == 0) { + Info("%s: %03d - Gone back into alarm state", name.c_str(), analysis_image_count); + shared_data->state = state = ALARM; + } + } else if (state == TAPE) { + // Already recording, but IDLE so switch to ALARM + shared_data->state = state = ALARM; + Debug(1, "Was in TAPE, going into ALARM"); } else { - // Create event from current snap - event = new Event(this, timestamp, "Continuous", noteSetMap); + Debug(1, "Staying in %s", State_Strings[state].c_str()); } - shared_data->last_event_id = event->Id(); + if (state == ALARM) { + last_alarm_count = analysis_image_count; + } // This is needed so post_event_count counts after last alarmed frames while in ALARM not single alarmed frames while ALERT - // lets construct alarm cause. It will contain cause + names of zones alarmed - std::string alarm_cause; - for (const Zone &zone : zones) { - if (zone.Alarmed()) { - if (!alarm_cause.empty()) alarm_cause += ","; - alarm_cause += std::string(zone.Label()); - } - } - alarm_cause = cause+" Continuous "+alarm_cause; - strncpy(shared_data->alarm_cause, alarm_cause.c_str(), sizeof(shared_data->alarm_cause)-1); - SetVideoWriterStartTime(event->StartTime()); + // snap->score -1 means didn't do motion detection so don't do state transition + // In Nodect, we may still have a triggered event, so need this code to run to end the event. + } else if (!score and ((snap->score == 0) or (function == NODECT || function == RECORD))) { + Debug(1, "!score %s", State_Strings[state].c_str()); + alert_to_alarm_frame_count = alarm_frame_count; // load same value configured for alarm_frame_count - Info("%s: %03d - Opened new event %" PRIu64 ", section start", - name.c_str(), analysis_image_count, event->Id()); - /* To prevent cancelling out an existing alert\prealarm\alarm state */ - if (state == IDLE) { - shared_data->state = state = TAPE; - } - } // end if ! event - } // end if RECORDING - - if (score and (function != MONITOR)) { - if ((state == IDLE) || (state == TAPE) || (state == PREALARM)) { - // If we should end then previous continuous event and start a new non-continuous event - if (event && event->Frames() - && !event->AlarmFrames() - && (event_close_mode == CLOSE_ALARM) - && ((timestamp - event->StartTime()) >= min_section_length) - && ((!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count - 1))) { - Info("%s: %03d - Closing event %" PRIu64 ", continuous end, alarm begins", - name.c_str(), image_count, event->Id()); - closeEvent(); - } else if (event) { - // This is so if we need more than 1 alarm frame before going into alarm, so it is basically if we have enough alarm frames - Debug(3, - "pre_alarm_count in event %d of %d, event frames %d, alarm frames %d event length %" PRIi64 " >=? %" PRIi64 " min close mode is ALARM? %d", - Event::PreAlarmCount(), pre_event_count, - event->Frames(), - event->AlarmFrames(), - static_cast(std::chrono::duration_cast(timestamp - event->StartTime()).count()), - static_cast(Seconds(min_section_length).count()), - (event_close_mode == CLOSE_ALARM)); - } - if ((!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count-1)) { - // lets construct alarm cause. It will contain cause + names of zones alarmed - std::string alarm_cause = ""; - for (const Zone &zone : zones) { - if (zone.Alarmed()) { - alarm_cause = alarm_cause + "," + std::string(zone.Label()); - } - } - if (!alarm_cause.empty()) alarm_cause[0] = ' '; - alarm_cause = cause + alarm_cause; - strncpy(shared_data->alarm_cause, alarm_cause.c_str(), sizeof(shared_data->alarm_cause)-1); - Info("%s: %03d - Gone into alarm state PreAlarmCount: %u > AlarmFrameCount:%u Cause:%s", - name.c_str(), image_count, Event::PreAlarmCount(), alarm_frame_count, shared_data->alarm_cause); - - if (!event) { - packetqueue_iterator *start_it = packetqueue.get_event_start_packet_it( - *analysis_it, - (pre_event_count > alarm_frame_count ? pre_event_count : alarm_frame_count) - ); - ZMLockedPacket *starting_packet_lock = nullptr; - std::shared_ptr starting_packet = nullptr; - if (*start_it != *analysis_it) { - starting_packet_lock = packetqueue.get_packet(start_it); - if (!starting_packet_lock) return false; - starting_packet = starting_packet_lock->packet_; + if (state == ALARM) { + Info("%s: %03d - Gone into alert state", name.c_str(), analysis_image_count); + shared_data->state = state = ALERT; + } else if (state == ALERT) { + if ( + ((analysis_image_count - last_alarm_count) > post_event_count) + && + (event->Duration() >= min_section_length)) { + Info("%s: %03d - Left alarm state (%" PRIu64 ") - %d(%d) images", + name.c_str(), analysis_image_count, event->Id(), event->Frames(), event->AlarmFrames()); + if ( + (function != RECORD && function != MOCORD) + || + (event_close_mode == CLOSE_ALARM || event_close_mode==CLOSE_IDLE) + ) { + shared_data->state = state = IDLE; + Info("%s: %03d - Closing event %" PRIu64 ", alarm end%s", + name.c_str(), analysis_image_count, event->Id(), (function==MOCORD)?", section truncated":"" ); + closeEvent(); } else { - starting_packet = snap; + shared_data->state = state = TAPE; } + } + } else if (state == PREALARM) { + // Back to IDLE + shared_data->state = state = ((function != MOCORD) ? IDLE : TAPE); + } else { + Debug(1, + "State %d %s because analysis_image_count(%d)-last_alarm_count(%d) > post_event_count(%d) and timestamp.tv_sec(%" PRIi64 ") - recording.tv_src(%" PRIi64 ") >= min_section_length(%" PRIi64 ")", + state, + State_Strings[state].c_str(), + analysis_image_count, + last_alarm_count, + post_event_count, + static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), + static_cast(std::chrono::duration_cast(GetVideoWriterStartTime().time_since_epoch()).count()), + static_cast(Seconds(min_section_length).count())); + } + if (Event::PreAlarmCount()) + Event::EmptyPreAlarmFrames(); + } // end if score or not - event = new Event(this, starting_packet->timestamp, cause, noteSetMap); - shared_data->last_event_id = event->Id(); - snprintf(video_store_data->event_file, sizeof(video_store_data->event_file), "%s", event->getEventFile()); - SetVideoWriterStartTime(event->StartTime()); - shared_data->state = state = ALARM; - - // Write out starting packets, do not modify packetqueue it will garbage collect itself - while (*start_it != *analysis_it) { - event->AddPacket(starting_packet); - - packetqueue.increment_it(start_it); - if ( (*start_it) == (*analysis_it) ) { - if (starting_packet_lock) delete starting_packet_lock; - break; - } - ZMLockedPacket *lp = packetqueue.get_packet(start_it); - delete starting_packet_lock; - if (!lp) { - // Shutting down event will be closed by ~Monitor() - // Perhaps we shouldn't do this. - return false; - } - starting_packet_lock = lp; - starting_packet = lp->packet_; - } - packetqueue.free_it(start_it); - delete start_it; - start_it = nullptr; + // At this point, snap ONLY has motion score, so this adds other sources + if (score > snap->score) + snap->score = score; + if (state == PREALARM) { + // incremement pre alarm image count + Event::AddPreAlarmFrame(snap->image, snap->timestamp, score, nullptr); + } else if (state == ALARM) { + if (event) { + if (noteSetMap.size() > 0) + event->updateNotes(noteSetMap); + if (section_length >= Seconds(min_section_length) && (event->Duration() >= section_length)) { + Warning("%s: %03d - event %" PRIu64 ", has exceeded desired section length. %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64, + name.c_str(), analysis_image_count, event->Id(), + static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), + static_cast(std::chrono::duration_cast(event->StartTime().time_since_epoch()).count()), + static_cast(std::chrono::duration_cast(event->Duration()).count()), + static_cast(Seconds(section_length).count())); + closeEvent(); + event = openEvent(snap, cause, noteSetMap); + } + } else { + if (!event) { + event = openEvent(snap, cause, noteSetMap); Info("%s: %03d - Opening new event %" PRIu64 ", alarm start", name.c_str(), analysis_image_count, event->Id()); - } else { - shared_data->state = state = ALARM; } // end if no event, so start it - if ( alarm_frame_count ) { + if (alarm_frame_count) { Debug(1, "alarm frame count so SavePreAlarmFrames"); event->SavePreAlarmFrames(); } - } else if (state != PREALARM) { - Info("%s: %03d - Gone into prealarm state", name.c_str(), analysis_image_count); - shared_data->state = state = PREALARM; } } else if (state == ALERT) { - alert_to_alarm_frame_count--; - Info("%s: %03d - Alarmed frame while in alert state. Consecutive alarmed frames left to return to alarm state: %03d", - name.c_str(), analysis_image_count, alert_to_alarm_frame_count); - if (alert_to_alarm_frame_count == 0) { - Info("%s: %03d - Gone back into alarm state", name.c_str(), analysis_image_count); - shared_data->state = state = ALARM; - } + // Alert means this frame has no motion, but we were alarmed and are still recording. + if ((noteSetMap.size() > 0) and event) + event->updateNotes(noteSetMap); } else if (state == TAPE) { - // Already recording, but IDLE so switch to ALARM - shared_data->state = state = ALARM; - Debug(1, "Was in TAPE, going into ALARM"); - } else { - Debug(1, "Staying in %s", State_Strings[state].c_str()); + // bulk frame code moved to event. + } // end if state machine - } - if (state == ALARM) { - last_alarm_count = analysis_image_count; - } // This is needed so post_event_count counts after last alarmed frames while in ALARM not single alarmed frames while ALERT - } else { // no score? - alert_to_alarm_frame_count = alarm_frame_count; // load same value configured for alarm_frame_count - if (state == ALARM) { - Info("%s: %03d - Gone into alert state", name.c_str(), analysis_image_count); - shared_data->state = state = ALERT; - } else if (state == ALERT) { - if ( - ((analysis_image_count - last_alarm_count) > post_event_count) - && - ((timestamp - event->StartTime()) >= min_section_length)) { - Info("%s: %03d - Left alarm state (%" PRIu64 ") - %d(%d) images", - name.c_str(), analysis_image_count, event->Id(), event->Frames(), event->AlarmFrames()); - //if ( function != MOCORD || event_close_mode == CLOSE_ALARM || event->Cause() == SIGNAL_CAUSE ) - if ( (function != RECORD && function != MOCORD ) || event_close_mode == CLOSE_ALARM ) { - shared_data->state = state = IDLE; - Info("%s: %03d - Closing event %" PRIu64 ", alarm end%s", - name.c_str(), analysis_image_count, event->Id(), (function==MOCORD)?", section truncated":"" ); + if (function == RECORD or function == MOCORD) { + // If doing record, check to see if we need to close the event or not. + if (event) { + Debug(2, "Have event %" PRIu64 " in record", event->Id()); + + if (section_length >= Seconds(min_section_length) && (event->Duration() >= section_length) + && ((function == MOCORD && event_close_mode != CLOSE_TIME) + || (function == RECORD && event_close_mode == CLOSE_TIME) + || std::chrono::duration_cast(snap->timestamp.time_since_epoch()) % section_length == Seconds(0))) { + Info("%s: %03d - Closing event %" PRIu64 ", section end forced %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64 , + name.c_str(), + snap->image_index, + event->Id(), + static_cast(std::chrono::duration_cast(snap->timestamp.time_since_epoch()).count()), + static_cast(std::chrono::duration_cast(event->StartTime().time_since_epoch()).count()), + static_cast(std::chrono::duration_cast(snap->timestamp - event->StartTime()).count()), + static_cast(Seconds(section_length).count())); closeEvent(); - } else { + } // end if section_length + } // end if event + + if (!event) { + event = openEvent(snap, cause.empty() ? "Continuous" : cause, noteSetMap); + + Info("%s: %03d - Opened new event %" PRIu64 ", continuous section start", + name.c_str(), analysis_image_count, event->Id()); + /* To prevent cancelling out an existing alert\prealarm\alarm state */ + // This ignores current score status. This should all come after the state machine calculations + if (state == IDLE) { shared_data->state = state = TAPE; } - } - } else if (state == PREALARM) { - // Back to IDLE - shared_data->state = state = ((function != MOCORD) ? IDLE : TAPE); - } else { - Debug(1, - "State %d %s because analysis_image_count(%d)-last_alarm_count(%d) > post_event_count(%d) and timestamp.tv_sec(%" PRIi64 ") - recording.tv_src(%" PRIi64 ") >= min_section_length(%" PRIi64 ")", - state, - State_Strings[state].c_str(), - analysis_image_count, - last_alarm_count, - post_event_count, - static_cast(std::chrono::duration_cast(timestamp.time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(GetVideoWriterStartTime().time_since_epoch()).count()), - static_cast(Seconds(min_section_length).count())); - } - if (Event::PreAlarmCount()) - Event::EmptyPreAlarmFrames(); - } // end if score or not + } // end if ! event + } // end if RECORDING - snap->score = score; - - if (state == PREALARM) { - // Generate analysis images if necessary - if ((savejpegs > 1) and snap->image) { - for (const Zone &zone : zones) { - if (zone.Alarmed() and zone.AlarmImage()) { - if (!snap->analysis_image) - snap->analysis_image = new Image(*(snap->image)); - snap->analysis_image->Overlay(*(zone.AlarmImage())); - } // end if zone is alarmed - } // end foreach zone - } // end if savejpegs - - // incremement pre alarm image count - Event::AddPreAlarmFrame(snap->image, timestamp, score, nullptr); - } else if (state == ALARM) { - for (const Zone &zone : zones) { - if (zone.Alarmed()) { - if (zone.AlarmImage() and (savejpegs > 1) and snap->image) { - if (!snap->analysis_image) - snap->analysis_image = new Image(*(snap->image)); - snap->analysis_image->Overlay(*(zone.AlarmImage())); + if (function == MODECT or function == MOCORD) { + if (!ref_image.Buffer()) { + if (0 and snap->in_frame && ( + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUV420P) + || + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUVJ420P) + ) ) { + Debug(1, "Assigning from vchannel"); + Image v_image(snap->in_frame->width, snap->in_frame->height, 1, ZM_SUBPIX_ORDER_NONE, snap->in_frame->data[0], 0); + ref_image.Assign(v_image); + } else if (snap->image) { + Debug(1, "Assigning"); + ref_image.Assign(*(snap->image)); } - } // end if zone is alarmed - } // end foreach zone - if (event) { - if (noteSetMap.size() > 0) - event->updateNotes(noteSetMap); - if (section_length != Seconds(0) && (timestamp - event->StartTime() >= section_length)) { - Warning("%s: %03d - event %" PRIu64 ", has exceeded desired section length. %" PRIi64 " - %" PRIi64 " = %" PRIi64 " >= %" PRIi64, - name.c_str(), analysis_image_count, event->Id(), - static_cast(std::chrono::duration_cast(timestamp.time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(GetVideoWriterStartTime().time_since_epoch()).count()), - static_cast(std::chrono::duration_cast(timestamp - GetVideoWriterStartTime()).count()), - static_cast(Seconds(section_length).count())); - closeEvent(); - event = new Event(this, timestamp, cause, noteSetMap); - shared_data->last_event_id = event->Id(); - //set up video store data - snprintf(video_store_data->event_file, sizeof(video_store_data->event_file), "%s", event->getEventFile()); - SetVideoWriterStartTime(event->StartTime()); - } - } else { - Error("ALARM but no event"); - } - } else if ( state == ALERT ) { - // Alert means this frame has no motion, but we were alarmed and are still recording. - if ((noteSetMap.size() > 0) and event) - event->updateNotes(noteSetMap); - } else if ( state == TAPE ) { - // bulk frame code moved to event. - } // end if state machine + } else { + if (0 and snap->in_frame && + ( + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUV420P) + || + ((AVPixelFormat)snap->in_frame->format == AV_PIX_FMT_YUVJ420P) + ) + ) { + Debug(1, "Blending from vchannel"); + Image v_image(snap->in_frame->width, snap->in_frame->height, 1, ZM_SUBPIX_ORDER_NONE, snap->in_frame->data[0], 0); + ref_image.Blend(v_image, ( state==ALARM ? alarm_ref_blend_perc : ref_blend_perc )); + } else if (snap->image) { + Debug(1, "Blending because %p and format %d != %d, %d", snap->in_frame, + (snap->in_frame ? snap->in_frame->format : -1), + AV_PIX_FMT_YUV420P, + AV_PIX_FMT_YUVJ420P + ); + ref_image.Blend(*(snap->image), ( state==ALARM ? alarm_ref_blend_perc : ref_blend_perc )); + Debug(1, "Done Blending"); + } + } // end if have image + } // end if detecting + last_signal = signal; + } // end if videostream + } // end if signal + } else { + Debug(3, "trigger == off"); + if (event) { + Info("%s: %03d - Closing event %" PRIu64 ", trigger off", name.c_str(), analysis_image_count, event->Id()); + closeEvent(); + } + shared_data->state = state = IDLE; + } // end if ( trigger_data->trigger_state != TRIGGER_OFF ) - if ( (function == MODECT or function == MOCORD) and snap->image ) { - if (!ref_image.Buffer()) { - Debug(1, "Assigning"); - ref_image.Assign(*(snap->image)); - } else { - Debug(1, "Blending"); - ref_image.Blend(*(snap->image), ( state==ALARM ? alarm_ref_blend_perc : ref_blend_perc )); - Debug(1, "Done Blending"); - } - } - last_signal = signal; - } // end if videostream - } // end if signal - shared_data->last_frame_score = score; - } else { - Debug(3, "trigger == off"); - if (event) { - Info("%s: %03d - Closing event %" PRIu64 ", trigger off", name.c_str(), analysis_image_count, event->Id()); - closeEvent(); + packetqueue.clearPackets(snap); + + if (snap->codec_type == AVMEDIA_TYPE_VIDEO) { + // Only do these if it's a video packet. + shared_data->last_read_index = snap->image_index; + analysis_image_count++; } - shared_data->state = state = IDLE; - } // end if ( trigger_data->trigger_state != TRIGGER_OFF ) - if (event) event->AddPacket(snap); + if (event) { + event->AddPacket(packet_lock); + } else { + // In the case where people have pre-alarm frames, the web ui will generate the frame images + // from the mp4. So no one will notice anyways. + if (snap->image and (videowriter == PASSTHROUGH)) { + if (!savejpegs) { + Debug(1, "Deleting image data for %d", snap->image_index); + // Don't need raw images anymore + delete snap->image; + snap->image = nullptr; + } + if (snap->analysis_image and !(savejpegs & 2)) { + Debug(1, "Deleting analysis image data for %d", snap->image_index); + delete snap->analysis_image; + snap->analysis_image = nullptr; + } + } + delete packet_lock; + } + } // end scope for event_lock - // In the case where people have pre-alarm frames, the web ui will generate the frame images - // from the mp4. So no one will notice anyways. - if (snap->image and (videowriter == PASSTHROUGH) and !savejpegs) { - Debug(1, "Deleting image data for %d", snap->image_index); - // Don't need raw images anymore - delete snap->image; - snap->image = nullptr; - } - - packetqueue.clearPackets(snap); - - if (snap->codec_type == AVMEDIA_TYPE_VIDEO) { - // Only do these if it's a video packet. - shared_data->last_read_index = snap->image_index; - analysis_image_count++; - } packetqueue.increment_it(analysis_it); - packetqueue.unlock(packet_lock); + //packetqueue.unlock(packet_lock); shared_data->last_read_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); return true; @@ -2522,8 +2473,6 @@ int Monitor::Capture() { } else { Debug(4, "Not Queueing audio packet"); } - // Don't update last_write_index because that is used for live streaming - //shared_data->last_write_time = image_buffer[index].timestamp->tv_sec; return 1; } else { Debug(1, "Unknown codec type %d", packet->codec_type); @@ -2563,7 +2512,8 @@ bool Monitor::Decode() { std::shared_ptr packet = packet_lock->packet_; if (packet->codec_type != AVMEDIA_TYPE_VIDEO) { Debug(4, "Not video"); - packetqueue.unlock(packet_lock); + //packetqueue.unlock(packet_lock); + delete packet_lock; return true; // Don't need decode } @@ -2653,7 +2603,7 @@ bool Monitor::Decode() { } // end if need_decoding Image* capture_image = nullptr; - unsigned int index = image_count % image_buffer_count; + unsigned int index = packet->image_index % image_buffer_count; if (packet->image) { capture_image = packet->image; @@ -2668,27 +2618,36 @@ bool Monitor::Decode() { } else if (deinterlacing_value == 3) { capture_image->Deinterlace_Blend(); } else if (deinterlacing_value == 4) { - ZMLockedPacket *deinterlace_packet_lock = nullptr; while (!zm_terminate) { - ZMLockedPacket *second_packet_lock = packetqueue.get_packet(decoder_it); - if (!second_packet_lock) { - packetqueue.unlock(packet_lock); + // ICON FIXME SHould we not clone decoder_it? + ZMLockedPacket *deinterlace_packet_lock = packetqueue.get_packet(decoder_it); + if (!deinterlace_packet_lock) { + delete packet_lock; + //packetqueue.unlock(packet_lock); return false; } - if (second_packet_lock->packet_->codec_type == packet->codec_type) { - deinterlace_packet_lock = second_packet_lock; + if (deinterlace_packet_lock->packet_->codec_type == packet->codec_type) { + if (!deinterlace_packet_lock->packet_->image) { + Error("Can't de-interlace when we have to decode. De-Interlacing should only be useful on old low res local cams"); + } else { + capture_image->Deinterlace_4Field(deinterlace_packet_lock->packet_->image, (deinterlacing>>8)&0xff); + } + delete deinterlace_packet_lock; + //packetqueue.unlock(deinterlace_packet_lock); break; } - packetqueue.unlock(second_packet_lock); + delete deinterlace_packet_lock; + //packetqueue.unlock(deinterlace_packet_lock); packetqueue.increment_it(decoder_it); } - if (zm_terminate) return false; - capture_image->Deinterlace_4Field(deinterlace_packet_lock->packet_->image, (deinterlacing>>8)&0xff); - packetqueue.unlock(deinterlace_packet_lock); + if (zm_terminate) { + delete packet_lock; + return false; + } } else if (deinterlacing_value == 5) { capture_image->Deinterlace_Blend_CustomRatio((deinterlacing>>8)&0xff); } - } + } // end if deinterlacing_value if (orientation != ROTATE_0) { Debug(3, "Doing rotation"); @@ -2715,7 +2674,7 @@ bool Monitor::Decode() { if (config.timestamp_on_capture) { Debug(3, "Timestamping"); - TimestampImage(packet->image, packet->timestamp); + TimestampImage(capture_image, packet->timestamp); } image_buffer[index]->Assign(*(packet->image)); @@ -2776,6 +2735,71 @@ void Monitor::TimestampImage(Image *ts_image, SystemTimePoint ts_time) const { Debug(2, "done annotating %s", label_text); } // end void Monitor::TimestampImage +Event * Monitor::openEvent( + const std::shared_ptr &snap, + const std::string &cause, + const Event::StringSetMap ¬eSetMap) { + + // FIXME this iterator is not protected from invalidation + packetqueue_iterator *start_it = packetqueue.get_event_start_packet_it( + *analysis_it, + (cause == "Continuous" ? 0 : (pre_event_count > alarm_frame_count ? pre_event_count : alarm_frame_count)) + ); + + // This gets a lock on the starting packet + + ZMLockedPacket *starting_packet_lock = nullptr; + std::shared_ptr starting_packet = nullptr; + if (*start_it != *analysis_it) { + starting_packet_lock = packetqueue.get_packet(start_it); + if (!starting_packet_lock) { + Warning("Unable to get starting packet lock"); + return nullptr; + } + starting_packet = starting_packet_lock->packet_; + } else { + starting_packet = snap; + } + + event = new Event(this, starting_packet->timestamp, cause, noteSetMap); + + shared_data->last_event_id = event->Id(); + strncpy(shared_data->alarm_cause, cause.c_str(), sizeof(shared_data->alarm_cause)-1); + + if (!event_start_command.empty()) { + if (fork() == 0) { + execlp(event_start_command.c_str(), + event_start_command.c_str(), + std::to_string(event->Id()).c_str(), + std::to_string(event->MonitorId()).c_str(), + nullptr); + Error("Error execing %s", event_start_command.c_str()); + } + } + + // Write out starting packets, do not modify packetqueue it will garbage collect itself + while (starting_packet and ((*start_it) != *analysis_it)) { + event->AddPacket(starting_packet_lock); + // Have added the packet, don't want to unlock it until we have locked the next + + packetqueue.increment_it(start_it); + if ((*start_it) == *analysis_it) { + //if (starting_packet_lock) delete starting_packet_lock; + break; + } + ZMLockedPacket *lp = packetqueue.get_packet(start_it); + //delete starting_packet_lock; + if (!lp) return nullptr; // only on terminate FIXME + starting_packet_lock = lp; + starting_packet = lp->packet_; + } + packetqueue.free_it(start_it); + delete start_it; + start_it = nullptr; + + return event; +} + void Monitor::closeEvent() { if (!event) return; @@ -2786,7 +2810,22 @@ void Monitor::closeEvent() { Debug(1, "close event thread is not joinable"); } Debug(1, "Starting thread to close event"); - close_event_thread = std::thread([](Event *e){ delete e; }, event); + close_event_thread = std::thread([](Event *e, const std::string &command){ + int64_t event_id = e->Id(); + int monitor_id = e->MonitorId(); + delete e; + + if (!command.empty()) { + if (fork() == 0) { + execlp(command.c_str(), command.c_str(), + std::to_string(event_id).c_str(), + std::to_string(monitor_id).c_str(), // monitor id + nullptr); + Error("Error execing %s", command.c_str()); + } + } + + }, event, event_end_command); Debug(1, "Nulling event"); event = nullptr; if (shared_data) video_store_data->recording = {}; @@ -3049,6 +3088,15 @@ int Monitor::PrimeCapture() { } } // end if rtsp_server + //Poller Thread + if (onvif_event_listener || janus_enabled || use_Amcrest_API) { + if (!Poller) { + Poller = zm::make_unique(this); + } else { + Poller->Start(); + } + } + if (decoding_enabled) { if (!decoder_it) decoder_it = packetqueue.get_video_it(false); if (!decoder) { @@ -3079,9 +3127,6 @@ int Monitor::PrimeCapture() { int Monitor::PreCapture() const { return camera->PreCapture(); } int Monitor::PostCapture() const { return camera->PostCapture(); } int Monitor::Close() { - if (close_event_thread.joinable()) { - close_event_thread.join(); - } // Because the stream indexes may change we have to clear out the packetqueue if (decoder) { decoder->Stop(); @@ -3089,6 +3134,29 @@ int Monitor::Close() { if (analysis_thread) { analysis_thread->Stop(); } + + //ONVIF Teardown + if (Poller) { + Poller->Stop(); + } +#ifdef WITH_GSOAP + if (onvif_event_listener && (soap != nullptr)) { + Debug(1, "Tearing Down Onvif"); + _wsnt__Unsubscribe wsnt__Unsubscribe; + _wsnt__UnsubscribeResponse wsnt__UnsubscribeResponse; + proxyEvent.Unsubscribe(response.SubscriptionReference.Address, NULL, &wsnt__Unsubscribe, wsnt__UnsubscribeResponse); + soap_destroy(soap); + soap_end(soap); + soap_free(soap); + soap = nullptr; + } //End ONVIF +#endif + //Janus Teardown + if (janus_enabled && (purpose == CAPTURE)) { + delete Janus_Manager; + } + + packetqueue.clear(); if (audio_fifo) { delete audio_fifo; @@ -3099,10 +3167,14 @@ int Monitor::Close() { video_fifo = nullptr; } + if (close_event_thread.joinable()) { + close_event_thread.join(); + } std::lock_guard lck(event_mutex); if (event) { Info("%s: image_count:%d - Closing event %" PRIu64 ", shutting down", name.c_str(), image_count, event->Id()); closeEvent(); + close_event_thread.join(); } if (camera) camera->Close(); return 1; @@ -3187,3 +3259,36 @@ StringVector Monitor::GroupNames() { } return groupnames; } // end Monitor::GroupNames() + +#ifdef WITH_GSOAP +//ONVIF Set Credentials +void Monitor::set_credentials(struct soap *soap) +{ + soap_wsse_delete_Security(soap); + soap_wsse_add_Timestamp(soap, NULL, 10); + soap_wsse_add_UsernameTokenDigest(soap, "Auth", onvif_username.c_str(), onvif_password.c_str()); +} + +//GSOAP boilerplate +int SOAP_ENV__Fault(struct soap *soap, char *faultcode, char *faultstring, char *faultactor, struct SOAP_ENV__Detail *detail, struct SOAP_ENV__Code *SOAP_ENV__Code, struct SOAP_ENV__Reason *SOAP_ENV__Reason, char *SOAP_ENV__Node, char *SOAP_ENV__Role, struct SOAP_ENV__Detail *SOAP_ENV__Detail) +{ + // populate the fault struct from the operation arguments to print it + soap_fault(soap); + // SOAP 1.1 + soap->fault->faultcode = faultcode; + soap->fault->faultstring = faultstring; + soap->fault->faultactor = faultactor; + soap->fault->detail = detail; + // SOAP 1.2 + soap->fault->SOAP_ENV__Code = SOAP_ENV__Code; + soap->fault->SOAP_ENV__Reason = SOAP_ENV__Reason; + soap->fault->SOAP_ENV__Node = SOAP_ENV__Node; + soap->fault->SOAP_ENV__Role = SOAP_ENV__Role; + soap->fault->SOAP_ENV__Detail = SOAP_ENV__Detail; + // set error + soap->error = SOAP_FAULT; + // handle or display the fault here with soap_stream_fault(soap, std::cerr); + // return HTTP 202 Accepted + return soap_send_empty_response(soap, SOAP_OK); +} +#endif diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 5b9a2ce46..30bc57d87 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -23,6 +23,7 @@ #include "zm_define.h" #include "zm_camera.h" #include "zm_analysis_thread.h" +#include "zm_poll_thread.h" #include "zm_decoder_thread.h" #include "zm_event.h" #include "zm_fifo.h" @@ -33,6 +34,13 @@ #include #include #include +#include + +#ifdef WITH_GSOAP +#include "soapPullPointSubscriptionBindingProxy.h" +#include "plugin/wsseapi.h" +#include +#endif class Group; @@ -40,6 +48,7 @@ class Group; #define MOTION_CAUSE "Motion" #define LINKED_CAUSE "Linked" + // // This is the main class for monitors. Each monitor is associated // with a camera and is effectively a collector for events. @@ -69,7 +78,7 @@ public: FILE, FFMPEG, LIBVLC, - CURL, + LIBCURL, NVSOCKET, VNC, } CameraType; @@ -84,7 +93,7 @@ public: } Orientation; typedef enum { - DEINTERLACE_DISABLED = 0x00000000, + DEINTERLACE_DISABLED = 0x00000000, DEINTERLACE_FOUR_FIELD_SOFT = 0x00001E04, DEINTERLACE_FOUR_FIELD_MEDIUM = 0x00001404, DEINTERLACE_FOUR_FIELD_HARD = 0x00000A04, @@ -145,12 +154,12 @@ protected: uint32_t last_frame_score; /* +60 */ uint32_t audio_frequency; /* +64 */ uint32_t audio_channels; /* +68 */ - /* + /* ** 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 + ** 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 */ 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; @@ -247,7 +256,51 @@ protected: bool hasAlarmed(); }; + class AmcrestAPI { protected: + Monitor *parent; + std::string amcrest_response; + CURLM *curl_multi = nullptr; + CURL *Amcrest_handle = nullptr; + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); + + public: + AmcrestAPI( Monitor *parent_); + ~AmcrestAPI(); + int API_Connect(); + void WaitForMessage(); + bool Amcrest_Alarmed; + int start_Amcrest(); + }; + + class JanusManager { + protected: + Monitor *parent; + CURL *curl = nullptr; + //helper class for CURL + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); + bool Janus_Healthy; + std::string janus_session; + std::string janus_handle; + std::string janus_endpoint; + std::string stream_key; + std::string rtsp_username; + std::string rtsp_password; + std::string rtsp_path; + + public: + JanusManager(Monitor *parent_); + ~JanusManager(); + int add_to_janus(); + int check_janus(); + int remove_from_janus(); + int get_janus_session(); + int get_janus_handle(); + int get_janus_plugin(); + std::string get_stream_key(); + }; + + // These are read from the DB and thereafter remain unchanged unsigned int id; std::string name; @@ -257,6 +310,8 @@ protected: Function function; // What the monitor is doing bool enabled; // Whether the monitor is enabled or asleep bool decoding_enabled; // 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 protocol; std::string method; @@ -268,6 +323,13 @@ protected: std::string path; std::string second_path; + std::string onvif_url; + std::string onvif_username; + std::string onvif_password; + std::string onvif_options; + bool onvif_event_listener; + bool use_Amcrest_API; + std::string device; int palette; int channel; @@ -351,7 +413,6 @@ protected: int first_alarm_count; int last_alarm_count; bool last_signal; - int last_section_mod; int buffer_count; State state; SystemTimePoint start_time; @@ -390,6 +451,7 @@ protected: VideoStore *videoStore; PacketQueue packetqueue; + std::unique_ptr Poller; packetqueue_iterator *analysis_it; std::unique_ptr analysis_thread; packetqueue_iterator *decoder_it; @@ -404,6 +466,8 @@ protected: int n_linked_monitors; MonitorLink **linked_monitors; + std::string event_start_command; + std::string event_end_command; std::vector groups; @@ -414,6 +478,25 @@ protected: std::string diag_path_ref; std::string diag_path_delta; + //ONVIF + bool Poll_Trigger_State; + bool Event_Poller_Healthy; + bool Event_Poller_Closes_Event; + + JanusManager *Janus_Manager; + AmcrestAPI *Amcrest_Manager; + +#ifdef WITH_GSOAP + struct soap *soap = nullptr; + _tev__CreatePullPointSubscription request; + _tev__CreatePullPointSubscriptionResponse response; + _tev__PullMessages tev__PullMessages; + _tev__PullMessagesResponse tev__PullMessagesResponse; + PullPointSubscriptionBindingProxy proxyEvent; + void set_credentials(struct soap *soap); +#endif + + // Used in check signal uint8_t red_val; uint8_t green_val; @@ -470,6 +553,19 @@ public: inline bool DecodingEnabled() const { return decoding_enabled; } + bool JanusEnabled() { + return janus_enabled; + } + bool JanusAudioEnabled() { + return janus_audio_enabled; + } + bool OnvifEnabled() { + return onvif_event_listener; + } + int check_janus(); //returns 1 for healthy, 0 for success but missing stream, negative for error. + bool EventPollerHealthy() { + return Event_Poller_Healthy; + } inline const char *EventPrefix() const { return event_prefix.c_str(); } inline bool Ready() const { if ( image_count >= ready_count ) { @@ -518,7 +614,7 @@ public: void SetVideoWriterStartTime(SystemTimePoint t) { video_store_data->recording = zm::chrono::duration_cast(t.time_since_epoch()); } - + unsigned int GetPreEventCount() const { return pre_event_count; }; int32_t GetImageBufferCount() const { return image_buffer_count; }; State GetState() const { return (State)shared_data->state; } @@ -533,6 +629,12 @@ public: std::string GetAudioFifoPath() const { return shared_data ? shared_data->audio_fifo_path : ""; }; std::string GetRTSPStreamName() const { return rtsp_streamname; }; + const std::string &getONVIF_URL() const { return onvif_url; }; + const std::string &getONVIF_Username() const { return onvif_username; }; + const std::string &getONVIF_Password() const { return onvif_password; }; + const std::string &getONVIF_Options() const { return onvif_options; }; + + Image *GetAlarmImage(); int GetImage(int32_t index=-1, int scale=100); ZMPacket *getSnapshot( int index=-1 ) const; SystemTimePoint GetTimestamp(int index = -1) const; @@ -589,8 +691,13 @@ public: bool CheckSignal( const Image *image ); bool Analyse(); bool Decode(); + bool Poll(); void DumpImage( Image *dump_image ) const; void TimestampImage(Image *ts_image, SystemTimePoint ts_time) const; + Event *openEvent( + const std::shared_ptr &snap, + const std::string &cause, + const Event::StringSetMap ¬eSetMap); void closeEvent(); void Reload(); diff --git a/src/zm_monitor_amcrest.cpp b/src/zm_monitor_amcrest.cpp new file mode 100644 index 000000000..04af8734c --- /dev/null +++ b/src/zm_monitor_amcrest.cpp @@ -0,0 +1,126 @@ +// +// ZoneMinder Monitor::AmcrestAPI Class Implementation, $Date$, $Revision$ +// Copyright (C) 2022 Jonathan Bennett +// +// 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. +// + +#include "zm_monitor.h" + + +Monitor::AmcrestAPI::AmcrestAPI(Monitor *parent_) { + parent = parent_; + curl_multi = curl_multi_init(); + start_Amcrest(); +} + +Monitor::AmcrestAPI::~AmcrestAPI() { + if (Amcrest_handle != nullptr) { //potentially clean up the old handle + curl_multi_remove_handle(curl_multi, Amcrest_handle); + curl_easy_cleanup(Amcrest_handle); + } + if (curl_multi != nullptr) curl_multi_cleanup(curl_multi); +} + +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); + curl_easy_cleanup(Amcrest_handle); + } + + std::string full_url = parent->onvif_url; + if (full_url.back() != '/') full_url += '/'; + full_url += "eventManager.cgi?action=attach&codes=[VideoMotion]"; + Amcrest_handle = curl_easy_init(); + if (!Amcrest_handle){ + Warning("Handle is null!"); + return -1; + } + curl_easy_setopt(Amcrest_handle, CURLOPT_URL, full_url.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(Amcrest_handle, CURLOPT_WRITEDATA, &amcrest_response); + curl_easy_setopt(Amcrest_handle, CURLOPT_USERNAME, parent->onvif_username.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_PASSWORD, parent->onvif_password.c_str()); + curl_easy_setopt(Amcrest_handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + curl_error = curl_multi_add_handle(curl_multi, Amcrest_handle); + if (curl_error != CURLM_OK) { + Warning("error of %i", curl_error); + } + curl_error = curl_multi_perform(curl_multi, &running_handles); + 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); + if (m && (m->msg == CURLMSG_DONE)) { + Warning("Libcurl exited Early: %i", m->data.result); + } + + curl_multi_wait(curl_multi, NULL, 0, 300, NULL); + curl_error = curl_multi_perform(curl_multi, &running_handles); + } + + if ((curl_error == CURLM_OK) && (running_handles > 0)) { + parent->Event_Poller_Healthy = TRUE; + } else { + Warning("Response: %s", amcrest_response.c_str()); + Warning("Seeing %i streams, and error of %i, url: %s", running_handles, curl_error, full_url.c_str()); + curl_easy_getinfo(Amcrest_handle, CURLINFO_OS_ERRNO, &response_code); + Warning("Response code: %lu", response_code); + } + +return 0; +} + +void Monitor::AmcrestAPI::WaitForMessage() { + int open_handles; + int transfers; + curl_multi_perform(curl_multi, &open_handles); + if (open_handles == 0) { + start_Amcrest(); //http transfer ended, need to restart. + } else { + curl_multi_wait(curl_multi, NULL, 0, 5000, &transfers); //wait for max 5 seconds for event. + if (transfers > 0) { //have data to deal with + curl_multi_perform(curl_multi, &open_handles); //actually grabs the data, populates amcrest_response + if (amcrest_response.find("action=Start") != std::string::npos) { + //Event Start + Debug(1,"Triggered on ONVIF"); + if (!parent->Poll_Trigger_State) { + Debug(1,"Triggered Event"); + parent->Poll_Trigger_State = TRUE; + } + } else if (amcrest_response.find("action=Stop") != std::string::npos){ + Debug(1, "Triggered off ONVIF"); + parent->Poll_Trigger_State = FALSE; + if (!parent->Event_Poller_Closes_Event) { //If we get a close event, then we know to expect them. + parent->Event_Poller_Closes_Event = TRUE; + Debug(1,"Setting ClosesEvent"); + } + } + amcrest_response.clear(); //We've dealt with the message, need to clear the queue + } + } + return; +} + +size_t Monitor::AmcrestAPI::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} diff --git a/src/zm_monitor_janus.cpp b/src/zm_monitor_janus.cpp new file mode 100644 index 000000000..2fe9b3f26 --- /dev/null +++ b/src/zm_monitor_janus.cpp @@ -0,0 +1,316 @@ +// +// ZoneMinder Monitor::JanusManager Class Implementation, $Date$, $Revision$ +// Copyright (C) 2022 Jonathan Bennett +// +// 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. +// + +#include "zm_monitor.h" + + +Monitor::JanusManager::JanusManager(Monitor *parent_) { //constructor takes care of init and calls add_to + std::string response; + std::size_t pos; + parent = parent_; + if ((config.janus_path != nullptr) && (config.janus_path[0] != '\0')) { + janus_endpoint = config.janus_path; //TODO: strip trailing / + } 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 pos2 = parent->path.find("@", pos); + if (pos2 != std::string::npos) { //If we find an @ symbol, we have a username/password. Otherwise, passwordless login. + + std::size_t pos = parent->path.find(":", 7); //Search for the colon, but only after the RTSP:// text. + if (pos == std::string::npos) throw std::runtime_error("Cannot Parse URL for Janus."); //Looks like an invalid url + rtsp_username = parent->path.substr(7, pos-7); + + rtsp_password = parent->path.substr(pos+1, pos2 - pos - 1); + rtsp_path = "RTSP://"; + rtsp_path += parent->path.substr(pos2 + 1); + + } else { + rtsp_username = ""; + rtsp_password = ""; + rtsp_path = parent->path; + } +} + +Monitor::JanusManager::~JanusManager() { + if (janus_session.empty()) get_janus_session(); + if (janus_handle.empty()) get_janus_handle(); + + std::string response; + std::string endpoint; + + std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; + //std::size_t pos; + CURLcode res; + + curl = curl_easy_init(); + if(!curl) return; + + endpoint = janus_endpoint; + endpoint += "/"; + endpoint += janus_session; + endpoint += "/"; + endpoint += janus_handle; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"destroy\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"id\" : "; + postData += std::to_string(parent->id); + postData += "}}"; + + 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); + if (res != CURLE_OK) { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return; + } + + Debug(1, "Removed stream from Janus: %s", response.c_str()); + curl_easy_cleanup(curl); + return; +} + + + +int Monitor::JanusManager::check_janus() { + if (janus_session.empty()) get_janus_session(); + if (janus_handle.empty()) get_janus_handle(); + + std::string response; + std::string endpoint = janus_endpoint; + std::string postData; + //std::size_t pos; + CURLcode res; + + curl = curl_easy_init(); + if(!curl) return -1; + + endpoint += "/"; + endpoint += janus_session; + endpoint += "/"; + endpoint += janus_handle; + + //Assemble our actual request + postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"info\", \"id\" : "; + postData += std::to_string(parent->id); + postData += "}}"; + + 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); + 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)); + curl_easy_cleanup(curl); + janus_session = ""; + janus_handle = ""; + return -1; + } + + curl_easy_cleanup(curl); + Debug(1, "Queried for stream status: %s", response.c_str()); + if (response.find("\"janus\": \"error\"") != std::string::npos) { + if (response.find("No such session") != std::string::npos) { + Warning("Janus Session timed out"); + janus_session = ""; + return -2; + } else if (response.find("No such handle") != std::string::npos) { + Warning("Janus Handle timed out"); + janus_handle = ""; + return -2; + } + } else if (response.find("No such mountpoint") != std::string::npos) { + Warning("Mountpoint Missing"); + return 0; + } + return 1; +} + +int Monitor::JanusManager::add_to_janus() { + if (janus_session.empty()) get_janus_session(); + if (janus_handle.empty()) get_janus_handle(); + + std::string response; + std::string endpoint = janus_endpoint; + + CURLcode res; + + curl = curl_easy_init(); + if (!curl) { + Error("Failed to init curl"); + return -1; + } + + endpoint += "/"; + endpoint += janus_session; + endpoint += "/"; + endpoint += janus_handle; + + //Assemble our actual request + std::string postData = "{\"janus\" : \"message\", \"transaction\" : \"randomString\", \"body\" : {"; + postData += "\"request\" : \"create\", \"admin_key\" : \""; + postData += config.janus_secret; + postData += "\", \"type\" : \"rtsp\", "; + postData += "\"url\" : \""; + postData += rtsp_path; + if (rtsp_username != "") { + postData += "\", \"rtsp_user\" : \""; + postData += rtsp_username; + postData += "\", \"rtsp_pwd\" : \""; + postData += rtsp_password; + } + postData += "\", \"id\" : "; + postData += std::to_string(parent->id); + if (parent->janus_audio_enabled) postData += ", \"audio\" : true"; + postData += ", \"video\" : true}}"; + Warning("Sending %s to %s", postData.c_str(), 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); + if (res != CURLE_OK) { + Error("Failed to curl_easy_perform adding rtsp stream"); + curl_easy_cleanup(curl); + return -1; + } + if (response.find("\"janus\": \"error\"") != std::string::npos) { + if (response.find("No such session") != std::string::npos) { + Warning("Janus Session timed out"); + janus_session = ""; + return -2; + } else if (response.find("No such handle") != std::string::npos) { + Warning("Janus Handle timed out"); + janus_handle = ""; + return -2; + } + } + //scan for missing session or handle id "No such session" "no such handle" + + Debug(1,"Added stream to Janus: %s", response.c_str()); + curl_easy_cleanup(curl); + return 0; +} + + +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; +} + +/* +void Monitor::JanusManager::generateKey() +{ + const std::string CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + std::random_device random_device; + std::mt19937 generator(random_device()); + std::uniform_int_distribution<> distribution(0, CHARACTERS.size() - 1); + + std::string random_string; + + for (std::size_t i = 0; i < 16; ++i) + { + random_string += CHARACTERS[distribution(generator)]; + } + + stream_key = random_string; +} +*/ + + +int Monitor::JanusManager::get_janus_session() { + janus_session = ""; + std::string endpoint = janus_endpoint; + + std::string response; + + std::string postData = "{\"janus\" : \"create\", \"transaction\" : \"randomString\"}"; + std::size_t pos; + CURLcode res; + curl = curl_easy_init(); + if(!curl) return -1; + + 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); + if (res != CURLE_OK) { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } + + pos = response.find("\"id\": "); + if (pos == std::string::npos) + { + curl_easy_cleanup(curl); + return -1; + } + janus_session = response.substr(pos + 6, 16); + curl_easy_cleanup(curl); + return 1; + +} //get_janus_session + +int Monitor::JanusManager::get_janus_handle() { + std::string response = ""; + std::string endpoint = janus_endpoint; + std::size_t pos; + + CURLcode res; + curl = curl_easy_init(); + if(!curl) return -1; + + endpoint += "/"; + 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_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str()); + res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + Warning("Libcurl attempted %s got %s", endpoint.c_str(), curl_easy_strerror(res)); + curl_easy_cleanup(curl); + return -1; + } + + pos = response.find("\"id\": "); + if (pos == std::string::npos) + { + curl_easy_cleanup(curl); + return -1; + } + janus_handle = response.substr(pos + 6, 16); + curl_easy_cleanup(curl); + return 1; +} //get_janus_handle diff --git a/src/zm_monitor_monitorlink.cpp b/src/zm_monitor_monitorlink.cpp new file mode 100644 index 000000000..9e7a60d49 --- /dev/null +++ b/src/zm_monitor_monitorlink.cpp @@ -0,0 +1,199 @@ +// +// ZoneMinder Monitor Class Implementation, $Date$, $Revision$ +// Copyright (C) 2001-2008 Philip Coombes +// +// 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. +// + +#include "zm_monitor.h" + +#include + +#if ZM_MEM_MAPPED +#include +#include +#include +#else // ZM_MEM_MAPPED +#include +#include +#endif // ZM_MEM_MAPPED + +Monitor::MonitorLink::MonitorLink(unsigned int p_id, const char *p_name) : + id(p_id), + shared_data(nullptr), + trigger_data(nullptr), + video_store_data(nullptr) +{ + strncpy(name, p_name, sizeof(name)-1); + +#if ZM_MEM_MAPPED + map_fd = -1; + mem_file = stringtf("%s/zm.mmap.%u", staticConfig.PATH_MAP.c_str(), id); +#else // ZM_MEM_MAPPED + shm_id = 0; +#endif // ZM_MEM_MAPPED + mem_size = 0; + mem_ptr = nullptr; + + last_event_id = 0; + last_state = IDLE; + + last_connect_time = 0; + connected = false; +} + +Monitor::MonitorLink::~MonitorLink() { + disconnect(); +} + +bool Monitor::MonitorLink::connect() { + SystemTimePoint now = std::chrono::system_clock::now(); + if (!last_connect_time || (now - std::chrono::system_clock::from_time_t(last_connect_time)) > Seconds(60)) { + last_connect_time = std::chrono::system_clock::to_time_t(now); + + mem_size = sizeof(SharedData) + sizeof(TriggerData); + + Debug(1, "link.mem.size=%jd", static_cast(mem_size)); +#if ZM_MEM_MAPPED + map_fd = open(mem_file.c_str(), O_RDWR, (mode_t)0600); + if (map_fd < 0) { + Debug(3, "Can't open linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); + disconnect(); + return false; + } + while (map_fd <= 2) { + int new_map_fd = dup(map_fd); + Warning("Got one of the stdio fds for our mmap handle. map_fd was %d, new one is %d", map_fd, new_map_fd); + close(map_fd); + map_fd = new_map_fd; + } + + struct stat map_stat; + if (fstat(map_fd, &map_stat) < 0) { + Error("Can't stat linked memory map file %s: %s", mem_file.c_str(), strerror(errno)); + disconnect(); + return false; + } + + if (map_stat.st_size == 0) { + Error("Linked memory map file %s is empty: %s", mem_file.c_str(), strerror(errno)); + 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(mem_size)); + disconnect(); + return false; + } + + mem_ptr = (unsigned char *)mmap(nullptr, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, 0); + if (mem_ptr == MAP_FAILED) { + Error("Can't map file %s (%jd bytes) to memory: %s", mem_file.c_str(), static_cast(mem_size), strerror(errno)); + disconnect(); + return false; + } +#else // ZM_MEM_MAPPED + shm_id = shmget((config.shm_key&0xffff0000)|id, mem_size, 0700); + if (shm_id < 0) { + Debug(3, "Can't shmget link memory: %s", strerror(errno)); + connected = false; + return false; + } + mem_ptr = (unsigned char *)shmat(shm_id, 0, 0); + if ((int)mem_ptr == -1) { + Debug(3, "Can't shmat link memory: %s", strerror(errno)); + connected = false; + return false; + } +#endif // ZM_MEM_MAPPED + + shared_data = (SharedData *)mem_ptr; + trigger_data = (TriggerData *)((char *)shared_data + sizeof(SharedData)); + + if (!shared_data->valid) { + Debug(3, "Linked memory not initialised by capture daemon"); + disconnect(); + return false; + } + + last_state = shared_data->state; + last_event_id = shared_data->last_event_id; + connected = true; + + return true; + } + return false; +} // end bool Monitor::MonitorLink::connect() + +bool Monitor::MonitorLink::disconnect() { + if (connected) { + connected = false; + +#if ZM_MEM_MAPPED + if (mem_ptr > (void *)0) { + msync(mem_ptr, mem_size, MS_ASYNC); + munmap(mem_ptr, mem_size); + } + if (map_fd >= 0) + close(map_fd); + + map_fd = -1; +#else // ZM_MEM_MAPPED + struct shmid_ds shm_data; + if (shmctl(shm_id, IPC_STAT, &shm_data) < 0) { + Debug(3, "Can't shmctl: %s", strerror(errno)); + return false; + } + + shm_id = 0; + + if (shm_data.shm_nattch <= 1) { + if (shmctl(shm_id, IPC_RMID, 0) < 0) { + Debug(3, "Can't shmctl: %s", strerror(errno)); + return false; + } + } + + if (shmdt(mem_ptr) < 0) { + Debug(3, "Can't shmdt: %s", strerror(errno)); + return false; + } +#endif // ZM_MEM_MAPPED + mem_size = 0; + mem_ptr = nullptr; + } + return true; +} + +bool Monitor::MonitorLink::isAlarmed() { + if (!connected) { + return false; + } + return( shared_data->state == ALARM ); +} + +bool Monitor::MonitorLink::inAlarm() { + if (!connected) { + return false; + } + return( shared_data->state == ALARM || shared_data->state == ALERT ); +} + +bool Monitor::MonitorLink::hasAlarmed() { + if (shared_data->state == ALARM) { + return true; + } + last_event_id = shared_data->last_event_id; + return false; +} diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index 9bc04a311..2bb82d360 100644 --- a/src/zm_monitorstream.cpp +++ b/src/zm_monitorstream.cpp @@ -134,6 +134,18 @@ void MonitorStream::processCommand(const CmdMsg *msg) { break; } break; + case CMD_MAXFPS : + { + double int_part = ((unsigned char) msg->msg_data[1] << 24) | ((unsigned char) msg->msg_data[2] << 16) + | ((unsigned char) msg->msg_data[3] << 8) | (unsigned char) msg->msg_data[4]; + double dec_part = ((unsigned char) msg->msg_data[5] << 24) | ((unsigned char) msg->msg_data[6] << 16) + | ((unsigned char) msg->msg_data[7] << 8) | (unsigned char) msg->msg_data[8]; + + maxfps = (int_part + dec_part / 1000000.0); + + Debug(1, "Got MAXFPS %f", maxfps); + break; + } case CMD_SLOWFWD : Debug(1, "Got SLOW FWD command"); paused = true; @@ -231,6 +243,14 @@ void MonitorStream::processCommand(const CmdMsg *msg) { Info("User initiated exit - CMD_QUIT"); zm_terminate = true; break; + case CMD_ANALYZE_ON : + frame_type = FRAME_ANALYSIS; + Debug(1, "ANALYSIS on"); + break; + case CMD_ANALYZE_OFF : + frame_type = FRAME_NORMAL; + Debug(1, "ANALYSIS off"); + break; case CMD_QUERY : Debug(1, "Got QUERY command, sending STATUS"); break; @@ -268,7 +288,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) { } else { FPSeconds elapsed = now - last_fps_update; if (elapsed.count()) { - actual_fps = (frame_count - last_frame_count) / elapsed.count(); + actual_fps = (actual_fps + (frame_count - last_frame_count) / elapsed.count())/2; last_frame_count = frame_count; last_fps_update = now; } @@ -288,9 +308,9 @@ void MonitorStream::processCommand(const CmdMsg *msg) { status_data.delayed = delayed; status_data.paused = paused; status_data.rate = replay_rate; - status_data.delay = FPSeconds(now - last_frame_timestamp).count(); + status_data.delay = FPSeconds(now - last_frame_sent).count(); status_data.zoom = zoom; - Debug(2, "fps: %.2f capture_fps: %.2f analysis_fps: %.2f Buffer Level:%d, Delayed:%d, Paused:%d, Rate:%d, delay:%.3f, Zoom:%d, Enabled:%d Forced:%d", + Debug(2, "viewing fps: %.2f capture_fps: %.2f analysis_fps: %.2f Buffer Level:%d, Delayed:%d, Paused:%d, Rate:%d, delay:%.3f, Zoom:%d, Enabled:%d Forced:%d", status_data.fps, status_data.capture_fps, status_data.analysis_fps, @@ -359,14 +379,15 @@ bool MonitorStream::sendFrame(const std::string &filepath, SystemTimePoint times fputs("\r\n", stdout); fflush(stdout); - TimePoint send_end_time = std::chrono::steady_clock::now(); - TimePoint::duration frame_send_time = send_end_time - send_start_time; + if (maxfps > 0.0) { + TimePoint send_end_time = std::chrono::steady_clock::now(); + TimePoint::duration frame_send_time = send_end_time - send_start_time; - if (frame_send_time > Milliseconds(lround(Milliseconds::period::den / maxfps))) { - maxfps /= 2; - Info("Frame send time %" PRIi64 " ms too slow, throttling maxfps to %.2f", - static_cast(std::chrono::duration_cast(frame_send_time).count()), - maxfps); + if (frame_send_time > Milliseconds(lround(Milliseconds::period::den / maxfps))) { + Info("Frame send time %" PRIi64 " ms too slow, throttling maxfps to %.2f", + static_cast(std::chrono::duration_cast(frame_send_time).count()), + maxfps); + } } last_frame_sent = now; @@ -377,12 +398,15 @@ bool MonitorStream::sendFrame(const std::string &filepath, SystemTimePoint times } bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) { - Image *send_image = prepareImage(image); if (!config.timestamp_on_capture) { - monitor->TimestampImage(send_image, timestamp); + monitor->TimestampImage(image, timestamp); } + Image *send_image = prepareImage(image); fputs("--" BOUNDARY "\r\n", stdout); + // Calculate how long it takes to actually send the frame + TimePoint send_start_time = std::chrono::steady_clock::now(); + if ( type == STREAM_MPEG ) { if ( !vid_stream ) { vid_stream = new VideoStream("pipe:", format, bitrate, effective_fps, send_image->Colours(), send_image->SubpixelOrder(), send_image->Width(), send_image->Height()); @@ -398,14 +422,17 @@ bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) { /* double pts = */ vid_stream->EncodeFrame(send_image->Buffer(), send_image->Size(), config.mpeg_timed_frames, delta_time.count()); } else { - static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; + if (temp_img_buffer_size < send_image->Size()) { + Debug(1, "Resizing image buffer from %zu to %u", + temp_img_buffer_size, send_image->Size()); + delete[] temp_img_buffer; + temp_img_buffer = new uint8_t[send_image->Size()]; + temp_img_buffer_size = send_image->Size(); + } int img_buffer_size = 0; unsigned char *img_buffer = temp_img_buffer; - // Calculate how long it takes to actually send the frame - TimePoint send_start_time = std::chrono::steady_clock::now(); - switch ( type ) { case STREAM_JPEG : send_image->EncodeJpeg(img_buffer, &img_buffer_size); @@ -446,19 +473,23 @@ bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) { fputs("\r\n", stdout); fflush(stdout); - TimePoint send_end_time = std::chrono::steady_clock::now(); - TimePoint::duration frame_send_time = send_end_time - send_start_time; - - if (frame_send_time > Milliseconds(lround(Milliseconds::period::den / maxfps))) { - maxfps /= 1.5; - Warning("Frame send time %" PRIi64 " msec too slow, throttling maxfps to %.2f", - static_cast(std::chrono::duration_cast(frame_send_time).count()), - maxfps); - } } // Not mpeg - last_frame_sent = now; + + last_frame_sent = std::chrono::steady_clock::now(); + if (maxfps > 0.0) { + TimePoint::duration frame_send_time = last_frame_sent - send_start_time; + TimePoint::duration maxfps_milliseconds = Milliseconds(lround(Milliseconds::period::den / maxfps)); + + if (frame_send_time > maxfps_milliseconds) { + //maxfps /= 1.5; + Warning("Frame send time %" PRIi64 " msec too slow (> %" PRIi64 ", throttling maxfps to %.3f", + static_cast(std::chrono::duration_cast(frame_send_time).count()), + static_cast(std::chrono::duration_cast(maxfps_milliseconds).count()), + maxfps); + } + } return true; -} +} // end bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) void MonitorStream::runStream() { if (type == STREAM_SINGLE) { @@ -501,7 +532,8 @@ void MonitorStream::runStream() { // point to end which is theoretically not a valid value because all indexes are % image_buffer_count int32_t last_read_index = monitor->image_buffer_count; - SystemTimePoint stream_start_time = std::chrono::system_clock::now(); + TimePoint stream_start_time = std::chrono::steady_clock::now(); + when_to_send_next_frame = stream_start_time; // initialize it to now so that we spit out a frame immediately frame_count = 0; @@ -570,7 +602,7 @@ void MonitorStream::runStream() { break; } - now = std::chrono::system_clock::now(); + now = std::chrono::steady_clock::now(); bool was_paused = paused; bool got_command = false; // commands like zoom should output a frame even if paused @@ -617,7 +649,7 @@ void MonitorStream::runStream() { temp_read_index = MOD_ADD(temp_read_index, (replay_rate>=0?-1:1), temp_image_buffer_count); } else { FPSeconds expected_delta_time = ((FPSeconds(swap_image->timestamp - last_frame_timestamp)) * ZM_RATE_BASE) / replay_rate; - SystemTimePoint::duration actual_delta_time = now - last_frame_sent; + TimePoint::duration actual_delta_time = now - last_frame_sent; // If the next frame is due if (actual_delta_time > expected_delta_time) { @@ -683,7 +715,8 @@ void MonitorStream::runStream() { if (last_read_index != monitor->shared_data->last_write_index) { // have a new image to send int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; - if ((frame_mod == 1) || ((frame_count%frame_mod) == 0)) { + //if ((frame_mod == 1) || ((frame_count%frame_mod) == 0)) { + if ( now >= when_to_send_next_frame ) { if (!paused && !delayed) { last_read_index = monitor->shared_data->last_write_index; Debug(2, "Sending frame index: %d: frame_mod: %d frame count: %d paused(%d) delayed(%d)", @@ -693,9 +726,22 @@ void MonitorStream::runStream() { // Perhaps we should use NOW instead. last_frame_timestamp = SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index])); - Image *image = monitor->image_buffer[index]; - if (!sendFrame(image, last_frame_timestamp)) { + Image *send_image = nullptr; + if ((frame_type == FRAME_ANALYSIS) && + (monitor->GetFunction() == Monitor::MOCORD || monitor->GetFunction() == Monitor::MODECT)) { + Debug(1, "Sending analysis image"); + send_image = monitor->GetAlarmImage(); + if (!send_image) { + Debug(1, "Falling back"); + send_image = monitor->image_buffer[index]; + } + } else { + Debug(1, "Sending regular image index %d", index); + send_image = monitor->image_buffer[index]; + } + + if (!sendFrame(send_image, last_frame_timestamp)) { Debug(2, "sendFrame failed, quiting."); zm_terminate = true; break; @@ -704,7 +750,7 @@ void MonitorStream::runStream() { if (frame_count == 0) { // Chrome will not display the first frame until it receives another. // Firefox is fine. So just send the first frame twice. - if (!sendFrame(image, last_frame_timestamp)) { + if (!sendFrame(send_image, last_frame_timestamp)) { Debug(2, "sendFrame failed, quiting."); zm_terminate = true; break; @@ -726,7 +772,7 @@ void MonitorStream::runStream() { frame_count++; frame_count++; } else { - SystemTimePoint::duration actual_delta_time = now - last_frame_sent; + TimePoint::duration actual_delta_time = now - last_frame_sent; if (actual_delta_time > Seconds(5)) { if (paused_image) { // Send keepalive @@ -742,9 +788,9 @@ void MonitorStream::runStream() { } // end if actual_delta_time > 5 } // end if change in zoom } // end if paused or not - } else { - frame_count++; - } // end if should send frame + //} else { + //frame_count++; + } // end if should send frame now > when_to_send_next_frame if (buffered_playback && !paused) { if (monitor->shared_data->valid) { @@ -775,17 +821,42 @@ void MonitorStream::runStream() { } } // end if buffered playback } else { - Debug(3, "Waiting for capture last_write_index=%u", monitor->shared_data->last_write_index); + Debug(3, "Waiting for capture last_write_index=%u == last_read_index=%u", + monitor->shared_data->last_write_index, + last_read_index); } // end if ( (unsigned int)last_read_index != monitor->shared_data->last_write_index ) - FPSeconds sleep_time = - FPSeconds(ZM_RATE_BASE / ((base_fps ? base_fps : 1) * (replay_rate ? abs(replay_rate * 2) : 2))); + FPSeconds sleep_time; + if (now >= when_to_send_next_frame) { + // sent a frame, so update + + double capture_fps = monitor->GetFPS(); + double fps = ((maxfps > 0.0) && (capture_fps > maxfps)) ? maxfps : capture_fps; + double sleep_time_seconds = (1 / ((fps ? fps : 1))) // 1 second / fps + * (replay_rate ? abs(replay_rate)/ZM_RATE_BASE : 1); // replay_rate is 100 for 1x + Debug(3, "Using %f for maxfps. capture_fps: %f maxfps %f * replay_rate: %d = %f", fps, capture_fps, maxfps, replay_rate, sleep_time_seconds); + + sleep_time = FPSeconds(sleep_time_seconds); + if (when_to_send_next_frame > now) + sleep_time -= when_to_send_next_frame - now; + + when_to_send_next_frame = now + std::chrono::duration_cast(sleep_time); + + if (last_frame_sent > now) { + FPSeconds elapsed = last_frame_sent - now; + if (sleep_time > elapsed) { + sleep_time -= elapsed; + } + } + } else { + sleep_time = when_to_send_next_frame - now; + } if (sleep_time > MonitorStream::MAX_SLEEP) { + Debug(3, "Sleeping for MAX_SLEEP_USEC instead of %" PRIi64 " us", + static_cast(std::chrono::duration_cast(sleep_time).count())); // Shouldn't sleep for long because we need to check command queue, etc. sleep_time = MonitorStream::MAX_SLEEP; - Debug(3, "Sleeping for MAX_SLEEP_USEC %" PRIi64 " us", - static_cast(std::chrono::duration_cast(sleep_time).count())); } else { Debug(3, "Sleeping for %" PRIi64 " us", static_cast(std::chrono::duration_cast(sleep_time).count())); @@ -797,13 +868,6 @@ void MonitorStream::runStream() { static_cast(std::chrono::duration_cast(ttl).count())); break; } - - if (last_frame_sent.time_since_epoch() == Seconds(0)) { - // If we didn't capture above, because frame_mod was bad? Then last_frame_sent will not have a value. - last_frame_sent = now; - Warning("no last_frame_sent. Shouldn't happen. frame_mod was (%d) frame_count (%d)", - frame_mod, frame_count); - } } // end while ! zm_terminate if (buffered_playback) { @@ -854,16 +918,16 @@ void MonitorStream::SingleImage(int scale) { int index = monitor->shared_data->last_write_index % monitor->image_buffer_count; Debug(1, "write index: %d %d", monitor->shared_data->last_write_index, index); Image *snap_image = monitor->image_buffer[index]; + if (!config.timestamp_on_capture) { + monitor->TimestampImage(snap_image, + SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index]))); + } if ( scale != ZM_SCALE_BASE ) { scaled_image.Assign(*snap_image); scaled_image.Scale(scale); snap_image = &scaled_image; } - if (!config.timestamp_on_capture) { - monitor->TimestampImage(snap_image, - SystemTimePoint(zm::chrono::duration_cast(monitor->shared_timestamps[index]))); - } snap_image->EncodeJpeg(img_buffer, &img_buffer_size); fprintf(stdout, diff --git a/src/zm_mpeg.cpp b/src/zm_mpeg.cpp index b8f7eeb66..275e36628 100644 --- a/src/zm_mpeg.cpp +++ b/src/zm_mpeg.cpp @@ -1,17 +1,17 @@ /* * ZoneMinder MPEG class implementation, $Date$, $Revision$ * Copyright (C) 2001-2008 Philip Coombes - * + * * 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. @@ -42,19 +42,26 @@ void VideoStream::SetupFormat( ) { ofc = nullptr; avformat_alloc_output_context2(&ofc, nullptr, format, filename); - if ( !ofc ) { + if (!ofc) { Fatal("avformat_alloc_..._context failed"); } of = ofc->oformat; - Debug(1, "Using output format: %s (%s)", of->name, of->long_name); + Debug(1, "Using output format: %s (%s)", of->name, of->long_name); } -void VideoStream::SetupCodec( int colours, int subpixelorder, int width, int height, int bitrate, double frame_rate ) { +int VideoStream::SetupCodec( + int colours, + int subpixelorder, + int width, + int height, + int bitrate, + double frame_rate + ) { /* ffmpeg format matching */ - switch ( colours ) { + switch (colours) { case ZM_COLOUR_RGB24: - if ( subpixelorder == ZM_SUBPIX_ORDER_BGR ) { + if (subpixelorder == ZM_SUBPIX_ORDER_BGR) { /* BGR subpixel order */ pf = AV_PIX_FMT_BGR24; } else { @@ -63,13 +70,13 @@ void VideoStream::SetupCodec( int colours, int subpixelorder, int width, int hei } break; case ZM_COLOUR_RGB32: - if ( subpixelorder == ZM_SUBPIX_ORDER_ARGB ) { + if (subpixelorder == ZM_SUBPIX_ORDER_ARGB) { /* ARGB subpixel order */ pf = AV_PIX_FMT_ARGB; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_ABGR ) { + } else if (subpixelorder == ZM_SUBPIX_ORDER_ABGR) { /* ABGR subpixel order */ pf = AV_PIX_FMT_ABGR; - } else if ( subpixelorder == ZM_SUBPIX_ORDER_BGRA ) { + } else if (subpixelorder == ZM_SUBPIX_ORDER_BGRA) { /* BGRA subpixel order */ pf = AV_PIX_FMT_BGRA; } else { @@ -85,22 +92,22 @@ void VideoStream::SetupCodec( int colours, int subpixelorder, int width, int hei break; } - if ( strcmp("rtp", of->name) == 0 ) { + if (strcmp("rtp", of->name) == 0) { // RTP must have a packet_size. // Not sure what this value should be really... ofc->packet_size = width*height; Debug(1,"Setting packet_size to %d", ofc->packet_size); - - if ( of->video_codec == AV_CODEC_ID_NONE ) { + + if (of->video_codec == AV_CODEC_ID_NONE) { // RTP does not have a default codec in ffmpeg <= 0.8. of->video_codec = AV_CODEC_ID_MPEG4; } } - + _AVCODECID codec_id = of->video_codec; - if ( codec_name ) { + if (codec_name) { AVCodec *a = avcodec_find_encoder_by_name(codec_name); - if ( a ) { + if (a) { codec_id = a->id; Debug(1, "Using codec \"%s\"", codec_name); } else { @@ -111,31 +118,29 @@ void VideoStream::SetupCodec( int colours, int subpixelorder, int width, int hei /* add the video streams using the default format codecs and initialize the codecs */ ost = nullptr; - if ( codec_id != AV_CODEC_ID_NONE ) { + if (codec_id != AV_CODEC_ID_NONE) { codec = avcodec_find_encoder(codec_id); - if ( !codec ) { - Fatal("Could not find encoder for '%s'", avcodec_get_name(codec_id)); + if (!codec) { + Error("Could not find encoder for '%s'", avcodec_get_name(codec_id)); + return -1; } Debug(1, "Found encoder for '%s'", avcodec_get_name(codec_id)); - - ost = avformat_new_stream( ofc, codec ); - - if ( !ost ) { - Fatal( "Could not alloc stream" ); - return; + ost = avformat_new_stream(ofc, codec); + if (!ost) { + Error("Could not alloc stream"); + return -1; } - Debug( 1, "Allocated stream (%d) !=? (%d)", ost->id , ofc->nb_streams - 1 ); + Debug(1, "Allocated stream (%d) !=? (%d)", ost->id , ofc->nb_streams - 1); ost->id = ofc->nb_streams - 1; codec_context = avcodec_alloc_context3(nullptr); //avcodec_parameters_to_context(codec_context, ost->codecpar); - codec_context->codec_id = codec->id; codec_context->codec_type = codec->type; codec_context->pix_fmt = strcmp("mjpeg", ofc->oformat->name) == 0 ? AV_PIX_FMT_YUVJ422P : AV_PIX_FMT_YUV420P; - if ( bitrate <= 100 ) { + if (bitrate <= 100) { // Quality based bitrate control (VBR). Scale is 1..31 where 1 is best. // This gets rid of artifacts in the beginning of the movie; and well, even quality. codec_context->flags |= AV_CODEC_FLAG_QSCALE; @@ -155,22 +160,22 @@ void VideoStream::SetupCodec( int colours, int subpixelorder, int width, int hei codec_context->time_base.num = 1; ost->time_base.den = frame_rate; ost->time_base.num = 1; - - Debug( 1, "Will encode in %d fps. %dx%d", codec_context->time_base.den, width, height ); - + /* emit one intra frame every second */ codec_context->gop_size = frame_rate; // some formats want stream headers to be separate - if ( of->flags & AVFMT_GLOBALHEADER ) + if (of->flags & AVFMT_GLOBALHEADER) codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; avcodec_parameters_from_context(ost->codecpar, codec_context); zm_dump_codecpar(ost->codecpar); } else { - Fatal( "of->video_codec == AV_CODEC_ID_NONE" ); - } + Error("of->video_codec == AV_CODEC_ID_NONE"); + return -1; + } + return 0; } void VideoStream::SetParameters( ) { @@ -198,11 +203,11 @@ const char *VideoStream::MimeType() const { bool VideoStream::OpenStream( ) { int ret; - /* now that all the parameters are set, we can open the + /* now that all the parameters are set, we can open the video codecs and allocate the necessary encode buffers */ if ( ost ) { Debug(1,"Opening codec"); - + /* open the codec */ if ((ret = avcodec_open2(codec_context, codec, nullptr)) < 0) { @@ -319,7 +324,7 @@ VideoStream::VideoStream( const char *in_filename, const char *in_format, int bi if ( !initialised ) { Initialise( ); } - + if ( format ) { int length = strlen(format); codec_and_format = new char[length+1];; @@ -337,13 +342,13 @@ VideoStream::VideoStream( const char *in_filename, const char *in_format, int bi SetupFormat( ); SetupCodec( colours, subpixelorder, width, height, bitrate, frame_rate ); SetParameters( ); - + // Allocate buffered packets. packet_buffers = new AVPacket*[2]; packet_buffers[0] = new AVPacket(); packet_buffers[1] = new AVPacket(); packet_index = 0; - + // Initialize mutex used by streaming thread. if ( pthread_mutex_init( buffer_copy_lock, nullptr ) != 0 ) { Fatal("pthread_mutex_init failed"); @@ -353,35 +358,35 @@ VideoStream::VideoStream( const char *in_filename, const char *in_format, int bi VideoStream::~VideoStream( ) { Debug( 1, "VideoStream destructor." ); - + // Stop streaming thread. if ( streaming_thread ) { do_streaming = false; void* thread_exit_code; - + Debug( 1, "Asking streaming thread to exit." ); - + // Wait for thread to exit. pthread_join(streaming_thread, &thread_exit_code); } - + if ( buffer_copy != nullptr ) { av_free( buffer_copy ); } - + if ( buffer_copy_lock ) { if ( pthread_mutex_destroy( buffer_copy_lock ) != 0 ) { Error( "pthread_mutex_destroy failed" ); } delete buffer_copy_lock; } - + if (packet_buffers) { delete packet_buffers[0]; delete packet_buffers[1]; delete[] packet_buffers; } - + /* close each codec */ if ( ost ) { avcodec_close( codec_context ); @@ -409,7 +414,7 @@ VideoStream::~VideoStream( ) { /* free the stream */ av_free( ofc ); - + /* free format and codec_name data. */ if ( codec_and_format ) { delete codec_and_format; @@ -420,12 +425,12 @@ double VideoStream::EncodeFrame( const uint8_t *buffer, int buffer_size, bool _a if ( pthread_mutex_lock(buffer_copy_lock) != 0 ) { Fatal( "EncodeFrame: pthread_mutex_lock failed." ); } - + if (buffer_copy_size < buffer_size) { if ( buffer_copy ) { av_free(buffer_copy); } - + // Allocate a buffer to store source images for the streaming thread to encode. buffer_copy = (uint8_t *)av_malloc(buffer_size); if ( !buffer_copy ) { @@ -435,35 +440,34 @@ double VideoStream::EncodeFrame( const uint8_t *buffer, int buffer_size, bool _a } buffer_copy_size = buffer_size; } - + add_timestamp = _add_timestamp; timestamp = _timestamp; buffer_copy_used = buffer_size; memcpy(buffer_copy, buffer, buffer_size); - + if ( pthread_mutex_unlock(buffer_copy_lock) != 0 ) { Fatal( "EncodeFrame: pthread_mutex_unlock failed." ); } - + if ( streaming_thread == 0 ) { Debug( 1, "Starting streaming thread" ); - + // Start a thread for streaming encoded video. if (pthread_create( &streaming_thread, nullptr, StreamingThreadCallback, (void*) this) != 0){ // Log a fatal error and exit the process. Fatal( "VideoStream failed to create streaming thread." ); } } - + //return ActuallyEncodeFrame( buffer, buffer_size, add_timestamp, timestamp); - + return _timestamp; } double VideoStream::ActuallyEncodeFrame( const uint8_t *buffer, int buffer_size, bool add_timestamp, unsigned int timestamp ) { - if ( codec_context->pix_fmt != pf ) { - static struct SwsContext *img_convert_ctx = nullptr; + static struct SwsContext *img_convert_ctx = nullptr; memcpy( tmp_opicture->data[0], buffer, buffer_size ); if ( !img_convert_ctx ) { img_convert_ctx = sws_getCachedContext( nullptr, codec_context->width, codec_context->height, pf, codec_context->width, codec_context->height, codec_context->pix_fmt, SWS_BICUBIC, nullptr, nullptr, nullptr ); @@ -475,37 +479,36 @@ double VideoStream::ActuallyEncodeFrame( const uint8_t *buffer, int buffer_size, memcpy( opicture->data[0], buffer, buffer_size ); } AVFrame *opicture_ptr = opicture; - + AVPacket *pkt = packet_buffers[packet_index]; av_init_packet( pkt ); - int got_packet = 0; - if (codec_context->codec_type == AVMEDIA_TYPE_VIDEO && - codec_context->codec_id == AV_CODEC_ID_RAWVIDEO) { - pkt->flags |= AV_PKT_FLAG_KEY; - pkt->stream_index = ost->index; - pkt->data = (uint8_t *)opicture_ptr; - pkt->size = sizeof (AVPicture); - got_packet = 1; + int got_packet = 0; + if (codec_context->codec_type == AVMEDIA_TYPE_VIDEO && + codec_context->codec_id == AV_CODEC_ID_RAWVIDEO) { + pkt->flags |= AV_PKT_FLAG_KEY; + pkt->stream_index = ost->index; + pkt->data = (uint8_t *)opicture_ptr; + pkt->size = sizeof (AVPicture); + got_packet = 1; } else { opicture_ptr->pts = codec_context->frame_number; opicture_ptr->quality = codec_context->global_quality; - avcodec_send_frame(codec_context, opicture_ptr); - int ret = avcodec_receive_packet(codec_context, pkt); - if ( ret < 0 ) { - if ( AVERROR_EOF != ret ) { - Error("ERror encoding video (%d) (%s)", ret, - av_err2str(ret)); - } - } else { - got_packet = 1; + avcodec_send_frame(codec_context, opicture_ptr); + int ret = avcodec_receive_packet(codec_context, pkt); + if (ret < 0) { + if (AVERROR_EOF != ret) { + Error("ERror encoding video (%d) (%s)", ret, av_err2str(ret)); } + } else { + got_packet = 1; + } - if ( got_packet ) { -// if ( c->coded_frame->key_frame ) -// { -// pkt->flags |= AV_PKT_FLAG_KEY; -// } + if (got_packet) { + // if ( c->coded_frame->key_frame ) + // { + // pkt->flags |= AV_PKT_FLAG_KEY; + // } if ( pkt->pts != (int64_t)AV_NOPTS_VALUE ) { pkt->pts = av_rescale_q( pkt->pts, codec_context->time_base, ost->time_base ); @@ -517,18 +520,17 @@ double VideoStream::ActuallyEncodeFrame( const uint8_t *buffer, int buffer_size, pkt->stream_index = ost->index; } } - - return ( opicture_ptr->pts); + + return opicture_ptr->pts; } int VideoStream::SendPacket(AVPacket *packet) { - - int ret = av_write_frame( ofc, packet ); - if ( ret != 0 ) { - Fatal( "Error %d while writing video frame: %s", ret, av_err2str( errno ) ); - } - av_packet_unref( packet ); - return ret; + int ret = av_write_frame(ofc, packet); + if (ret < 0) { + Error("Error %d while writing video frame: %s", ret, av_err2str(errno)); + } + av_packet_unref(packet); + return ret; } void *VideoStream::StreamingThreadCallback(void *ctx) { diff --git a/src/zm_mpeg.h b/src/zm_mpeg.h index b6a6f49be..552652a1d 100644 --- a/src/zm_mpeg.h +++ b/src/zm_mpeg.h @@ -68,7 +68,7 @@ protected: static void Initialise(); void SetupFormat( ); - void SetupCodec( int colours, int subpixelorder, int width, int height, int bitrate, double frame_rate ); + int SetupCodec( int colours, int subpixelorder, int width, int height, int bitrate, double frame_rate ); void SetParameters(); void ActuallyOpenStream(); double ActuallyEncodeFrame( const uint8_t *buffer, int buffer_size, bool add_timestamp=false, unsigned int timestamp=0 ); diff --git a/src/zm_packet.h b/src/zm_packet.h index 9cce4dd03..8a6a31de5 100644 --- a/src/zm_packet.h +++ b/src/zm_packet.h @@ -57,6 +57,7 @@ class ZMPacket { int64_t pts; // pts in the packet can be in another time base. This MUST be in AV_TIME_BASE_Q bool decoded; std::vector zone_stats; + std::string alarm_cause; public: AVPacket *av_packet() { return &packet; } diff --git a/src/zm_packetqueue.cpp b/src/zm_packetqueue.cpp index 509a25ee6..26f19de75 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -116,15 +116,16 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { , max_video_packet_count); for ( - auto it = ++pktQueue.begin(); - it != pktQueue.end() and *it != add_packet; + auto it = ++pktQueue.begin(); + //it != pktQueue.end() and // can't git end because we added our packet + *it != add_packet; + // iterator is incremented by erase ) { std::shared_ptr zm_packet = *it; - ZMLockedPacket *lp = new ZMLockedPacket(zm_packet); - if (!lp->trylock()) { - Debug(1, "Found locked packet when trying to free up video packets. Skipping to next one"); - delete lp; + ZMLockedPacket lp(zm_packet); + if (!lp.trylock()) { + Warning("Found locked packet when trying to free up video packets. This basically means that decoding is not keeping up."); ++it; continue; } @@ -136,7 +137,7 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { ) { auto iterator_it = *iterators_it; // Have to check each iterator and make sure it doesn't point to the packet we are about to delete - if ((*iterator_it!=pktQueue.end()) and (*(*iterator_it) == zm_packet)) { + if (*(*iterator_it) == zm_packet) { Debug(1, "Bumping IT because it is at the front that we are deleting"); ++(*iterator_it); } @@ -153,8 +154,6 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { max_video_packet_count, pktQueue.size()); - delete lp; - if (zm_packet->packet.stream_index == video_stream_id) break; } // end while @@ -162,7 +161,7 @@ bool PacketQueue::queuePacket(std::shared_ptr add_packet) { } // end lock scope // We signal on every packet because someday we may analyze sound Debug(4, "packetqueue queuepacket, unlocked signalling"); - condition.notify_all(); + condition.notify_one(); return true; } // end bool PacketQueue::queuePacket(ZMPacket* zm_packet) @@ -312,7 +311,6 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { pktQueue.size()); pktQueue.pop_front(); packet_counts[zm_packet->packet.stream_index] -= 1; - //delete zm_packet; } } // end if have at least max_video_packet_count video packets remaining // We signal on every packet because someday we may analyze sound diff --git a/src/zm_poll_thread.cpp b/src/zm_poll_thread.cpp new file mode 100644 index 000000000..ee46dbfe5 --- /dev/null +++ b/src/zm_poll_thread.cpp @@ -0,0 +1,32 @@ +#include "zm_poll_thread.h" + +#include "zm_monitor.h" +#include "zm_signal.h" +#include "zm_time.h" + +PollThread::PollThread(Monitor *monitor) : + monitor_(monitor), terminate_(false) { + thread_ = std::thread(&PollThread::Run, this); +} + +PollThread::~PollThread() { + Stop(); +} + +void PollThread::Start() { + if (thread_.joinable()) thread_.join(); + terminate_ = false; + Debug(3, "Starting polling thread"); + thread_ = std::thread(&PollThread::Run, this); +} +void PollThread::Stop() { + terminate_ = true; + if (thread_.joinable()) { + thread_.join(); + } +} +void PollThread::Run() { + while (!(terminate_ or zm_terminate)) { + monitor_->Poll(); + } +} diff --git a/src/zm_poll_thread.h b/src/zm_poll_thread.h new file mode 100644 index 000000000..16444a951 --- /dev/null +++ b/src/zm_poll_thread.h @@ -0,0 +1,29 @@ +#ifndef ZM_POLL_THREAD_H +#define ZM_POLL_THREAD_H + +#include +#include +#include + +class Monitor; + +class PollThread { + public: + explicit PollThread(Monitor *monitor); + ~PollThread(); + PollThread(PollThread &rhs) = delete; + PollThread(PollThread &&rhs) = delete; + + void Start(); + void Stop(); + bool Stopped() const { return terminate_; } + + private: + void Run(); + + Monitor *monitor_; + std::atomic terminate_; + std::thread thread_; +}; + +#endif diff --git a/src/zm_remote_camera_rtsp.cpp b/src/zm_remote_camera_rtsp.cpp index 8cdecdf94..ffb4a061d 100644 --- a/src/zm_remote_camera_rtsp.cpp +++ b/src/zm_remote_camera_rtsp.cpp @@ -22,6 +22,7 @@ #include "zm_config.h" #include "zm_monitor.h" #include "zm_packet.h" +#include "zm_signal.h" RemoteCameraRtsp::RemoteCameraRtsp( const Monitor *monitor, @@ -126,8 +127,8 @@ int RemoteCameraRtsp::Disconnect() { int RemoteCameraRtsp::PrimeCapture() { Debug(2, "Waiting for sources"); - for (int i = 0; i < 100 && !rtspThread->hasSources(); i++) { - std::this_thread::sleep_for(Microseconds(100)); + for (int i = 100; i && !zm_terminate && !rtspThread->hasSources(); i--) { + std::this_thread::sleep_for(Microseconds(10000)); } if (!rtspThread->hasSources()) { @@ -168,8 +169,10 @@ int RemoteCameraRtsp::PrimeCapture() { } } // end foreach stream - if ( mVideoStreamId == -1 ) - Fatal("Unable to locate video stream"); + if ( mVideoStreamId == -1 ) { + Error("Unable to locate video stream"); + return -1; + } if ( mAudioStreamId == -1 ) Debug(3, "Unable to locate audio stream"); @@ -179,17 +182,22 @@ int RemoteCameraRtsp::PrimeCapture() { // Find the decoder for the video stream AVCodec *codec = avcodec_find_decoder(mVideoCodecContext->codec_id); - if ( codec == nullptr ) - Panic("Unable to locate codec %d decoder", mVideoCodecContext->codec_id); + if ( codec == nullptr ) { + Error("Unable to locate codec %d decoder", mVideoCodecContext->codec_id); + return -1; + } // Open codec - if ( avcodec_open2(mVideoCodecContext, codec, nullptr) < 0 ) - Panic("Can't open codec"); + if ( avcodec_open2(mVideoCodecContext, codec, nullptr) < 0 ) { + Error("Can't open codec"); + return -1; + } int pSize = av_image_get_buffer_size(imagePixFormat, width, height, 1); if ( (unsigned int)pSize != imagesize ) { - Fatal("Image size mismatch. Required: %d Available: %llu", pSize, imagesize); + Error("Image size mismatch. Required: %d Available: %llu", pSize, imagesize); + return -1; } return 1; @@ -208,18 +216,13 @@ int RemoteCameraRtsp::PreCapture() { int RemoteCameraRtsp::Capture(std::shared_ptr &zm_packet) { int frameComplete = false; AVPacket *packet = &zm_packet->packet; - if ( !zm_packet->image ) { - Debug(1, "Allocating image %dx%d %d colours %d", width, height, colours, subpixelorder); - zm_packet->image = new Image(width, height, colours, subpixelorder); - } - while (!frameComplete) { buffer.clear(); - if (!rtspThread || rtspThread->IsStopped()) + if (!rtspThread || rtspThread->IsStopped() || zm_terminate) return -1; - if ( rtspThread->getFrame(buffer) ) { + if (rtspThread->getFrame(buffer)) { Debug(3, "Read frame %d bytes", buffer.size()); Hexdump(4, buffer.head(), 16); @@ -254,36 +257,20 @@ int RemoteCameraRtsp::Capture(std::shared_ptr &zm_packet) { //while ( (!frameComplete) && (buffer.size() > 0) ) { if ( buffer.size() > 0 ) { - packet->data = buffer.head(); + packet->data = (uint8_t*)av_malloc(buffer.size()); + memcpy(packet->data, buffer.head(), buffer.size()); + //packet->data = buffer.head(); packet->size = buffer.size(); bytes += packet->size; + buffer -= packet->size; struct timeval now; - gettimeofday(&now, NULL); + gettimeofday(&now, nullptr); packet->pts = packet->dts = now.tv_sec*1000000+now.tv_usec; - - int bytes_consumed = zm_packet->decode(mVideoCodecContext); - if ( bytes_consumed < 0 ) { - Error("Error while decoding frame %d", frameCount); - //Hexdump(Logger::ERROR, buffer.head(), buffer.size()>256?256:buffer.size()); - } - buffer -= packet->size; - if ( bytes_consumed ) { - zm_dump_video_frame(zm_packet->in_frame, "remote_rtsp_decode"); - if (!mVideoStream->codecpar->width) { - zm_dump_codec(mVideoCodecContext); - zm_dump_codecpar(mVideoStream->codecpar); - mVideoStream->codecpar->width = zm_packet->in_frame->width; - mVideoStream->codecpar->height = zm_packet->in_frame->height; - zm_dump_codecpar(mVideoStream->codecpar); - } - zm_packet->codec_type = mVideoCodecContext->codec_type; - zm_packet->stream = mVideoStream; - frameComplete = true; - Debug(2, "Frame: %d - %d/%d", frameCount, bytes_consumed, buffer.size()); - packet->data = nullptr; - packet->size = 0; - } + zm_packet->codec_type = mVideoCodecContext->codec_type; + zm_packet->stream = mVideoStream; + frameComplete = true; + Debug(2, "Frame: %d - %d/%d", frameCount, packet->size, buffer.size()); } } /* getFrame() */ } // end while true diff --git a/src/zm_rgb.h b/src/zm_rgb.h index 58ad9bfae..ef1d46ce3 100644 --- a/src/zm_rgb.h +++ b/src/zm_rgb.h @@ -118,41 +118,34 @@ constexpr Rgb kRGBTransparent = 0x01000000; /* Convert RGB colour value into BGR\ARGB\ABGR */ inline Rgb rgb_convert(Rgb p_col, int p_subpixorder) { - Rgb result; + Rgb result = 0; - switch(p_subpixorder) { - + switch (p_subpixorder) { case ZM_SUBPIX_ORDER_BGR: case ZM_SUBPIX_ORDER_BGRA: - { - BLUE_PTR_BGRA(&result) = BLUE_VAL_RGBA(p_col); - GREEN_PTR_BGRA(&result) = GREEN_VAL_RGBA(p_col); - RED_PTR_BGRA(&result) = RED_VAL_RGBA(p_col); - } - break; + BLUE_PTR_BGRA(&result) = BLUE_VAL_RGBA(p_col); + GREEN_PTR_BGRA(&result) = GREEN_VAL_RGBA(p_col); + RED_PTR_BGRA(&result) = RED_VAL_RGBA(p_col); + break; case ZM_SUBPIX_ORDER_ARGB: - { - BLUE_PTR_ARGB(&result) = BLUE_VAL_RGBA(p_col); - GREEN_PTR_ARGB(&result) = GREEN_VAL_RGBA(p_col); - RED_PTR_ARGB(&result) = RED_VAL_RGBA(p_col); - } - break; + BLUE_PTR_ARGB(&result) = BLUE_VAL_RGBA(p_col); + GREEN_PTR_ARGB(&result) = GREEN_VAL_RGBA(p_col); + RED_PTR_ARGB(&result) = RED_VAL_RGBA(p_col); + break; case ZM_SUBPIX_ORDER_ABGR: - { - BLUE_PTR_ABGR(&result) = BLUE_VAL_RGBA(p_col); - GREEN_PTR_ABGR(&result) = GREEN_VAL_RGBA(p_col); - RED_PTR_ABGR(&result) = RED_VAL_RGBA(p_col); - } - break; - /* Grayscale */ + BLUE_PTR_ABGR(&result) = BLUE_VAL_RGBA(p_col); + GREEN_PTR_ABGR(&result) = GREEN_VAL_RGBA(p_col); + RED_PTR_ABGR(&result) = RED_VAL_RGBA(p_col); + break; + /* Grayscale */ case ZM_SUBPIX_ORDER_NONE: - result = p_col & 0xff; - break; + result = p_col & 0xff; + break; default: - return p_col; - break; + result = p_col; + break; } - + return result; } diff --git a/src/zm_rtp_ctrl.cpp b/src/zm_rtp_ctrl.cpp index 25d34f0ff..a82ff2b2a 100644 --- a/src/zm_rtp_ctrl.cpp +++ b/src/zm_rtp_ctrl.cpp @@ -277,7 +277,7 @@ void RtpCtrlThread::Run() { TimePoint last_receive = std::chrono::steady_clock::now(); bool timeout = false; // used as a flag that we had a timeout, and then sent an RR to see if we wake back up. Real timeout will happen when this is true. - while (!mTerminate && select.wait() >= 0) { + while (!mTerminate && (select.wait() >= 0)) { TimePoint now = std::chrono::steady_clock::now(); zm::Select::CommsList readable = select.getReadable(); if ( readable.size() == 0 ) { diff --git a/src/zm_rtp_source.cpp b/src/zm_rtp_source.cpp index 1862c1886..56ca2cf0d 100644 --- a/src/zm_rtp_source.cpp +++ b/src/zm_rtp_source.cpp @@ -45,8 +45,10 @@ RtpSource::RtpSource( mFrame(65536), mFrameCount(0), mFrameGood(true), + prevM(false), mFrameReady(false), - mFrameProcessed(false) + mFrameProcessed(false), + mTerminate(false) { char hostname[256] = ""; gethostname(hostname, sizeof(hostname)); diff --git a/src/zm_rtp_source.h b/src/zm_rtp_source.h index a39e8225f..71be9af2c 100644 --- a/src/zm_rtp_source.h +++ b/src/zm_rtp_source.h @@ -91,8 +91,6 @@ private: bool mFrameGood; bool prevM; - bool mTerminate; - bool mFrameReady; std::condition_variable mFrameReadyCv; std::mutex mFrameReadyMutex; @@ -100,6 +98,7 @@ private: bool mFrameProcessed; std::condition_variable mFrameProcessedCv; std::mutex mFrameProcessedMutex; + bool mTerminate; private: void init(uint16_t seq); diff --git a/src/zm_rtsp_server.cpp b/src/zm_rtsp_server.cpp index ea6f4e2b7..3da273e00 100644 --- a/src/zm_rtsp_server.cpp +++ b/src/zm_rtsp_server.cpp @@ -298,6 +298,14 @@ int main(int argc, char *argv[]) { session->GetMediaSessionId(), xop::channel_1, audioFifoPath); audioSource->setFrequency(monitor->GetAudioFrequency()); audioSource->setChannels(monitor->GetAudioChannels()); + } else if (std::string::npos != audioFifoPath.find("pcm_alaw")) { + Debug(1, "Adding G711A source at %dHz %d channels", + monitor->GetAudioFrequency(), monitor->GetAudioChannels()); + session->AddSource(xop::channel_1, xop::G711ASource::CreateNew()); + audioSource = new ADTS_ZoneMinderFifoSource(rtspServer, + session->GetMediaSessionId(), xop::channel_1, audioFifoPath); + audioSource->setFrequency(monitor->GetAudioFrequency()); + audioSource->setChannels(monitor->GetAudioChannels()); } else { Warning("Unknown format in %s", audioFifoPath.c_str()); } diff --git a/src/zm_rtsp_server_fifo_source.cpp b/src/zm_rtsp_server_fifo_source.cpp index 40dbc14fc..1845c96ef 100644 --- a/src/zm_rtsp_server_fifo_source.cpp +++ b/src/zm_rtsp_server_fifo_source.cpp @@ -109,7 +109,7 @@ void ZoneMinderFifoSource::WriteRun() { fuNal.buffer()[2] = fuNal.buffer()[2]&~0x80; // FU header (no S bit) headerSize = 3; } - while (nalRemaining) { + while (nalRemaining && !stop_) { if ( nalRemaining < maxNalSize ) { // This is the last fragment: fuNal.buffer()[headerSize-1] |= 0x40; // set the E bit in the FU header @@ -166,7 +166,7 @@ int ZoneMinderFifoSource::getNextFrame() { } Debug(3, "%s bytes read %d bytes, buffer size %u", m_fifo.c_str(), bytes_read, m_buffer.size()); - while (m_buffer.size()) { + while (m_buffer.size() and !stop_) { unsigned int data_size = 0; int64_t pts; unsigned char *header_end = nullptr; @@ -224,7 +224,7 @@ int ZoneMinderFifoSource::getNextFrame() { int bytes_needed = data_size - (m_buffer.size() - header_size); if (bytes_needed > 0) { Debug(4, "Need another %d bytes. Trying to read them", bytes_needed); - while (bytes_needed) { + while (bytes_needed and !stop_) { bytes_read = m_buffer.read_into(m_fd, bytes_needed); if (bytes_read <= 0) { Debug(1, "Failed to read another %d bytes, got %d.", bytes_needed, bytes_read); @@ -252,13 +252,14 @@ int ZoneMinderFifoSource::getNextFrame() { { std::unique_lock lck(mutex_); Debug(3, "have lock"); - while (framesList.size()) { + while (!stop_ && framesList.size()) { std::pair nal = framesList.front(); framesList.pop_front(); NAL_Frame *Nal = new NAL_Frame(nal.first, nal.second, pts); m_nalQueue.push(Nal); } } + Debug(3, "notifying"); condition_.notify_all(); } // end while m_buffer.size() return 1; diff --git a/src/zm_rtsp_server_server_media_subsession.h b/src/zm_rtsp_server_server_media_subsession.h index d3780cd6e..04632c44b 100644 --- a/src/zm_rtsp_server_server_media_subsession.h +++ b/src/zm_rtsp_server_server_media_subsession.h @@ -21,7 +21,7 @@ class ZoneMinderDeviceSource; class BaseServerMediaSubsession { public: - BaseServerMediaSubsession(StreamReplicator* replicator): + explicit BaseServerMediaSubsession(StreamReplicator* replicator): m_replicator(replicator) {}; FramedSource* createSource( diff --git a/src/zm_sendfile.h b/src/zm_sendfile.h index fd72e2e61..4edabccab 100644 --- a/src/zm_sendfile.h +++ b/src/zm_sendfile.h @@ -3,34 +3,44 @@ #ifdef HAVE_SENDFILE4_SUPPORT #include -int zm_sendfile(int out_fd, int in_fd, off_t *offset, size_t size) { - int err; - - err = sendfile(out_fd, in_fd, offset, size); - if ( err < 0 ) - return -errno; - - return err; -} #elif HAVE_SENDFILE7_SUPPORT #include #include #include -int zm_sendfile(int out_fd, int in_fd, off_t *offset, off_t size) { - int err; - err = sendfile(in_fd, out_fd, *offset, size, nullptr, &size, 0); - if (err && errno != EAGAIN) - return -errno; - - if (size) { - *offset += size; - return size; - } - - return -EAGAIN; -} #else -#error "Your platform does not support sendfile. Sorry." +#include #endif +/* Function to send the contents of a file. Will use sendfile or fall back to reading/writing */ + +ssize_t zm_sendfile(int out_fd, int in_fd, off_t *offset, size_t size) { +#ifdef HAVE_SENDFILE4_SUPPORT + ssize_t err = sendfile(out_fd, in_fd, offset, size); + if (err < 0) { + return -errno; + } + return err; + +#elif HAVE_SENDFILE7_SUPPORT + ssize_t err = sendfile(in_fd, out_fd, *offset, size, nullptr, &size, 0); + if (err && errno != EAGAIN) + return -errno; + return size; +#else + uint8_t buffer[size]; + ssize_t err = read(in_fd, buffer, size); + if (err < 0) { + Error("Unable to read %zu bytes: %s", size, strerror(errno)); + return -errno; + } + + err = fwrite(out_fd, buffer, size); + if (err < 0) { + Error("Unable to write %zu bytes: %s", size, strerror(errno)); + return -errno; + } + return err; +#endif +} + #endif // ZM_SENDFILE_H diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index 5255d3ab2..b3b3aa120 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -31,10 +31,8 @@ constexpr Seconds StreamBase::MAX_STREAM_DELAY; constexpr Milliseconds StreamBase::MAX_SLEEP; StreamBase::~StreamBase() { - if (vid_stream) { - delete vid_stream; - vid_stream = nullptr; - } + delete vid_stream; + delete temp_img_buffer; closeComms(); } @@ -128,10 +126,10 @@ bool StreamBase::checkCommandQueue() { return true; } } else if ( connkey ) { - Warning("No sd in checkCommandQueue, comms not open?"); + Warning("No sd in checkCommandQueue, comms not open for connkey %06d?", connkey); } else { // Perfectly valid if only getting a snapshot - Debug(1, "No sd in checkCommandQueue, comms not open?"); + Debug(1, "No sd in checkCommandQueue, comms not open."); } return false; } // end bool StreamBase::checkCommandQueue() @@ -157,7 +155,6 @@ Image *StreamBase::prepareImage(Image *image) { int disp_image_width = (image->Width() * scale) / ZM_SCALE_BASE, disp_image_height = (image->Height() * scale) / ZM_SCALE_BASE; int last_disp_image_width = (image->Width() * last_scale) / ZM_SCALE_BASE, last_disp_image_height = (image->Height() * last_scale) / ZM_SCALE_BASE; int send_image_width = (disp_image_width * act_mag ) / mag, send_image_height = (disp_image_height * act_mag ) / mag; - int last_send_image_width = (last_disp_image_width * last_act_mag ) / last_mag, last_send_image_height = (last_disp_image_height * last_act_mag ) / last_mag; Debug(3, "Scaling by %d, zooming by %d = magnifying by %d(%d)\n" @@ -169,8 +166,7 @@ Image *StreamBase::prepareImage(Image *image) { "Last actual image width = %d, height = %d\n" "Display image width = %d, height = %d\n" "Last display image width = %d, height = %d\n" - "Send image width = %d, height = %d\n" - "Last send image width = %d, height = %d\n", + "Send image width = %d, height = %d\n", scale, zoom, mag, act_mag, last_scale, last_zoom, last_mag, last_act_mag, base_image_width, base_image_height, @@ -180,8 +176,7 @@ Image *StreamBase::prepareImage(Image *image) { last_act_image_width, last_act_image_height, disp_image_width, disp_image_height, last_disp_image_width, last_disp_image_height, - send_image_width, send_image_height, - last_send_image_width, last_send_image_height + send_image_width, send_image_height ); if ( ( mag != ZM_SCALE_BASE ) && (act_mag != ZM_SCALE_BASE) ) { @@ -386,9 +381,9 @@ void StreamBase::openComms() { strncpy(rem_addr.sun_path, rem_sock_path, sizeof(rem_addr.sun_path)); rem_addr.sun_family = AF_UNIX; - last_comm_update = std::chrono::system_clock::now(); + last_comm_update = std::chrono::steady_clock::now(); + Debug(3, "comms open at %s", loc_sock_path); } // end if connKey > 0 - Debug(3, "comms open at %s", loc_sock_path); } // end void StreamBase::openComms() void StreamBase::closeComms() { diff --git a/src/zm_stream.h b/src/zm_stream.h index 8650801ea..de58fcf7b 100644 --- a/src/zm_stream.h +++ b/src/zm_stream.h @@ -40,6 +40,7 @@ public: STREAM_SINGLE, STREAM_MPEG } StreamType; + typedef enum { FRAME_NORMAL, FRAME_ANALYSIS } FrameType; protected: static constexpr Seconds MAX_STREAM_DELAY = Seconds(5); @@ -88,6 +89,9 @@ protected: CMD_VARPLAY, CMD_GET_IMAGE, CMD_QUIT, + CMD_MAXFPS, + CMD_ANALYZE_ON, + CMD_ANALYZE_OFF, CMD_QUERY=99 } MsgCommand; @@ -96,6 +100,7 @@ protected: std::shared_ptr monitor; StreamType type; + FrameType frame_type; const char *format; int replay_rate; int scale; @@ -118,26 +123,30 @@ protected: bool paused; int step; - SystemTimePoint now; - SystemTimePoint last_comm_update; + TimePoint now; + TimePoint last_comm_update; double maxfps; double base_fps; // Should be capturing fps, hence a rough target double effective_fps; // Target fps after taking max_fps into account double actual_fps; // sliding calculated actual streaming fps achieved - SystemTimePoint last_fps_update; + TimePoint last_fps_update; int frame_count; // Count of frames sent int last_frame_count; // Used in calculating actual_fps from frame_count - last_frame_count int frame_mod; - SystemTimePoint last_frame_sent; + TimePoint last_frame_sent; SystemTimePoint last_frame_timestamp; + TimePoint when_to_send_next_frame; // When to send next frame so if now < send_next_frame, skip VideoStream *vid_stream; CmdMsg msg; + unsigned char *temp_img_buffer; // Used when encoding or sending file data + size_t temp_img_buffer_size; + protected: bool loadMonitor(int monitor_id); bool checkInitialised(); @@ -151,6 +160,7 @@ public: monitor_id(0), monitor(nullptr), type(DEFAULT_TYPE), + frame_type(FRAME_NORMAL), format(""), replay_rate(DEFAULT_RATE), scale(DEFAULT_SCALE), @@ -175,7 +185,9 @@ public: actual_fps(0.0), frame_count(0), last_frame_count(0), - frame_mod(1) + frame_mod(1), + temp_img_buffer(nullptr), + temp_img_buffer_size(0) { memset(&loc_sock_path, 0, sizeof(loc_sock_path)); memset(&loc_addr, 0, sizeof(loc_addr)); @@ -196,7 +208,9 @@ public: type = STREAM_RAW; } #endif - + } + void setStreamFrameType(FrameType p_type) { + frame_type = p_type; } void setStreamFormat(const char *p_format) { format = p_format; @@ -207,10 +221,11 @@ public: scale = DEFAULT_SCALE; } void setStreamReplayRate(int p_rate) { - Debug(2, "Setting replay_rate %d", p_rate); + Debug(1, "Setting replay_rate %d", p_rate); replay_rate = p_rate; } void setStreamMaxFPS(double p_maxfps) { + Debug(1, "Setting max fps to %f", p_maxfps); maxfps = p_maxfps; } void setStreamBitrate(int p_bitrate) { diff --git a/src/zm_swscale.cpp b/src/zm_swscale.cpp index 20b322ab9..68e7af4ef 100644 --- a/src/zm_swscale.cpp +++ b/src/zm_swscale.cpp @@ -22,7 +22,14 @@ #include "zm_image.h" #include "zm_logger.h" -SWScale::SWScale() : gotdefaults(false), swscale_ctx(nullptr), input_avframe(nullptr), output_avframe(nullptr) { +SWScale::SWScale() : + gotdefaults(false), + swscale_ctx(nullptr), + input_avframe(nullptr), + output_avframe(nullptr), + default_width(0), + default_height(0) +{ Debug(4, "SWScale object created"); } diff --git a/src/zm_time.cpp b/src/zm_time.cpp new file mode 100644 index 000000000..041962003 --- /dev/null +++ b/src/zm_time.cpp @@ -0,0 +1,52 @@ +// +// ZoneMinder Time Functions & Definitions, $Date$, $Revision$ +// Copyright (C) 2001-2008 Philip Coombes +// +// 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. +// + +#include "zm_time.h" + +#include + +std::string SystemTimePointToString(SystemTimePoint tp) { + time_t tp_sec = std::chrono::system_clock::to_time_t(tp); + Microseconds now_frac = std::chrono::duration_cast( + tp.time_since_epoch() - std::chrono::duration_cast(tp.time_since_epoch())); + + std::string timeString; + timeString.reserve(64); + char *timePtr = &*(timeString.begin()); + tm tp_tm = {}; + timePtr += strftime(timePtr, timeString.capacity(), "%x %H:%M:%S", localtime_r(&tp_sec, &tp_tm)); + snprintf(timePtr, timeString.capacity() - (timePtr - timeString.data()), ".%06" PRIi64, static_cast(now_frac.count())); + return timeString; +} + +std::string TimePointToString(TimePoint tp) { + time_t tp_sec = std::chrono::system_clock::to_time_t( + std::chrono::system_clock::now() + (tp - std::chrono::steady_clock::now())); + + Microseconds now_frac = std::chrono::duration_cast( + tp.time_since_epoch() - std::chrono::duration_cast(tp.time_since_epoch())); + + std::string timeString; + timeString.reserve(64); + char *timePtr = &*(timeString.begin()); + tm tp_tm = {}; + timePtr += strftime(timePtr, timeString.capacity(), "%x %H:%M:%S", localtime_r(&tp_sec, &tp_tm)); + snprintf(timePtr, timeString.capacity() - (timePtr - timeString.data()), ".%06" PRIi64, static_cast(now_frac.count())); + return timeString; +} diff --git a/src/zm_time.h b/src/zm_time.h index d3d5b95c5..d9d131944 100644 --- a/src/zm_time.h +++ b/src/zm_time.h @@ -21,6 +21,7 @@ #define ZM_TIME_H #include +#include #include typedef std::chrono::microseconds Microseconds; @@ -90,7 +91,7 @@ Duration duration_cast(timeval const &tv) { // class TimeSegmentAdder { public: - TimeSegmentAdder(Microseconds &in_target) : + explicit TimeSegmentAdder(Microseconds &in_target) : target_(in_target), start_time_(std::chrono::steady_clock::now()), finished_(false) { @@ -120,4 +121,7 @@ class TimeSegmentAdder { bool finished_; }; +std::string SystemTimePointToString(SystemTimePoint tp); +std::string TimePointToString(TimePoint tp); + #endif // ZM_TIME_H diff --git a/src/zm_utils.cpp b/src/zm_utils.cpp index 5da5509ff..6ea4e0dae 100644 --- a/src/zm_utils.cpp +++ b/src/zm_utils.cpp @@ -122,13 +122,14 @@ std::string stringtf(const char* format, ...) { va_start(args, format); va_list args2; va_copy(args2, args); - - int size = vsnprintf(nullptr, 0, format, args) + 1; // Extra space for '\0' + int size = vsnprintf(nullptr, 0, format, args); va_end(args); - if (size <= 0) { + if (size < 0) { + va_end(args2); throw std::runtime_error("Error during formatting."); } + size += 1; // Extra space for '\0' std::unique_ptr buf(new char[size]); vsnprintf(buf.get(), size, format, args2); @@ -252,8 +253,17 @@ void HwCapsDetect() { #elif defined(__arm__) // ARM processor in 32bit mode // To see if it supports NEON, we need to get that information from the kernel + #ifdef __linux__ unsigned long auxval = getauxval(AT_HWCAP); if (auxval & HWCAP_ARM_NEON) { + #elif defined(__FreeBSD__) + unsigned long auxval = 0; + elf_aux_info(AT_HWCAP, &auxval, sizeof(auxval)); + if (auxval & HWCAP_NEON) { + #else + { + #error Unsupported OS. + #endif Debug(1,"Detected ARM (AArch32) processor with Neon"); neonversion = 1; } else { diff --git a/src/zm_videostore.cpp b/src/zm_videostore.cpp index ad401b22c..249ff222a 100644 --- a/src/zm_videostore.cpp +++ b/src/zm_videostore.cpp @@ -401,6 +401,10 @@ bool VideoStore::open() { } else { audio_in_ctx = avcodec_alloc_context3(audio_out_codec); ret = avcodec_parameters_to_context(audio_in_ctx, audio_in_stream->codecpar); + if (ret < 0) + Error("Failure from avcodec_parameters_to_context %s", + av_make_error_string(ret).c_str()); + audio_in_ctx->time_base = audio_in_stream->time_base; audio_out_ctx = avcodec_alloc_context3(audio_out_codec); @@ -497,8 +501,16 @@ bool VideoStore::open() { Debug(1, "using movflags %s", movflags_entry->value); } if ((ret = avformat_write_header(oc, &opts)) < 0) { - Warning("Unable to set movflags trying with defaults."); - ret = avformat_write_header(oc, nullptr); + // we crash if we try again + if (ENOSPC != ret) { + Warning("Unable to set movflags trying with defaults.%d %s", + ret, av_make_error_string(ret).c_str()); + + ret = avformat_write_header(oc, nullptr); + Debug(1, "Done %d", ret); + } else { + Error("ENOSPC. fail"); + } } else if (av_dict_count(opts) != 0) { Info("some options not used, turn on debugging for a list."); AVDictionaryEntry *e = nullptr; @@ -729,7 +741,6 @@ bool VideoStore::setup_resampler() { audio_out_ctx->sample_fmt = audio_in_ctx->sample_fmt; audio_out_ctx->channels = audio_in_ctx->channels; audio_out_ctx->channel_layout = audio_in_ctx->channel_layout; - audio_out_ctx->sample_fmt = audio_in_ctx->sample_fmt; if (!audio_out_ctx->channel_layout) { Debug(3, "Correcting channel layout from (%" PRIi64 ") to (%" PRIi64 ")", audio_out_ctx->channel_layout, @@ -852,7 +863,7 @@ bool VideoStore::setup_resampler() { return false; } if ((ret = swr_init(resample_ctx)) < 0) { - Error("Could not open resampler"); + Error("Could not open resampler %d", ret); av_frame_free(&in_frame); av_frame_free(&out_frame); swr_free(&resample_ctx); diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index f0c09ec78..29511f6bd 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -219,7 +219,7 @@ bool Zone::CheckAlarms(const Image *delta_image) { int alarm_mid_x = -1; int alarm_mid_y = -1; - unsigned int lo_x = polygon.Extent().Lo().x_; + //unsigned int lo_x = polygon.Extent().Lo().x_; unsigned int lo_y = polygon.Extent().Lo().y_; unsigned int hi_x = polygon.Extent().Hi().x_; unsigned int hi_y = polygon.Extent().Hi().y_; @@ -699,6 +699,7 @@ bool Zone::CheckAlarms(const Image *delta_image) { if ((type < PRECLUSIVE) && (check_method >= BLOBS) && (monitor->GetOptSaveJPEGs() > 1)) { + unsigned int lo_x = polygon.Extent().Lo().x_; // First mask out anything we don't want for (unsigned int y = lo_y; y <= hi_y; y++) { pdiff = diff_buff + ((diff_width * y) + lo_x); @@ -878,16 +879,23 @@ std::vector Zone::Load(Monitor *monitor) { continue; } - if (polygon.Extent().Lo().x_ < 0 || polygon.Extent().Hi().x_ > static_cast(monitor->Width()) - || polygon.Extent().Lo().y_ < 0 || polygon.Extent().Hi().y_ > static_cast(monitor->Height())) { - Error("Zone %d/%s for monitor %s extends outside of image dimensions, (%d,%d), (%d,%d), fixing", + if (polygon.Extent().Lo().x_ < 0 + || + polygon.Extent().Hi().x_ > static_cast(monitor->Width()) + || + polygon.Extent().Lo().y_ < 0 + || + polygon.Extent().Hi().y_ > static_cast(monitor->Height())) { + Error("Zone %d/%s for monitor %s extends outside of image dimensions, (%d,%d), (%d,%d) != (%d,%d), fixing", Id, Name, monitor->Name(), polygon.Extent().Lo().x_, polygon.Extent().Lo().y_, polygon.Extent().Hi().x_, - polygon.Extent().Hi().y_); + polygon.Extent().Hi().y_, + monitor->Width(), + monitor->Height()); polygon.Clip(Box( {0, 0}, diff --git a/src/zmc.cpp b/src/zmc.cpp index 669fa7af3..0030292aa 100644 --- a/src/zmc.cpp +++ b/src/zmc.cpp @@ -221,12 +221,13 @@ int main(int argc, char *argv[]) { zmSetDefaultTermHandler(); zmSetDefaultDieHandler(); - sigset_t block_set; - sigemptyset(&block_set); - - sigaddset(&block_set, SIGHUP); - sigaddset(&block_set, SIGUSR1); - sigaddset(&block_set, SIGUSR2); + struct sigaction sa; + sa.sa_handler = SIG_IGN; //handle signal by ignoring + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + if (sigaction(SIGCHLD, &sa, 0) == -1) { + Error("Unable to set SIGCHLD to ignore. There may be zombies."); + } int result = 0; int prime_capture_log_count = 0; @@ -287,7 +288,6 @@ int main(int argc, char *argv[]) { Microseconds sleep_time = Microseconds(0); while (!zm_terminate) { - //sigprocmask(SIG_BLOCK, &block_set, 0); for (size_t i = 0; i < monitors.size(); i++) { monitors[i]->CheckAction(); @@ -365,6 +365,7 @@ int main(int argc, char *argv[]) { monitor->Id()); zmDbDo(sql); } + monitors.clear(); Image::Deinitialise(); Debug(1, "terminating"); diff --git a/src/zms.cpp b/src/zms.cpp index b38f405a1..34d390b4c 100644 --- a/src/zms.cpp +++ b/src/zms.cpp @@ -65,6 +65,7 @@ int main(int argc, const char *argv[], char **envp) { double maxfps = 10.0; unsigned int bitrate = 100000; unsigned int ttl = 0; + bool analysis_frames = false; EventStream::StreamMode replay = EventStream::MODE_NONE; std::string username; std::string password; @@ -116,7 +117,14 @@ int main(int argc, const char *argv[], char **envp) { char const *value = strtok(nullptr, "="); if ( !value ) value = ""; - if ( !strcmp(name, "source") ) { + if ( !strcmp(name, "analysis") ) { + if ( !strcmp(value, "true") ) { + analysis_frames = true; + } else { + analysis_frames = (atoi(value) == 1); + } + Debug(1, "Viewing analysis frames"); + } else if ( !strcmp(name, "source") ) { if ( !strcmp(value, "event") ) { source = ZMS_EVENT; } else if ( !strcmp(value, "fifo") ) { @@ -273,6 +281,7 @@ int main(int argc, const char *argv[], char **envp) { zmDbClose(); return -1; } + stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL); if ( mode == ZMS_JPEG ) { stream.setStreamType(MonitorStream::STREAM_JPEG); @@ -309,6 +318,7 @@ int main(int argc, const char *argv[], char **envp) { Debug(3, "Setting stream start to frame (%d)", frame_id); stream.setStreamStart(event_id, frame_id); } + stream.setStreamFrameType(analysis_frames ? StreamBase::FRAME_ANALYSIS: StreamBase::FRAME_NORMAL); if ( mode == ZMS_JPEG ) { stream.setStreamType(EventStream::STREAM_JPEG); } else { diff --git a/src/zmu.cpp b/src/zmu.cpp index ec4ae7b86..b85227fc4 100644 --- a/src/zmu.cpp +++ b/src/zmu.cpp @@ -482,7 +482,7 @@ int main(int argc, char *argv[]) { exit_zmu(-1); } if ( !ValidateAccess(user, mon_id, function) ) { - Error("Insufficient privileges for requested action"); + Error("Insufficient privileges for user %s for requested function %x", username, function); exit_zmu(-1); } } // end if auth @@ -497,6 +497,16 @@ int main(int argc, char *argv[]) { if ( verbose ) { printf("Monitor %u(%s)\n", monitor->Id(), monitor->Name()); } + + if (monitor->GetFunction() == Monitor::NONE) { + if (verbose) { + printf("Current state: None\n"); + } else { + printf("%d", Monitor::UNKNOWN); + } + exit_zmu(-1); + } + if ( !monitor->connect() ) { Error("Can't connect to capture daemon: %d %s", monitor->Id(), monitor->Name()); exit_zmu(-1); diff --git a/utils/do_debian_package.sh b/utils/do_debian_package.sh index 6c8c06c00..f20ee6183 100755 --- a/utils/do_debian_package.sh +++ b/utils/do_debian_package.sh @@ -87,11 +87,7 @@ else fi; if [ "$DISTROS" == "" ]; then - if [ "$RELEASE" != "" ]; then - DISTROS="bionic,focal,hirsute,impish" - else - DISTROS=`lsb_release -a 2>/dev/null | grep Codename | awk '{print $2}'`; - fi; + DISTROS=`lsb_release -a 2>/dev/null | grep Codename | awk '{print $2}'`; echo "Defaulting to $DISTROS for distribution"; else echo "Building for $DISTROS"; @@ -116,52 +112,6 @@ else echo "Defaulting to ZoneMinder upstream git" GITHUB_FORK="ZoneMinder" fi; - if [ "$SNAPSHOT" == "stable" ]; then - if [ "$BRANCH" == "" ]; then - #REV=$(git rev-list --tags --max-count=1) - BRANCH=`git describe --tags $(git rev-list --tags --max-count=1)`; - if [ -z "$BRANCH" ]; then - # This should only happen in CI environments where tag info isn't available - BRANCH=`cat version` - echo "Building branch $BRANCH" - fi - if [ "$BRANCH" == "" ]; then - echo "Unable to determine latest stable branch!" - exit 0; - fi - echo "Latest stable branch is $BRANCH"; - fi; - else - if [ "$BRANCH" == "" ]; then - echo "Defaulting to master branch"; - BRANCH="master"; - fi; - if [ "$SNAPSHOT" == "NOW" ]; then - SNAPSHOT=`date +%Y%m%d%H%M%S`; - else - if [ "$SNAPSHOT" == "CURRENT" ]; then - SNAPSHOT="`date +%Y%m%d.`$(git rev-list ${versionhash}..HEAD --count)" - fi; - fi; - fi; -fi - -IFS='.' read -r -a VERSION_PARTS <<< "$RELEASE" -if [ "$PPA" == "" ]; then - if [ "$RELEASE" != "" ]; then - # We need to use our official tarball for the original source, so grab it and overwrite our generated one. - if [ "${VERSION_PARTS[0]}.${VERSION_PARTS[1]}" == "1.30" ]; then - PPA="ppa:iconnor/zoneminder-stable" - else - PPA="ppa:iconnor/zoneminder-${VERSION_PARTS[0]}.${VERSION_PARTS[1]}" - fi; - else - if [ "$BRANCH" == "" ]; then - PPA="ppa:iconnor/zoneminder-master"; - else - PPA="ppa:iconnor/zoneminder-$BRANCH"; - fi; - fi; fi; # Instead of cloning from github each time, if we have a fork lying around, update it and pull from there instead. @@ -169,17 +119,10 @@ if [ ! -d "${GITHUB_FORK}_zoneminder_release" ]; then if [ -d "${GITHUB_FORK}_ZoneMinder.git" ]; then echo "Using local clone ${GITHUB_FORK}_ZoneMinder.git to pull from." cd "${GITHUB_FORK}_ZoneMinder.git" - echo "git fetch..." - git fetch - echo "git checkout $BRANCH" - git checkout $BRANCH - if [ $? -ne 0 ]; then - echo "Failed to switch to branch." - exit 1; - fi; echo "git pull..." git pull cd ../ + echo "git clone ${GITHUB_FORK}_ZoneMinder.git ${GITHUB_FORK}_zoneminder_release" git clone "${GITHUB_FORK}_ZoneMinder.git" "${GITHUB_FORK}_zoneminder_release" else @@ -192,14 +135,59 @@ else fi; cd "${GITHUB_FORK}_zoneminder_release" - git checkout $BRANCH -cd ../ -VERSION=`cat ${GITHUB_FORK}_zoneminder_release/version` +if [ "$SNAPSHOT" == "stable" ]; then + if [ "$BRANCH" == "" ]; then + #REV=$(git rev-list --tags --max-count=1) + BRANCH=`git describe --tags $(git rev-list --tags --max-count=1)`; + if [ -z "$BRANCH" ]; then + # This should only happen in CI environments where tag info isn't available + BRANCH=`cat version` + echo "Building branch $BRANCH" + fi + if [ "$BRANCH" == "" ]; then + echo "Unable to determine latest stable branch!" + exit 0; + fi + echo "Latest stable branch is $BRANCH"; + fi; +else + if [ "$BRANCH" == "" ]; then + echo "Defaulting to master branch"; + BRANCH="master"; + fi; + if [ "$SNAPSHOT" == "NOW" ]; then + SNAPSHOT=`date +%Y%m%d%H%M%S`; + else + if [ "$SNAPSHOT" == "CURRENT" ]; then + # git the latest (short) commit hash of the version file + versionhash=$(git log -n1 --pretty=format:%h version) + # Number of commits since the version file was last changed + numcommits=$(git rev-list ${versionhash}..HEAD --count) + SNAPSHOT="`date +%Y%m%d.`$(git rev-list ${versionhash}..HEAD --count)" + fi; + fi; +fi; + + +echo "git checkout $BRANCH" +git checkout $BRANCH +if [ $? -ne 0 ]; then + echo "Failed to switch to branch." + exit 1; +fi; +echo "git pull..." +git pull +# Grab the ZoneMinder version from the contents of the version file +VERSION=$(cat version) if [ -z "$VERSION" ]; then exit 1; fi; +IFS='.' read -r -a VERSION_PARTS <<< "$VERSION" + +cd ../ + if [ "$SNAPSHOT" != "stable" ] && [ "$SNAPSHOT" != "" ]; then VERSION="$VERSION~$SNAPSHOT"; fi; @@ -230,11 +218,13 @@ rm .gitignore cd ../ -if [ ! -e "$DIRECTORY.orig.tar.gz" ]; then - read -p "$DIRECTORY.orig.tar.gz does not exist, create it? [Y/n]" +if [ -e "$DIRECTORY.orig.tar.gz" ]; then + read -p "$DIRECTORY.orig.tar.gz exists, overwrite it? [Y/n]" if [[ "$REPLY" == "" || "$REPLY" == [yY] ]]; then tar zcf $DIRECTORY.orig.tar.gz $DIRECTORY.orig fi; +else + tar zcf $DIRECTORY.orig.tar.gz $DIRECTORY.orig fi; IFS=',' ;for DISTRO in `echo "$DISTROS"`; do @@ -357,6 +347,22 @@ EOF fi; else SC="zoneminder_${VERSION}-${DISTRO}${PACKAGE_VERSION}_source.changes"; + if [ "$PPA" == "" ]; then + if [ "$RELEASE" != "" ]; then + # We need to use our official tarball for the original source, so grab it and overwrite our generated one. + if [ "${VERSION_PARTS[0]}.${VERSION_PARTS[1]}" == "1.30" ]; then + PPA="ppa:iconnor/zoneminder-stable" + else + PPA="ppa:iconnor/zoneminder-${VERSION_PARTS[0]}.${VERSION_PARTS[1]}" + fi; + else + if [ "$BRANCH" == "" ]; then + PPA="ppa:iconnor/zoneminder-master"; + else + PPA="ppa:iconnor/zoneminder-$BRANCH"; + fi; + fi; + fi; dput="Y"; if [ "$INTERACTIVE" != "no" ]; then diff --git a/utils/packpack/startpackpack.sh b/utils/packpack/startpackpack.sh index 2ea949078..9737c794a 100755 --- a/utils/packpack/startpackpack.sh +++ b/utils/packpack/startpackpack.sh @@ -118,7 +118,7 @@ commonprep () { fi fi - RTSPVER="cd7fd49becad6010a1b8466bfebbd93999a39878" + RTSPVER="eab32851421ffe54fec0229c3efc44c642bc8d46" if [ -e "build/RtspServer-${RTSPVER}.tar.gz" ]; then echo "Found existing RtspServer ${RTSPVER} tarball..." else diff --git a/version b/version index 9cf86ad0f..f951feb88 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.37.1 +1.37.11 diff --git a/web/ajax/events.php b/web/ajax/events.php index 090fe476e..28512dfd1 100644 --- a/web/ajax/events.php +++ b/web/ajax/events.php @@ -48,7 +48,7 @@ if (isset($_REQUEST['order'])) { } else if (strtolower($_REQUEST['order']) == 'desc') { $order = 'DESC'; } else { - Warning("Invalid value for order " . $_REQUEST['order']); + Warning('Invalid value for order ' . $_REQUEST['order']); } } @@ -170,18 +170,26 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim // The names of columns shown in the event view that are NOT dB columns in the database $col_alt = array('Monitor', 'Storage'); - if (!in_array($sort, array_merge($columns, $col_alt))) { - ZM\Error('Invalid sort field: ' . $sort); - $sort = 'Id'; + if ( $sort != '' ) { + if (!in_array($sort, array_merge($columns, $col_alt))) { + ZM\Error('Invalid sort field: ' . $sort); + $sort = ''; + } else if ( $sort == 'Monitor' ) { + $sort = 'M.Name'; + } else { + $sort = 'E.'.$sort; + } } $values = array(); $likes = array(); $where = $filter->sql()?' WHERE ('.$filter->sql().')' : ''; - $sort = $sort == 'Monitor' ? 'M.Name' : 'E.'.$sort; $col_str = 'E.*, M.Name AS Monitor'; - $sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id'.$where.' ORDER BY '.$sort.' '.$order; + $sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id'.$where.($sort?' ORDER BY '.$sort.' '.$order:''); + if ($filter->limit() and !count($filter->pre_sql_conditions()) and !count($filter->post_sql_conditions())) { + $sql .= ' LIMIT '.$filter->limit(); + } $storage_areas = ZM\Storage::find(); $StorageById = array(); @@ -208,6 +216,12 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim $unfiltered_rows[] = $row; } # end foreach row + # Filter limits come before pagination limits. + if ($filter->limit() and ($filter->limit() > count($unfiltered_rows))) { + ZM\Debug("Filtering rows due to filter->limit " . count($unfiltered_rows)." limit: ".$filter->limit()); + $unfiltered_rows = array_slice($unfiltered_rows, 0, $filter->limit()); + } + ZM\Debug('Have ' . count($unfiltered_rows) . ' events matching base filter.'); $filtered_rows = null; @@ -246,8 +260,10 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim $filtered_rows = $unfiltered_rows; } # end if search_filter->terms() > 1 - if ($limit) + if ($limit) { + ZM\Debug("Filtering rows due to limit " . count($filtered_rows)." offset: $offset limit: $limit"); $filtered_rows = array_slice($filtered_rows, $offset, $limit); + } $returned_rows = array(); foreach ($filtered_rows as $row) { @@ -259,7 +275,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim 'mode'=>'jpeg', 'scale'=>$scale, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'replay'=>'single', 'rate'=>'400'), '&'); // Modify the row data as needed - $row['imgHtml'] = 'Event '.$event->Id().''; + $row['imgHtml'] = 'Event '.$event->Id().''; $row['Name'] = validHtmlStr($row['Name']); $row['Archived'] = $row['Archived'] ? translate('Yes') : translate('No'); $row['Emailed'] = $row['Emailed'] ? translate('Yes') : translate('No'); diff --git a/web/ajax/modals/filterdebug.php b/web/ajax/modals/filterdebug.php index 725539a99..d3ffaaf01 100644 --- a/web/ajax/modals/filterdebug.php +++ b/web/ajax/modals/filterdebug.php @@ -9,15 +9,22 @@ '; - } else { - $filter = new ZM\Filter($_REQUEST['fid']); - if ( ! $filter->Id() ) { + + $filter = null; + if ($fid) { + $filter = new ZM\Filter($fid); + if (!$filter->Id()) { echo '
Filter not found for id '.$_REQUEST['fid'].'
'; } + } else { + $filter = new ZM\Filter(); + if ( isset($_REQUEST['filter'])) { + $filter->set($_REQUEST['filter']); + } else { + echo '
No filter id or contents specified.
'; + } } ?>
@@ -25,7 +32,16 @@ // We have to manually insert the csrf key into the form when using a modal generated via ajax call echo getCSRFinputHTML(); ?> -

sql() ?>

+

+FROM Monitors AS M INNER JOIN Events AS E ON (M.Id = E.MonitorId)
WHERE
'; + $sql .= $filter->sql(); + $sql .= $filter->sort_field() ? ' ORDER BY '.$filter->sort_field(). ' ' .($filter->sort_asc() ? 'ASC' : 'DESC') : ''; + $sql .= $filter->limit() ? ' LIMIT '.$filter->limit() : ''; + $sql .= $filter->skip_locked() ? ' SKIP LOCKED' : ''; + + echo $sql; +?>

diff --git a/web/ajax/modals/function.php b/web/ajax/modals/function.php index 08b470840..c47664c4b 100644 --- a/web/ajax/modals/function.php +++ b/web/ajax/modals/function.php @@ -69,6 +69,26 @@ if ( !canEdit('Monitors') ) return; } ?> + +
+ + +'.$OLANG['FUNCTION_JANUS_ENABLED']['Help'].'
'; + } +?> + + +
+ + +'.$OLANG['FUNCTION_JANUS_AUDIO_ENABLED']['Help'].'
'; + } +?> +