diff --git a/.eslintignore b/.eslintignore index 4682851df..91b0fd196 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,7 +4,7 @@ web/api/lib web/includes/csrf/ web/js/videojs.zoomrotate.js -web/skins/classic/js/bootstrap.js +web/skins/classic/js/bootstrap-4.5.0.js web/skins/classic/js/chosen web/skins/classic/js/dateTimePicker web/skins/classic/js/jquery-*.js diff --git a/CMakeLists.txt b/CMakeLists.txt index c34a9f808..3b789e4c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,6 +167,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 @@ -407,21 +409,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) @@ -540,6 +545,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) 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/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 328dca6b7..d20e006d4 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -283,6 +283,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 +413,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,6 +449,8 @@ 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', `Capturing` enum('None','Ondemand', 'Always') NOT NULL default 'Always', @@ -457,6 +461,8 @@ CREATE TABLE `Monitors` ( `DecodingEnabled` tinyint(3) unsigned NOT NULL default '1', `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 '', @@ -976,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 @@ -1120,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.14.sql b/db/zm_update-1.35.14.sql index daa8239ff..f3c8bf779 100644 --- a/db/zm_update-1.35.14.sql +++ b/db/zm_update-1.35.14.sql @@ -28,8 +28,8 @@ SET @s = (SELECT IF( AND table_name = 'Monitors' AND column_name = 'TotalEvents' ) > 0, -"SELECT 'Column TotalEvents is already removed from Monitors'", -"ALTER TABLE `Monitors` DROP `TotalEvents`" +"ALTER TABLE `Monitors` DROP `TotalEvents`", +"SELECT 'Column TotalEvents is already removed from Monitors'" )); PREPARE stmt FROM @s; EXECUTE stmt; @@ -50,8 +50,8 @@ SET @s = (SELECT IF( AND table_name = 'Monitors' AND column_name = 'TotalEventDiskSpace' ) > 0, -"SELECT 'Column TotalEventDiskSpace is already removed from Monitors'", -"ALTER TABLE `Monitors` DROP `TotalEventDiskSpace`" +"ALTER TABLE `Monitors` DROP `TotalEventDiskSpace`", +"SELECT 'Column TotalEventDiskSpace is already removed from Monitors'" )); PREPARE stmt FROM @s; EXECUTE stmt; 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.3.sql b/db/zm_update-1.37.3.sql index 2db14e593..8525d80ee 100644 --- a/db/zm_update-1.37.3.sql +++ b/db/zm_update-1.37.3.sql @@ -1,60 +1,74 @@ - SET @s = (SELECT IF( (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() AND table_name = 'Monitors' - AND column_name = 'Capturing' + AND column_name = 'ManufacturerId' ) > 0, -"SELECT 'Column Capturing already exists in Monitors'", -"ALTER TABLE `Monitors` ADD `Capturing` enum('None','Ondemand', 'Always') NOT NULL default 'Always' AFTER `Function`" +"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 = 'Analysing' + AND column_name = 'ModelId' ) > 0, -"SELECT 'Column Analysing already exists in Monitors'", -"ALTER TABLE `Monitors` ADD `Analysing` enum('None','Always') NOT NULL default 'Always'" +"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.COLUMNS WHERE table_schema = DATABASE() + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() AND table_name = 'Monitors' - AND column_name = 'AnalysisSource' + AND column_name = 'ModelId' ) > 0, -"SELECT 'Column AnalysisSource already exists in Monitors'", -"ALTER TABLE `Monitors` ADD `AnalysisSource` enum('Primary','Secondary') NOT NULL DEFAULT 'Primary' AFTER `Analysing`" +"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.COLUMNS WHERE table_schema = DATABASE() - AND table_name = 'Monitors' - AND column_name = 'Recording' + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_schema = DATABASE() + AND table_name = 'MonitorPresets' + AND column_name = 'ModelId' ) > 0, -"SELECT 'Column Recording already exists in Monitors'", -"ALTER TABLE `Monitors` ADD `Recording` enum('None', 'OnMotion', 'Always') NOT NULL default 'Always'" +"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; -SET @s = (SELECT IF( - (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() - AND table_name = 'Monitors' - AND column_name = 'RecordingSource' - ) > 0, -"SELECT 'Column RecordingSource already exists in Monitors'", -"ALTER TABLE `Monitors` ADD `RecordingSource` enum('Primary','Secondary','Both') NOT NULL DEFAULT 'Primary' AFTER `RecordAudio`" -)); - -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.multistream.sql b/db/zm_update-1.37.multistream.sql new file mode 100644 index 000000000..5817bcde5 --- /dev/null +++ b/db/zm_update-1.37.multistream.sql @@ -0,0 +1,59 @@ +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'Capturing' + ) > 0, +"SELECT 'Column Capturing already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `Capturing` enum('None','Ondemand', 'Always') NOT NULL default 'Always' AFTER `Function`" +)); + +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 = 'Analysing' + ) > 0, +"SELECT 'Column Analysing already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `Analysing` enum('None','Always') NOT NULL default 'Always'" +)); + +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 = 'AnalysisSource' + ) > 0, +"SELECT 'Column AnalysisSource already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `AnalysisSource` enum('Primary','Secondary') NOT NULL DEFAULT 'Primary' AFTER `Analysing`" +)); + +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 = 'Recording' + ) > 0, +"SELECT 'Column Recording already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `Recording` enum('None', 'OnMotion', 'Always') NOT NULL default 'Always'" +)); + +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 = 'RecordingSource' + ) > 0, +"SELECT 'Column RecordingSource already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `RecordingSource` enum('Primary','Secondary','Both') NOT NULL DEFAULT 'Primary' AFTER `RecordAudio`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 69e7fbc03..f5aeebf2d 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -36,7 +36,7 @@ %global _hardened_build 1 Name: zoneminder -Version: 1.37.1 +Version: 1.37.6 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons diff --git a/distros/ubuntu2004/control b/distros/ubuntu2004/control index a4683bfde..d14c3fb52 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 @@ -70,7 +69,6 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,policykit-1 ,rsyslog | system-log-daemon ,zip - ,libpcre3 ,libcrypt-eksblowfish-perl ,libdata-entropy-perl ,libvncclient1|libvncclient0 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.postinst b/distros/ubuntu2004/zoneminder.postinst index 4be136a71..6f26bca24 100644 --- a/distros/ubuntu2004/zoneminder.postinst +++ b/distros/ubuntu2004/zoneminder.postinst @@ -5,6 +5,12 @@ set +e create_db () { echo "Checking for db" mysqladmin --defaults-file=/etc/mysql/debian.cnf -f reload + if [ $? -ne 0 ]; then + echo "Cannot talk to database. You will have to create the db manually with something like:"; + echo "cat /usr/share/zoneminder/db/zm_create.sql | mysql -u root"; + return; + fi + # test if database if already present... if ! $(echo quit | mysql --defaults-file=/etc/mysql/debian.cnf zm > /dev/null 2> /dev/null) ; then echo "Creating zm db" diff --git a/docs/faq.rst b/docs/faq.rst index dd0273667..f23fa2fc1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -225,7 +225,7 @@ change the 3 to a 1 I can't see more than 6 monitors in montage on my browser --------------------------------------------------------- -Browsers such a Chrome and Safari only support upto 6 streams from the same domain. To work around that, take a look at the multi-port configuration discussed in the ``MIN_STREAMING_PORT`` configuration in :doc:`/userguide/options/options_network` +Browsers such a Chrome and Safari only support up to 6 streams from the same domain. To work around that, take a look at the multi-port configuration discussed in the ``MIN_STREAMING_PORT`` configuration in :doc:`/userguide/options/options_network` Why is ZoneMinder using so much CPU? --------------------------------------- diff --git a/docs/installationguide/debian.rst b/docs/installationguide/debian.rst index c6e0cebcf..b92ff267a 100644 --- a/docs/installationguide/debian.rst +++ b/docs/installationguide/debian.rst @@ -3,6 +3,50 @@ Debian .. contents:: +Easy Way: Debian 11 (Bullseye) +------------------------------ + +This procedure will guide you through the installation of ZoneMinder on Debian 11 (Bullseye). + +**Step 1:** Setup Sudo (optional but recommended) + +By default Debian does not come with sudo, so you have to install it and configure it manually. +This step is optional but recommended and the following instructions assume that you have setup sudo. +If you prefer to setup ZoneMinder as root, do it at your own risk and adapt the following instructions accordingly. + +:: + + apt install sudo + usermod -a -G sudo + exit + +Now your terminal session is back under your normal user. You can check that +you are now part of the sudo group with the command ``groups``, "sudo" should +appear in the list. If not, run ``newgrp sudo`` and check again with ``groups``. + +**Step 2:** Update system and install zoneminder + +Run the following commands. + +:: + + sudo apt update + sudo apt upgrade + 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``. + +**Step 3:** Setup permissions for zm.conf + +To make sure zoneminder can read the configuration file, run the following command. + +:: + + sudo chgrp -c www-data /etc/zm/zm.conf + +Congratulations! You should now be able to access zoneminder at ``http://yourhostname/zm`` + Easy Way: Debian Buster ------------------------ @@ -60,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 diff --git a/docs/installationguide/easydocker.rst b/docs/installationguide/easydocker.rst index 899ce0e5d..a7d37a9e5 100644 --- a/docs/installationguide/easydocker.rst +++ b/docs/installationguide/easydocker.rst @@ -2,7 +2,7 @@ An Easy To Use Docker Image =========================== If you are interested in trying out ZoneMinder quickly, user Dan Landon maintains an easy to use docker image for ZoneMinder. With a few simple configuration changes, it also provides complete Event Notification Server and Machine Learning hook support. Please follow instructions in his repostory. He maintains two repositories: -* If you want to run the latest stable release, please use his `zoneminder repository `__. +* If you want to run the latest stable release, please use his `zoneminder machine learning repository `__. * If you want to run the latest zoneminder master, please use his `zoneminder master repository `__. In both cases, instructions are provided in the repo README files. diff --git a/docs/userguide/definemonitor.rst b/docs/userguide/definemonitor.rst index a5b622872..ab58b656c 100644 --- a/docs/userguide/definemonitor.rst +++ b/docs/userguide/definemonitor.rst @@ -86,7 +86,7 @@ Source Path Use this field to enter the full URL of the stream or file your camera supports. This is usually an RTSP url. There are several methods to learn this: * Check the documentation that came with your camera - * Look for your camera in the hardware compatibilty list in the `hardware compatibility wiki `__ + * Look for your camera in the hardware compatibility list in the `hardware compatibility wiki `__ * Try ZoneMinder's new ONVIF probe feature * Download and install the `ONVIF Device Manager `__ onto a Windows machine * Use Google to find third party sites, such as ispy, which document this information @@ -179,12 +179,12 @@ Storage Tab The storage section allows for each monitor to configure if and how video and audio are recorded. Save JPEGs - Records video in individual JPEG frames. Storing JPEG frames requires more storage space than h264 but it allows to view an event anytime while it is being recorded. + Records video in individual JPEG frames. Storing JPEG frames requires more storage space than h264 but it allows one to view an event anytime while it is being recorded. * Disabled – video is not recorded as JPEG frames. If this setting is selected, then "Video Writer" should be enabled otherwise there is no video recording at all. * Frames only – video is recorded in individual JPEG frames. - * Analysis images only (if available) – video is recorded in invidual JPEG frames with an overlay of the motion detection analysis information. Note that this overlay remains permanently visible in the frames. - * Frames + Analysis images (if available) – video is recorded twice, once as normal individual JPEG frames and once in invidual JPEG frames with analysis information overlaid. + * Analysis images only (if available) – video is recorded in individual JPEG frames with an overlay of the motion detection analysis information. Note that this overlay remains permanently visible in the frames. + * Frames + Analysis images (if available) – video is recorded twice, once as normal individual JPEG frames and once in individual JPEG frames with analysis information overlaid. Video Writer Records video in real video format. It provides much better compression results than saving JPEGs, thus longer video history can be stored. diff --git a/docs/userguide/filterevents.rst b/docs/userguide/filterevents.rst index b30a3185d..77f811fba 100644 --- a/docs/userguide/filterevents.rst +++ b/docs/userguide/filterevents.rst @@ -34,7 +34,7 @@ Here is what the filter window looks like * Update used disk space: calculates how much disk space is currently taken by the event and updates the db record. * Create video for all matches: creates a video file of all the events that match * Create video for all matches: ffmpeg will be used to create a video file (mp4) out of all the stored jpgs if using jpeg storage. - * Execute command on all matches: Allows you to execute any arbitrary command on the matched events. You can use replacement tokens as subsequent arguents to the command, the last argument will be the absolute path to the event, preceeded by replacement arguents. eg: /usr/bin/script.sh %MN% will excecute as /usr/bin/script.sh MonitorName /path/to/event. Please note that urls may contain characters like & that need quoting. So you may need to put quotes around them like /usr/bin/scrupt.sh "%MN%". + * Execute command on all matches: Allows you to execute any arbitrary command on the matched events. You can use replacement tokens as subsequent arguents to the command, the last argument will be the absolute path to the event, preceded by replacement arguents. eg: /usr/bin/script.sh %MN% will execute as /usr/bin/script.sh MonitorName /path/to/event. Please note that urls may contain characters like & that need quoting. So you may need to put quotes around them like /usr/bin/scrupt.sh "%MN%". * Delete all matches: Deletes all the matched events. * Email details of all matches: Sends an email to the configured address with details about the event. * Copy all matches: copies the event files to another location, specified in the Copy To dropdown. The other location must be setup in the Storage Tab under options. diff --git a/docs/userguide/gettingstarted.rst b/docs/userguide/gettingstarted.rst index 99f2ea4ba..61d4cb523 100644 --- a/docs/userguide/gettingstarted.rst +++ b/docs/userguide/gettingstarted.rst @@ -53,7 +53,7 @@ This screen is called the "console" screen in ZoneMinder and shows a summary of * **B**: This brings up a color coded log window that shows various system and component level logs. This window is useful if you are trying to diagnose issues. Refer to :doc:`logging`. * **C**: ZoneMinder allows you to group monitors for logical separation. This option lets you create new groups, associate monitors to them and edit/delete existing groups. * **D**: Filters are a powerful mechanism to perform actions when certain conditions are met. ZoneMinder comes with some preset filters that keep a tab of disk space and others. Many users create their own filters for more advanced actions like sending emails when certain events occur and more. Refer to :doc:`filterevents`. -* **E**: The Cycle option allows you to rotate between live views of each cofigured monitor. +* **E**: The Cycle option allows you to rotate between live views of each configured monitor. * **F**: The Montage option shows a collage of your monitors. You can customize them including moving them around. * **G**: Montage Review allows you to simultaneously view past events for different monitors. Note that this is a very resource intensive page and its performance will vary based on your system capabilities. * **H**: Audit Events Report is more of a power user feature. This option looks for recording gaps in events and recording issues in mp4 files. diff --git a/docs/userguide/options/options_users.rst b/docs/userguide/options/options_users.rst index def8744d7..4258c4ed0 100644 --- a/docs/userguide/options/options_users.rst +++ b/docs/userguide/options/options_users.rst @@ -30,7 +30,7 @@ This screen allows you to configure various permissions on a per user basis. The .. note:: if you are using zmNinja, users are required to have 'View' access to system because multi-server information is only available as part of this permission - Bandwidth - - Specifies the maximum bandwith that this user can configure (Low, Medium or High) + - Specifies the maximum bandwidth that this user can configure (Low, Medium or High) - API enabled - Specifies if the ZoneMinder API is enabled for this user (needs to be on, if you are using a mobile app such as zmNinja) @@ -42,4 +42,4 @@ Here is an example of a restricted user, for example: .. image:: images/Options_Users_Example.png -This user "home" is enabled, can view live streams and events, but only from "DoorBell" and "DeckCamera". This user also cannot control PTZ. \ No newline at end of file +This user "home" is enabled, can view live streams and events, but only from "DoorBell" and "DeckCamera". This user also cannot control PTZ. diff --git a/onvif/proxy/lib/ONVIF/Analytics/Types/MotionInCells.pm b/onvif/proxy/lib/ONVIF/Analytics/Types/MotionInCells.pm index 89de61e18..b6a5a487a 100644 --- a/onvif/proxy/lib/ONVIF/Analytics/Types/MotionInCells.pm +++ b/onvif/proxy/lib/ONVIF/Analytics/Types/MotionInCells.pm @@ -147,7 +147,7 @@ This attribute is of type L object. @@ -3093,7 +3093,7 @@ Returns a L object. @@ -3288,7 +3288,7 @@ Returns a L object. @@ -3298,7 +3298,7 @@ Returns a L object. diff --git a/onvif/proxy/lib/ONVIF/Device/Types/MotionInCells.pm b/onvif/proxy/lib/ONVIF/Device/Types/MotionInCells.pm index a4bb4696b..87c17a350 100644 --- a/onvif/proxy/lib/ONVIF/Device/Types/MotionInCells.pm +++ b/onvif/proxy/lib/ONVIF/Device/Types/MotionInCells.pm @@ -147,7 +147,7 @@ This attribute is of type L object. diff --git a/onvif/proxy/lib/ONVIF/Media/Types/FaultCodesOpenEnumType.pm b/onvif/proxy/lib/ONVIF/Media/Types/FaultCodesOpenEnumType.pm index 12e921a67..f6bdde62a 100644 --- a/onvif/proxy/lib/ONVIF/Media/Types/FaultCodesOpenEnumType.pm +++ b/onvif/proxy/lib/ONVIF/Media/Types/FaultCodesOpenEnumType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/ONVIF/Media/Types/MotionInCells.pm b/onvif/proxy/lib/ONVIF/Media/Types/MotionInCells.pm index 687131675..e88c29862 100644 --- a/onvif/proxy/lib/ONVIF/Media/Types/MotionInCells.pm +++ b/onvif/proxy/lib/ONVIF/Media/Types/MotionInCells.pm @@ -147,7 +147,7 @@ This attribute is of type L or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm b/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm index d1c5faa29..950c26344 100644 --- a/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm +++ b/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm @@ -987,7 +987,7 @@ Returns a L object. diff --git a/onvif/proxy/lib/ONVIF/PTZ/Types/FaultCodesOpenEnumType.pm b/onvif/proxy/lib/ONVIF/PTZ/Types/FaultCodesOpenEnumType.pm index e69cc37ad..e827b659b 100644 --- a/onvif/proxy/lib/ONVIF/PTZ/Types/FaultCodesOpenEnumType.pm +++ b/onvif/proxy/lib/ONVIF/PTZ/Types/FaultCodesOpenEnumType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/ONVIF/PTZ/Types/MotionInCells.pm b/onvif/proxy/lib/ONVIF/PTZ/Types/MotionInCells.pm index 8f68d1ef5..825f45ffa 100644 --- a/onvif/proxy/lib/ONVIF/PTZ/Types/MotionInCells.pm +++ b/onvif/proxy/lib/ONVIF/PTZ/Types/MotionInCells.pm @@ -147,7 +147,7 @@ This attribute is of type L or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSDiscovery10/Types/FaultCodeOpenType.pm b/onvif/proxy/lib/WSDiscovery10/Types/FaultCodeOpenType.pm index cd183e240..d89688025 100644 --- a/onvif/proxy/lib/WSDiscovery10/Types/FaultCodeOpenType.pm +++ b/onvif/proxy/lib/WSDiscovery10/Types/FaultCodeOpenType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSDiscovery10/Types/OpenRelationshipType.pm b/onvif/proxy/lib/WSDiscovery10/Types/OpenRelationshipType.pm index 45aab20e5..84f5985f8 100644 --- a/onvif/proxy/lib/WSDiscovery10/Types/OpenRelationshipType.pm +++ b/onvif/proxy/lib/WSDiscovery10/Types/OpenRelationshipType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSDiscovery11/Interfaces/WSDiscovery/WSDiscoveryPort.pm b/onvif/proxy/lib/WSDiscovery11/Interfaces/WSDiscovery/WSDiscoveryPort.pm index a7dcd3532..0aa2a393c 100644 --- a/onvif/proxy/lib/WSDiscovery11/Interfaces/WSDiscovery/WSDiscoveryPort.pm +++ b/onvif/proxy/lib/WSDiscovery11/Interfaces/WSDiscovery/WSDiscoveryPort.pm @@ -100,7 +100,7 @@ of the corresponding class can be passed instead of the marked hash ref. You may pass any combination of objects, hash and list refs to these methods, as long as you meet the structure. -List items (i.e. multiple occurences) are not displayed in the synopsis. +List items (i.e. multiple occurrences) are not displayed in the synopsis. You may generally pass a list ref of hash refs (or objects) instead of a hash ref - this may result in invalid XML if used improperly, though. Note that SOAP::WSDL always expects list references at maximum depth position. diff --git a/onvif/proxy/lib/WSDiscovery11/Types/FaultCodeOpenType.pm b/onvif/proxy/lib/WSDiscovery11/Types/FaultCodeOpenType.pm index 52f5d2c8f..42fe97b83 100644 --- a/onvif/proxy/lib/WSDiscovery11/Types/FaultCodeOpenType.pm +++ b/onvif/proxy/lib/WSDiscovery11/Types/FaultCodeOpenType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSDiscovery11/Types/FaultCodesOpenEnumType.pm b/onvif/proxy/lib/WSDiscovery11/Types/FaultCodesOpenEnumType.pm index 89d8704e8..072948d96 100644 --- a/onvif/proxy/lib/WSDiscovery11/Types/FaultCodesOpenEnumType.pm +++ b/onvif/proxy/lib/WSDiscovery11/Types/FaultCodesOpenEnumType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSDiscovery11/Types/RelationshipTypeOpenEnum.pm b/onvif/proxy/lib/WSDiscovery11/Types/RelationshipTypeOpenEnum.pm index dc3e004c2..31c9df482 100644 --- a/onvif/proxy/lib/WSDiscovery11/Types/RelationshipTypeOpenEnum.pm +++ b/onvif/proxy/lib/WSDiscovery11/Types/RelationshipTypeOpenEnum.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSNotification/Types/AbsoluteOrRelativeTimeType.pm b/onvif/proxy/lib/WSNotification/Types/AbsoluteOrRelativeTimeType.pm index ac8dc805d..c80f98fe0 100644 --- a/onvif/proxy/lib/WSNotification/Types/AbsoluteOrRelativeTimeType.pm +++ b/onvif/proxy/lib/WSNotification/Types/AbsoluteOrRelativeTimeType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSNotification/Types/FaultCodesOpenEnumType.pm b/onvif/proxy/lib/WSNotification/Types/FaultCodesOpenEnumType.pm index 06e4df192..fae356a95 100644 --- a/onvif/proxy/lib/WSNotification/Types/FaultCodesOpenEnumType.pm +++ b/onvif/proxy/lib/WSNotification/Types/FaultCodesOpenEnumType.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/onvif/proxy/lib/WSNotification/Types/RelationshipTypeOpenEnum.pm b/onvif/proxy/lib/WSNotification/Types/RelationshipTypeOpenEnum.pm index e598093cb..0125e612c 100644 --- a/onvif/proxy/lib/WSNotification/Types/RelationshipTypeOpenEnum.pm +++ b/onvif/proxy/lib/WSNotification/Types/RelationshipTypeOpenEnum.pm @@ -44,7 +44,7 @@ not checked yet. The current implementation of union resorts to inheriting from the base type, which means (quoted from the XML Schema specs): "If the or -alternative is chosen, then the simple ur-type definition·." +alternative is chosen, then the simple ur-type definition." diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index ff802dd15..48d494614 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -1064,7 +1064,7 @@ our @options = ( }, { name => 'ZM_FFMPEG_FORMATS', - default => 'mpg mpeg wmv asf avi* mov swf 3gp**', + default => 'mp4* mpg mpeg wmv asf avi mov swf 3gp**', description => 'Formats to allow for ffmpeg video generation', help => q` Ffmpeg can generate video in many different formats. This diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control.pm index feb12a0ca..e4d052700 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control.pm @@ -29,6 +29,7 @@ use strict; use warnings; require ZoneMinder::Base; +require ZoneMinder::Object; require ZoneMinder::Monitor; our $VERSION = $ZoneMinder::Base::VERSION; @@ -42,24 +43,116 @@ our $VERSION = $ZoneMinder::Base::VERSION; use ZoneMinder::Logger qw(:all); use ZoneMinder::Database qw(:all); +use parent qw(ZoneMinder::Object); + +use vars qw/ $table $primary_key %fields $serial %defaults $debug/; +$table = 'Controls'; +$serial = $primary_key = 'Id'; +%fields = map { $_ => $_ } qw( + Id + Name + Type + Protocol + CanWake + CanSleep + CanReset + CanReboot + CanZoom + CanAutoZoom + CanZoomAbs + CanZoomRel + CanZoomCon + MinZoomRange + MaxZoomRange + MinZoomStep + MaxZoomStep + HasZoomSpeed + MinZoomSpeed + MaxZoomSpeed + CanFocus + CanAutoFocus + CanFocusAbs + CanFocusRel + CanFocusCon + MinFocusRange + MaxFocusRange + MinFocusStep + MaxFocusStep + HasFocusSpeed + MinFocusSpeed + MaxFocusSpeed + CanIris + CanAutoIris + CanIrisAbs + CanIrisRel + CanIrisCon + MinIrisRange + MaxIrisRange + MinIrisStep + MaxIrisStep + HasIrisSpeed + MinIrisSpeed + MaxIrisSpeed + CanGain + CanAutoGain + CanGainAbs + CanGainRel + CanGainCon + MinGainRange + MaxGainRange + MinGainStep + MaxGainStep + HasGainSpeed + MinGainSpeed + MaxGainSpeed + CanWhite + CanAutoWhite + CanWhiteAbs + CanWhiteRel + CanWhiteCon + MinWhiteRange + MaxWhiteRange + MinWhiteStep + MaxWhiteStep + HasWhiteSpeed + MinWhiteSpeed + MaxWhiteSpeed + HasPresets + NumPresets + HasHomePreset + CanSetPresets + CanMove + CanMoveDiag + CanMoveMap + CanMoveAbs + CanMoveRel + CanMoveCon + CanPan + MinPanRange + MaxPanRange + MinPanStep + MaxPanStep + HasPanSpeed + MinPanSpeed + MaxPanSpeed + HasTurboPan + TurboPanSpeed + CanTilt + MinTiltRange + MaxTiltRange + MinTiltStep + MaxTiltStep + HasTiltSpeed + MinTiltSpeed + MaxTiltSpeed + HasTurboTilt + TurboTiltSpeed + CanAutoScan + NumScanPaths + ); + our $AUTOLOAD; -sub new { - my $class = shift; - my $id = shift; - if ( !defined($id) ) { - Fatal('No monitor defined when invoking protocol '.$class); - } - my $self = {}; - $self->{name} = $class; - $self->{id} = $id; - bless($self, $class); - return $self; -} - -sub DESTROY { -} - sub AUTOLOAD { my $self = shift; my $class = ref($self); @@ -79,24 +172,24 @@ sub AUTOLOAD { sub getKey { my $self = shift; - return $self->{id}; + return $self->{Id}; } sub open { my $self = shift; - Fatal('No open method defined for protocol '.$self->{name}); + Fatal('No open method defined for protocol '.$self->{Protocol}); } sub close { my $self = shift; $self->{state} = 'closed'; - Debug('No close method defined for protocol '.$self->{name}); + Debug('No close method defined for protocol '.$self->{Protocol}); } sub loadMonitor { my $self = shift; if ( !$self->{Monitor} ) { - if ( !($self->{Monitor} = ZoneMinder::Monitor->find_one(Id=>$self->{id})) ) { + if ( !($self->{Monitor} = ZoneMinder::Monitor->find_one(Id=>$self->{MonitorId})) ) { Fatal('Monitor id '.$self->{id}.' not found'); } if ( defined($self->{Monitor}->{AutoStopTimeout}) ) { 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/Vivotek_ePTZ.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/Vivotek_ePTZ.pm index 58ebe4c63..bcf1905c5 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/Vivotek_ePTZ.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/Vivotek_ePTZ.pm @@ -41,120 +41,133 @@ our @ISA = qw(ZoneMinder::Control); use ZoneMinder::Logger qw(:all); use ZoneMinder::Config qw(:all); +use ZoneMinder::General qw(:all); use Time::HiRes qw( usleep ); +use URI::Encode qw(uri_encode); -sub open -{ - my $self = shift; +our $REALM = ''; +our $PROTOCOL = 'http://'; +our $USERNAME = 'admin'; +our $PASSWORD = ''; +our $ADDRESS = ''; +our $BASE_URL = ''; - $self->loadMonitor(); - Debug( "Camera open" ); - use LWP::UserAgent; - $self->{ua} = LWP::UserAgent->new; - $self->{ua}->agent( "ZoneMinder Control Agent/".ZoneMinder::Base::ZM_VERSION ); +sub open { + my $self = shift; + $self->loadMonitor(); - $self->{state} = 'open'; + if (($self->{Monitor}->{ControlAddress} =~ /^(?https?:\/\/)?(?[^:@]+)?:?(?[^\/@]+)?@?(?
.*)$/)) { + $PROTOCOL = $+{PROTOCOL} if $+{PROTOCOL}; + $USERNAME = $+{USERNAME} if $+{USERNAME}; + $PASSWORD = $+{PASSWORD} if $+{PASSWORD}; + $ADDRESS = $+{ADDRESS} if $+{ADDRESS}; + } else { + Error('Failed to parse auth from address ' . $self->{Monitor}->{ControlAddress}); + $ADDRESS = $self->{Monitor}->{ControlAddress}; + } + if ( !($ADDRESS =~ /:/) ) { + Error('You generally need to also specify the port. I will append :80'); + $ADDRESS .= ':80'; + } + $BASE_URL = $PROTOCOL.($USERNAME?$USERNAME.':'.$PASSWORD.'@':'').$ADDRESS; + + use LWP::UserAgent; + $self->{ua} = LWP::UserAgent->new; + $self->{ua}->agent( 'ZoneMinder Control Agent/'.ZoneMinder::Base::ZM_VERSION ); + $self->{state} = 'open'; } -sub close -{ - my $self = shift; - $self->{state} = 'closed'; +sub close { + my $self = shift; + $self->{state} = 'closed'; } -sub printMsg -{ - my $msg = shift; - my $msg_len = length($msg); +sub sendCmd { + my ($self, $cmd, $speedcmd) = @_; - Debug( $msg."[".$msg_len."]" ); + $self->printMsg( $speedcmd, 'Tx' ); + $self->printMsg( $cmd, 'Tx' ); + + my $req = HTTP::Request->new( GET => $BASE_URL."/cgi-bin/camctrl/eCamCtrl.cgi?stream=0&$speedcmd&$cmd"); + my $res = $self->{ua}->request($req); + + if (!$res->is_success) { + Error('Request failed: '.$res->status_line().' (URI: '.$req->as_string().')'); + } + return $res->is_success; } -sub sendCmd -{ - my ($self, $cmd, $speedcmd) = @_; - - my $result = undef; - - printMsg( $speedcmd, "Tx" ); - printMsg( $cmd, "Tx" ); - - my $req = HTTP::Request->new( GET => "http://" . $self->{Monitor}->{ControlAddress} . "/cgi-bin/camctrl/eCamCtrl.cgi?stream=0&$speedcmd&$cmd" ); - my $res = $self->{ua}->request($req); - - if ( $res->is_success ) - { - $result = !undef; - } - else - { - Error( "Request failed: '" . $res->status_line() . "' (URI: '" . $req->as_string() . "')" ); - } - - return( $result ); +sub moveConUp { + my ($self, $params) = @_; + my $speed = 'speedtilt=' . ($params->{tiltspeed} - 6); + $self->sendCmd( 'move=up', $speed ); } -sub moveConUp -{ - my ($self, $params) = @_; - my $speed = 'speedtilt=' . ($params->{tiltspeed} - 6); - Debug( "Move Up" ); - $self->sendCmd( 'move=up', $speed ); +sub moveConDown { + my ($self, $params) = @_; + my $speed = 'speedtilt=' . ($params->{tiltspeed} - 6); + $self->sendCmd( 'move=down', $speed ); } -sub moveConDown -{ - my ($self, $params) = @_; - my $speed = 'speedtilt=' . ($params->{tiltspeed} - 6); - Debug( "Move Down" ); - $self->sendCmd( 'move=down', $speed ); +sub moveConLeft { + my ($self, $params) = @_; + my $speed = 'speedpan=-' . $params->{panspeed}; + $self->sendCmd( 'move=left', $speed ); } -sub moveConLeft -{ - my ($self, $params) = @_; - my $speed = 'speedpan=-' . $params->{panspeed}; - Debug( "Move Left" ); - $self->sendCmd( 'move=left', $speed ); +sub moveConRight { + my ($self, $params) = @_; + my $speed = 'speedpan=' . ($params->{panspeed} - 6); + $self->sendCmd( 'move=right', $speed ); } -sub moveConRight -{ - my ($self, $params) = @_; - my $speed = 'speedpan=' . ($params->{panspeed} - 6); - Debug( "Move Right" ); - $self->sendCmd( 'move=right', $speed ); +sub moveStop { + my $self = shift; + Debug( "Move Stop: not implemented" ); + # not implemented } -sub moveStop -{ - my $self = shift; - Debug( "Move Stop" ); - # not implemented +sub zoomConTele { + my ($self, $params) = @_; + my $speed = 'speedzoom=' . ($params->{speed} - 6); + $self->sendCmd( 'zoom=tele', $speed ); } -sub zoomConTele -{ - my ($self, $params) = @_; - my $speed = 'speedzoom=' . ($params->{speed} - 6); - Debug( "Zoom In" ); - $self->sendCmd( 'zoom=tele', $speed ); +sub zoomConWide { + my ($self, $params) = @_; + my $speed = 'speedzoom=' . ($params->{speed} - 6); + $self->sendCmd( 'zoom=wide', $speed ); } -sub zoomConWide -{ - my ($self, $params) = @_; - my $speed = 'speedzoom=' . ($params->{speed} - 6); - Debug( "Zoom Out" ); - $self->sendCmd( 'zoom=wide', $speed ); +sub reset { + my $self = shift; + $self->sendCmd( 'move=home' ); } -sub reset -{ - my $self = shift; - Debug( "Camera Reset" ); - $self->sendCmd( 'move=home' ); +sub get_config { + my $self = shift; + + my $url = $BASE_URL.'/cgi-bin/admin/lsctrl.cgi?cmd=queryStatus&retType=javascript'; + my $req = new HTTP::Request(GET => $url); + my $response = $self->{ua}->request($req); + if ( $response->is_success() ) { + my $resp = $response->decoded_content; + return ZoneMinder::General::parseNameEqualsValueToHash($resp); + } + Warn("Failed to get config from $url: " . $response->status_line()); + return; +} # end sub get_config + +sub set_config { + my $self = shift; + my $diff = shift; + + my $url = $BASE_URL.'/cgi-bin/'.$USERNAME.'/setparam.cgi?'. + join('&', map { $_.'='.uri_encode($$diff{$_}) } keys %$diff); + my $response = $self->{ua}->get($url); + Debug($response->content); + return $response->is_success(); } 1; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm index 48544a911..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); @@ -584,6 +585,7 @@ sub DiskSpace { return $_[0]{DiskSpace}; } +# Icon: I removed the locking from this. So we now have an assumption that the Event object is up to date. sub CopyTo { my ( $self, $NewStorage ) = @_; @@ -600,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; @@ -614,16 +616,12 @@ sub CopyTo { Debug("$NewPath is good"); } - $ZoneMinder::Database::dbh->begin_work(); - $self->lock_and_load(); # data is reloaded, so need to check that the move hasn't already happened. if ( $$self{StorageId} == $$NewStorage{Id} ) { - $ZoneMinder::Database::dbh->commit(); return 'Event has already been moved by someone else.'; } if ( $$OldStorage{Id} != $$self{StorageId} ) { - $ZoneMinder::Database::dbh->commit(); return 'Old Storage path changed, Event has moved somewhere else.'; } @@ -661,39 +659,21 @@ sub CopyTo { } my $event_path = $subpath.$self->RelativePath(); - if ( 0 ) { # Not neccessary - Debug("Making directory $event_path/"); - if ( !$bucket->add_key($event_path.'/', '') ) { - Warning("Unable to add key for $event_path/ :". $s3->err . ': '. $s3->errstr()); - } - } my @files = glob("$OldPath/*"); Debug("Files to move @files"); - foreach my $file ( @files ) { + foreach my $file (@files) { next if $file =~ /^\./; - ( $file ) = ( $file =~ /^(.*)$/ ); # De-taint + ($file) = ($file =~ /^(.*)$/); # De-taint my $starttime = [gettimeofday]; Debug("Moving file $file to $NewPath"); my $size = -s $file; - if ( ! $size ) { + if (!$size) { Info('Not moving file with 0 size'); } - if ( 0 ) { - my $file_contents = File::Slurp::read_file($file); - if ( ! $file_contents ) { - die 'Loaded empty file, but it had a size. Giving up'; - } - - my $filename = $event_path.'/'.File::Basename::basename($file); - if ( ! $bucket->add_key($filename, $file_contents) ) { - die "Unable to add key for $filename : ".$s3->err . ': ' . $s3->errstr; - } - } else { - my $filename = $event_path.'/'.File::Basename::basename($file); - if ( ! $bucket->add_key_filename($filename, $file) ) { - die "Unable to add key for $filename " . $s3->err . ': '. $s3->errstr; - } + my $filename = $event_path.'/'.File::Basename::basename($file); + if (!$bucket->add_key_filename($filename, $file)) { + die "Unable to add key for $filename " . $s3->err . ': '. $s3->errstr; } my $duration = tv_interval($starttime); @@ -704,16 +684,15 @@ sub CopyTo { }; Error($@) if $@; } else { - Error("Unable to parse S3 Url into it's component parts."); + Error('Unable to parse S3 Url into it\'s component parts.'); } - #die $@ if $@; } # end if Url } # end if s3 my $error = ''; - if ( !$moved ) { + if (!$moved) { File::Path::make_path($NewPath, {error => \my $err}); - if ( @$err ) { + if (@$err) { for my $diag (@$err) { my ($file, $message) = %$diag; next if $message eq 'File exists'; @@ -724,23 +703,16 @@ sub CopyTo { } } } - if ( $error ) { - $ZoneMinder::Database::dbh->commit(); - return $error; - } + return $error if $error; my @files = glob("$OldPath/*"); - if ( ! @files ) { - $ZoneMinder::Database::dbh->commit(); - return 'No files to move.'; - } + return 'No files to move.' if !@files; for my $file (@files) { next if $file =~ /^\./; - ( $file ) = ( $file =~ /^(.*)$/ ); # De-taint + ($file) = ($file =~ /^(.*)$/); # De-taint my $starttime = [gettimeofday]; - Debug("Moving file $file to $NewPath"); my $size = -s $file; - if ( ! File::Copy::copy( $file, $NewPath ) ) { + if (!File::Copy::copy($file, $NewPath)) { $error .= "Copy failed: for $file to $NewPath: $!"; last; } @@ -749,34 +721,38 @@ sub CopyTo { } # end foreach file. } # end if ! moved - if ( $error ) { - $ZoneMinder::Database::dbh->commit(); - return $error; - } + return $error; } # end sub CopyTo sub MoveTo { - my ( $self, $NewStorage ) = @_; + my ($self, $NewStorage) = @_; - if ( !$self->canEdit() ) { + if (!$self->canEdit()) { Warning('No permission to move event.'); return 'No permission to move event.'; } - my $OldStorage = $self->Storage(undef); + my $was_in_transaction = !$ZoneMinder::Database::dbh->{AutoCommit}; + $ZoneMinder::Database::dbh->begin_work() if !$was_in_transaction; + 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); + if (!$error) { + # 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 + } + $ZoneMinder::Database::dbh->commit() if !$was_in_transaction; return $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(); - if ( $error ) { - $ZoneMinder::Database::dbh->commit(); - return $error; - } - $ZoneMinder::Database::dbh->commit(); $self->delete_files($OldStorage); return $error; } # end sub MoveTo diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm new file mode 100644 index 000000000..0086b5bac --- /dev/null +++ b/scripts/ZoneMinder/lib/ZoneMinder/Event_Summary.pm @@ -0,0 +1,99 @@ +# ========================================================================== +# +# ZoneMinder Event_Summary Module +# Copyright (C) 2020 ZoneMinder +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# ========================================================================== +# +# This module contains the common definitions and functions used by the rest +# of the ZoneMinder scripts +# +package ZoneMinder::Event_Summary; + +use 5.006; +use strict; +use warnings; + +require ZoneMinder::Base; +require ZoneMinder::Object; + +#our @ISA = qw(Exporter ZoneMinder::Base); +use parent qw(ZoneMinder::Object); + +use vars qw/ $table $primary_key %fields $serial %defaults $debug/; +$table = 'Event_Summaries'; +$serial = $primary_key = 'MonitorId'; +%fields = map { $_ => $_ } qw( + MonitorId + TotalEvents + TotalEventDiskSpace + HourEvents + HourEventDiskSpace + DayEvents + DayEventDiskSpace + WeekEvents + WeekEventDiskSpace + MonthEvents + MonthEventDiskSpace + ArchivedEvents + ArchivedEventDiskSpace + ); + +%defaults = ( + TotalEvents => undef, + TotalEventDiskSpace => undef, + HourEvents => undef, + HourEventDiskSpace => undef, + DayEvents => undef, + DayEventDiskSpace => undef, + WeekEvents => undef, + WeekEventDiskSpace => undef, + MonthEvents => undef, + MonthEventDiskSpace => undef, + ArchivedEvents => undef, + ArchivedEventDiskSpace => undef, + ); + +sub Monitor { + return new ZoneMinder::Monitor( $_[0]{MonitorId} ); +} # end sub Monitor + +1; +__END__ + +=head1 NAME + +ZoneMinder::Event_Summary - Perl Class for Event Summaries + +=head1 SYNOPSIS + +use ZoneMinder::Event_Summary; + +=head1 AUTHOR + +Isaac Connor, Eisaac@zoneminder.comE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2001-2017 ZoneMinder LLC + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8.3 or, +at your option, any later version of Perl 5 you may have available. + + +=cut diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index 3a3308938..726685a7e 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 ? 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' @@ -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 d68967fa9..b14b08aae 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/General.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/General.pm @@ -31,6 +31,8 @@ our %EXPORT_TAGS = ( systemStatus packageControl daemonControl + parseNameEqualsValueToHash + hash_diff ) ] ); push( @{$EXPORT_TAGS{all}}, @{$EXPORT_TAGS{$_}} ) foreach keys %EXPORT_TAGS; @@ -534,6 +536,42 @@ sub jsonDecode { return $result; } +sub parseNameEqualsValueToHash { + my %settings; + foreach my $line ( split ( /\r?\n/, $_[0] ) ) { + next if ! $line; + next if ! ( $line =~ /=/ ); + my ($name, $value ) = split('=', $line); + $value =~ s/^'//; + $value =~ s/'$//; + $settings{$name} = defined $value ? $value : ''; + } + return %settings; +} + +sub hash_diff { + # assumes keys of second hash are all in the first hash + my ( $settings, $defaults ) = @_; + my %updates; + + foreach my $setting ( keys %{$settings} ) { + next if ! exists $$defaults{$setting}; + if ( + ($$settings{$setting} and ! $$defaults{$setting}) + or + (!$$settings{$setting} and $$defaults{$setting}) + or + ( + ($$settings{$setting} and $$defaults{$setting} and ( + $$settings{$setting} ne $$defaults{$setting})) + ) + ) { + $updates{$setting} = $$defaults{$setting}; + } + } # end foreach setting + return %updates; +} + sub packageControl { my $command = shift; my $string = $Config{ZM_PATH_BIN}.'/zmpkg.pl '.$command; @@ -598,6 +636,8 @@ of the ZoneMinder scripts packageControl daemonControl systemStatus + parseNameEqualsValueToHash + hash_diff ) ] diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in index 3b085fdc1..48a3700fd 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 d01ef3455..d179534d4 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -35,7 +35,9 @@ require ZoneMinder::Storage; require ZoneMinder::Server; require ZoneMinder::Memory; require ZoneMinder::Monitor_Status; +require ZoneMinder::Event_Summary; require ZoneMinder::Zone; +use ZoneMinder::Logger qw(:all); #our @ISA = qw(Exporter ZoneMinder::Base); use parent qw(ZoneMinder::Object); @@ -239,20 +241,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 @@ -266,6 +274,15 @@ sub Status { return $$self{Status}; } +sub Event_Summary { + my $self = shift; + $$self{Event_Summary} = shift if @_; + if ( ! $$self{Event_Summary} ) { + $$self{Event_Summary} = ZoneMinder::Event_Summary->find_one(MonitorId=>$$self{Id}); + } + return $$self{Event_Summary}; +} + sub connect { my $self = shift; return ZoneMinder::Memory::zmMemVerify($self); @@ -313,6 +330,37 @@ sub resumeMotionDetection { return 1; } +sub Control { + my $self = shift; + 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}"); + } + } else { + Info("No ControlId set in monitor $$self{Id}") + } + } + return $$self{Control}; +} + 1; __END__ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor_Status.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor_Status.pm index 9a9077653..1fafd3b0b 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor_Status.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor_Status.pm @@ -43,18 +43,6 @@ $serial = $primary_key = 'MonitorId'; CaptureFPS AnalysisFPS CaptureBandwidth - TotalEvents - TotalEventDiskSpace - HourEvents - HourEventDiskSpace - DayEvents - DayEventDiskSpace - WeekEvents - WeekEventDiskSpace - MonthEvents - MonthEventDiskSpace - ArchivedEvents - ArchivedEventDiskSpace ); %defaults = ( @@ -62,18 +50,6 @@ $serial = $primary_key = 'MonitorId'; CaptureFPS => undef, AnalysisFPS => undef, CaptureBandwidth => undef, - TotalEvents => undef, - TotalEventDiskSpace => undef, - HourEvents => undef, - HourEventDiskSpace => undef, - DayEvents => undef, - DayEventDiskSpace => undef, - WeekEvents => undef, - WeekEventDiskSpace => undef, - MonthEvents => undef, - MonthEventDiskSpace => undef, - ArchivedEvents => undef, - ArchivedEventDiskSpace => undef, ); sub Monitor { diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm index f3d750338..e54bb15ce 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm @@ -218,7 +218,7 @@ sub save { my $serial = eval '$'.$type.'::serial'; my @identified_by = eval '@'.$type.'::identified_by'; - my $ac = ZoneMinder::Database::start_transaction( $local_dbh ); + my $ac = ZoneMinder::Database::start_transaction( $local_dbh ) if $local_dbh->{AutoCommit}; if ( ! $serial ) { my $insert = $force_insert; my %serial = eval '%'.$type.'::serial'; @@ -234,8 +234,8 @@ $log->debug("No serial") if $debug; if ( ! ( ( $_ = $local_dbh->prepare("DELETE FROM `$table` WHERE $where") ) and $_->execute( @$self{@identified_by} ) ) ) { $where =~ s/\?/\%s/g; $log->error("Error deleting: DELETE FROM $table WHERE " . sprintf($where, map { defined $_ ? $_ : 'undef' } ( @$self{@identified_by}) ).'):' . $local_dbh->errstr); - $local_dbh->rollback(); - ZoneMinder::Database::end_transaction( $local_dbh, $ac ); + $local_dbh->rollback() if $ac; + ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac; return $local_dbh->errstr; } elsif ( $debug ) { $log->debug("SQL succesful DELETE FROM $table WHERE $where"); @@ -267,8 +267,8 @@ $log->debug("No serial") if $debug; my $error = $local_dbh->errstr; $command =~ s/\?/\%s/g; $log->error('SQL statement execution failed: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys}) ).'):' . $local_dbh->errstr); - $local_dbh->rollback(); - ZoneMinder::Database::end_transaction( $local_dbh, $ac ); + $local_dbh->rollback() if $ac; + ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac; return $error; } # end if if ( $debug or DEBUG_ALL ) { @@ -282,8 +282,8 @@ $log->debug("No serial") if $debug; my $error = $local_dbh->errstr; $command =~ s/\?/\%s/g; $log->error('SQL failed: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys, @$fields{@identified_by}}) ).'):' . $local_dbh->errstr); - $local_dbh->rollback(); - ZoneMinder::Database::end_transaction( $local_dbh, $ac ); + $local_dbh->rollback() if $ac; + ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac; return $error; } # end if if ( $debug or DEBUG_ALL ) { @@ -321,8 +321,8 @@ $log->debug("No serial") if $debug; $command =~ s/\?/\%s/g; my $error = $local_dbh->errstr; $log->error('SQL failed: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys}) ).'):' . $error); - $local_dbh->rollback(); - ZoneMinder::Database::end_transaction( $local_dbh, $ac ); + $local_dbh->rollback() if $ac; + ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac; return $error; } # end if if ( $debug or DEBUG_ALL ) { @@ -340,8 +340,8 @@ $log->debug("No serial") if $debug; my $error = $local_dbh->errstr; $command =~ s/\?/\%s/g; $log->error('SQL failed: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys}, @sql{@$fields{@identified_by}} ) ).'):' . $error) if $log; - $local_dbh->rollback(); - ZoneMinder::Database::end_transaction( $local_dbh, $ac ); + $local_dbh->rollback() if $ac; + ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac; return $error; } # end if if ( $debug or DEBUG_ALL ) { @@ -350,7 +350,7 @@ $log->debug("No serial") if $debug; } # end if } # end if } # end if - ZoneMinder::Database::end_transaction( $local_dbh, $ac ); + ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac; #$self->load(); #if ( $$fields{id} ) { #if ( ! $ZoneMinder::Object::cache{$type}{$$self{id}} ) { diff --git a/scripts/zmcontrol.pl.in b/scripts/zmcontrol.pl.in index 8a93a19a4..a9f26ac80 100644 --- a/scripts/zmcontrol.pl.in +++ b/scripts/zmcontrol.pl.in @@ -30,7 +30,6 @@ use autouse 'Pod::Usage'=>qw(pod2usage); use POSIX qw/strftime EPIPE EINTR/; use Socket; use Data::Dumper; -use Module::Load::Conditional qw{can_load}; use constant MAX_CONNECT_DELAY => 15; use constant MAX_COMMAND_WAIT => 1800; @@ -102,40 +101,21 @@ if ($options{command}) { } } else { # The server isn't there - my $monitor = zmDbGetMonitorAndControl($id); + require ZoneMinder::Monitor; + + my $monitor = ZoneMinder::Monitor->find_one(Id=>$id); Fatal("Unable to load control data for monitor $id") if !$monitor; - my $protocol = $monitor->{Protocol}; + my $control = $monitor->Control(); + + my $protocol = $control->{Protocol}; if (!$protocol) { Fatal('No protocol is set in monitor. Please edit the monitor, edit control type, select the control capability and fill in the Protocol field'); } - if (-x $protocol) { - # Protocol is actually a script! - # Holdover from previous versions - my $command .= $protocol.' '.$arg_string; - Debug($command); - - my $output = qx($command); - my $status = $? >> 8; - if ($status || logDebugging()) { - chomp($output); - Debug("Output: $output"); - } - if ($status) { - Error("Command '$command' exited with status: $status"); - exit($status); - } - exit(0); - } - Info("Starting control server $id/$protocol"); close(CLIENT); - if (!can_load(modules => {'ZoneMinder::Control::'.$protocol => undef})) { - Fatal("Can't load ZoneMinder::Control::$protocol\n$Module::Load::Conditional::ERROR"); - } - my $zm_terminate = 0; sub TermHandler { Info('Received TERM, exiting'); @@ -150,7 +130,6 @@ if ($options{command}) { $0 = $0.' --id '.$id; - my $control = ('ZoneMinder::Control::'.$protocol)->new($id); my $control_key = $control->getKey(); $control->loadMonitor(); 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 1e62cb229..c0cdf7235 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}); + 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); @@ -665,10 +648,10 @@ sub substituteTags { # We have a filter and an event, do we need any more # monitor information? my $need_monitor = $text =~ /%(?:MN|MET|MEH|MED|MEW|MEN|MEA)%/; - my $need_status = $text =~ /%(?:MET|MEH|MED|MEW|MEN|MEA)%/; + my $need_summary = $text =~ /%(?:MET|MEH|MED|MEW|MEN|MEA)%/; my $Monitor = $Event->Monitor() if $need_monitor; - my $Status = $Monitor->Status() if $need_status; + my $Summary = $Monitor->Event_Summary() if $need_summary; # Do we need the image information too? my $need_images = $text =~ /%(?:EPI1|EPIM|EI1|EIM|EI1A|EIMA|EIMOD|EIMODG)%/; @@ -692,19 +675,19 @@ sub substituteTags { } $rows ++; } - Debug("Frames: rows: $rows first alarm frame: $first_alarm_frame max_alaarm_frame: $max_alarm_frame, score: $max_alarm_score"); + Debug("Frames: rows: $rows first alarm frame: $first_alarm_frame max_alarm_frame: $max_alarm_frame, score: $max_alarm_score"); $sth->finish(); } my $url = $Config{ZM_URL}; $text =~ s/%ZP%/$url/g; $text =~ s/%MN%/$Monitor->{Name}/g; - $text =~ s/%MET%/$Status->{TotalEvents}/g; - $text =~ s/%MEH%/$Status->{HourEvents}/g; - $text =~ s/%MED%/$Status->{DayEvents}/g; - $text =~ s/%MEW%/$Status->{WeekEvents}/g; - $text =~ s/%MEM%/$Status->{MonthEvents}/g; - $text =~ s/%MEA%/$Status->{ArchivedEvents}/g; + $text =~ s/%MET%/$Summary->{TotalEvents}/g; + $text =~ s/%MEH%/$Summary->{HourEvents}/g; + $text =~ s/%MED%/$Summary->{DayEvents}/g; + $text =~ s/%MEW%/$Summary->{WeekEvents}/g; + $text =~ s/%MEM%/$Summary->{MonthEvents}/g; + $text =~ s/%MEA%/$Summary->{ArchivedEvents}/g; $text =~ s/%MP%/$url?view=watch&mid=$Event->{MonitorId}/g; $text =~ s/%MPS%/$url?view=watch&mid=$Event->{MonitorId}&mode=stream/g; $text =~ s/%MPI%/$url?view=watch&mid=$Event->{MonitorId}&mode=still/g; @@ -1051,9 +1034,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 +1044,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..83fcf29f5 100644 --- a/scripts/zmtrigger.pl.in +++ b/scripts/zmtrigger.pl.in @@ -166,13 +166,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 +195,32 @@ 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 ($$monitor{Function} eq 'None') { + $monitor_reload_time = 0; + next; + } - if ( ! zmMemVerify($monitor) ) { + if (!zmMemVerify($monitor)) { # 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. 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}) diff --git a/scripts/zmwatch.pl.in b/scripts/zmwatch.pl.in index e04e41d30..429e3589a 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); @@ -80,9 +81,6 @@ 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())) { @@ -91,82 +89,67 @@ while (!$zm_terminate) { } } - 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'; + next if $monitor->{Capturing} eq 'Ondemand'; my $now = time(); my $restart = 0; - if (zmMemVerify($monitor)) { - next if $monitor->{Capturing} eq 'Ondemand'; - -# 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}) { + Warning( + "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) { + Warning('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) @@ -178,25 +161,14 @@ while (!$zm_terminate) { 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; + ." 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 80e09e9c3..8852391a4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -60,6 +60,7 @@ 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 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_db.cpp b/src/zm_db.cpp index c6cc452b9..f0b13d538 100644 --- a/src/zm_db.cpp +++ b/src/zm_db.cpp @@ -251,6 +251,13 @@ void zmDbQueue::process() { mCondition.wait(lock); } while (!mQueue.empty()) { + if (mQueue.size() > 20) { + Logger *log = Logger::fetch(); + Logger::Level db_level = log->databaseLevel(); + log->databaseLevel(Logger::NOLOG); + Warning("db queue size has grown larger %zu than 20 entries", mQueue.size()); + log->databaseLevel(db_level); + } std::string sql = mQueue.front(); mQueue.pop(); // My idea for leaving the locking around each sql statement is to allow @@ -264,8 +271,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 adda18655..2d95ec23a 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -60,8 +60,8 @@ Event::Event( //snapshit_file(), //alarm_file(""), videoStore(nullptr), - //video_name(""), //video_file(""), + //video_path(""), last_db_frame(0), have_video_keyframe(false), //scheme @@ -103,7 +103,14 @@ Event::Event( // Copy it in case opening the mp4 doesn't work we can set it to another value save_jpegs = monitor->GetOptSaveJPEGs(); - Storage * storage = monitor->getStorage(); + Storage *storage = monitor->getStorage(); + if (monitor->GetOptVideoWriter() != 0) { + container = monitor->OutputContainer(); + if ( container == "auto" || container == "" ) { + container = "mp4"; + } + video_incomplete_file = "incomplete."+container; + } std::string sql = stringtf( "INSERT INTO `Events` " @@ -120,28 +127,27 @@ Event::Event( state_id, monitor->getOrientation(), 0, - "", + video_incomplete_file.c_str(), save_jpegs, storage->SchemeString().c_str() ); id = zmDbDoInsert(sql); - if ( !SetPath(storage) ) { + 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() ) + if (monitor->ServerId()) sql += stringtf(" AND ServerId=%u", monitor->ServerId()); - Debug(1, "%s", sql.c_str()); storage = nullptr; MYSQL_RES *result = zmDbFetch(sql); - if ( result ) { - for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) { + if (result) { + for (int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++) { storage = new Storage(atoi(dbrow[0])); - if ( SetPath(storage) ) + if (SetPath(storage)) break; delete storage; storage = nullptr; @@ -149,18 +155,18 @@ Event::Event( mysql_free_result(result); result = nullptr; } - if ( !storage ) { + 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() ) + if (monitor->ServerId()) sql += stringtf(" OR ServerId != %u", monitor->ServerId()); result = zmDbFetch(sql); - if ( result ) { + if (result) { for ( int i = 0; MYSQL_ROW dbrow = mysql_fetch_row(result); i++ ) { storage = new Storage(atoi(dbrow[0])); - if ( SetPath(storage) ) + if (SetPath(storage)) break; delete storage; storage = nullptr; @@ -169,7 +175,7 @@ Event::Event( result = nullptr; } } - if ( !storage ) { + if (!storage) { storage = new Storage(); Warning("Failed to find a storage area to save events."); } @@ -178,24 +184,16 @@ Event::Event( } // end if ! setPath(Storage) Debug(1, "Using storage area at %s", path.c_str()); - video_name = ""; - snapshot_file = path + "/snapshot.jpg"; alarm_file = path + "/alarm.jpg"; - /* Save as video */ + video_incomplete_path = path + "/" + video_incomplete_file; - if ( monitor->GetOptVideoWriter() != 0 ) { - std::string container = monitor->OutputContainer(); - if ( container == "auto" || container == "" ) { - container = "mp4"; - } + if (monitor->GetOptVideoWriter() != 0) { + /* Save as video */ - video_name = stringtf("%" PRIu64 "-%s.%s", id, "video", container.c_str()); - video_file = path + "/" + video_name; - Debug(1, "Writing video file to %s", video_file.c_str()); videoStore = new VideoStore( - video_file.c_str(), + video_incomplete_path.c_str(), container.c_str(), monitor->GetVideoStream(), monitor->GetVideoCodecContext(), @@ -213,20 +211,32 @@ Event::Event( zmDbDo(sql); } } else { - sql = stringtf("UPDATE Events SET Videoed=1, DefaultVideo = '%s' WHERE Id=%" PRIu64, video_name.c_str(), id); - zmDbDo(sql); + 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; } 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. /* Close the video file */ - if ( videoStore != nullptr ) { + if (videoStore != nullptr) { Debug(4, "Deleting video store"); delete videoStore; videoStore = nullptr; + int result = rename(video_incomplete_path.c_str(), video_path.c_str()); + if (result == 0) { + Debug(1, "File successfully renamed"); + } else { + Error("Failed renaming %s to %s", video_incomplete_path.c_str(), video_path.c_str()); + // So that we don't update the event record + video_file = video_incomplete_file; + } } // endtime is set in AddFrame, so SHOULD be set to the value of the last frame timestamp. @@ -245,21 +255,23 @@ Event::~Event() { } std::string sql = stringtf( - "UPDATE Events SET Name='%s%" PRIu64 "', EndDateTime = from_unixtime(%ld), Length = %.2f, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d WHERE Id = %" PRIu64 " AND Name='New Event'", + "UPDATE Events SET Name='%s%" PRIu64 "', EndDateTime = from_unixtime(%ld), Length = %.2f, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d, DefaultVideo='%s' WHERE Id = %" PRIu64 " AND Name='New Event'", monitor->EventPrefix(), id, std::chrono::system_clock::to_time_t(end_time), delta_time.count(), frames, alarm_frames, tot_score, static_cast(alarm_frames ? (tot_score / alarm_frames) : 0), max_score, + video_file.c_str(), // defaults to "" id); if (!zmDbDoUpdate(sql)) { // Name might have been changed during recording, so just do the update without changing the name. sql = stringtf( - "UPDATE Events SET EndDateTime = from_unixtime(%ld), Length = %.2f, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d WHERE Id = %" PRIu64, + "UPDATE Events SET EndDateTime = from_unixtime(%ld), Length = %.2f, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d, DefaultVideo='%s' WHERE Id = %" PRIu64, std::chrono::system_clock::to_time_t(end_time), delta_time.count(), frames, alarm_frames, tot_score, static_cast(alarm_frames ? (tot_score / alarm_frames) : 0), max_score, + video_file.c_str(), // defaults to "" id); zmDbDoUpdate(sql); } // end if no changed rows due to Name change during recording @@ -312,32 +324,32 @@ void Event::updateNotes(const StringSetMap &newNoteSetMap) { bool update = false; //Info( "Checking notes, %d <> %d", noteSetMap.size(), newNoteSetMap.size() ); - if ( newNoteSetMap.size() > 0 ) { - if ( noteSetMap.size() == 0 ) { + if (newNoteSetMap.size() > 0) { + if (noteSetMap.size() == 0) { noteSetMap = newNoteSetMap; update = true; } else { - for ( StringSetMap::const_iterator newNoteSetMapIter = newNoteSetMap.begin(); + for (StringSetMap::const_iterator newNoteSetMapIter = newNoteSetMap.begin(); newNoteSetMapIter != newNoteSetMap.end(); - ++newNoteSetMapIter ) { + ++newNoteSetMapIter) { const std::string &newNoteGroup = newNoteSetMapIter->first; const StringSet &newNoteSet = newNoteSetMapIter->second; //Info( "Got %d new strings", newNoteSet.size() ); - if ( newNoteSet.size() > 0 ) { + if (newNoteSet.size() > 0) { StringSetMap::iterator noteSetMapIter = noteSetMap.find(newNoteGroup); - if ( noteSetMapIter == noteSetMap.end() ) { - //Info( "Can't find note group %s, copying %d strings", newNoteGroup.c_str(), newNoteSet.size() ); + if (noteSetMapIter == noteSetMap.end()) { + //Debug(3, "Can't find note group %s, copying %d strings", newNoteGroup.c_str(), newNoteSet.size()); noteSetMap.insert(StringSetMap::value_type(newNoteGroup, newNoteSet)); update = true; } else { StringSet ¬eSet = noteSetMapIter->second; - //Info( "Found note group %s, got %d strings", newNoteGroup.c_str(), newNoteSet.size() ); - for ( StringSet::const_iterator newNoteSetIter = newNoteSet.begin(); + //Debug(3, "Found note group %s, got %d strings", newNoteGroup.c_str(), newNoteSet.size()); + for (StringSet::const_iterator newNoteSetIter = newNoteSet.begin(); newNoteSetIter != newNoteSet.end(); - ++newNoteSetIter ) { + ++newNoteSetIter) { const std::string &newNote = *newNoteSetIter; StringSet::iterator noteSetIter = noteSet.find(newNote); - if ( noteSetIter == noteSet.end() ) { + if (noteSetIter == noteSet.end()) { noteSet.insert(newNote); update = true; } @@ -479,7 +491,7 @@ void Event::AddFrame(Image *image, Debug(1, "Writing snapshot"); WriteFrameImage(image, timestamp, snapshot_file.c_str()); } else { - Debug(1, "Not Writing snapshot"); + Debug(1, "Not Writing snapshot because score %d > max %d", score, max_score); } // We are writing an Alarm frame @@ -491,7 +503,7 @@ void Event::AddFrame(Image *image, Debug(1, "Writing alarm image"); WriteFrameImage(image, timestamp, alarm_file.c_str()); } else { - Debug(1, "Not Writing alarm image"); + Debug(3, "Not Writing alarm image because alarm frame already written"); } if (alarm_image and (save_jpegs & 2)) { @@ -534,7 +546,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(); diff --git a/src/zm_event.h b/src/zm_event.h index 8b8e49322..611b2f716 100644 --- a/src/zm_event.h +++ b/src/zm_event.h @@ -84,8 +84,13 @@ class Event { std::string alarm_file; VideoStore *videoStore; - std::string video_name; + std::string container; + std::string codec; std::string video_file; + std::string video_path; + std::string video_incomplete_file; + std::string video_incomplete_path; + int last_db_frame; bool have_video_keyframe; // a flag to tell us if we have had a video keyframe when writing an mp4. The first frame SHOULD be a video keyframe. Storage::Schemes scheme; diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 4bad2d61a..7ef3619c2 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -141,7 +141,7 @@ bool EventStream::loadEventData(uint64_t event_id) { event_data->storage_id = dbrow[1] ? atoi(dbrow[1]) : 0; event_data->frame_count = dbrow[2] == nullptr ? 0 : atoi(dbrow[2]); event_data->start_time = SystemTimePoint(Seconds(atoi(dbrow[3]))); - event_data->end_time = dbrow[4] ? SystemTimePoint(Seconds(atoi(dbrow[4]))) : SystemTimePoint(); + event_data->end_time = dbrow[4] ? SystemTimePoint(Seconds(atoi(dbrow[4]))) : std::chrono::system_clock::now(); event_data->duration = std::chrono::duration_cast(event_data->end_time - event_data->start_time); event_data->frames_duration = std::chrono::duration_cast(dbrow[5] ? FPSeconds(atof(dbrow[5])) : FPSeconds(0.0)); @@ -663,6 +663,7 @@ bool EventStream::checkEventLoaded() { else curr_frame_id = 1; Debug(2, "New frame id = %ld", curr_frame_id); + start = std::chrono::steady_clock::now(); return true; } else { Debug(2, "No next event loaded using %s. Pausing", sql.c_str()); @@ -810,7 +811,7 @@ bool EventStream::sendFrame(Microseconds delta_us) { fputs("Content-Type: image/x-rgbz\r\n", stdout); break; case STREAM_RAW : - img_buffer = (uint8_t*)(send_image->Buffer()); + img_buffer = send_image->Buffer(); img_buffer_size = send_image->Size(); fputs("Content-Type: image/x-rgb\r\n", stdout); break; @@ -836,12 +837,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; @@ -850,13 +852,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; @@ -903,7 +905,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]; @@ -957,23 +959,25 @@ void EventStream::runStream() { static_cast(std::chrono::duration_cast(delta).count())); // if effective > base we should speed up frame delivery - delta = std::chrono::duration_cast((delta * base_fps) / effective_fps); - Debug(3, "delta %" PRIi64 " us = base_fps (%f) / effective_fps (%f)", + if (base_fps < effective_fps) { + delta = std::chrono::duration_cast((delta * base_fps) / effective_fps); + Debug(3, "delta %" PRIi64 " us = base_fps (%f) / effective_fps (%f)", static_cast(std::chrono::duration_cast(delta).count()), base_fps, effective_fps); - // but must not exceed maxfps - delta = std::max(delta, Microseconds(lround(Microseconds::period::den / maxfps))); - Debug(3, "delta %" PRIi64 " us = base_fps (%f) /effective_fps (%f) from 30fps", + // but must not exceed maxfps + delta = std::max(delta, Microseconds(lround(Microseconds::period::den / maxfps))); + Debug(3, "delta %" PRIi64 " us = base_fps (%f) / effective_fps (%f) from 30fps", static_cast(std::chrono::duration_cast(delta).count()), base_fps, effective_fps); + } // +/- 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) && ( diff --git a/src/zm_eventstream.h b/src/zm_eventstream.h index 387748b08..969e6725d 100644 --- a/src/zm_eventstream.h +++ b/src/zm_eventstream.h @@ -76,7 +76,7 @@ class EventStream : public StreamBase { long 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; diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index b122e0f2b..728b51783 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -458,6 +458,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_fifo.cpp b/src/zm_fifo.cpp index 319c35c31..a999adc23 100644 --- a/src/zm_fifo.cpp +++ b/src/zm_fifo.cpp @@ -143,8 +143,8 @@ bool Fifo::writePacket(std::string filename, const ZMPacket &packet) { bool Fifo::write(uint8_t *data, size_t bytes, int64_t pts) { if (!(outfile or open())) return false; // Going to write a brief header - Debug(1, "Writing header ZM %lu %" PRId64, bytes, pts); - if ( fprintf(outfile, "ZM %lu %" PRId64 "\n", bytes, pts) < 0 ) { + Debug(1, "Writing header ZM %zu %" PRId64, bytes, pts); + if (fprintf(outfile, "ZM %zu %" PRId64 "\n", bytes, pts) < 0) { if (errno != EAGAIN) { Error("Problem during writing: %s", strerror(errno)); } else { 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_image.cpp b/src/zm_image.cpp index 2b7bc69e2..295426254 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -270,7 +270,6 @@ int Image::PopulateFrame(AVFrame *frame) { frame->width = width; frame->height = height; frame->format = imagePixFormat; - Debug(1, "PopulateFrame: width %d height %d linesize %d colours %d imagesize %d", width, height, linesize, colours, size); zm_dump_video_frame(frame, "Image.Populate(frame)"); return 1; } // int Image::PopulateFrame(AVFrame *frame) diff --git a/src/zm_image.h b/src/zm_image.h index 24626d789..74e5931eb 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -179,9 +179,11 @@ class Image { } } - /* Internal buffer should not be modified from functions outside of this class */ + inline uint8_t* Buffer() { return buffer; } inline const uint8_t* Buffer() const { return buffer; } + inline uint8_t* Buffer(unsigned int x, unsigned int y=0) { return &buffer[(y*linesize) + x*colours]; } inline const uint8_t* Buffer(unsigned int x, unsigned int y=0) const { return &buffer[(y*linesize) + x*colours]; } + /* Request writeable buffer */ uint8_t* WriteBuffer(const unsigned int p_width, const unsigned int p_height, const unsigned int p_colours, const unsigned int p_subpixelorder); // Is only acceptable on a pre-allocated buffer 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_logger.cpp b/src/zm_logger.cpp index 5c7c5e9a7..f43ea6f30 100644 --- a/src/zm_logger.cpp +++ b/src/zm_logger.cpp @@ -43,11 +43,11 @@ Logger::IntMap Logger::smSyslogPriorities; void Logger::usrHandler(int sig) { Logger *logger = fetch(); - if ( sig == SIGUSR1 ) + if (sig == SIGUSR1) logger->level(logger->level()+1); - else if ( sig == SIGUSR2 ) + else if (sig == SIGUSR2) logger->level(logger->level()-1); - Info("Logger - Level changed to %d", logger->level()); + Info("Logger - Level changed to %d %s", logger->level(), smCodes[logger->level()].c_str()); } Logger::Logger() : @@ -296,23 +296,23 @@ const std::string &Logger::id(const std::string &id) { } Logger::Level Logger::level(Logger::Level level) { - if ( level > NOOPT ) { + if (level > NOOPT) { mLevel = limit(level); mEffectiveLevel = NOLOG; - if ( mTerminalLevel > mEffectiveLevel ) + if (mTerminalLevel > mEffectiveLevel) mEffectiveLevel = mTerminalLevel; - if ( mDatabaseLevel > mEffectiveLevel ) + if (mDatabaseLevel > mEffectiveLevel) mEffectiveLevel = mDatabaseLevel; - if ( mFileLevel > mEffectiveLevel ) + if (mFileLevel > mEffectiveLevel) mEffectiveLevel = mFileLevel; - if ( mSyslogLevel > mEffectiveLevel ) + if (mSyslogLevel > mEffectiveLevel) mEffectiveLevel = mSyslogLevel; - if ( mEffectiveLevel > mLevel) + if (mEffectiveLevel > mLevel) mEffectiveLevel = mLevel; // DEBUG levels should flush - if ( mLevel > INFO ) + if (mLevel > INFO) mFlush = true; } return mLevel; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 46055e60c..607f41216 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -70,7 +70,7 @@ // 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, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, `Enabled`, `DecodingEnabled`, " -"`LinkedMonitors`, `AnalysisFPSLimit`, `AnalysisUpdateDelay`, `MaxFPS`, `AlarmMaxFPS`," +"`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`, " @@ -86,6 +86,7 @@ std::string load_monitor_sql = "`SignalCheckPoints`, `SignalCheckColour`, `Importance`-1 FROM `Monitors`"; std::string CameraType_Strings[] = { + "Unknown", "Local", "Remote", "File", @@ -93,10 +94,21 @@ std::string CameraType_Strings[] = { "LibVLC", "NVSOCKET", "CURL", - "VNC", + "VNC" +}; + +std::string Function_Strings[] = { + "Unknown", + "None", + "Monitor", + "Modect", + "Record", + "Mocord", + "Nodect" }; std::string State_Strings[] = { + "Unknown", "IDLE", "PREALARM", "ALARM", @@ -143,7 +155,7 @@ bool Monitor::MonitorLink::connect() { mem_size = sizeof(SharedData) + sizeof(TriggerData); - Debug(1, "link.mem.size=%jd", mem_size); + 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) { @@ -170,14 +182,14 @@ bool Monitor::MonitorLink::connect() { disconnect(); return false; } else if (map_stat.st_size < mem_size) { - Error("Got unexpected memory map file size %ld, expected %jd", map_stat.st_size, 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(), mem_size, strerror(errno)); + Error("Can't map file %s (%jd bytes) to memory: %s", mem_file.c_str(), static_cast(mem_size), strerror(errno)); disconnect(); return false; } @@ -424,7 +436,8 @@ Monitor::Monitor() /* std::string load_monitor_sql = "SELECT `Id`, `Name`, `ServerId`, `StorageId`, `Type`, `Function`+0, `Capturing`+0, `Analysing`+0, `AnalysisSource`, `Recording`+0, `RecordingSource`, `Enabled`, `DecodingEnabled`, " -, LinkedMonitors, AnalysisFPSLimit, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS," + "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, " "SaveJPEGs, VideoWriter, EncoderParameters, @@ -480,18 +493,11 @@ 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++; - decoding_enabled = !( - ( function == RECORD or function == NODECT ) - and - ( savejpegs == 0 ) - and - ( videowriter == PASSTHROUGH ) - and - !decoding_enabled - ); - Debug(1, "Decoding enabled: %d", decoding_enabled); + // See below after save_jpegs for a recalculation of decoding_enabled 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++; @@ -558,6 +564,17 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) { videowriter = (VideoWriter)atoi(dbrow[col]); col++; encoderparams = dbrow[col] ? dbrow[col] : ""; col++; + decoding_enabled = !( + ( function == RECORD or function == NODECT ) + and + ( savejpegs == 0 ) + and + ( videowriter == PASSTHROUGH ) + and + !decoding_enabled + ); + Debug(3, "Decoding enabled: %d function %d %s savejpegs %d videowriter %d", decoding_enabled, function, Function_Strings[function].c_str(), savejpegs, videowriter); + /*"`OutputCodec`, `Encoder`, `OutputContainer`, " */ output_codec = dbrow[col] ? atoi(dbrow[col]) : 0; col++; encoder = dbrow[col] ? dbrow[col] : ""; col++; @@ -939,7 +956,7 @@ bool Monitor::connect() { map_fd = -1; return false; } else { - Error("Got unexpected memory map file size %ld, expected %jd", map_stat.st_size, mem_size); + Error("Got unexpected memory map file size %ld, expected %jd", map_stat.st_size, static_cast(mem_size)); close(map_fd); map_fd = -1; return false; @@ -951,18 +968,18 @@ bool Monitor::connect() { mem_ptr = (unsigned char *)mmap(nullptr, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_LOCKED, map_fd, 0); if (mem_ptr == MAP_FAILED) { if (errno == EAGAIN) { - Debug(1, "Unable to map file %s (%jd bytes) to locked memory, trying unlocked", mem_file.c_str(), mem_size); + Debug(1, "Unable to map file %s (%jd bytes) to locked memory, trying unlocked", mem_file.c_str(), static_cast(mem_size)); #endif mem_ptr = (unsigned char *)mmap(nullptr, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, 0); - Debug(1, "Mapped file %s (%jd bytes) to unlocked memory", mem_file.c_str(), mem_size); + Debug(1, "Mapped file %s (%jd bytes) to unlocked memory", mem_file.c_str(), static_cast(mem_size)); #ifdef MAP_LOCKED } else { - Error("Unable to map file %s (%jd bytes) to locked memory (%s)", mem_file.c_str(), mem_size, strerror(errno)); + Error("Unable to map file %s (%jd bytes) to locked memory (%s)", mem_file.c_str(), static_cast(mem_size), strerror(errno)); } } #endif if ((mem_ptr == MAP_FAILED) or (mem_ptr == nullptr)) { - Error("Can't map file %s (%jd bytes) to memory: %s(%d)", mem_file.c_str(), mem_size, strerror(errno), errno); + Error("Can't map file %s (%jd bytes) to memory: %s(%d)", mem_file.c_str(), static_cast(mem_size), strerror(errno), errno); close(map_fd); map_fd = -1; mem_ptr = nullptr; @@ -1648,7 +1665,7 @@ void Monitor::CheckAction() { } } -void Monitor::UpdateCaptureFPS() { +void Monitor::UpdateFPS() { if ( fps_report_interval and ( !(image_count%fps_report_interval) @@ -1667,82 +1684,35 @@ void Monitor::UpdateCaptureFPS() { uint32 new_camera_bytes = camera->Bytes(); uint32 new_capture_bandwidth = static_cast((new_camera_bytes - last_camera_bytes) / elapsed.count()); - last_camera_bytes = new_camera_bytes; + double new_analysis_fps = (motion_frame_count - last_motion_frame_count) / elapsed.count(); - Debug(4, "%s: %d - last %d = %d now:%lf, last %lf, elapsed %lf = %lffps", - "Capturing", + Debug(4, "FPS: capture count %d - last capture count %d = %d now:%lf, last %lf, elapsed %lf = capture: %lf fps analysis: %lf fps", image_count, last_capture_image_count, image_count - last_capture_image_count, FPSeconds(now.time_since_epoch()).count(), - FPSeconds(last_analysis_fps_time.time_since_epoch()).count(), + FPSeconds(last_fps_time.time_since_epoch()).count(), elapsed.count(), - new_capture_fps); + new_capture_fps, + new_analysis_fps); - Info("%s: %d - Capturing at %.2lf fps, capturing bandwidth %ubytes/sec", - name.c_str(), image_count, new_capture_fps, new_capture_bandwidth); + Info("%s: %d - Capturing at %.2lf fps, capturing bandwidth %ubytes/sec Analysing at %.2lf fps", + name.c_str(), image_count, new_capture_fps, new_capture_bandwidth, new_analysis_fps); shared_data->capture_fps = new_capture_fps; last_fps_time = now; last_capture_image_count = image_count; + shared_data->analysis_fps = new_analysis_fps; + last_motion_frame_count = motion_frame_count; + last_camera_bytes = new_camera_bytes; std::string sql = stringtf( - "UPDATE LOW_PRIORITY Monitor_Status SET CaptureFPS = %.2lf, CaptureBandwidth=%u WHERE MonitorId=%u", - new_capture_fps, new_capture_bandwidth, id); + "UPDATE LOW_PRIORITY Monitor_Status SET CaptureFPS = %.2lf, CaptureBandwidth=%u, AnalysisFPS = %.2lf WHERE MonitorId=%u", + new_capture_fps, new_capture_bandwidth, new_analysis_fps, id); dbQueue.push(std::move(sql)); } // now != last_fps_time } // end if report fps -} // void Monitor::UpdateCaptureFPS() - -void Monitor::UpdateAnalysisFPS() { - Debug(1, "analysis_image_count(%d) motion_count(%d) fps_report_interval(%d) mod%d", - analysis_image_count, motion_frame_count, fps_report_interval, - ((analysis_image_count && fps_report_interval) ? !(analysis_image_count%fps_report_interval) : -1 ) ); - - if ( - ( analysis_image_count and fps_report_interval and !(analysis_image_count%fps_report_interval) ) - or - // In startup do faster updates - ( (analysis_image_count < fps_report_interval) and !(analysis_image_count%10) ) - ) { - SystemTimePoint now = std::chrono::system_clock::now(); - - FPSeconds elapsed = now - last_analysis_fps_time; - Debug(4, "%s: %d - now: %.2f, last %lf, diff %lf", - name.c_str(), - analysis_image_count, - FPSeconds(now.time_since_epoch()).count(), - FPSeconds(last_analysis_fps_time.time_since_epoch()).count(), - elapsed.count()); - - if (elapsed > Seconds(1)) { - double new_analysis_fps = (motion_frame_count - last_motion_frame_count) / elapsed.count(); - Info("%s: %d - Analysing at %.2lf fps from %d - %d=%d / %lf - %lf = %lf", - name.c_str(), - analysis_image_count, - new_analysis_fps, - motion_frame_count, - last_motion_frame_count, - (motion_frame_count - last_motion_frame_count), - FPSeconds(now.time_since_epoch()).count(), - FPSeconds(last_analysis_fps_time.time_since_epoch()).count(), - elapsed.count()); - - if (new_analysis_fps != shared_data->analysis_fps) { - shared_data->analysis_fps = new_analysis_fps; - - std::string sql = stringtf("UPDATE LOW_PRIORITY Monitor_Status SET AnalysisFPS = %.2lf WHERE MonitorId=%u", - new_analysis_fps, id); - dbQueue.push(std::move(sql)); - last_analysis_fps_time = now; - last_motion_frame_count = motion_frame_count; - } else { - Debug(4, "No change in fps"); - } // end if change in fps - } // end if at least 1 second has passed since last update - - } // end if time to do an update -} // end void Monitor::UpdateAnalysisFPS +} // void Monitor::UpdateFPS() // Would be nice if this JUST did analysis // This idea is that we should be analysing as close to the capture frame as possible. @@ -1901,8 +1871,6 @@ bool Monitor::Analyse() { Debug(3, "signal and active and modect"); Event::StringSet zoneSet; - int motion_score = last_motion_score; - if (analysis_fps_limit) { double capture_fps = get_capture_fps(); motion_frame_skip = capture_fps / analysis_fps_limit; @@ -1914,38 +1882,45 @@ bool Monitor::Analyse() { if (snap->image) { // decoder may not have been able to provide an image if (!ref_image.Buffer()) { - Debug(1, "Assigning instead of Dectecting"); + Debug(1, "Assigning instead of Detecting"); ref_image.Assign(*(snap->image)); } else { Debug(1, "Detecting motion on image %d, image %p", snap->image_index, snap->image); // Get new score. - motion_score = DetectMotion(*(snap->image), zoneSet); + int motion_score = DetectMotion(*(snap->image), zoneSet); + // lets construct alarm cause. It will contain cause + names of zones alarmed + std::string alarm_cause; 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 (!alarm_cause.empty()) alarm_cause += ","; + alarm_cause += std::string(zone.Label()); + } } + if (!alarm_cause.empty()) + cause = cause+" "+alarm_cause; Debug(3, "After motion detection, score:%d last_motion_score(%d), new motion score(%d)", score, last_motion_score, motion_score); motion_frame_count += 1; - // Why are we updating the last_motion_score too? last_motion_score = motion_score; + if (motion_score) { + if (cause.length()) cause += ", "; + cause += MOTION_CAUSE; + noteSetMap[MOTION_CAUSE] = zoneSet; + } // end if motion_score } } else { Debug(1, "no image so skipping motion detection"); } // end if has image } else { - Debug(1, "Skipped motion detection last motion score was %d", motion_score); + Debug(1, "Skipped motion detection last motion score was %d", last_motion_score); } - if (motion_score) { - score += motion_score; - if (cause.length()) cause += ", "; - cause += MOTION_CAUSE; - noteSetMap[MOTION_CAUSE] = zoneSet; - } // end if 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, @@ -1953,12 +1928,13 @@ bool Monitor::Analyse() { ); } // 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 - GetVideoWriterStartTime() >= section_length) + 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))) { @@ -1967,77 +1943,17 @@ bool Monitor::Analyse() { 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(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 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 */ - ); + event = openEvent(snap, cause.empty() ? "Continuous" : cause, noteSetMap); - // 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; - } - - 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_; - } - packetqueue.free_it(start_it); - delete start_it; - start_it = nullptr; - } else { - // Create event from current snap - event = new Event(this, timestamp, "Continuous", noteSetMap); - } - shared_data->last_event_id = event->Id(); - - // 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()); - - Info("%s: %03d - Opened new event %" PRIu64 ", section start", + 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 */ if (state == IDLE) { @@ -2046,86 +1962,35 @@ bool Monitor::Analyse() { } // end if ! event } // end if RECORDING - if (score) { - + 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 - GetVideoWriterStartTime() >= min_section_length - && (!pre_event_count || Event::PreAlarmCount() >= alarm_frame_count - 1)) { + && (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, event frames %d, alarm frames %d event length %" PRIi64 " >=? %" PRIi64 " min", - Event::PreAlarmCount(), + "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 - GetVideoWriterStartTime()).count()), - static_cast(Seconds(min_section_length).count())); + 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); + name.c_str(), image_count, Event::PreAlarmCount(), alarm_frame_count, cause.c_str()); 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_; - } else { - starting_packet = snap; - } - - 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()); + event = openEvent(snap, cause, noteSetMap); 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; - Info("%s: %03d - Opening new event %" PRIu64 ", alarm start", name.c_str(), analysis_image_count, event->Id()); } else { shared_data->state = state = ALARM; @@ -2163,8 +2028,10 @@ bool Monitor::Analyse() { 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 - GetVideoWriterStartTime() >= min_section_length) { + 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 ) @@ -2182,7 +2049,8 @@ bool Monitor::Analyse() { shared_data->state = state = ((function != MOCORD) ? IDLE : TAPE); } else { Debug(1, - "State %s because 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 %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, @@ -2201,18 +2069,15 @@ bool Monitor::Analyse() { // Generate analysis images if necessary if ((savejpegs > 1) and snap->image) { for (const Zone &zone : zones) { - if (zone.Alarmed()) { - if (zone.AlarmImage()) { + 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 - //have_pre_alarmed_frames ++; Event::AddPreAlarmFrame(snap->image, timestamp, score, nullptr); } else if (state == ALARM) { for (const Zone &zone : zones) { @@ -2227,7 +2092,7 @@ bool Monitor::Analyse() { if (event) { if (noteSetMap.size() > 0) event->updateNotes(noteSetMap); - if (section_length != Seconds(0) && (timestamp - GetVideoWriterStartTime() >= section_length)) { + 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()), @@ -2235,11 +2100,7 @@ bool Monitor::Analyse() { 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()); + event = openEvent(snap, cause, noteSetMap); } } else { Error("ALARM but no event"); @@ -2292,8 +2153,6 @@ bool Monitor::Analyse() { // Only do these if it's a video packet. shared_data->last_read_index = snap->image_index; analysis_image_count++; - if (function == MODECT or function == MOCORD) - UpdateAnalysisFPS(); } packetqueue.increment_it(analysis_it); packetqueue.unlock(packet_lock); @@ -2356,7 +2215,7 @@ void Monitor::ReloadLinkedMonitors(const char *p_linked_monitors) { while ( 1 ) { dest_ptr = link_id_str; while ( *src_ptr >= '0' && *src_ptr <= '9' ) { - if ( (dest_ptr-link_id_str) < (unsigned int)(sizeof(link_id_str)-1) ) { + if ( (unsigned int)(dest_ptr-link_id_str) < (unsigned int)(sizeof(link_id_str)-1) ) { *dest_ptr++ = *src_ptr++; } else { break; @@ -2574,7 +2433,6 @@ int Monitor::Capture() { // Will only be queued if there are iterators allocated in the queue. packetqueue.queuePacket(packet); - UpdateCaptureFPS(); } else { // result == 0 // Question is, do we update last_write_index etc? return 0; @@ -2614,7 +2472,7 @@ bool Monitor::Decode() { // //capture_image = packet->image = new Image(width, height, camera->Colours(), camera->SubpixelOrder()); int ret = packet->decode(camera->getVideoCodecContext()); - if (ret > 0) { + if (ret > 0 and !zm_terminate) { if (packet->in_frame and !packet->image) { packet->image = new Image(camera_width, camera_height, camera->Colours(), camera->SubpixelOrder()); AVFrame *input_frame = packet->in_frame; @@ -2784,7 +2642,7 @@ void Monitor::TimestampImage(Image *ts_image, SystemTimePoint ts_time) const { const char *s_ptr = label_time_text; char *d_ptr = label_text; - while (*s_ptr && ((d_ptr - label_text) < (unsigned int) sizeof(label_text))) { + while (*s_ptr && ((unsigned int)(d_ptr - label_text) < (unsigned int) sizeof(label_text))) { if ( *s_ptr == config.timestamp_code_char[0] ) { bool found_macro = false; switch ( *(s_ptr+1) ) { @@ -2800,7 +2658,7 @@ void Monitor::TimestampImage(Image *ts_image, SystemTimePoint ts_time) const { typedef std::chrono::duration Centiseconds; Centiseconds centi_sec = std::chrono::duration_cast( ts_time.time_since_epoch() - std::chrono::duration_cast(ts_time.time_since_epoch())); - d_ptr += snprintf(d_ptr, sizeof(label_text) - (d_ptr - label_text), "%02ld", centi_sec.count()); + d_ptr += snprintf(d_ptr, sizeof(label_text) - (d_ptr - label_text), "%02lld", static_cast(centi_sec.count())); found_macro = true; break; } @@ -2817,6 +2675,67 @@ 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 noteSetMap) { + + // 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(), 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); + // 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; @@ -2827,7 +2746,18 @@ 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(); + delete e; + + if (!command.empty()) { + if (fork() == 0) { + execlp(command.c_str(), command.c_str(), std::to_string(event_id).c_str(), 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 = {}; @@ -3120,9 +3050,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(); @@ -3140,10 +3067,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; diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 412b65c0c..b56a5fdd8 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -128,7 +128,7 @@ public: } Deinterlace; typedef enum { - UNKNOWN=-1, + UNKNOWN, IDLE, PREALARM, ALARM, @@ -443,6 +443,8 @@ protected: int n_linked_monitors; MonitorLink **linked_monitors; + std::string event_start_command; + std::string event_end_command; std::vector groups; @@ -612,8 +614,7 @@ public: unsigned int GetLastWriteIndex() const; uint64_t GetLastEventId() const; double GetFPS() const; - void UpdateAnalysisFPS(); - void UpdateCaptureFPS(); + void UpdateFPS(); void ForceAlarmOn( int force_score, const char *force_case, const char *force_text="" ); void ForceAlarmOff(); void CancelForced(); @@ -658,6 +659,10 @@ public: bool Decode(); 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 noteSetMap); void closeEvent(); void Reload(); diff --git a/src/zm_monitorstream.cpp b/src/zm_monitorstream.cpp index 30063a4ef..64cc5fc46 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; @@ -229,6 +241,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) { break; case CMD_QUIT : Info("User initiated exit - CMD_QUIT"); + zm_terminate = true; break; case CMD_QUERY : Debug(1, "Got QUERY command, sending STATUS"); @@ -267,7 +280,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; } @@ -287,9 +300,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, @@ -311,13 +324,6 @@ void MonitorStream::processCommand(const CmdMsg *msg) { Error("Can't sendto on sd %d: %s", sd, strerror(errno)); } Debug(2, "Number of bytes sent to (%s): (%d)", rem_addr.sun_path, nbytes); - - // quit after sending a status, if this was a quit request - if ((MsgCommand)msg->msg_data[0] == CMD_QUIT) { - zm_terminate = true; - Debug(2, "Quitting"); - return; - } } // end void MonitorStream::processCommand(const CmdMsg *msg) bool MonitorStream::sendFrame(const std::string &filepath, SystemTimePoint timestamp) { @@ -365,7 +371,6 @@ bool MonitorStream::sendFrame(const std::string &filepath, SystemTimePoint times 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); @@ -379,12 +384,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()); @@ -404,8 +412,6 @@ bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) { 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 : @@ -414,7 +420,7 @@ bool MonitorStream::sendFrame(Image *image, SystemTimePoint timestamp) { break; case STREAM_RAW : fputs("Content-Type: image/x-rgb\r\n", stdout); - img_buffer = (uint8_t*)send_image->Buffer(); + img_buffer = send_image->Buffer(); img_buffer_size = send_image->Size(); break; case STREAM_ZIP : @@ -447,19 +453,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) { + 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 +511,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 +581,7 @@ void MonitorStream::runStream() { break; } - now = std::chrono::system_clock::now(); + now = std::chrono::steady_clock::now(); monitor->setLastViewed(now); bool was_paused = paused; @@ -618,7 +629,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) { @@ -684,7 +695,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)", @@ -743,9 +755,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) { @@ -776,17 +788,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 && (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())); @@ -798,13 +835,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) { @@ -855,16 +885,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_packetqueue.cpp b/src/zm_packetqueue.cpp index 3cf77b866..8b831ec2d 100644 --- a/src/zm_packetqueue.cpp +++ b/src/zm_packetqueue.cpp @@ -116,14 +116,15 @@ 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 *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"); + Warning("Found locked packet when trying to free up video packets. This basically means that decoding is not keeping up."); delete lp; ++it; continue; @@ -209,7 +210,7 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { --it; } } - Debug(1, "Tail count is %d, queue size is %lu", tail_count, pktQueue.size()); + Debug(1, "Tail count is %d, queue size is %zu", tail_count, pktQueue.size()); if (!keep_keyframes) { // If not doing passthrough, we don't care about starting with a keyframe so logic is simpler @@ -260,6 +261,7 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { zm_packet = *it; lp = new ZMLockedPacket(zm_packet); if (!lp->trylock()) { + Debug(3, "Failed locking packet %d", zm_packet->image_index); delete lp; break; } @@ -280,7 +282,7 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { next_front = it; } ++video_packets_to_delete; - Debug(4, "Counted %d video packets. Which would leave %d in packetqueue tail count is %d", + Debug(3, "Counted %d video packets. Which would leave %d in packetqueue tail count is %d", video_packets_to_delete, packet_counts[video_stream_id]-video_packets_to_delete, tail_count); if (packet_counts[video_stream_id] - video_packets_to_delete <= pre_event_video_packet_count + tail_count) { break; @@ -289,7 +291,7 @@ void PacketQueue::clearPackets(const std::shared_ptr &add_packet) { ++it; } // end while } // end if first packet not locked - Debug(1, "Resulting pointing at latest packet? %d, next front points to begin? %d", + Debug(1, "Resulting it pointing at latest packet? %d, next front points to begin? %d", ( *it == add_packet ), ( next_front == pktQueue.begin() ) ); @@ -311,7 +313,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_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_signal.cpp b/src/zm_signal.cpp index 9a92b5eac..4107e96c8 100644 --- a/src/zm_signal.cpp +++ b/src/zm_signal.cpp @@ -46,6 +46,7 @@ RETSIGTYPE zm_die_handler(int signal, siginfo_t * info, void *context) RETSIGTYPE zm_die_handler(int signal) #endif { + zm_terminate = true; Error("Got signal %d (%s), crashing", signal, strsignal(signal)); #if (defined(__i386__) || defined(__x86_64__)) // Get more information if available diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index 5255d3ab2..aba98e0c3 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -128,10 +128,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() @@ -386,9 +386,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..09bd5bd80 100644 --- a/src/zm_stream.h +++ b/src/zm_stream.h @@ -88,6 +88,7 @@ protected: CMD_VARPLAY, CMD_GET_IMAGE, CMD_QUIT, + CMD_MAXFPS, CMD_QUERY=99 } MsgCommand; @@ -118,21 +119,22 @@ 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; @@ -207,10 +209,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_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..7aaea7316 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; @@ -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..409429667 100644 --- a/src/zm_utils.cpp +++ b/src/zm_utils.cpp @@ -252,8 +252,15 @@ 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) { + #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 1bd0724d5..ad401b22c 100644 --- a/src/zm_videostore.cpp +++ b/src/zm_videostore.cpp @@ -151,6 +151,7 @@ bool VideoStore::open() { Debug(3, "Encoder Option %s=%s", e->key, e->value); } } + av_dict_free(&opts); if (video_in_stream) { zm_dump_codecpar(video_in_stream->codecpar); @@ -184,6 +185,7 @@ bool VideoStore::open() { } } // end if orientation + av_dict_parse_string(&opts, Options.c_str(), "=", ",#\n", 0); if (av_dict_get(opts, "new_extradata", nullptr, AV_DICT_MATCH_CASE)) { av_dict_set(&opts, "new_extradata", nullptr, 0); // Special flag to tell us to open a codec to get new extraflags to fix weird h265 @@ -230,8 +232,8 @@ bool VideoStore::open() { if (ret < 0) { Error("Could not initialize stream parameteres"); } + av_dict_free(&opts); } // end if extradata_entry - av_dict_free(&opts); } else if (monitor->GetOptVideoWriter() == Monitor::ENCODE) { int wanted_codec = monitor->OutputCodec(); if (!wanted_codec) { @@ -485,6 +487,7 @@ bool VideoStore::open() { zm_dump_stream_format(oc, 0, 0, 1); if (audio_out_stream) zm_dump_stream_format(oc, 1, 0, 1); + av_dict_parse_string(&opts, Options.c_str(), "=", ",#\n", 0); const AVDictionaryEntry *movflags_entry = av_dict_get(opts, "movflags", nullptr, AV_DICT_MATCH_CASE); if (!movflags_entry) { Debug(1, "setting movflags to frag_keyframe+empty_moov"); @@ -616,7 +619,8 @@ VideoStore::~VideoStore() { Debug(1, "Writing trailer"); /* Write the trailer before close */ - if (int rc = av_write_trailer(oc)) { + int rc; + if ((rc = av_write_trailer(oc)) < 0) { Error("Error writing trailer %s", av_err2str(rc)); } else { Debug(3, "Success Writing trailer"); @@ -626,7 +630,7 @@ VideoStore::~VideoStore() { if (!(out_format->flags & AVFMT_NOFILE)) { /* Close the out file. */ Debug(4, "Closing"); - if (int rc = avio_close(oc->pb)) { + if ((rc = avio_close(oc->pb)) < 0) { Error("Error closing avio %s", av_err2str(rc)); } } else { diff --git a/src/zm_videostore.h b/src/zm_videostore.h index 682fc147b..c73ed19db 100644 --- a/src/zm_videostore.h +++ b/src/zm_videostore.h @@ -111,6 +111,13 @@ class VideoStore { int writePacket(const std::shared_ptr &pkt); int write_packets(PacketQueue &queue); void flush_codecs(); + const char *get_codec() { + if (chosen_codec_data) + return chosen_codec_data->codec_codec; + if (video_out_stream) + return avcodec_get_name(video_out_stream->codecpar->codec_id); + return ""; + } }; #endif // ZM_VIDEOSTORE_H diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index bc7086614..4fc1bf61b 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -206,7 +206,7 @@ bool Zone::CheckAlarms(const Image *delta_image) { // Get the difference image Image *diff_image = image = new Image(*delta_image); int diff_width = diff_image->Width(); - uint8_t* diff_buff = (uint8_t*)diff_image->Buffer(); + uint8_t* diff_buff = diff_image->Buffer(); uint8_t* pdiff; unsigned int pixel_diff_count = 0; @@ -283,7 +283,7 @@ bool Zone::CheckAlarms(const Image *delta_image) { int lo_x = ranges[y].lo_x; int hi_x = ranges[y].hi_x; - pdiff = (uint8_t*)diff_image->Buffer(lo_x, y); + pdiff = diff_image->Buffer(lo_x, y); for (int x = lo_x; x <= hi_x; x++, pdiff++) { if (*pdiff == kWhite) { @@ -366,7 +366,7 @@ bool Zone::CheckAlarms(const Image *delta_image) { int lo_x = ranges[y].lo_x; int hi_x = ranges[y].hi_x; - pdiff = (uint8_t*)diff_image->Buffer(lo_x, y); + pdiff = diff_image->Buffer(lo_x, y); for (int x = lo_x; x <= hi_x; x++, pdiff++) { if (*pdiff == kWhite) { Debug(9, "Got white pixel at %d,%d (%p)", x, y, pdiff); @@ -878,16 +878,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}, @@ -980,7 +987,7 @@ void Zone::std_alarmedpixels( unsigned int hi_x = ranges[y].hi_x; Debug(7, "Checking line %d from %d -> %d", y, lo_x, hi_x); - uint8_t *pdiff = (uint8_t*)pdiff_image->Buffer(lo_x, y); + uint8_t *pdiff = pdiff_image->Buffer(lo_x, y); const uint8_t *ppoly = ppoly_image->Buffer(lo_x, y); for ( unsigned int x = lo_x; x <= hi_x; x++, pdiff++, ppoly++ ) { diff --git a/src/zmbenchmark.cpp b/src/zmbenchmark.cpp index 244094a54..5e6b93e42 100644 --- a/src/zmbenchmark.cpp +++ b/src/zmbenchmark.cpp @@ -140,7 +140,7 @@ std::shared_ptr GenerateRandomImage( Image *image = new Image(width, height, ZM_COLOUR_GRAY8, ZM_SUBPIX_ORDER_NONE); // Set it to black initially. - memset((void *) image->Buffer(0, 0), 0, (size_t) image->LineSize() * (size_t) image->Height()); + memset(image->Buffer(0, 0), 0, (size_t) image->LineSize() * (size_t) image->Height()); // Now randomize the pixels inside a box. const int box_width = (width * change_box_percent) / 100; @@ -149,7 +149,7 @@ std::shared_ptr GenerateRandomImage( const int box_y = (int) ((uint64_t) mt_rand() * (height - box_height) / RAND_MAX); for (int y = 0 ; y < box_height ; y++) { - uint8_t *row = (uint8_t *) image->Buffer(box_x, box_y + y); + uint8_t *row = image->Buffer(box_x, box_y + y); for (int x = 0 ; x < box_width ; x++) { row[x] = (uint8_t) mt_rand(); } diff --git a/src/zmc.cpp b/src/zmc.cpp index eacaa43e6..edc5a6611 100644 --- a/src/zmc.cpp +++ b/src/zmc.cpp @@ -220,12 +220,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; @@ -273,7 +274,7 @@ int main(int argc, char *argv[]) { std::this_thread::sleep_for(sleep_time); } - if (zm_terminate){ + if (zm_terminate) { break; } @@ -317,6 +318,7 @@ int main(int argc, char *argv[]) { result = -1; break; } + monitors[i]->UpdateFPS(); // capture_delay is the amount of time we should sleep in useconds to achieve the desired framerate. Microseconds delay = (monitors[i]->GetState() == Monitor::ALARM) ? monitors[i]->GetAlarmCaptureDelay() @@ -373,6 +375,7 @@ int main(int argc, char *argv[]) { monitor->Id()); zmDbDo(sql); } + monitors.clear(); Image::Deinitialise(); Debug(1, "terminating"); 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 2a4dd6989..00dcd44e9 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. @@ -171,15 +121,8 @@ if [ ! -d "${GITHUB_FORK}_zoneminder_release" ]; then 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,12 +218,11 @@ rm .gitignore cd ../ -if [ !-e "$DIRECTORY.orig.tar.gz" ]; then -read -p "$DIRECTORY.orig.tar.gz does not exist, create it? [Y/n]" - if [[ $REPLY == [yY] ]]; then - - tar zcf $DIRECTORY.orig.tar.gz $DIRECTORY.orig -fi; +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; fi; IFS=',' ;for DISTRO in `echo "$DISTROS"`; do @@ -358,6 +345,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/version b/version index 9cf86ad0f..e22b12fd6 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.37.1 +1.37.6 diff --git a/web/ajax/event.php b/web/ajax/event.php index 906fe255a..a0b9cb57a 100644 --- a/web/ajax/event.php +++ b/web/ajax/event.php @@ -93,7 +93,7 @@ if ( canView('Events') or canView('Snapshots') ) { $exportFormat, $exportCompress, $exportStructure, - (!empty($_REQUEST['exportFile'])?$_REQUEST['exportFile']:'zmExport'), + (!empty($_REQUEST['exportFile'])?$_REQUEST['exportFile']:'zmExport') )) { ajaxResponse(array('exportFile'=>$exportFile)); } else { diff --git a/web/ajax/events.php b/web/ajax/events.php index 562cce9c2..4db6b99cc 100644 --- a/web/ajax/events.php +++ b/web/ajax/events.php @@ -6,28 +6,30 @@ $data = array(); // INITIALIZE AND CHECK SANITY // -if ( !canView('Events') ) $message = 'Insufficient permissions for user '.$user['Username']; +if (!canView('Events')) + $message = 'Insufficient permissions for user '.$user['Username'].'
'; -if ( empty($_REQUEST['task']) ) { - $message = 'Must specify a task'; +if (empty($_REQUEST['task'])) { + $message = 'Must specify a task
'; } else { $task = $_REQUEST['task']; } -if ( empty($_REQUEST['eids']) ) { - if ( isset($_REQUEST['task']) && $_REQUEST['task'] != 'query' ) $message = 'No event id(s) supplied'; +if (empty($_REQUEST['eids'])) { + if (isset($_REQUEST['task']) && $_REQUEST['task'] != 'query') + $message = 'No event id(s) supplied
'; } else { $eids = $_REQUEST['eids']; } -if ( $message ) { +if ($message) { ajaxError($message); return; } require_once('includes/Filter.php'); $filter = isset($_REQUEST['filter']) ? ZM\Filter::parse($_REQUEST['filter']) : new ZM\Filter(); -if ( $user['MonitorIds'] ) { +if ($user['MonitorIds']) { $filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>$user['MonitorIds'])); } @@ -39,10 +41,19 @@ $search = isset($_REQUEST['search']) ? $_REQUEST['search'] : ''; $advsearch = isset($_REQUEST['advsearch']) ? json_decode($_REQUEST['advsearch'], JSON_OBJECT_AS_ARRAY) : array(); // Order specifies the sort direction, either asc or desc -$order = (isset($_REQUEST['order']) and (strtolower($_REQUEST['order']) == 'asc')) ? 'ASC' : 'DESC'; +$order = $filter->sort_asc() ? 'ASC' : 'DESC'; +if (isset($_REQUEST['order'])) { + if (strtolower($_REQUEST['order']) == 'asc') { + $order = 'ASC'; + } else if (strtolower($_REQUEST['order']) == 'desc') { + $order = 'DESC'; + } else { + Warning('Invalid value for order ' . $_REQUEST['order']); + } +} // Sort specifies the name of the column to sort on -$sort = 'StartDateTime'; +$sort = $filter->sort_field(); if (isset($_REQUEST['sort'])) { $sort = $_REQUEST['sort']; if ($sort == 'EndDateTime') { @@ -56,20 +67,19 @@ if (isset($_REQUEST['sort'])) { // Offset specifies the starting row to return, used for pagination $offset = 0; -if ( isset($_REQUEST['offset']) ) { - if ( ( !is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']) ) ) { +if (isset($_REQUEST['offset'])) { + if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) { ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']); } else { $offset = $_REQUEST['offset']; } } - // Limit specifies the number of rows to return // Set the default to 0 for events view, to prevent an issue with ALL pagination $limit = 0; -if ( isset($_REQUEST['limit']) ) { - if ( ( !is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']) ) ) { +if (isset($_REQUEST['limit'])) { + if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) { ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']); } else { $limit = $_REQUEST['limit']; @@ -80,25 +90,24 @@ if ( isset($_REQUEST['limit']) ) { // MAIN LOOP // -switch ( $task ) { +switch ($task) { case 'archive' : - foreach ( $eids as $eid ) archiveRequest($task, $eid); + foreach ($eids as $eid) archiveRequest($task, $eid); break; case 'unarchive' : # The idea is that anyone can archive, but only people with Event Edit permission can unarchive.. - if ( !canEdit('Events') ) { + if (!canEdit('Events')) { ajaxError('Insufficient permissions for user '.$user['Username']); return; } - foreach ( $eids as $eid ) archiveRequest($task, $eid); + foreach ($eids as $eid) archiveRequest($task, $eid); break; case 'delete' : - if ( !canEdit('Events') ) { + if (!canEdit('Events')) { ajaxError('Insufficient permissions for user '.$user['Username']); return; } - - foreach ( $eids as $eid ) $data[] = deleteRequest($eid); + foreach ($eids as $eid) $data[] = deleteRequest($eid); break; case 'query' : $data = queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $limit); @@ -128,6 +137,8 @@ function deleteRequest($eid) { $message[] = array($eid=>'Event not found.'); } else if ( $event->Archived() ) { $message[] = array($eid=>'Event is archived, cannot delete it.'); + } else if (!$event->canEdit()) { + $message[] = array($eid=>'You do not have permission to delete event '.$event->Id()); } else { $event->delete(); } @@ -136,7 +147,6 @@ function deleteRequest($eid) { } function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $limit) { - $data = array( 'total' => 0, 'totalNotFiltered' => 0, @@ -145,7 +155,7 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim ); $failed = !$filter->test_pre_sql_conditions(); - if ( $failed ) { + if ($failed) { ZM\Debug('Pre conditions failed, not doing sql'); return $data; } @@ -160,22 +170,27 @@ 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:''); $storage_areas = ZM\Storage::find(); $StorageById = array(); - foreach ( $storage_areas as $S ) { + foreach ($storage_areas as $S) { $StorageById[$S->Id()] = $S; } @@ -184,41 +199,43 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim ZM\Debug('Calling the following sql query: ' .$sql); $query = dbQuery($sql, $values); - if ( $query ) { - while ( $row = dbFetchNext($query) ) { - $event = new ZM\Event($row); - $event->remove_from_cache(); - if ( !$filter->test_post_sql_conditions($event) ) { - continue; - } - $event_ids[] = $event->Id(); - $unfiltered_rows[] = $row; - } # end foreach row + if (!$query) { + ajaxError(dbError($sql)); + return; } + while ($row = dbFetchNext($query)) { + $event = new ZM\Event($row); + $event->remove_from_cache(); + if (!$filter->test_post_sql_conditions($event)) { + continue; + } + $event_ids[] = $event->Id(); + $unfiltered_rows[] = $row; + } # end foreach row ZM\Debug('Have ' . count($unfiltered_rows) . ' events matching base filter.'); $filtered_rows = null; - if ( count($advsearch) or $search != '' ) { + if (count($advsearch) or $search != '') { $search_filter = new ZM\Filter(); $search_filter = $search_filter->addTerm(array('cnj'=>'and', 'attr'=>'Id', 'op'=>'IN', 'val'=>$event_ids)); // There are two search bars in the log view, normal and advanced // Making an exuctive decision to ignore the normal search, when advanced search is in use // Alternatively we could try to do both - if ( count($advsearch) ) { + if (count($advsearch)) { $terms = array(); - foreach ( $advsearch as $col=>$text ) { + foreach ($advsearch as $col=>$text) { $terms[] = array('cnj'=>'and', 'attr'=>$col, 'op'=>'LIKE', 'val'=>$text); } # end foreach col in advsearch $terms[0]['obr'] = 1; $terms[count($terms)-1]['cbr'] = 1; $search_filter->addTerms($terms); - } else if ( $search != '' ) { + } else if ($search != '') { $search = '%' .$search. '%'; $terms = array(); - foreach ( $columns as $col ) { + foreach ($columns as $col) { $terms[] = array('cnj'=>'or', 'attr'=>$col, 'op'=>'LIKE', 'val'=>$search); } $terms[0]['obr'] = 1; @@ -228,15 +245,17 @@ function queryRequest($filter, $search, $advsearch, $sort, $offset, $order, $lim } # end if search $sql = 'SELECT ' .$col_str. ' FROM `Events` AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE '.$search_filter->sql().' ORDER BY ' .$sort. ' ' .$order; - ZM\Debug('Calling the following sql query: ' .$sql); $filtered_rows = dbFetchAll($sql); - ZM\Debug('Have ' . count($filtered_rows) . ' events matching search filter.'); + ZM\Debug('Have ' . count($filtered_rows) . ' events matching search filter: '.$sql); } else { $filtered_rows = $unfiltered_rows; } # end if search_filter->terms() > 1 + if ($limit) + $filtered_rows = array_slice($filtered_rows, $offset, $limit); + $returned_rows = array(); - foreach ( array_slice($filtered_rows, $offset, $limit) as $row ) { + foreach ($filtered_rows as $row) { $event = new ZM\Event($row); $scale = intval(5*100*ZM_WEB_LIST_THUMB_WIDTH / $event->Width()); diff --git a/web/ajax/modals/filterdebug.php b/web/ajax/modals/filterdebug.php index 725539a99..f3a961874 100644 --- a/web/ajax/modals/filterdebug.php +++ b/web/ajax/modals/filterdebug.php @@ -9,13 +9,13 @@ '; } else { $filter = new ZM\Filter($_REQUEST['fid']); - if ( ! $filter->Id() ) { + if (!$filter->Id()) { echo '
Filter not found for id '.$_REQUEST['fid'].'
'; } } @@ -25,7 +25,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/settings.php b/web/ajax/modals/settings.php index de42d6c87..e0bdf679c 100644 --- a/web/ajax/modals/settings.php +++ b/web/ajax/modals/settings.php @@ -38,7 +38,7 @@ if ($zmuOutput) { $ctls = shell_exec('v4l2-ctl -d '.$monitor->Device().' --list-ctrls'); if (!$ctls) { -ZM\Warning("Guessing v4l ctrls. We need v4l2-ctl please install it"); +ZM\Warning('Guessing v4l ctrls. We need v4l2-ctl please install it'); $ctls = ' brightness 0x00980900 (int) : min=-10 max=10 step=1 default=0 value=8 contrast 0x00980901 (int) : min=0 max=20 step=1 default=10 value=12 @@ -83,10 +83,15 @@ foreach ($ctls as $line) { } } + $label = translate($setting_uc); + if ($label == $setting_uc) { + $label = ucwords(str_replace('_', ' ', $label)); + } + if ($setting == 'brightness' or $setting == 'colour' or $setting == 'contrast' or $setting == 'hue') { echo ' - '.translate($setting_uc).' + '.$label.' '.$min.''.$max.' '; @@ -94,7 +99,7 @@ foreach ($ctls as $line) { if ($type == '(bool)') { echo ' - '.translate($setting_uc).' + '.$label.' '.html_radio('new'.$setting_uc, array('0'=>translate('True'), '1', translate('False')), $value, array('disabled'=>'disabled')).' @@ -102,14 +107,14 @@ foreach ($ctls as $line) { } else if ($type == '(int)') { echo ' - '.translate($setting_uc).' + '.$label.' '; } else { echo ' - '.translate($setting_uc).' + '.$label.' '.$value.' '; diff --git a/web/ajax/models.php b/web/ajax/models.php new file mode 100644 index 000000000..3a4f54c5b --- /dev/null +++ b/web/ajax/models.php @@ -0,0 +1,24 @@ +$_REQUEST['ManufacturerId']), array('order'=>'lower(Name)')); +ajaxResponse(array('models'=>$models)); +?> diff --git a/web/ajax/stats.php b/web/ajax/stats.php index 06ce9d733..9226814b4 100644 --- a/web/ajax/stats.php +++ b/web/ajax/stats.php @@ -1,7 +1,6 @@ 1 ) { - $stat['BlobSizes'] = sprintf( "%d-%d (%d%%-%d%%)", $stat['MinBlobSize'], $stat['MaxBlobSize'], (100*$stat['MinBlobSize']/$stat['Area']), (100*$stat['MaxBlobSize']/$stat['Area']) ); + if ($stat['Blobs'] > 1) { + $stat['BlobSizes'] = sprintf('%d-%d (%d%%-%d%%)', $stat['MinBlobSize'], $stat['MaxBlobSize'], (100*$stat['MinBlobSize']/$stat['Area']), (100*$stat['MaxBlobSize']/$stat['Area'])); } else { - $stat['BlobSizes'] = sprintf( "%d (%d%%)", $stat['MinBlobSize'], 100*$stat['MinBlobSize']/$stat['Area'] ); + $stat['BlobSizes'] = sprintf('%d (%d%%)', $stat['MinBlobSize'], 100*$stat['MinBlobSize']/$stat['Area']); } - $stat['AlarmLimits'] = validHtmlStr($stat['MinX'].",".$stat['MinY']."-".$stat['MaxX'].",".$stat['MaxY']); - } - $data['raw'] = $stat; + $stat['AlarmLimits'] = validHtmlStr($stat['MinX'].','.$stat['MinY'].'-'.$stat['MaxX'].','.$stat['MaxY']); + $data['raw'][] = $stat; + } # end foreach stat/zone } else { $data['html'] = getStatsTableHTML($eid, $fid, $row); $data['id'] = '#contentStatsTable' .$row; diff --git a/web/ajax/stream.php b/web/ajax/stream.php index 5c8c07312..ade101845 100644 --- a/web/ajax/stream.php +++ b/web/ajax/stream.php @@ -1,5 +1,5 @@ $data)); break; default : - ajaxError("Unexpected received message type '$type'"); + ajaxError('Unexpected received message type '.$data['type']); } sem_release($semaphore); } else { - ZM\Debug('Couldn\'t get semaphore'); - ajaxResponse(array()); + ajaxError("Unable to get semaphore."); } ajaxError('Unrecognised action or insufficient permissions in ajax/stream'); diff --git a/web/api/app/Config/routes.php b/web/api/app/Config/routes.php index 8dd2c1f63..7c6352602 100644 --- a/web/api/app/Config/routes.php +++ b/web/api/app/Config/routes.php @@ -23,8 +23,6 @@ /** * Load the API / REST routes */ - /* Add new API to retrieve camera controls - for PTZ */ - /* refer to https://github.com/ZoneMinder/ZoneMinder/issues/799#issuecomment-105233112 */ Router::mapResources('configs'); Router::mapResources('controls'); Router::mapResources('events'); @@ -32,7 +30,11 @@ Router::mapResources('groups'); Router::mapResources('host'); Router::mapResources('logs'); + Router::mapResources('manufacturers'); + Router::mapResources('models'); + Router::mapResources('cameramodels'); Router::mapResources('monitors'); + Router::mapResources('servers'); Router::mapResources('states'); Router::mapResources('users'); Router::mapResources('zonepresets'); diff --git a/web/api/app/Controller/CameraModelsController.php b/web/api/app/Controller/CameraModelsController.php new file mode 100644 index 000000000..691fd0767 --- /dev/null +++ b/web/api/app/Controller/CameraModelsController.php @@ -0,0 +1,156 @@ +CameraModel->recursive = 0; + + $options = ''; + $models = $this->CameraModel->find('all', $options); + $this->set(array( + 'models' => $models, + '_serialize' => array('models') + )); + } + +/** + * view method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function view($id = null) { + $this->CameraModel->recursive = 0; + if ( !$this->CameraModel->exists($id) ) { + throw new NotFoundException(__('Invalid model')); + } + $restricted = ''; + + $options = array('conditions' => array( + array('CameraModel.'.$this->CameraModel->primaryKey => $id), + $restricted + ) + ); + $model = $this->CameraModel->find('first', $options); + $this->set(array( + 'model' => $model, + '_serialize' => array('model') + )); + } + +/** + * add method + * + * @return void + */ + public function add() { + if ($this->request->is('post')) { + + global $user; + $canEdit = (!$user) || ($user['System'] == 'Edit'); + if (!$canEdit) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + + $this->CameraModel->create(); + if ($this->CameraModel->save($this->request->data)) { + return $this->flash(__('The model has been saved.'), array('action' => 'index')); + } + } + } + +/** + * edit method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function edit($id = null) { + $this->CameraModel->id = $id; + + global $user; + $canEdit = (!$user) || ($user['System'] == 'Edit'); + if (!$canEdit) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + + if (!$this->CameraModel->exists($id)) { + throw new NotFoundException(__('Invalid model')); + } + if ($this->CameraModel->save($this->request->data)) { + $message = 'Saved'; + } else { + $message = 'Error'; + } + + $this->set(array( + 'message' => $message, + '_serialize' => array('message') + )); + } + +/** + * delete method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function delete($id = null) { + global $user; + $canEdit = (!$user) || ($user['System'] == 'Edit'); + if (!$canEdit) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + + $this->CameraModel->id = $id; + if (!$this->CameraModel->exists()) { + throw new NotFoundException(__('Invalid model')); + } + $this->request->allowMethod('post', 'delete'); + + if ($this->CameraModel->delete()) { + return $this->flash(__('The model has been deleted.'), array('action' => 'index')); + } else { + return $this->flash(__('The model could not be deleted. Please, try again.'), array('action' => 'index')); + } + } +} diff --git a/web/api/app/Controller/ManufacturersController.php b/web/api/app/Controller/ManufacturersController.php new file mode 100644 index 000000000..0249af6af --- /dev/null +++ b/web/api/app/Controller/ManufacturersController.php @@ -0,0 +1,162 @@ +Manufacturer->recursive = 0; + + $options = ''; + $manufacturers = $this->Manufacturer->find('all', $options); + $this->set(array( + 'manufacturers' => $manufacturers, + '_serialize' => array('manufacturers') + )); + } + +/** + * view method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function view($id = null) { + $this->Manufacturer->recursive = 0; + if ( !$this->Manufacturer->exists($id) ) { + throw new NotFoundException(__('Invalid manufacturer')); + } + $restricted = ''; + + $options = array('conditions' => array( + array('Manufacturer.'.$this->Manufacturer->primaryKey => $id), + $restricted + ) + ); + $manufacturer = $this->Manufacturer->find('first', $options); + $this->set(array( + 'manufacturer' => $manufacturer, + '_serialize' => array('manufacturer') + )); + } + +/** + * add method + * + * @return void + */ + public function add() { + if ( $this->request->is('post') ) { + + global $user; + $canEdit = (!$user) || ($user['System'] == 'Edit'); + if ( !$canEdit ) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + + $this->Manufacturer->create(); + if ( $this->Manufacturer->save($this->request->data) ) { + # Might be nice to send it a start request + #$this->daemonControl($this->Manufacturer->id, 'start', $this->request->data); + return $this->flash(__('The manufacturer has been saved.'), array('action' => 'index')); + } + } + } + +/** + * edit method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function edit($id = null) { + $this->Manufacturer->id = $id; + + global $user; + $canEdit = (!$user) || ($user['System'] == 'Edit'); + if ( !$canEdit ) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + + if ( !$this->Manufacturer->exists($id) ) { + throw new NotFoundException(__('Invalid manufacturer')); + } + if ( $this->Manufacturer->save($this->request->data) ) { + $message = 'Saved'; + } else { + $message = 'Error'; + } + + $this->set(array( + 'message' => $message, + '_serialize' => array('message') + )); + // - restart this manufacturer after change + #$this->daemonControl($this->Manufacturer->id, 'restart', $this->request->data); + } + +/** + * delete method + * + * @throws NotFoundException + * @param string $id + * @return void + */ + public function delete($id = null) { + global $user; + $canEdit = (!$user) || ($user['System'] == 'Edit'); + if ( !$canEdit ) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + + $this->Manufacturer->id = $id; + if ( !$this->Manufacturer->exists() ) { + throw new NotFoundException(__('Invalid manufacturer')); + } + $this->request->allowMethod('post', 'delete'); + + #$this->daemonControl($this->Manufacturer->id, 'stop'); + + if ( $this->Manufacturer->delete() ) { + return $this->flash(__('The manufacturer has been deleted.'), array('action' => 'index')); + } else { + return $this->flash(__('The manufacturer could not be deleted. Please, try again.'), array('action' => 'index')); + } + } +} diff --git a/web/api/app/Controller/MonitorsController.php b/web/api/app/Controller/MonitorsController.php index 07098623e..193e8a61a 100644 --- a/web/api/app/Controller/MonitorsController.php +++ b/web/api/app/Controller/MonitorsController.php @@ -321,12 +321,20 @@ class MonitorsController extends AppController { } $monitor = $this->Monitor->find('first', array( - 'fields' => array('Id', 'Type', 'Device'), + 'fields' => array('Id', 'Type', 'Device', 'Function'), 'conditions' => array('Id' => $id) )); // Clean up the returned array $monitor = Set::extract('/Monitor/.', $monitor); + if ($monitor[0]['Function'] == 'None') { + $this->set(array( + 'status' => false, + 'statustext' => 'Monitor function is set to None', + '_serialize' => array('status','statustext'), + )); + return; + } // Pass -d for local, otherwise -m if ( $monitor[0]['Type'] == 'Local' ) { diff --git a/web/api/app/Controller/ServersController.php b/web/api/app/Controller/ServersController.php index c3ac6fad7..bbde27fae 100644 --- a/web/api/app/Controller/ServersController.php +++ b/web/api/app/Controller/ServersController.php @@ -89,8 +89,6 @@ class ServersController extends AppController { $this->Server->create(); if ( $this->Server->save($this->request->data) ) { - # Might be nice to send it a start request - #$this->daemonControl($this->Server->id, 'start', $this->request->data); return $this->flash(__('The server has been saved.'), array('action' => 'index')); } } @@ -126,8 +124,6 @@ class ServersController extends AppController { 'message' => $message, '_serialize' => array('message') )); - // - restart this server after change - #$this->daemonControl($this->Server->id, 'restart', $this->request->data); } /** @@ -151,8 +147,6 @@ class ServersController extends AppController { } $this->request->allowMethod('post', 'delete'); - #$this->daemonControl($this->Server->id, 'stop'); - if ( $this->Server->delete() ) { return $this->flash(__('The server has been deleted.'), array('action' => 'index')); } else { diff --git a/web/api/app/Model/CameraModel.php b/web/api/app/Model/CameraModel.php new file mode 100644 index 000000000..e953b5db8 --- /dev/null +++ b/web/api/app/Model/CameraModel.php @@ -0,0 +1,70 @@ + array( + 'notBlank' => array( + 'rule' => array('notBlank'))), + 'Id' => array( + 'numeric' => array( + 'rule' => array('numeric'), + //'message' => 'Your custom message here', + //'allowEmpty' => false, + //'required' => false, + //'last' => false, // Stop validation after this rule + //'on' => 'create', // Limit validation to 'create' or 'update' operations + ), + ), + ); + + //The Associations below have been created with all possible keys, those that are not needed can be removed + +/** + * hasMany associations + * + * @var array + */ + public $hasOne = array( + 'Manufacturer' => array( + 'className' => 'Manufacturer', + 'joinTable' => 'Manufacturers', + 'foreignKey' => 'Id', + ), + ); + //var $actsAs = array( 'Containable' ); +} diff --git a/web/api/app/Model/Manufacturer.php b/web/api/app/Model/Manufacturer.php new file mode 100644 index 000000000..34a6c88c0 --- /dev/null +++ b/web/api/app/Model/Manufacturer.php @@ -0,0 +1,77 @@ + array( + 'numeric' => array( + 'rule' => array('numeric'), + //'message' => 'Your custom message here', + //'allowEmpty' => false, + //'required' => false, + //'last' => false, // Stop validation after this rule + //'on' => 'create', // Limit validation to 'create' or 'update' operations + ), + ), + 'Name' => array( + 'notBlank' => array( + 'rule' => array('notBlank'))), + ); + + //The Associations below have been created with all possible keys, those that are not needed can be removed + +/** + * hasMany associations + * + * @var array + */ + public $hasMany = array( + 'Model' => array( + 'className' => 'Model', + 'foreignKey' => 'ManufacturerId', + 'dependent' => false, + 'conditions' => '', + 'fields' => '', + 'order' => '', + 'limit' => '', + 'offset' => '', + 'exclusive' => '', + 'finderQuery' => '', + 'counterQuery' => '' + ) + ); +} diff --git a/web/api/app/Model/Monitor.php b/web/api/app/Model/Monitor.php index de9a7685a..7a7d03588 100644 --- a/web/api/app/Model/Monitor.php +++ b/web/api/app/Model/Monitor.php @@ -139,6 +139,16 @@ class Monitor extends AppModel { 'className' => 'Event_Summary', 'foreignKey' => 'MonitorId', 'joinTable' => 'Event_Summaries', + ), + 'Manufacturer' => array( + 'className' => 'Manufacturer', + 'foreignKey' => 'Id', + 'joinTable' => 'Manufacturers', + ), + 'CameraModel' => array( + 'className' => 'CameraModel', + 'foreignKey' => 'Id', + 'joinTable' => 'Models', ) ); diff --git a/web/api/app/View/Manufacturers/json/edit.ctp b/web/api/app/View/Manufacturers/json/edit.ctp new file mode 100644 index 000000000..a32d9cfad --- /dev/null +++ b/web/api/app/View/Manufacturers/json/edit.ctp @@ -0,0 +1,2 @@ +echo json_encode($message); +echo json_encode($manufacturer); diff --git a/web/api/app/View/Manufacturers/json/index.ctp b/web/api/app/View/Manufacturers/json/index.ctp new file mode 100644 index 000000000..4c54bdf95 --- /dev/null +++ b/web/api/app/View/Manufacturers/json/index.ctp @@ -0,0 +1 @@ +echo json_encode($manufacturers); diff --git a/web/api/app/View/Manufacturers/json/view.ctp b/web/api/app/View/Manufacturers/json/view.ctp new file mode 100644 index 000000000..4e0a14ae6 --- /dev/null +++ b/web/api/app/View/Manufacturers/json/view.ctp @@ -0,0 +1 @@ +echo json_encode($manufacturer); diff --git a/web/api/app/View/Manufacturers/xml/edit.ctp b/web/api/app/View/Manufacturers/xml/edit.ctp new file mode 100644 index 000000000..09fb8979a --- /dev/null +++ b/web/api/app/View/Manufacturers/xml/edit.ctp @@ -0,0 +1,2 @@ +$xml = Xml::fromArray(array('response' => $message)); +echo $xml->asXML(); diff --git a/web/api/app/View/Manufacturers/xml/index.ctp b/web/api/app/View/Manufacturers/xml/index.ctp new file mode 100644 index 000000000..9bb514fff --- /dev/null +++ b/web/api/app/View/Manufacturers/xml/index.ctp @@ -0,0 +1,2 @@ +$xml = Xml::fromArray(array('response' => $servers)); +echo $xml->asXML(); diff --git a/web/api/app/View/Manufacturers/xml/view.ctp b/web/api/app/View/Manufacturers/xml/view.ctp new file mode 100644 index 000000000..3b2a3fdad --- /dev/null +++ b/web/api/app/View/Manufacturers/xml/view.ctp @@ -0,0 +1,2 @@ +$xml = Xml::fromArray(array('response' => $server)); +echo $xml->asXML(); diff --git a/web/fonts/license.md b/web/fonts/license.md index 197f20b13..b56a5654f 100644 --- a/web/fonts/license.md +++ b/web/fonts/license.md @@ -1,6 +1,12 @@ ZoneMinder uses certain 3rd party media assets/libraries for UI display purposes. Their licenses are listed in this file +### Font Awesome icons + +Origin: http://fontawesome.io + +License: Font: SIL OFL 1.1, CSS: MIT License (http://fontawesome.io/license) + ### Material Design icons Origin: https://github.com/google/material-design-icons diff --git a/web/includes/Event.php b/web/includes/Event.php index 328b06f66..e042849e9 100644 --- a/web/includes/Event.php +++ b/web/includes/Event.php @@ -650,6 +650,23 @@ class Event extends ZM_Object { } return false; } + function canEdit($u=null) { + global $user; + if (!$u) $u=$user; + if (!$u) { + # auth turned on and not logged in + return false; + } + if (!empty($u['MonitorIds']) ) { + if (!in_array($this->{'MonitorId'}, explode(',', $u['MonitorIds']))) { + return false; + } + } + if ($u['Events'] != 'Edit') { + return false; + } + return true; + } } # end class ?> diff --git a/web/includes/Filter.php b/web/includes/Filter.php index 3578a1d62..4fdd8e43f 100644 --- a/web/includes/Filter.php +++ b/web/includes/Filter.php @@ -9,6 +9,8 @@ class Filter extends ZM_Object { protected $defaults = array( 'Id' => null, 'Name' => '', + 'UserId' => 0, + 'ExecuteInterval' => 60, 'AutoExecute' => 0, 'AutoExecuteCmd' => '', 'AutoEmail' => 0, @@ -26,7 +28,6 @@ class Filter extends ZM_Object { 'AutoCopy' => 0, 'AutoCopyTo' => 0, 'UpdateDiskSpace' => 0, - 'UserId' => 0, 'Background' => 0, 'Concurrent' => 0, 'Query_json' => '', @@ -60,6 +61,10 @@ class Filter extends ZM_Object { foreach ( $this->FilterTerms() as $term ) { $this->_querystring .= $term->querystring($objectname, $separator); } # end foreach term + $this->_querystring .= $separator.urlencode($objectname.'[Query][sort_asc]').'='.$this->sort_asc(); + $this->_querystring .= $separator.urlencode($objectname.'[Query][sort_field]').'='.$this->sort_field(); + $this->_querystring .= $separator.urlencode($objectname.'[Query][skip_locked]').'='.$this->skip_locked(); + $this->_querystring .= $separator.urlencode($objectname.'[Query][limit]').'='.$this->limit(); if ( $this->Id() ) { $this->_querystring .= $separator.$objectname.urlencode('[Id]').'='.$this->Id(); } @@ -204,18 +209,29 @@ class Filter extends ZM_Object { } public function sort_asc( ) { - if ( func_num_args( ) ) { + if (func_num_args()) { $Query = $this->Query(); $Query['sort_asc'] = func_get_arg(0); $this->Query($Query); } - if ( isset( $this->Query()['sort_asc'] ) ) { + if (isset($this->Query()['sort_asc'])) { return $this->{'Query'}['sort_asc']; } return ZM_WEB_EVENT_SORT_ORDER == 'asc' ? 1 : 0; #return $this->defaults{'sort_asc'}; } + public function skip_locked() { + if (func_num_args()) { + $Query = $this->Query(); + $Query['skip_locked'] = func_get_arg(0); + $this->Query($Query); + } + if (isset($this->Query()['skip_locked'])) + return $this->{'Query'}['skip_locked']; + return false; + } + public function limit( ) { if ( func_num_args( ) ) { $Query = $this->Query(); diff --git a/web/includes/Group.php b/web/includes/Group.php index 026d1f7f4..34db0e869 100644 --- a/web/includes/Group.php +++ b/web/includes/Group.php @@ -167,9 +167,15 @@ class Group extends ZM_Object { public function Parents() { $Parents = array(); $Parent = $this->Parent(); - while( $Parent ) { + $seen_parents = array(); + while ($Parent) { + $seen_parents[$Parent->Id()] = $Parent; array_unshift($Parents, $Parent); $Parent = $Parent->Parent(); + if ($Parent and isset($seen_parents[$Parent->Id()])) { + Warning("Detected hierarchy loop in group {$Parent->Name()}"); + break; + } } return $Parents; } @@ -189,6 +195,9 @@ class Group extends ZM_Object { public function canView($u=null) { global $user; if (!$u) $u = $user; + if (!count($this->Monitors()) and !count($this->Children())) { + return true; + } # Can view if we can view any of the monitors in it. foreach ($this->Monitors() as $monitor) { if ($monitor->canView($u)) return true; diff --git a/web/includes/Manufacturer.php b/web/includes/Manufacturer.php new file mode 100644 index 000000000..73c46ad6b --- /dev/null +++ b/web/includes/Manufacturer.php @@ -0,0 +1,23 @@ + null, + 'Name' => '', + ); + + public static function find( $parameters = array(), $options = array() ) { + return ZM_Object::_find(get_class(), $parameters, $options); + } + + public static function find_one( $parameters = array(), $options = array() ) { + return ZM_Object::_find_one(get_class(), $parameters, $options); + } +} # end class Manufacturer +?> diff --git a/web/includes/Model.php b/web/includes/Model.php new file mode 100644 index 000000000..35562f9f9 --- /dev/null +++ b/web/includes/Model.php @@ -0,0 +1,23 @@ + null, + 'Name' => '', + 'ManufacturerId' => null, + ); + + public static function find( $parameters = array(), $options = array() ) { + return ZM_Object::_find(get_class(), $parameters, $options); + } + + public static function find_one( $parameters = array(), $options = array() ) { + return ZM_Object::_find_one(get_class(), $parameters, $options); + } +} # end class Model +?> diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 1a3b1afdc..11f34eb28 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -1,11 +1,13 @@ '', 'ServerId' => 0, 'StorageId' => 0, + 'ManufacturerId' => null, + 'ModelId' => null, 'Type' => 'Ffmpeg', 'Function' => 'Mocord', 'Capturing' => 'Always', @@ -114,6 +118,8 @@ class Monitor extends ZM_Object { 'DecodingEnabled' => array('type'=>'boolean','default'=>1), 'LinkedMonitors' => array('type'=>'set', 'default'=>null), 'Triggers' => array('type'=>'set','default'=>''), + 'EventStartCommand' => '', + 'EventEndCommand' => '', 'ONVIF_URL' => '', 'ONVIF_Username' => '', 'ONVIF_Password' => '', @@ -557,6 +563,10 @@ class Monitor extends ZM_Object { return $this->Server()->UrlToIndex($port); } + public function UrlToZMS($port=null) { + return $this->Server()->UrlToZMS($port).'?mid='.$this->Id(); + } + public function sendControlCommand($command) { // command is generally a command option list like --command=blah but might be just the word quit @@ -744,5 +754,29 @@ class Monitor extends ZM_Object { function DisableAlarms() { $output = $this->AlarmCommand('disable'); } + function Model() { + if (!property_exists($this, 'Model')) { + if ($this->{'ModelId'}) { + $this->{'Model'} = Model::find_one(array('Id'=>$this->ModelId())); + if (!$this->{'Model'}) + $this->{'Model'} = new Model(); + } else { + $this->{'Model'} = new Model(); + } + } + return $this->{'Model'}; + } + function Manufacturer() { + if (!property_exists($this, 'Manufacturer')) { + if ($this->{'ManufacturerId'}) { + $this->{'Manufacturer'} = Manufacturer::find_one(array('Id'=>$this->ManufacturerId())); + if (!$this->{'Manufacturer'}) + $this->{'Manufacturer'} = new Manufacturer(); + } else { + $this->{'Manufacturer'} = new Manufacturer(); + } + } + return $this->{'Manufacturer'}; + } } // end class Monitor ?> diff --git a/web/includes/Object.php b/web/includes/Object.php index 96a572c01..1a9011586 100644 --- a/web/includes/Object.php +++ b/web/includes/Object.php @@ -28,7 +28,7 @@ class ZM_Object { $this->{$k} = $v; } global $object_cache; - if ( ! isset($object_cache[$class]) ) { + if (!isset($object_cache[$class])) { $object_cache[$class] = array(); } $cache = &$object_cache[$class]; @@ -103,13 +103,13 @@ class ZM_Object { } $sql .= implode(' AND ', $fields ); } - if ( $options ) { - if ( isset($options['order']) ) { - $sql .= ' ORDER BY ' . $options['order']; + if ($options) { + if (isset($options['order'])) { + $sql .= ' ORDER BY '.$options['order']; } - if ( isset($options['limit']) ) { - if ( is_integer($options['limit']) or ctype_digit($options['limit']) ) { - $sql .= ' LIMIT ' . $options['limit']; + if (isset($options['limit'])) { + if (is_integer($options['limit']) or ctype_digit($options['limit'])) { + $sql .= ' LIMIT '.$options['limit']; } else { $backTrace = debug_backtrace(); Error('Invalid value for limit('.$options['limit'].') passed to '.get_class()."::find from ".print_r($backTrace,true)); @@ -119,8 +119,8 @@ class ZM_Object { } $rows = dbFetchAll($sql, NULL, $values); $results = array(); - if ( $rows ) { - foreach ( $rows as $row ) { + if ($rows) { + foreach ($rows as $row) { array_push($results , new $class($row)); } } @@ -129,7 +129,7 @@ class ZM_Object { public static function _find_one($class, $parameters = array(), $options = array() ) { global $object_cache; - if ( ! isset($object_cache[$class]) ) { + if (!isset($object_cache[$class])) { $object_cache[$class] = array(); } $cache = &$object_cache[$class]; @@ -179,11 +179,11 @@ class ZM_Object { } public function set($data) { - foreach ( $data as $field => $value ) { - if ( method_exists($this, $field) and is_callable(array($this, $field), false) ) { + foreach ($data as $field => $value) { + if (method_exists($this, $field) and is_callable(array($this, $field), false)) { $this->$field($value); } else { - if ( is_array($value) ) { + if (is_array($value)) { # perhaps should turn into a comma-separated string $this->{$field} = implode(',', $value); } else if (is_string($value)) { @@ -212,11 +212,11 @@ class ZM_Object { } else { $this->{$field} = $value; } - } else if ( is_integer($value) ) { + } else if (is_integer($value)) { $this->{$field} = $value; - } else if ( is_bool($value) ) { + } else if (is_bool($value)) { $this->{$field} = $value; - } else if ( is_null($value) ) { + } else if (is_null($value)) { $this->{$field} = $value; } else { Error("Unknown type $field => $value of var " . gettype($value)); @@ -307,7 +307,7 @@ class ZM_Object { $class = get_class($this); $table = $class::$table; - if ( $new_values ) { + if ($new_values) { $this->set($new_values); } diff --git a/web/includes/Zone.php b/web/includes/Zone.php index 9f3850bb1..eb216565a 100644 --- a/web/includes/Zone.php +++ b/web/includes/Zone.php @@ -13,6 +13,10 @@ class Zone extends ZM_Object { 'Name' => '', 'Type' => 'Active', 'Units' => 'Pixels', + 'NumCoords' => '0', + 'Coords' => 0, + 'Area' => '0', + 'AlarmRGB' => '0', 'CheckMethod' => 'Blobs', 'MinPixelThreshold' => null, 'MaxPixelThreshold' => null, @@ -46,5 +50,17 @@ class Zone extends ZM_Object { return new Monitor(); } + public function Points() { + return coordsToPoints($this->Coords()); + } + + public function AreaCoords() { + return preg_replace('/\s+/', ',', $this->Coords()); + } + + public function svg_polygon() { + return ''; + } + } # end class Zone ?> diff --git a/web/includes/actions/monitor.php b/web/includes/actions/monitor.php index ee041967c..2d900433a 100644 --- a/web/includes/actions/monitor.php +++ b/web/includes/actions/monitor.php @@ -19,33 +19,66 @@ // // Monitor edit actions, monitor id derived, require edit permissions for that monitor -if ( !canEdit('Monitors') ) { +if (!canEdit('Monitors')) { ZM\Warning('Monitor actions require Monitors Permissions'); return; } -if ( $action == 'save' ) { +global $error_message; + +if ($action == 'save') { $mid = 0; - if ( !empty($_REQUEST['mid']) ) { + if (!empty($_REQUEST['mid'])) { $mid = validInt($_REQUEST['mid']); - if ( !canEdit('Monitors', $mid) ) { + if (!canEdit('Monitors', $mid)) { ZM\Warning('You do not have permission to edit this monitor'); return; } - if ( ZM_OPT_X10 ) { + if (ZM_OPT_X10) { $x10Monitor = dbFetchOne('SELECT * FROM TriggersX10 WHERE MonitorId=?', NULL, array($mid)); - if ( !$x10Monitor ) $x10Monitor = array(); + if (!$x10Monitor) $x10Monitor = array(); } } else { - if ( $user['MonitorIds'] ) { + if ($user['MonitorIds']) { ZM\Warning('You are restricted to certain monitors so cannot add a new one.'); return; } - if ( ZM_OPT_X10 ) { + if (ZM_OPT_X10) { $x10Monitor = array(); } } + # For convenience + $newMonitor = $_REQUEST['newMonitor']; + ZM\Debug("newMonitor: ". print_r($newMonitor, true)); + + if (!$newMonitor['ManufacturerId'] and ($newMonitor['Manufacturer'] != '')) { + # Need to add a new Manufacturer entry + $newManufacturer = ZM\Manufacturer::find_one(array('Name'=>$newMonitor['Manufacturer'])); + if (!$newManufacturer) { + $newManufacturer = new ZM\Manufacturer(); + if (!$newManufacturer->save(array('Name'=>$newMonitor['Manufacturer']))) { + $error_message .= "Error saving new Manufacturer: " . $newManufacturer->get_last_error().'
'; + } + } + $newMonitor['ManufacturerId'] = $newManufacturer->Id(); + } + + if (!$newMonitor['ModelId'] and ($newMonitor['Model'] != '')) { + # Need to add a new Model entry + $newModel = ZM\Model::find_one(array('Name'=>$newMonitor['Model'])); + if (!$newModel) { + $newModel = new ZM\Model(); + if (!$newModel->save(array( + 'Name'=>$newMonitor['Model'], + 'ManufacturerId'=>$newMonitor['ManufacturerId'] + ))) { + $error_message .= "Error saving new Model: " . $newModel->get_last_error().'
'; + } + } + $newMonitor['ModelId'] = $newModel->Id(); + } + $monitor = new ZM\Monitor($mid); // Define a field type for anything that's not simple text equivalent @@ -68,33 +101,35 @@ if ( $action == 'save' ) { # Checkboxes don't return an element in the POST data, so won't be present in newMonitor. # So force a value for these fields - foreach ( $types as $field => $value ) { - if ( ! isset($_REQUEST['newMonitor'][$field] ) ) { - $_REQUEST['newMonitor'][$field] = $value; + foreach ($types as $field => $value) { + if (!isset($newMonitor[$field])) { + $newMonitor[$field] = $value; } } # end foreach type - if ( $_REQUEST['newMonitor']['ServerId'] == 'auto' ) { - $_REQUEST['newMonitor']['ServerId'] = dbFetchOne( + if ($newMonitor['ServerId'] == 'auto') { + $newMonitor['ServerId'] = dbFetchOne( 'SELECT Id FROM Servers WHERE Status=\'Running\' ORDER BY FreeMem DESC, CpuLoad ASC LIMIT 1', 'Id'); - ZM\Debug('Auto selecting server: Got ' . $_REQUEST['newMonitor']['ServerId']); - if ( ( !$_REQUEST['newMonitor'] ) and defined('ZM_SERVER_ID') ) { - $_REQUEST['newMonitor']['ServerId'] = ZM_SERVER_ID; + ZM\Debug('Auto selecting server: Got ' . $newMonitor['ServerId']); + if ((!$newMonitor['ServerId']) and defined('ZM_SERVER_ID')) { + $newMonitor['ServerId'] = ZM_SERVER_ID; ZM\Debug('Auto selecting server to ' . ZM_SERVER_ID); } } - $changes = $monitor->changes($_REQUEST['newMonitor']); + ZM\Debug("newMonitor: ". print_r($newMonitor, true)); + $changes = $monitor->changes($newMonitor); + ZM\Debug("Changes: ". print_r($changes, true)); $restart = false; - if ( count($changes) ) { + if (count($changes)) { // monitor->Id() has a value when the db record exists - if ( $monitor->Id() ) { + if ($monitor->Id()) { # If we change anything that changes the shared mem size, zma can complain. So let's stop first. - if ( $monitor->Type() != 'WebSite' ) { + if ($monitor->Type() != 'WebSite') { $monitor->zmcControl('stop'); - if ( $monitor->Controllable() ) { + if ($monitor->Controllable()) { $monitor->sendControlCommand('stop'); } } @@ -103,8 +138,7 @@ if ( $action == 'save' ) { $oldW = $monitor->Width(); $oldH = $monitor->Height(); - if ( $monitor->save($changes) ) { - + if ($monitor->save($changes)) { // Groups will be added below if ( isset($changes['Name']) or isset($changes['StorageId']) ) { // creating symlinks when symlink already exists reports errors, but is perfectly ok @@ -112,28 +146,28 @@ if ( $action == 'save' ) { $OldStorage = $monitor->Storage(); $saferOldName = basename($monitor->Name()); - if ( file_exists($OldStorage->Path().'/'.$saferOldName) ) + if (file_exists($OldStorage->Path().'/'.$saferOldName)) unlink($OldStorage->Path().'/'.$saferOldName); - $NewStorage = new ZM\Storage($_REQUEST['newMonitor']['StorageId']); - if ( !file_exists($NewStorage->Path().'/'.$mid) ) { - if ( !mkdir($NewStorage->Path().'/'.$mid, 0755) ) { - ZM\Error('Unable to mkdir ' . $NewStorage->Path().'/'.$mid); + $NewStorage = new ZM\Storage($newMonitor['StorageId']); + if (!file_exists($NewStorage->Path().'/'.$mid)) { + if (!mkdir($NewStorage->Path().'/'.$mid, 0755)) { + ZM\Error('Unable to mkdir '.$NewStorage->Path().'/'.$mid); } } - $saferNewName = basename($_REQUEST['newMonitor']['Name']); + $saferNewName = basename($newMonitor['Name']); $link_path = $NewStorage->Path().'/'.$saferNewName; // Use a relative path for the target so the link continues to work from backups or directory changes. - if ( !symlink($mid, $link_path) ) { - if ( ! ( file_exists($link_path) and is_link($link_path) ) ) { + if (!symlink($mid, $link_path)) { + if (!(file_exists($link_path) and is_link($link_path))) { ZM\Warning('Unable to symlink ' . $NewStorage->Path().'/'.$mid . ' to ' . $NewStorage->Path().'/'.$saferNewName); } } } // end if Name or Storage Area Change - if ( isset($changes['Width']) || isset($changes['Height']) ) { - $newW = $_REQUEST['newMonitor']['Width']; - $newH = $_REQUEST['newMonitor']['Height']; + if (isset($changes['Width']) || isset($changes['Height'])) { + $newW = $newMonitor['Width']; + $newH = $newMonitor['Height']; $zones = dbFetchAll('SELECT * FROM Zones WHERE MonitorId=?', NULL, array($mid)); @@ -204,8 +238,7 @@ if ( $action == 'save' ) { } // end if rotation or just size change } // end if changes in width or height } else { - global $error_message; - $error_message = dbError('unknown'); + $error_message .= $monitor->get_last_error(); } // end if successful save $restart = true; } else { // new monitor @@ -217,14 +250,27 @@ if ( $action == 'save' ) { if ( $monitor->insert($changes) ) { $mid = $monitor->Id(); - $zoneArea = $_REQUEST['newMonitor']['Width'] * $_REQUEST['newMonitor']['Height']; - dbQuery("INSERT INTO Zones SET MonitorId = ?, Name = 'All', Type = 'Active', Units = 'Percent', NumCoords = 4, Coords = ?, Area=?, AlarmRGB = 0xff0000, CheckMethod = 'Blobs', MinPixelThreshold = 25, MinAlarmPixels=?, MaxAlarmPixels=?, FilterX = 3, FilterY = 3, MinFilterPixels=?, MaxFilterPixels=?, MinBlobPixels=?, MinBlobs = 1", array( $mid, sprintf( "%d,%d %d,%d %d,%d %d,%d", 0, 0, $_REQUEST['newMonitor']['Width']-1, 0, $_REQUEST['newMonitor']['Width']-1, $_REQUEST['newMonitor']['Height']-1, 0, $_REQUEST['newMonitor']['Height']-1 ), $zoneArea, intval(($zoneArea*3)/100), intval(($zoneArea*75)/100), intval(($zoneArea*3)/100), intval(($zoneArea*75)/100), intval(($zoneArea*2)/100) ) ); - //$view = 'none'; + $zoneArea = $newMonitor['Width'] * $newMonitor['Height']; + dbQuery("INSERT INTO Zones SET MonitorId = ?, Name = 'All', Type = 'Active', Units = 'Percent', NumCoords = 4, Coords = ?, Area=?, AlarmRGB = 0xff0000, CheckMethod = 'Blobs', MinPixelThreshold = 25, MinAlarmPixels=?, MaxAlarmPixels=?, FilterX = 3, FilterY = 3, MinFilterPixels=?, MaxFilterPixels=?, MinBlobPixels=?, MinBlobs = 1", array( $mid, + sprintf( '%d,%d %d,%d %d,%d %d,%d', 0, 0, + $newMonitor['Width']-1, + 0, + $newMonitor['Width']-1, + $newMonitor['Height']-1, + 0, + $newMonitor['Height']-1), + $zoneArea, + intval(($zoneArea*3)/100), + intval(($zoneArea*75)/100), + intval(($zoneArea*3)/100), + intval(($zoneArea*75)/100), + intval(($zoneArea*2)/100) + )); $Storage = $monitor->Storage(); error_reporting(0); mkdir($Storage->Path().'/'.$mid, 0755); - $saferName = basename($_REQUEST['newMonitor']['Name']); + $saferName = basename($newMonitor['Name']); symlink($mid, $Storage->Path().'/'.$saferName); } else { diff --git a/web/includes/actions/monitors.php b/web/includes/actions/monitors.php index 118af1533..c133ac83b 100644 --- a/web/includes/actions/monitors.php +++ b/web/includes/actions/monitors.php @@ -19,24 +19,27 @@ // // Monitor edit actions, monitor id derived, require edit permissions for that monitor -if ( ! canEdit('Monitors') ) { +if (!canEdit('Monitors')) { ZM\Warning("Monitor actions require Monitors Permissions"); return; } -if ( $action == 'save' ) { - foreach ( $_REQUEST['mids'] as $mid ) { +if ($action == 'save') { + foreach ($_REQUEST['mids'] as $mid) { $mid = ValidInt($mid); - if ( ! canEdit('Monitors', $mid) ) { - ZM\Warning("Cannot edit monitor $mid"); + if (!canEdit('Monitors', $mid)) { + ZM\Warning('Cannot edit monitor '.$mid); continue; } $Monitor = new ZM\Monitor($mid); - if ( $Monitor->Type() != 'WebSite' ) { + if ($Monitor->Type() != 'WebSite') { $Monitor->zmcControl('stop'); } - $Monitor->save($_REQUEST['newMonitor']); - if ( $Monitor->Function() != 'None' && $Monitor->Type() != 'WebSite' ) { + if (!$Monitor->save($_REQUEST['newMonitor'])) { + global $error_message; + $error_message .= 'Error saving monitor: ' . $Monitor->get_last_error().'
'; + } + if ($Monitor->Function() != 'None' && $Monitor->Type() != 'WebSite') { $Monitor->zmcControl('start'); } } // end foreach mid diff --git a/web/includes/config.php.in b/web/includes/config.php.in index e044df58f..fcd3a936d 100644 --- a/web/includes/config.php.in +++ b/web/includes/config.php.in @@ -87,11 +87,12 @@ define('ZM_PCRE', '@ZM_PCRE@'); // PCRE support enabled // // Alarm states // -define('STATE_IDLE', 0); -define('STATE_PREALARM', 1); -define('STATE_ALARM', 2); -define('STATE_ALERT', 3); -define('STATE_TAPE', 4); +define('STATE_UNKNOWN', 0); +define('STATE_IDLE', 1); +define('STATE_PREALARM', 2); +define('STATE_ALARM', 3); +define('STATE_ALERT', 4); +define('STATE_TAPE', 5); // // DVR Control Commands @@ -118,6 +119,7 @@ define('CMD_NEXT', 13); define('CMD_SEEK', 14 ); define('CMD_VARPLAY', 15); define('CMD_QUIT', 17); +define('CMD_MAXFPS', 18); define('CMD_QUERY', 99); // diff --git a/web/includes/database.php b/web/includes/database.php index 34ea384fa..01926a26e 100644 --- a/web/includes/database.php +++ b/web/includes/database.php @@ -112,7 +112,7 @@ function dbLog($sql, $update=false) { function dbError($sql) { global $dbConn; $error = $dbConn->errorInfo(); - if ( !$error[0] ) + if (!$error[0]) return ''; $message = "SQL-ERR '".implode("\n", $dbConn->errorInfo())."', statement was '".$sql."'"; @@ -130,17 +130,17 @@ function dbEscape( $string ) { function dbQuery($sql, $params=NULL, $debug = false) { global $dbConn; - if ( dbLog($sql, true) ) + if (dbLog($sql, true)) return; $result = NULL; try { - if ( isset($params) ) { - if ( ! $result = $dbConn->prepare($sql) ) { + if (isset($params)) { + if (!$result = $dbConn->prepare($sql)) { ZM\Error("SQL: Error preparing $sql: " . $pdo->errorInfo); return NULL; } - if ( ! $result->execute($params) ) { + if (!$result->execute($params)) { ZM\Error("SQL: Error executing $sql: " . print_r($result->errorInfo(), true)); return NULL; } diff --git a/web/includes/functions.php b/web/includes/functions.php index 89d2cc8ad..6f292357e 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -2070,7 +2070,9 @@ function getStreamHTML($monitor, $options = array()) { if ( ! isset($options['height'] ) ) $options['height'] = 0; - $options['maxfps'] = ZM_WEB_VIDEO_MAXFPS; + if (!isset($options['maxfps'])) { + $options['maxfps'] = ZM_WEB_VIDEO_MAXFPS; + } if ( $monitor->StreamReplayBuffer() ) $options['buffer'] = $monitor->StreamReplayBuffer(); //Warning("width: " . $options['width'] . ' height: ' . $options['height']. ' scale: ' . $options['scale'] ); diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index a10d90008..feb4b3611 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -3,8 +3,10 @@ function MonitorStream(monitorData) { this.id = monitorData.id; this.connKey = monitorData.connKey; this.url = monitorData.url; + this.url_to_zms = monitorData.url_to_zms; this.width = monitorData.width; this.height = monitorData.height; + this.scale = 100; this.status = null; this.alarmState = STATE_IDLE; this.lastAlarmState = STATE_IDLE; @@ -15,19 +17,68 @@ function MonitorStream(monitorData) { }; this.type = monitorData.type; this.refresh = monitorData.refresh; + this.element = null; + this.getElement = function() { + if (this.element) return this.element; + this.element = document.getElementById('liveStream'+this.id); + if (!this.element) { + console.error("No img for #liveStream"+this.id); + } + return this.element; + }; + + /* if the img element didn't have a src, this would fill it in, causing it to show. */ + this.show = function() { + const stream = this.getElement(); + if (!stream.src) { + stream.src = this.url_to_zms+"&mode=single&scale=100&connkey="+this.connKey; + } + }; + + this.setScale = function(newscale) { + const img = this.getElement(); + if (!img) return; + + this.scale = newscale; + + const oldSrc = img.getAttribute('src'); + let newSrc = ''; + + img.setAttribute('src', ''); + console.log("Scaling to: " + newscale); + + if (newscale == '0' || newscale == 'auto') { + let bottomElement = document.getElementById('replayStatus'); + if (!bottomElement) { + bottomElement = document.getElementById('monitorState'); + } + var newSize = scaleToFit(this.width, this.height, $j(img), $j(bottomElement)); + + console.log(newSize); + newWidth = newSize.width; + newHeight = newSize.height; + autoScale = parseInt(newSize.autoScale); + // This is so that we don't waste bandwidth and let the browser do all the scaling. + if (autoScale > 100) autoScale = 100; + if (autoScale) { + newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+autoScale); + } + } else { + newWidth = this.width * newscale / SCALE_BASE; + newHeight = this.height * newscale / SCALE_BASE; + img.width(newWidth); + img.height(newHeight); + if (newscale > 100) newscale = 100; + newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+newscale); + } + img.setAttribute('src', newSrc); + }; this.start = function(delay) { // Step 1 make sure we are streaming instead of a static image - var stream = $j('#liveStream'+this.id); - if (!stream.length) { - console.log('No live stream'); - return; - } - stream = stream[0]; - if ( !stream ) { - console.log('No live stream'); - return; - } - if ( !stream.src ) { + const stream = this.getElement(); + if (!stream) return; + + if (!stream.src) { // Website Monitors won't have an img tag console.log('No src for #liveStream'+this.id); console.log(stream); @@ -38,7 +89,7 @@ function MonitorStream(monitorData) { src += '&connkey='+this.connKey; } if ( stream.src != src ) { - console.log("Setting to streaming"); + console.log("Setting to streaming: " + src); stream.src = ''; stream.src = src; } diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 8b412cd39..8c6ce9adb 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -663,8 +663,33 @@ $SLANG = array( 'PrivacyCookiesText' => 'Whether you use a web browser or a mobile app to communicate with the ZoneMinder server, a ZMSESSID cookie is created on the client to uniquely identify a session with the ZoneMinder server. ZmCSS and zmSkin cookies are created to remember your style and skin choices.', 'PrivacyTelemetry' => 'Telemetry', 'PrivacyTelemetryText' => 'Because ZoneMinder is open-source, anyone can install it without registering. This makes it difficult to answer questions such as: how many systems are out there, what is the largest system out there, what kind of systems are out there, or where are these systems located? Knowing the answers to these questions, helps users who ask us these questions, and it helps us set priorities based on the majority user base.', - 'PrivacyTelemetryList' => 'The ZoneMinder Telemetry daemon collects the following data about your system:
  • A unique identifier (UUID)
  • City based location is gathered by querying ipinfo.io. City, region, country, latitude, and longitude parameters are saved. The latitude and longitude coordinates are accurate down to the city or town level only!
  • Current time
  • Total number of monitors
  • Total number of events
  • System architecture
  • Operating system kernel, distro, and distro version
  • Version of ZoneMinder
  • Total amount of memory
  • Number of cpu cores
', - 'PrivacyMonitorList' => 'The following configuration parameters from each monitor are collected:
  • Id
  • Name
  • Type
  • Function
  • Width
  • Height
  • Colours
  • MaxFPS
  • AlarmMaxFPS
', + 'PrivacyTelemetryList' => 'The ZoneMinder Telemetry daemon collects the following data about your system: +
    +
  • A unique identifier (UUID)
  • +
  • City based location is gathered by querying ipinfo.io. City, region, country, latitude, and longitude parameters are saved. The latitude and longitude coordinates are accurate down to the city or town level only!
  • +
  • Current time
  • +
  • Total number of monitors
  • +
  • Total number of events
  • +
  • System architecture
  • +
  • Operating system kernel, distro, and distro version
  • +
  • Version of ZoneMinder
  • +
  • Total amount of memory
  • +
  • Number of cpu cores
  • +
', + 'PrivacyMonitorList' => 'The following configuration parameters from each monitor are collected: +
    +
  • Id
  • +
  • Name
  • +
  • Manufacturer
  • +
  • Model
  • +
  • Type
  • +
  • Function
  • +
  • Width
  • +
  • Height
  • +
  • Colours
  • +
  • MaxFPS
  • +
  • AlarmMaxFPS
  • +
', 'PrivacyConclusionText' => 'We are NOT collecting any image specific data from your cameras. We don’t know what your cameras are watching. This data will not be sold or used for any purpose not stated herein. By clicking accept, you agree to send us this data to help make ZoneMinder a better product. By clicking decline, you can still freely use ZoneMinder and all its features.', 'Probe' => 'Probe', 'ProfileProbe' => 'Stream Probe', diff --git a/web/skins/classic/css/base/views/event.css b/web/skins/classic/css/base/views/event.css index f392cb99c..da59db6aa 100644 --- a/web/skins/classic/css/base/views/event.css +++ b/web/skins/classic/css/base/views/event.css @@ -77,7 +77,7 @@ height: 100%; position: relative; } -#imageFeed { +#videoFeed { display: inline-block; position: relative; text-align: center; @@ -263,3 +263,17 @@ height: 100%; height: 100%; background-color: #999999; } +svg.zones { + position:absolute; + top: 0; + left: 0; + background: none; + width: 100%; + /* + height: 100%; + */ +} +#videoobj { + width: 100%; + height: 100%; +} diff --git a/web/skins/classic/css/base/views/monitor.css b/web/skins/classic/css/base/views/monitor.css index 7fca80aac..f827af5ca 100644 --- a/web/skins/classic/css/base/views/monitor.css +++ b/web/skins/classic/css/base/views/monitor.css @@ -50,6 +50,7 @@ select.chosen { } tr td:first-child { min-width: 300px; + vertical-align: top; } .OutputContainer { display: none; diff --git a/web/skins/classic/css/base/views/watch.css b/web/skins/classic/css/base/views/watch.css index 17bf1593d..77a4a4e96 100644 --- a/web/skins/classic/css/base/views/watch.css +++ b/web/skins/classic/css/base/views/watch.css @@ -1,6 +1,14 @@ #header { - display: flex; - justify-content: space-between; +} + +.container-fluid { + padding-left: 0; + padding-right: 0; +} + +#sidebar { + min-width: 140px; + padding-right: 1px; } #menuControls { @@ -19,19 +27,22 @@ #monitorStatus { margin: 4px auto; text-align: center; -} - -#monitorStatus #enableDisableAlarms { - float: left; -} - -#monitorStatus #forceCancelAlarm { - float: right; + display: inline-block; } #monitorStatus #monitorState { } +#replayStatus { + margin: 3px 0 2px; + text-align: center; + display: inline-block; +} + +#replayStatus > span { + padding: 0 4px; +} + #dvrControls { margin-top: 3px; margin-bottom: 2px; @@ -67,15 +78,6 @@ cursor: default; } -#replayStatus { - margin: 3px 0 2px; - text-align: center; - clear: both; -} - -#replayStatus > span { - padding: 0 4px; -} #events { margin: 0 auto; @@ -116,3 +118,22 @@ span.alert { background-color: #DCDCDC; } +.controlHeader { + width: 100%; +} + +#viewingFPS, +#captureFPS, +#analysisFPS +{ + display: inline-block; +} +#stateValue, +#viewingFPSValue, +#captureFPSValue, +#analysisFPSValue +{ + display: inline-block; + min-width: 40px; + text-align: right; +} diff --git a/web/skins/classic/css/base/zones.css b/web/skins/classic/css/base/zones.css new file mode 100644 index 000000000..82c96e6b2 --- /dev/null +++ b/web/skins/classic/css/base/zones.css @@ -0,0 +1,19 @@ +.zones polygon { + fill-opacity: 0.25; +} +.Active { + stroke: #ff0000; + fill: #ff0000; +} +.Inclusive { + stroke: #FFA500; + fill: #FFA500; +} +.Exclusive { + stroke: #800080; + fill: #800080; +} +.Preclusive { + stroke: #0000FF; + fill: #0000FF; +} diff --git a/web/skins/classic/includes/export_functions.php b/web/skins/classic/includes/export_functions.php index 0e348152f..f34d53546 100644 --- a/web/skins/classic/includes/export_functions.php +++ b/web/skins/classic/includes/export_functions.php @@ -786,7 +786,7 @@ function exportFileList( } closedir($dir); } - ZM\Debug(print_r($files, true)); + ZM\Debug('All available files: '.print_r($files, true)); $exportFileList = array(); @@ -843,6 +843,18 @@ function exportFileList( ZM\Debug('Not including frame images'); } # end if exportImages + if ($exportVideo) { + $filesLeft = array(); + foreach ($files as $file) { + if (preg_match('/\.(?:mpg|mpeg|mov|swf|mp4|mkv|avi|asf|3gp)$/', $file)) { + $exportFileList[$file] = $file; + } else { + $filesLeft[$file] = $file; + } + } + $files = $filesLeft; + } + if ($exportMisc) { foreach ($files as $file) { $exportFileList[$file] = $file; diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index 1b01a73e8..c593601aa 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -916,7 +916,7 @@ function xhtmlFooter() { ?> - + container.innerWidth()) { newWidth = container.innerWidth(); @@ -598,13 +609,15 @@ function scaleToFit(baseWidth, baseHeight, scaleEl, bottomEl) { return parseInt($j(this).val()); }).get(); scales.shift(); - var closest; + var closest = null; $j(scales).each(function() { //Set zms scale to nearest regular scale. Zoom does not like arbitrary scale values. if (closest == null || Math.abs(this - autoScale) < Math.abs(closest - autoScale)) { closest = this.valueOf(); } }); - autoScale = closest; + if (closest) { + autoScale = closest; + } return {width: Math.floor(newWidth), height: Math.floor(newHeight), autoScale: autoScale}; } @@ -947,3 +960,29 @@ function initThumbAnimation() { }); } } + +/* View in fullscreen */ +function openFullscreen(elem) { + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + /* Safari */ + elem.webkitRequestFullscreen(); + } else if (elem.msRequestFullscreen) { + /* IE11 */ + elem.msRequestFullscreen(); + } +} + +/* Close fullscreen */ +function closeFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + /* Safari */ + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + /* IE11 */ + document.msExitFullscreen(); + } +} diff --git a/web/skins/classic/js/skin.js.php b/web/skins/classic/js/skin.js.php index 802a21095..2b7980c3a 100644 --- a/web/skins/classic/js/skin.js.php +++ b/web/skins/classic/js/skin.js.php @@ -54,6 +54,7 @@ foreach ( $perms as $perm ) { ?> var ANIMATE_THUMBS = ; +var SCALE_BASE = ; var refreshParent = ; var running = ; + +var STATE_UNKNOWN = ; +var STATE_IDLE = ; +var STATE_PREALARM = ; +var STATE_ALARM = ; +var STATE_ALERT = ; +var STATE_TAPE = ; + +var stateStrings = new Array(); +stateStrings[STATE_UNKNOWN] = ""; +stateStrings[STATE_IDLE] = ""; +stateStrings[STATE_PREALARM] = ""; +stateStrings[STATE_ALARM] = ""; +stateStrings[STATE_ALERT] = ""; +stateStrings[STATE_TAPE] = ""; diff --git a/web/skins/classic/views/_monitor_filters.php b/web/skins/classic/views/_monitor_filters.php index f80bd1f5b..6fdde8be1 100644 --- a/web/skins/classic/views/_monitor_filters.php +++ b/web/skins/classic/views/_monitor_filters.php @@ -178,7 +178,6 @@ $html .= ' ' . ( count($conditions) ? ' WHERE ' . implode(' AND ', $conditions) : '' ).' ORDER BY Sequence ASC'; $monitors = dbFetchAll($sql, null, $values); - ZM\Debug(print_r($monitors, true)); $displayMonitors = array(); $monitors_dropdown = array(); diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index f0f654f56..e49b2c467 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -260,31 +260,37 @@ for( $monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1 ) { ?> @@ -317,7 +323,7 @@ for( $monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1 ) { } ?> - lens + lens ' : '>') . validHtmlStr($monitor['Name']) ?>
diff --git a/web/skins/classic/views/event.php b/web/skins/classic/views/event.php index a07c95042..b096fda97 100644 --- a/web/skins/classic/views/event.php +++ b/web/skins/classic/views/event.php @@ -25,47 +25,57 @@ if ( !canView('Events') ) { require_once('includes/Event.php'); require_once('includes/Filter.php'); +require_once('includes/Zone.php'); $eid = validInt($_REQUEST['eid']); $fid = !empty($_REQUEST['fid']) ? validInt($_REQUEST['fid']) : 1; $Event = new ZM\Event($eid); -if ( $user['MonitorIds'] ) { - $monitor_ids = explode(',', $user['MonitorIds']); - if ( count($monitor_ids) and ! in_array($Event->MonitorId(), $monitor_ids) ) { - $view = 'error'; - return; - } -} -$Monitor = $Event->Monitor(); +$monitor = $Event->Monitor(); -if ( isset($_REQUEST['rate']) ) { +if (!$monitor->canView()) { + $view = 'error'; + return; +} + +zm_session_start(); +if (isset($_REQUEST['rate']) ) { $rate = validInt($_REQUEST['rate']); -} else if ( isset($_COOKIE['zmEventRate']) ) { +} else if (isset($_COOKIE['zmEventRate'])) { $rate = $_COOKIE['zmEventRate']; } else { - $rate = reScale(RATE_BASE, $Monitor->DefaultRate(), ZM_WEB_DEFAULT_RATE); + $rate = reScale(RATE_BASE, $monitor->DefaultRate(), ZM_WEB_DEFAULT_RATE); } -if ( isset($_REQUEST['scale']) ) { +if (isset($_REQUEST['scale'])) { $scale = validInt($_REQUEST['scale']); -} else if ( isset($_COOKIE['zmEventScale'.$Event->MonitorId()]) ) { +} else if (isset($_COOKIE['zmEventScale'.$Event->MonitorId()])) { $scale = $_COOKIE['zmEventScale'.$Event->MonitorId()]; } else { - $scale = $Monitor->DefaultScale(); + $scale = $monitor->DefaultScale(); +} + +$showZones = false; +if (isset($_REQUEST['showZones'])) { + $showZones = $_REQUEST['showZones'] == 1; + $_SESSION['zmEventShowZones'.$monitor->Id()] = $showZones; +} else if (isset($_COOKIE['zmEventShowZones'.$monitor->Id()])) { + $showZones = $_COOKIE['zmEventShowZones'.$monitor->Id()] == 1; +} else if (isset($_SESSION['zmEventShowZones'.$monitor->Id()]) ) { + $showZones = $_SESSION['zmEventShowZones'.$monitor->Id()]; } $codec = 'auto'; -if ( isset($_REQUEST['codec']) ) { +if (isset($_REQUEST['codec'])) { $codec = $_REQUEST['codec']; - zm_session_start(); $_SESSION['zmEventCodec'.$Event->MonitorId()] = $codec; - session_write_close(); } else if ( isset($_SESSION['zmEventCodec'.$Event->MonitorId()]) ) { $codec = $_SESSION['zmEventCodec'.$Event->MonitorId()]; } else { - $codec = $Monitor->DefaultCodec(); + $codec = $monitor->DefaultCodec(); } +session_write_close(); + $codecs = array( 'auto' => translate('Auto'), 'MP4' => translate('MP4'), @@ -79,32 +89,30 @@ $replayModes = array( 'gapless' => translate('ReplayGapless'), ); -if ( isset($_REQUEST['streamMode']) ) +if (isset($_REQUEST['streamMode'])) $streamMode = validHtmlStr($_REQUEST['streamMode']); else $streamMode = 'video'; $replayMode = ''; -if ( isset($_REQUEST['replayMode']) ) +if (isset($_REQUEST['replayMode'])) $replayMode = validHtmlStr($_REQUEST['replayMode']); -if ( isset($_COOKIE['replayMode']) && preg_match('#^[a-z]+$#', $_COOKIE['replayMode']) ) +if (isset($_COOKIE['replayMode']) && preg_match('#^[a-z]+$#', $_COOKIE['replayMode'])) $replayMode = validHtmlStr($_COOKIE['replayMode']); -if ( ( !$replayMode ) or ( !$replayModes[$replayMode] ) ) { +if ((!$replayMode) or !$replayModes[$replayMode]) { $replayMode = 'none'; } -$video_tag = false; -if ( $Event->DefaultVideo() and ( $codec == 'MP4' or $codec == 'auto' ) ) { - $video_tag = true; -} +$video_tag = ($Event->DefaultVideo() and ($codec == 'MP4' or $codec == 'auto')); + // videojs zoomrotate only when direct recording $Zoom = 1; $Rotation = 0; -if ( $Monitor->VideoWriter() == '2' ) { +if ($monitor->VideoWriter() == '2') { # Passthrough $Rotation = $Event->Orientation(); - if ( in_array($Event->Orientation(),array('90','270')) ) + if (in_array($Event->Orientation(),array('90','270'))) $Zoom = $Event->Height()/$Event->Width(); } @@ -143,20 +151,28 @@ if ( $Event->Id() and !file_exists($Event->Path()) )
-Id() ) { ?> +Id()) { ?> DefaultVideo() ? '' : 'style="display:none;"' ?> > + -Id ?> + +

Id() ?>

@@ -188,10 +204,10 @@ if ( $Event->Id() and !file_exists($Event->Path()) )
-
+ -
-
-getStreamSrc(array('mode'=>'mpeg', 'scale'=>$scale, 'rate'=>$rate, 'bitrate'=>ZM_WEB_VIDEO_BITRATE, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'format'=>ZM_MPEG_REPLAY_FORMAT, 'replay'=>$replayMode),'&'); outputVideoStream('evtStream', $streamSrc, reScale( $Event->Width(), $scale ).'px', reScale( $Event->Height(), $scale ).'px', ZM_MPEG_LIVE_FORMAT ); } else { $streamSrc = $Event->getStreamSrc(array('mode'=>'jpeg', 'frame'=>$fid, 'scale'=>$scale, 'rate'=>$rate, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'replay'=>$replayMode),'&'); if ( canStreamNative() ) { - outputImageStream('evtStream', $streamSrc, reScale($Event->Width(), $scale).'px', reScale($Event->Height(), $scale).'px', validHtmlStr($Event->Name())); + outputImageStream('evtStream', $streamSrc, '100%', '100%', validHtmlStr($Event->Name())); } else { - outputHelperStream('evtStream', $streamSrc, reScale($Event->Width(), $scale).'px', reScale($Event->Height(), $scale).'px' ); + outputHelperStream('evtStream', $streamSrc, '100%', '100%'); } } // end if stream method ?> @@ -229,10 +241,18 @@ if ( (ZM_WEB_STREAM_METHOD == 'mpeg') && ZM_MPEG_LIVE_FORMAT ) {
-
+ +$monitor->Id()), array('order'=>'Area DESC')) as $zone) { + echo $zone->svg_polygon(); + } // end foreach zone +?> + Sorry, your browser does not support inline SVG + +

- + diff --git a/web/skins/classic/views/events.php b/web/skins/classic/views/events.php index 52cb5c3f0..1c93e0b92 100644 --- a/web/skins/classic/views/events.php +++ b/web/skins/classic/views/events.php @@ -79,11 +79,14 @@ getBodyTopHTML(); data-cookie-id-table="zmEventsTable" data-cookie-expire="2y" data-click-to-select="true" - data-remember-order="true" + data-remember-order="false" data-show-columns="true" data-show-export="true" data-uncheckAll="true" data-toolbar="#toolbar" + data-sort-name="sort_field() ?>" + data-sort-order="sort_asc() ? 'asc' : 'desc' ?>" + data-server-sort="true" data-show-fullscreen="true" data-click-to-select="true" data-maintain-meta-data="true" diff --git a/web/skins/classic/views/filter.php b/web/skins/classic/views/filter.php index b425f7896..8445d2c12 100644 --- a/web/skins/classic/views/filter.php +++ b/web/skins/classic/views/filter.php @@ -152,7 +152,7 @@ $booleanValues = array( $focusWindow = true; -$storageareas = array('' => 'All') + ZM\ZM_Object::Objects_Indexed_By_Id('ZM\Storage'); +$storageareas = array('' => array('Name'=>'NULL Unspecified'), '0' => array('Name'=>'Zero')) + ZM\ZM_Object::Objects_Indexed_By_Id('ZM\Storage'); $weekdays = array(); for ( $i = 0; $i < 7; $i++ ) { @@ -367,6 +367,7 @@ for ( $i=0; $i < count($terms); $i++ ) { translate('None'), 'Id' => translate('AttrId'), 'Name' => translate('AttrName'), 'Cause' => translate('AttrCause'), @@ -383,10 +384,18 @@ $sort_fields = array( ); echo htmlSelect('filter[Query][sort_field]', $sort_fields, $filter->sort_field()); $sort_dirns = array( - '1' => translate('SortAsc'), - '0' => translate('SortDesc') - ); -echo htmlSelect( 'filter[Query][sort_asc]', $sort_dirns, $filter->sort_asc() ); + '1' => translate('SortAsc'), + '0' => translate('SortDesc') +); +echo htmlSelect('filter[Query][sort_asc]', $sort_dirns, $filter->sort_asc()); +?> + + + +translate('No'), '1'=>translate('Yes')), + $filter->skip_locked()); ?> @@ -468,9 +477,13 @@ if ( ZM_OPT_MESSAGE ) {

- + Background() ) { ?> checked="checked" data-on-click-this="updateButtons"/>

+

+ + +

Concurrent() ) { ?> checked="checked" data-on-click-this="updateButtons"/> diff --git a/web/skins/classic/views/frame.php b/web/skins/classic/views/frame.php index bdd2e1071..9931c910a 100644 --- a/web/skins/classic/views/frame.php +++ b/web/skins/classic/views/frame.php @@ -18,30 +18,28 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // -if ( !canView('Events') ) { - $view = 'error'; - return; -} - require_once('includes/Frame.php'); $eid = validInt($_REQUEST['eid']); $fid = empty($_REQUEST['fid']) ? 0 : validInt($_REQUEST['fid']); $Event = new ZM\Event($eid); +if (!$Event->canView()) { + $view = 'error'; + return; +} $Monitor = $Event->Monitor(); # This is kinda weird.. so if we pass fid=0 or some other non-integer, then it loads max score # perhaps we should consider being explicit, like fid = maxscore -if ( !empty($fid) ) { - $sql = 'SELECT * FROM Frames WHERE EventId = ? AND FrameId = ?'; - if ( !($frame = dbFetchOne($sql, NULL, array($eid, $fid))) ) +if (!empty($fid)) { + $sql = 'SELECT * FROM Frames WHERE EventId=? AND FrameId=?'; + if (!($frame = dbFetchOne($sql, NULL, array($eid, $fid)))) $frame = array('EventId'=>$eid, 'FrameId'=>$fid, 'Type'=>'Normal', 'Score'=>0); } else { - $frame = dbFetchOne('SELECT * FROM Frames WHERE EventId = ? AND Score = ?', NULL, array($eid, $Event->MaxScore())); + $frame = dbFetchOne('SELECT * FROM Frames WHERE EventId=? AND Score=?', NULL, array($eid, $Event->MaxScore())); } $Frame = new ZM\Frame($frame); - $maxFid = $Event->Frames(); $firstFid = 1; @@ -51,11 +49,11 @@ $lastFid = $maxFid; $alarmFrame = ( $Frame->Type() == 'Alarm' ) ? 1 : 0; -if ( isset($_REQUEST['scale']) ) { +if (isset($_REQUEST['scale'])) { $scale = validNum($_REQUEST['scale']); -} else if ( isset($_COOKIE['zmWatchScale'.$Monitor->Id()]) ) { +} else if (isset($_COOKIE['zmWatchScale'.$Monitor->Id()])) { $scale = validNum($_COOKIE['zmWatchScale'.$Monitor->Id()]); -} else if ( isset($_COOKIE['zmWatchScale']) ) { +} else if (isset($_COOKIE['zmWatchScale'])) { $scale = validNum($_COOKIE['zmWatchScale']); } else { $scale = max(reScale(SCALE_BASE, $Monitor->DefaultScale(), ZM_WEB_DEFAULT_SCALE), SCALE_BASE); @@ -63,7 +61,7 @@ if ( isset($_REQUEST['scale']) ) { $scale = $scale ? $scale : 0; $imageData = $Event->getImageSrc($frame, $scale, 0); -if ( !$imageData ) { +if (!$imageData) { ZM\Error("No data found for Event $eid frame $fid"); $imageData = array(); } @@ -92,78 +90,80 @@ xhtmlHeaders(__FILE__, translate('Frame').' - '.$Event->Id().' - '.$Frame->Frame

- - - - + + + +

Id().'-'.$Frame->FrameId().' ('.$Frame->Score().')' ?>

-
'changeScale','id'=>'scale')); ?>
+
+ + 'changeScale','id'=>'scale')); ?> +
- diff --git a/web/skins/classic/views/js/event.js b/web/skins/classic/views/js/event.js index 727976e76..04bf66b7e 100644 --- a/web/skins/classic/views/js/event.js +++ b/web/skins/classic/views/js/event.js @@ -34,7 +34,7 @@ function streamReq(data) { data.view = 'request'; data.request = 'stream'; - $j.getJSON(thisUrl, data) + $j.getJSON(monitorUrl, data) .done(getCmdResponse) .fail(logAjaxFail); } @@ -177,7 +177,7 @@ function changeScale() { var newWidth; var newHeight; var autoScale; - var eventViewer= $j(vid ? '#videoobj' : '#evtStream'); + var eventViewer= $j(vid ? '#videoobj' : '#videoFeed'); var alarmCue = $j('div.alarmCue'); var bottomEl = $j('#replayStatus'); @@ -300,7 +300,7 @@ function getCmdResponse(respObj, respText) { if (streamStatus.auth) { // Try to reload the image stream. - var streamImg = $j('#evtStream'); + var streamImg = document.getElementById('evtStream'); if (streamImg) { streamImg.src = streamImg.src.replace(/auth=\w+/i, 'auth='+streamStatus.auth); } @@ -446,8 +446,10 @@ function streamFastRev(action) { function streamPrev(action) { if (action) { $j(".vjsMessage").remove(); - location.replace(thisUrl + '?view=event&eid=' + prevEventId + filterQuery + sortQuery); - return; + if (prevEventId != 0) { + location.replace(thisUrl + '?view=event&eid=' + prevEventId + filterQuery + sortQuery); + return; + } /* Ideally I'd like to get back to this style if ( vid && PrevEventDefVideoPath.indexOf("view_video") > 0 ) { @@ -619,8 +621,8 @@ function getNearEventsResponse(respObj, respText) { if (checkStreamForErrors('getNearEventsResponse', respObj)) { return; } - prevEventId = respObj.nearevents.PrevEventId; - nextEventId = respObj.nearevents.NextEventId; + prevEventId = parseInt(respObj.nearevents.PrevEventId); + nextEventId = parseInt(respObj.nearevents.NextEventId); prevEventStartTime = Date.parse(respObj.nearevents.PrevEventStartTime); nextEventStartTime = Date.parse(respObj.nearevents.NextEventStartTime); PrevEventDefVideoPath = respObj.nearevents.PrevEventDefVideoPath; @@ -657,6 +659,7 @@ function getFrameResponse(respObj, respText) { function frameQuery(eventId, frameId, loadImage) { var data = {}; + if (auth_hash) data.auth = auth_hash; data.loopback = loadImage; data.id = {eventId, frameId}; @@ -708,7 +711,7 @@ function renameEvent() { } function exportEvent() { - window.location.assign('?view=export&eid='+eventData.Id); + window.location.assign('?view=export&eids[]='+eventData.Id); } function showEventFrames() { @@ -767,14 +770,16 @@ function handleClick(event) { // Manage the DELETE CONFIRMATION modal button function manageDelConfirmModalBtns() { document.getElementById("delConfirmBtn").addEventListener("click", function onDelConfirmClick(evt) { - if ( !canEdit.Events ) { + if (!canEdit.Events) { enoperm(); return; } + pauseClicked(); evt.preventDefault(); - $j.getJSON(thisUrl + '?request=events&task=delete&eids[]='+eventData.Id) + $j.getJSON(thisUrl + '?request=event&action=delete&id='+eventData.Id) .done(function(data) { + $j('#deleteConfirm').modal('hide'); streamNext(true); }) .fail(logAjaxFail); @@ -907,12 +912,12 @@ function initPage() { progressBarNav(); streamCmdTimer = setTimeout(streamQuery, 500); if (canStreamNative) { - if (!$j('#imageFeed')) { - console.log('No element with id tag imageFeed found.'); + if (!$j('#videoFeed')) { + console.log('No element with id tag videoFeed found.'); } else { - var streamImg = $j('#imageFeed img'); + var streamImg = $j('#videoFeed img'); if (!streamImg) { - streamImg = $j('#imageFeed object'); + streamImg = $j('#videoFeed object'); } $j(streamImg).click(function(event) { handleClick(event); @@ -1015,7 +1020,13 @@ function initPage() { // Manage the EXPORT button bindButton('#exportBtn', 'click', null, function onExportClick(evt) { evt.preventDefault(); - window.location.assign('?view=export&eids[]='+eventData.Id); + exportEvent(); + }); + + // Manage the generateVideo button + bindButton('#videoBtn', 'click', null, function onExportClick(evt) { + evt.preventDefault(); + videoEvent(); }); // Manage the Event STATISTICS Button @@ -1062,5 +1073,27 @@ function initPage() { }); } // end initPage +document.getElementById('toggleZonesButton').addEventListener('click', toggleZones); + +function toggleZones(e) { + const zones = $j('#zones'+eventData.MonitorId); + const button = document.getElementById('toggleZonesButton'); + if (zones.length) { + if (zones.is(":visible")) { + zones.hide(); + button.setAttribute('title', showZonesString); + button.innerHTML = 'layers'; + setCookie('zmEventShowZones'+eventData.MonitorId, '0', 3600); + } else { + zones.show(); + button.setAttribute('title', hideZonesString); + button.innerHTML = 'layers_clear'; + setCookie('zmEventShowZones'+eventData.MonitorId, '1', 3600); + } + } else { + console.error("Zones svg not found"); + } +} + // Kick everything off $j(document).ready(initPage); diff --git a/web/skins/classic/views/js/event.js.php b/web/skins/classic/views/js/event.js.php index cdd4e9041..5d4d57f7c 100644 --- a/web/skins/classic/views/js/event.js.php +++ b/web/skins/classic/views/js/event.js.php @@ -1,7 +1,7 @@ Id() ?>', Name: 'Name() ?>', MonitorId: 'MonitorId() ?>', - MonitorName: 'Name()) ?>', + MonitorName: 'Name()) ?>', Cause: 'Cause()) ?>', + Notes: 'Notes()?>', Width: 'Width() ?>', Height: 'Height() ?>', Length: 'Length() ?>', @@ -72,6 +73,7 @@ var eventDataStrings = { MonitorId: '', MonitorName: '', Cause: '', + Notes: '', StartDateTimeShort: '', Length: '', Frames: '', @@ -93,7 +95,7 @@ var sortQuery = '; var rate = ''; // really only used when setting up initial playback rate. var scale = ""; -var LabelFormat = "LabelFormat())?>"; +var LabelFormat = "LabelFormat())?>"; var streamTimeout = ; @@ -105,6 +107,8 @@ var streamMode = ''; // var deleteString = ""; var causeString = ""; +var showZonesString = ""; +var hideZonesString = ""; var WEB_LIST_THUMB_WIDTH = ''; var WEB_LIST_THUMB_HEIGHT = ''; var popup = ''; diff --git a/web/skins/classic/views/js/events.js b/web/skins/classic/views/js/events.js index 9f6d34f75..bafd16763 100644 --- a/web/skins/classic/views/js/events.js +++ b/web/skins/classic/views/js/events.js @@ -35,12 +35,16 @@ var params = // Called by bootstrap-table to retrieve zm event data function ajaxRequest(params) { - if ( params.data && params.data.filter ) { + if (params.data && params.data.filter) { params.data.advsearch = params.data.filter; delete params.data.filter; } $j.getJSON(thisUrl + '?view=request&request=events&task=query'+filterQuery, params.data) .done(function(data) { + if (data.result == 'Error') { + alert(data.message); + return; + } var rows = processRows(data.rows); // rearrange the result into what bootstrap-table expects params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); @@ -59,13 +63,13 @@ function processRows(rows) { row.Id = '' + eid + ''; row.Name = '' + row.Name + '' + - '
' + archived + emailed + '
'; + '
' + archived + emailed + '
'; if ( canEdit.Monitors ) row.Monitor = '' + row.Monitor + ''; if ( canEdit.Events ) row.Cause = '' + row.Cause + ''; if ( row.Notes.indexOf('detected:') >= 0 ) { - row.Cause = row.Cause + '
' + row.Notes + '
'; + row.Cause = row.Cause + '
' + row.Notes + '
'; } else if ( row.Notes != 'Forced Web: ' ) { - row.Cause = row.Cause + '
' + row.Notes + '
'; + row.Cause = row.Cause + '
' + row.Notes + '
'; } row.Frames = '' + row.Frames + ''; row.AlarmFrames = '' + row.AlarmFrames + ''; diff --git a/web/skins/classic/views/js/filter.js b/web/skins/classic/views/js/filter.js index b59c68417..5765a0e76 100644 --- a/web/skins/classic/views/js/filter.js +++ b/web/skins/classic/views/js/filter.js @@ -57,7 +57,7 @@ function validateForm(form) { form.elements['filter[AutoUnarchive]'].checked || form.elements['filter[UpdateDiskSpace]'].checked || form.elements['filter[AutoVideo]'].checked || - (form.elements['filter[AutoEmail]'].checked && form.elements['filter[AutoEmail]'].checked) || + (form.elements['filter[AutoEmail]'] && form.elements['filter[AutoEmail]'].checked) || (form.elements['filter[AutoMessage]'] && form.elements['filter[AutoMessage]'].checked) || form.elements['filter[AutoExecute]'].checked || form.elements['filter[AutoDelete]'].checked || @@ -167,7 +167,6 @@ function submitToExport(element) { } function submitAction(button) { - console.log(button.value); var form = button.form; form.elements['action'].value = button.value; form.submit(); @@ -175,7 +174,6 @@ function submitAction(button) { function deleteFilter(element) { var form = element.form; - console.log(form); if (confirm(deleteSavedFilterString+" '"+form.elements['filter[Name]'].value+"'?")) { form.elements['action'].value = 'delete'; form.submit(); @@ -384,7 +382,6 @@ function debugFilter() { } function manageModalBtns(id) { - console.log(id); // Manage the CANCEL modal button var cancelBtn = document.getElementById(id+"CancelBtn"); if ( cancelBtn ) { diff --git a/web/skins/classic/views/js/frame.js b/web/skins/classic/views/js/frame.js index c10b6e87a..1040d5a48 100644 --- a/web/skins/classic/views/js/frame.js +++ b/web/skins/classic/views/js/frame.js @@ -12,10 +12,10 @@ function changeScale() { last: $j('#lastLink') }; - if ( img ) { + if (img) { var baseWidth = $j('#base_width').val(); var baseHeight = $j('#base_height').val(); - if ( ! parseInt(scale) ) { + if (!parseInt(scale)) { var newSize = scaleToFit(baseWidth, baseHeight, img, $j('#controls')); newWidth = newSize.width; newHeight = newSize.height; @@ -30,7 +30,7 @@ function changeScale() { } setCookie('zmWatchScale', scale, 3600); $j.each(controlsLinks, function(k, anchor) { //Make frames respect scale choices - if ( anchor ) { + if (anchor) { anchor.prop('href', anchor.prop('href').replace(/scale=.*&/, 'scale=' + scale + '&')); } }); @@ -39,11 +39,11 @@ function changeScale() { onStatsResize(newWidth); } -function getFrmStatsCookie() { +function getFrameStatsCookie() { var cookie = 'zmFrameStats'; var stats = getCookie(cookie); - if ( !stats ) { + if (!stats) { stats = 'on'; setCookie(cookie, stats, 10*365); } @@ -53,29 +53,33 @@ function getFrmStatsCookie() { function getStat(params) { $j.getJSON(thisUrl + '?view=request&request=stats&raw=true', params) .done(function(data) { - var stat = data.raw; + var stats = data.raw; + $j('#frameStatsTable').empty().append(''); - $j.each( statHeaderStrings, function( key ) { - var th = $j('').addClass('text-right').text(statHeaderStrings[key]); - var tdString; + for (const stat of stats) { + $j.each(statHeaderStrings, function(key) { + var th = $j('').addClass('text-right').text(statHeaderStrings[key]); + var tdString; - switch (stat ? key : 'n/a') { - case 'FrameId': - tdString = '' + stat[key] + ''; - break; - case 'n/a': - tdString = 'n/a'; - break; - default: - tdString = stat[key]; - } + switch (stat ? key : 'n/a') { + case 'FrameId': + case 'EventId': + //tdString = '' + stat[key] + ''; + break; + case 'n/a': + tdString = 'n/a'; + break; + default: + tdString = stat[key]; + } - var td = $j('').html(tdString); - var row = $j('').append(th, td); + var td = $j('').html(tdString); + var row = $j('').append(th, td); - $j('#frameStatsTable tbody').append(row); - }); + $j('#frameStatsTable tbody').append(row); + }); + } // end foreach stat }) .fail(logAjaxFail); } @@ -85,16 +89,16 @@ function onStatsResize(vidwidth) { var width = $j(window).width() - vidwidth; // Hide the stats table if we have run out of room to show it properly - if ( width < minWidth ) { + if (width < minWidth) { statsBtn.prop('disabled', true); - if ( table.is(':visible') ) { + if (table.is(':visible')) { table.toggle(false); wasHidden = true; } // Show the stats table if we hid it previously and sufficient room becomes available - } else if ( width >= minWidth ) { + } else if (width >= minWidth) { statsBtn.prop('disabled', false); - if ( !table.is(':visible') && wasHidden ) { + if (!table.is(':visible') && wasHidden) { table.toggle(true); wasHidden = false; } @@ -102,7 +106,7 @@ function onStatsResize(vidwidth) { } function initPage() { - if ( scale == '0' || scale == 'auto' ) changeScale(); + if (scale == '0' || scale == 'auto') changeScale(); // Don't enable the back button if there is no previous zm page to go back to backBtn.prop('disabled', !document.referrer.length); @@ -125,7 +129,7 @@ function initPage() { var cookie = 'zmFrameStats'; // Toggle the visiblity of the stats table and write an appropriate cookie - if ( table.is(':visible') ) { + if (table.is(':visible')) { setCookie(cookie, 'off', 10*365); table.toggle(false); } else { @@ -143,7 +147,7 @@ function initPage() { // Load the frame stats getStat({eid: eid, fid: fid}); - if ( getFrmStatsCookie() != 'on' ) { + if (getFrameStatsCookie() != 'on') { table.toggle(false); } else { onStatsResize($j('#base_width').val() * scale / SCALE_BASE); diff --git a/web/skins/classic/views/js/monitor.js b/web/skins/classic/views/js/monitor.js index 21a6bca1d..0b69d6f6e 100644 --- a/web/skins/classic/views/js/monitor.js +++ b/web/skins/classic/views/js/monitor.js @@ -324,7 +324,6 @@ function update_estimated_ram_use() { var max_buffer_count = parseInt(document.querySelectorAll('input[name="newMonitor[MaxImageBufferCount]"]')[0].value); if (max_buffer_count) { var max_buffer_size = (min_buffer_count + max_buffer_count) * width * height * colours; - console.log(max_buffer_size); document.getElementById('estimated_ram_use').innerHTML += ' Max: ' + human_filesize(max_buffer_size); } else { document.getElementById('estimated_ram_use').innerHTML += ' Max: Unlimited'; @@ -365,4 +364,85 @@ function SecondPath_onChange(e) { } } +function populate_models(ManufacturerId) { + const dropdown = $j('[name="newMonitor[ModelId]"]'); + if (!dropdown.length) { + console.log("No element found for ModelId"); + return; + } + + dropdown.empty(); + dropdown.append(''); + dropdown.prop('selectedIndex', 0); + + if (ManufacturerId) { + // Populate dropdown with list of provinces + $j.getJSON(thisUrl+'?request=models&ManufacturerId='+ManufacturerId, function(data) { + if (data.result == 'Ok') { + $j.each(data.models, function(key, entry) { + dropdown.append($j('').attr('value', entry.Id).text(entry.Name)); + }); + dropdown.chosen("destroy"); + dropdown.chosen(); + } else { + alert(data.result); + } + }); + } + dropdown.chosen("destroy"); + dropdown.chosen(); +} + +function ManufacturerId_onchange(ManufacturerId_select) { + if (ManufacturerId_select.value) { + ManufacturerId_select.form.elements['newMonitor[Manufacturer]'].style['display'] = 'none'; + populate_models(ManufacturerId_select.value); + } else { + ManufacturerId_select.form.elements['newMonitor[Manufacturer]'].style['display'] = 'inline'; + // Set models dropdown to Unknown, text area visible + const ModelId_dropdown = $j('[name="newMonitor[ModelId]"]'); + ModelId_dropdown.empty(); + ModelId_dropdown.append(''); + ModelId_dropdown.prop('selectedIndex', 0); + $j('[name="newMonitor[Model]"]').show(); + } +} + +function select_by_value_case_insensitive(dropdown, value) { + const test_value = value.toLowerCase(); + for (i=1; i < dropdown.options.length; i++) { + if (dropdown.options[i].text.toLowerCase() == test_value) { + dropdown.selectedIndex = i; + dropdown.options[i].selected = true; + $j(dropdown).chosen("destroy").chosen(); + return; + } + } + if (dropdown.selectedIndex != 0) { + dropdown.selectedIndex = 0; + $j(dropdown).chosen("destroy").chosen(); + } +} + +function Manufacturer_onchange(input) { + if (!input.value) { + return; + } + ManufacturerId_select = input.form.elements['newMonitor[ManufacturerId]']; + select_by_value_case_insensitive(ManufacturerId_select, input.value); + populate_models(ManufacturerId_select.value); +} + +function ModelId_onchange(ModelId_select) { + if (parseInt(ModelId_select.value)) { + $j('[name="newMonitor[Model]"]').hide(); + } else { + $j('[name="newMonitor[Model]"]').show(); + } +} + +function Model_onchange(input) { + select_by_value_case_insensitive(input.form.elements['newMonitor[ModelId]'], input.value); +} + window.addEventListener('DOMContentLoaded', initPage); diff --git a/web/skins/classic/views/js/montage.js b/web/skins/classic/views/js/montage.js index 5312928a6..75b3a74d2 100644 --- a/web/skins/classic/views/js/montage.js +++ b/web/skins/classic/views/js/montage.js @@ -317,5 +317,10 @@ function initPage() { } selectLayout('#zmMontageLayout'); } + +function watchFullscreen() { + const content = document.getElementById('content'); + openFullscreen(content); +} // Kick everything off $j(document).ready(initPage); diff --git a/web/skins/classic/views/js/montage.js.php b/web/skins/classic/views/js/montage.js.php index 9b6962119..1c1c4130e 100644 --- a/web/skins/classic/views/js/montage.js.php +++ b/web/skins/classic/views/js/montage.js.php @@ -1,18 +1,6 @@ // // Import constants // -var STATE_IDLE = ; -var STATE_PREALARM = ; -var STATE_ALARM = ; -var STATE_ALERT = ; -var STATE_TAPE = ; - -var stateStrings = new Array(); -stateStrings[STATE_IDLE] = ""; -stateStrings[STATE_PREALARM] = ""; -stateStrings[STATE_ALARM] = ""; -stateStrings[STATE_ALERT] = ""; -stateStrings[STATE_TAPE] = ""; var CMD_QUERY = ; diff --git a/web/skins/classic/views/js/montagereview.js b/web/skins/classic/views/js/montagereview.js index 787a910d0..60856f4ca 100644 --- a/web/skins/classic/views/js/montagereview.js +++ b/web/skins/classic/views/js/montagereview.js @@ -57,7 +57,7 @@ function getFrame(monId, time, last_Frame) { var events_for_monitor = events_by_monitor_id[monId]; if ( !events_for_monitor ) { - console.log("No events for monitor " + monId); + //console.log("No events for monitor " + monId); return; } @@ -648,8 +648,11 @@ function setSpeed(speed_index) { } function setLive(value) { + // When we submit the context etc goes away but we may still be trying to update + // So kill the timer. + clearInterval(timerObj); liveMode = value; - var form = $j('#montagereview_form')[0]; + var form = document.getElementById('montagereview_form'); form.elements['live'].value = value; form.submit(); return false; @@ -985,6 +988,19 @@ function initPage() { }); }); + if ( !liveMode ) { + canvas = document.getElementById('timeline'); + + canvas.addEventListener('mousemove', mmove, false); + canvas.addEventListener('touchmove', tmove, false); + canvas.addEventListener('mousedown', mdown, false); + canvas.addEventListener('mouseup', mup, false); + canvas.addEventListener('mouseout', mout, false); + + ctx = canvas.getContext('2d'); + drawGraph(); + } + for ( var i = 0, len = monitorPtr.length; i < len; i += 1 ) { var monId = monitorPtr[i]; if ( !monId ) continue; @@ -1006,18 +1022,6 @@ function initPage() { } } // end foreach monitor - if ( !liveMode ) { - canvas = document.getElementById('timeline'); - - canvas.addEventListener('mousemove', mmove, false); - canvas.addEventListener('touchmove', tmove, false); - canvas.addEventListener('mousedown', mdown, false); - canvas.addEventListener('mouseup', mup, false); - canvas.addEventListener('mouseout', mout, false); - - ctx = canvas.getContext('2d'); - drawGraph(); - } setSpeed(speedIndex); //setFit(fitMode); // will redraw //setLive(liveMode); // will redraw diff --git a/web/skins/classic/views/js/montagereview.js.php b/web/skins/classic/views/js/montagereview.js.php index d82f103ed..f4b6222f4 100644 --- a/web/skins/classic/views/js/montagereview.js.php +++ b/web/skins/classic/views/js/montagereview.js.php @@ -239,6 +239,6 @@ echo "];\n"; var cWidth; // save canvas width var cHeight; // save canvas height var canvas; // global canvas definition so we don't have to keep looking it up -var ctx; +var ctx = null; var underSlider; // use this to hold what is hidden by the slider var underSliderX; // Where the above was taken from (left side, Y is zero) diff --git a/web/skins/classic/views/js/video.js b/web/skins/classic/views/js/video.js index 970119f4f..c764f33b5 100644 --- a/web/skins/classic/views/js/video.js +++ b/web/skins/classic/views/js/video.js @@ -16,6 +16,9 @@ function generateVideoResponse( data, responseText ) { } function generateVideo() { + $j.ajaxSetup({ + timeout: 0 + }); var form = $j('#videoForm').serialize(); $j.getJSON(thisUrl + '?view=request&request=event&action=video', form) .done(generateVideoResponse) diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index 8c8ea480d..89f774ed1 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -44,7 +44,7 @@ function ajaxRequest(params) { data.view = 'request'; data.request = 'watch'; data.mid = monitorId; - if ( auth_hash ) data.auth = auth_hash; + if (auth_hash) data.auth = auth_hash; $j.getJSON(thisUrl, data) .done(function(data) { @@ -94,17 +94,63 @@ function showPtzControls() { showMode = 'control'; } +function changeSize() { + var width = $j('#width').val(); + var height = $j('#height').val(); + + // Scale the frame + monitor_frame = $j('#imageFeed'); + if (!monitor_frame) { + console.log('Error finding frame'); + return; + } + if (width) monitor_frame.css('width', width); + if (height) monitor_frame.css('height', height); + + var streamImg = document.getElementById('liveStream'+monitorData[monIdx].id); + if (streamImg) { + if (streamImg.nodeName == 'IMG') { + let src = streamImg.src; + streamImg.src = ''; + src = src.replace(/width=[\.\d]+/i, 'width='+parseInt(width)); + src = src.replace(/height=[\.\d]+/i, 'height='+parseInt(height)); + src = src.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) )); + streamImg.src = src; + } + streamImg.style.width = width ? width : null; + streamImg.style.height = height ? height : null; + } else { + console.log('Did not find liveStream'+monitorData[monIdx].id); + } + $j('#scale').val(''); + setCookie('zmCycleScale', '', 3600); + setCookie('zmCycleWidth', width, 3600); + setCookie('zmCycleHeight', height, 3600); +} // end function changeSize() + function changeScale() { var scale = $j('#scale').val(); + setCookie('zmWatchScale'+monitorId, scale, 3600); + $j('#width').val('auto'); + $j('#height').val('auto'); + setCookie('zmCycleScale', scale, 3600); + setCookie('zmCycleWidth', 'auto', 3600); + setCookie('zmCycleHeight', 'auto', 3600); + var newWidth; var newHeight; var autoScale; + var streamImg = $j('#liveStream'+monitorId); + if (!streamImg.length) { + console.error('No element found for liveStream'+monitorId); + } + // Always turn it off, we will re-add it below. I don't know if you can add a callback multiple // times and what the consequences would be $j(window).off('resize', endOfResize); //remove resize handler when Scale to Fit is not active if (scale == '0' || scale == 'auto') { - var newSize = scaleToFit(monitorWidth, monitorHeight, $j('#liveStream'+monitorId), $j('#replayStatus')); + const newSize = scaleToFit(monitorWidth, monitorHeight, streamImg, $j('#dvrControls')); newWidth = newSize.width; newHeight = newSize.height; autoScale = newSize.autoScale; @@ -114,22 +160,21 @@ function changeScale() { newHeight = monitorHeight * scale / SCALE_BASE; } - setCookie('zmWatchScale'+monitorId, scale, 3600); - - var streamImg = $j('#liveStream'+monitorId); - if (streamImg) { - var oldSrc = streamImg.attr('src'); + if (streamImg.prop('nodeName') == 'IMG') { + const oldSrc = streamImg.attr('src'); streamImg.attr('src', ''); // This is so that we don't waste bandwidth and let the browser do all the scaling. if (autoScale > 100) autoScale = 100; if (scale > 100) scale = 100; - var newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+((scale == 'auto' || scale == '0') ? autoScale : scale)); + const newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+((scale == 'auto' || scale == '0') ? autoScale : scale)); + streamImg.css('width', newWidth+'px'); streamImg.width(newWidth); + streamImg.css('height', newHeight+'px'); streamImg.height(newHeight); streamImg.attr('src', newSrc); } else { - console.error('No element found for liveStream'+monitorId); + console.log("Not an IMG, can't set size"); } } // end function changeScale @@ -184,6 +229,15 @@ function setAlarmState(currentAlarmState) { lastAlarmState = alarmState; } // end function setAlarmState( currentAlarmState ) +function streamCmdReq(data) { + if (auth_hash) data.auth = auth_hash; + $j.getJSON(monitorUrl + '?view=request&request=stream&connkey='+connKey, data) + .done(getStreamCmdResponse) + .fail(getStreamCmdError); + + streamCmdTimer = null; +} + function getStreamCmdError(text, error) { console.log(error); // Error are normally due to failed auth. reload the page. @@ -200,9 +254,15 @@ function getStreamCmdResponse(respObj, respText) { // The get status command can get backed up, in which case we won't be able to get the semaphore and will exit. if (respObj.status) { streamStatus = respObj.status; - $j('#fpsValue').text(streamStatus.fps); - $j('#capturefpsValue').text(streamStatus.capturefps); - $j('#analysisfpsValue').text(streamStatus.analysisfps); + if ($j('#viewingFPSValue').text() != streamStatus.fps) { + $j('#viewingFPSValue').text(streamStatus.fps); + } + if ($j('#captureFPSValue').text() != streamStatus.capturefps) { + $j('#captureFPSValue').text(streamStatus.capturefps); + } + if ($j('#analysisFPSValue').text() != streamStatus.analysisfps) { + $j('#analysisFPSValue').text(streamStatus.analysisfps); + } setAlarmState(streamStatus.state); @@ -287,31 +347,21 @@ function getStreamCmdResponse(respObj, respText) { // Try to reload the image stream. var streamImg = $j('#liveStream'+monitorId); if (streamImg) { - var oldSrc = streamImg.attr('src'); - var newSrc = oldSrc.replace(/auth=\w+/i, 'auth='+streamStatus.auth); - if (oldSrc != newSrc) { - streamImg.attr('src', newSrc); - table.bootstrapTable('refresh'); + const oldSrc = streamImg.attr('src'); + if (oldSrc) { + const newSrc = oldSrc.replace(/auth=\w+/i, 'auth='+streamStatus.auth); + if (oldSrc != newSrc) { + streamImg.attr('src', newSrc); + table.bootstrapTable('refresh'); + } } } } // end if have a new auth hash } // end if respObj.status } else { + console.log("Not ok"); checkStreamForErrors('getStreamCmdResponse', respObj);//log them - // Try to reload the image stream. - // If it's an auth error, we should reload the whole page. - console.log("have error"); - //window.location.reload(); - var streamImg = $j('#liveStream'+monitorId); - if (streamImg) { - var oldSrc = streamImg.attr('src'); - var newSrc = oldSrc.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) )); - - streamImg.attr('src', newSrc); - console.log('Changing livestream src to ' + newSrc); - } else { - console.log('Unable to find streamImg liveStream'); - } + fetchImage($j('#imageFeed img')); } var streamCmdTimeout = statusRefreshTimeout; @@ -321,6 +371,10 @@ function getStreamCmdResponse(respObj, respText) { streamCmdTimer = setTimeout(streamCmdQuery, streamCmdTimeout); } +function streamCmdQuery() { + streamCmdReq({command: CMD_QUERY}); +} + function streamCmdPause(action) { setButtonState('pauseBtn', 'active'); setButtonState('playBtn', 'inactive'); @@ -360,20 +414,10 @@ function streamCmdPlay(action) { } } if (action) { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_PLAY; - streamCmdReq(data); + streamCmdReq({command: CMD_PLAY}); } } -function streamCmdReq(data) { - $j.getJSON(monitorUrl + '?view=request&request=stream&connkey='+connKey, data) - .done(getStreamCmdResponse) - .fail(getStreamCmdError); - - streamCmdTimer = null; -} function streamCmdStop(action) { setButtonState('pauseBtn', 'inactive'); @@ -386,10 +430,7 @@ function streamCmdStop(action) { setButtonState('fastRevBtn', 'unavail'); } if (action) { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_STOP; - streamCmdReq(data); + streamCmdReq({command: CMD_STOP}); } setButtonState('stopBtn', 'unavail'); setButtonState('playBtn', 'active'); @@ -406,10 +447,7 @@ function streamCmdFastFwd(action) { setButtonState('fastRevBtn', 'inactive'); } if (action) { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_FASTFWD; - streamCmdReq(data); + streamCmdReq({command: CMD_FASTFWD}); } } @@ -424,10 +462,7 @@ function streamCmdSlowFwd(action) { setButtonState('fastRevBtn', 'inactive'); } if (action) { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_SLOWFWD; - streamCmdReq(data); + streamCmdReq({command: CMD_SLOWFWD}); } setButtonState('pauseBtn', 'active'); if (monitorStreamReplayBuffer) { @@ -446,10 +481,7 @@ function streamCmdSlowRev(action) { setButtonState('fastRevBtn', 'inactive'); } if (action) { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_SLOWREV; - streamCmdReq(data); + streamCmdReq({command: CMD_SLOWREV}); } setButtonState('pauseBtn', 'active'); if (monitorStreamReplayBuffer) { @@ -468,16 +500,12 @@ function streamCmdFastRev(action) { setButtonState('fastRevBtn', 'inactive'); } if (action) { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_FASTREV; - streamCmdReq(data); + streamCmdReq({command: CMD_FASTREV}); } } function streamCmdZoomIn(x, y) { var data = {}; - if (auth_hash) data.auth = auth_hash; data.x = x; data.y = y; data.command = CMD_ZOOMIN; @@ -485,15 +513,11 @@ function streamCmdZoomIn(x, y) { } function streamCmdZoomOut() { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_ZOOMOUT; - streamCmdReq(data); + streamCmdReq({command: CMD_ZOOMOUT}); } function streamCmdScale(scale) { var data = {}; - if (auth_hash) data.auth = auth_hash; data.command = CMD_SCALE; data.scale = scale; streamCmdReq(data); @@ -501,20 +525,14 @@ function streamCmdScale(scale) { function streamCmdPan(x, y) { var data = {}; - if (auth_hash) data.auth = auth_hash; data.x = x; data.y = y; data.command = CMD_PAN; streamCmdReq(data); } -function streamCmdQuery() { - var data = {}; - if (auth_hash) data.auth = auth_hash; - data.command = CMD_QUERY; - streamCmdReq(data); -} +/* getStatusCmd is used when not streaming, since there is no persistent zms */ function getStatusCmdResponse(respObj, respText) { watchdogOk('status'); if (statusCmdTimer) { @@ -522,32 +540,33 @@ function getStatusCmdResponse(respObj, respText) { } if (respObj.result == 'Ok') { - $j('#fpsValue').text(respObj.monitor.FrameRate); + $j('#captureFPSValue').text(respObj.monitor.FrameRate); setAlarmState(respObj.monitor.Status); } else { checkStreamForErrors('getStatusCmdResponse', respObj); } var statusCmdTimeout = statusRefreshTimeout; - if ( alarmState == STATE_ALARM || alarmState == STATE_ALERT ) { + if (alarmState == STATE_ALARM || alarmState == STATE_ALERT) { statusCmdTimeout = statusCmdTimeout/5; } statusCmdTimer = setTimeout(statusCmdQuery, statusCmdTimeout); } function statusCmdQuery() { - $j.getJSON(monitorUrl + '?view=request&request=status&entity=monitor&element[]=Status&element[]=FrameRate&id='+monitorId) + $j.getJSON(monitorUrl + '?view=request&request=status&entity=monitor&element[]=Status&element[]=FrameRate&id='+monitorId+'&'+auth_relay) .done(getStatusCmdResponse) .fail(logAjaxFail); - streamCmdTimer = null; + statusCmdTimer = null; } function alarmCmdReq(data) { + if (auth_hash) data.auth = auth_hash; $j.getJSON(monitorUrl + '?view=request&request=alarm&id='+monitorId, data) .done(getAlarmCmdResponse) .fail(function(jqxhr, textStatus, error) { - if ( textStatus === "timeout" ) { + if (textStatus === 'timeout') { streamCmdQuery(); } else { logAjaxFail(jqxhr, textStatus, error); @@ -561,14 +580,12 @@ function getAlarmCmdResponse(respObj, respText) { function cmdDisableAlarms() { var data = {}; - if (auth_hash) data.auth = auth_hash; data.command = 'disableAlarms'; alarmCmdReq(data); } function cmdEnableAlarms() { var data = {}; - if (auth_hash) data.auth = auth_hash; data.command = 'enableAlarms'; alarmCmdReq(data); } @@ -583,7 +600,6 @@ function cmdAlarm() { function cmdForceAlarm() { var data = {}; - if (auth_hash) data.auth = auth_hash; data.command = 'forceAlarm'; alarmCmdReq(data); if (window.event) window.event.preventDefault(); @@ -591,7 +607,6 @@ function cmdForceAlarm() { function cmdCancelForcedAlarm() { var data = {}; - if (auth_hash) data.auth = auth_hash; data.command = 'cancelForcedAlarm'; alarmCmdReq(data); if (window.event) window.event.preventDefault(); @@ -607,6 +622,7 @@ function cmdForce() { } function controlReq(data) { + if (auth_hash) data.auth = auth_hash; $j.getJSON(monitorUrl + '?view=request&request=control&id='+monitorId, data) .done(getControlResponse) .fail(logAjaxFail); @@ -639,16 +655,16 @@ function controlCmd(event) { var data = {}; if (event && (xtell || ytell)) { - var target = event.target; - var offset = $j(target).offset(); - var width = $j(target).width(); - var height = $j(target).height(); + const target = event.target; + const offset = $j(target).offset(); + const width = $j(target).width(); + const height = $j(target).height(); - var x = event.pageX - offset.left; - var y = event.pageY - offset.top; + const x = event.pageX - offset.left; + const y = event.pageY - offset.top; if (xtell) { - var xge = parseInt((x*100)/width); + let xge = parseInt((x*100)/width); if (xtell == -1) { xge = 100 - xge; } else if (xtell == 2) { @@ -657,7 +673,7 @@ function controlCmd(event) { data.xge = xge; } if (ytell) { - var yge = parseInt((y*100)/height); + let yge = parseInt((y*100)/height); if (ytell == -1) { yge = 100 - yge; } else if (ytell == 2) { @@ -667,7 +683,6 @@ function controlCmd(event) { } } - if (auth_hash) data.auth = auth_hash; data.control = control; controlReq(data); @@ -678,7 +693,6 @@ function controlCmd(event) { function controlCmdImage(x, y) { var data = {}; - if (auth_hash) data.auth = auth_hash; data.scale = scale; data.control = imageControlMode; data.x = x; @@ -691,7 +705,9 @@ function controlCmdImage(x, y) { } function fetchImage(streamImage) { - streamImage.attr('src', streamImage.attr('src').replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) ))); + const oldsrc = streamImage.attr('src'); + streamImage.attr('src', ''); + streamImage.attr('src', oldsrc.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) ))); } function handleClick(event) { @@ -768,7 +784,6 @@ function reloadWebSite() { function updatePresetLabels() { var lblNdx = $j('#ctrlPresetForm option:selected').val(); - $j('#newLabel').val(labels[lblNdx]); } @@ -878,23 +893,21 @@ function initPage() { // Load the PTZ Preset modal into the DOM if (monitorControllable) getCtrlPresetModal(); // Load the settings modal into the DOM - if (monitorType == "Local") getSettingsModal(); + if (monitorType == 'Local') getSettingsModal(); } if (monitorType != 'WebSite') { if (streamMode == 'single') { - statusCmdTimer = setTimeout(statusCmdQuery, (Math.random()+0.1)*statusRefreshTimeout); + statusCmdTimer = setTimeout(statusCmdQuery, 200); setInterval(watchdogCheck, statusRefreshTimeout*2, 'status'); } else { - streamCmdTimer = setTimeout(streamCmdQuery, (Math.random()+0.1)*statusRefreshTimeout); + streamCmdTimer = setTimeout(streamCmdQuery, 200); setInterval(watchdogCheck, statusRefreshTimeout*2, 'stream'); } if (canStreamNative || (streamMode == 'single')) { var streamImg = $j('#imageFeed img'); - if (!streamImg) { - streamImg = $j('#imageFeed object'); - } + if (!streamImg) streamImg = $j('#imageFeed object'); if (!streamImg) { console.error('No streamImg found for imageFeed'); } else { @@ -905,6 +918,11 @@ function initPage() { streamImg.click(function(event) { handleClick(event); }); + streamImg.on("error", function(thing) { + console.log("Error loading image"); + console.log(thing); + setInterval(fetchImage, 100, $j('#imageFeed img')); + }); } } // end if have streamImg } // streamMode native or single @@ -919,6 +937,9 @@ function initPage() { el.onchange = window['changeScale']; }); changeScale(); + document.querySelectorAll('select[name="changeRate"]').forEach(function(el) { + el.onchange = window['changeRate'].bind(el, el); + }); } else if (monitorRefresh > 0) { setInterval(reloadWebSite, monitorRefresh*1000); } @@ -944,8 +965,20 @@ function initPage() { $j('#settingsModal').modal('show'); }); + bindButton('#cyclePlayBtn', 'click', null, cycleStart); + bindButton('#cyclePauseBtn', 'click', null, cyclePause); + bindButton('#cycleNextBtn', 'click', null, cycleNext); + bindButton('#cyclePrevBtn', 'click', null, cyclePrev); + bindButton('#cycleToggle', 'click', null, cycleToggle); + bindButton('#cyclePeriod', 'change', null, cyclePeriodChange); + if (cycle) { + cycleStart(); + } else { + cyclePause(); + } + // Only enable the settings button for local cameras - settingsBtn.prop('disabled', !(canView.Control && monitorType == 'Local')); + settingsBtn.prop('disabled', !(canView.Control && (monitorType == 'Local'))); // Init the bootstrap-table if (monitorType != 'WebSite') table.bootstrapTable({icons: icons}); @@ -970,5 +1003,106 @@ function initPage() { }); } // initPage +function watchFullscreen() { + const btn = document.getElementById('fullscreenBtn'); + if (btn.firstElementChild.innerHTML=='fullscreen') { + const content = document.getElementById('content'); + openFullscreen(content); + btn.firstElementChild.innerHTML='fullscreen_exit'; + btn.setAttribute('title', translate["Exit Fullscreen"]); + } else { + closeFullscreen(); + btn.firstElementChild.innerHTML='fullscreen'; + btn.setAttribute('title', translate["Fullscreen"]); + } +} + +var intervalId; +var secondsToCycle = 0; + +function nextCycleView() { + secondsToCycle --; + if (secondsToCycle<=0) { + window.location.replace('?view=watch&mid='+nextMid+'&mode='+mode+'&cycle=true'); + } + $j('#secondsToCycle').text(secondsToCycle); +} + +function cyclePause() { + clearInterval(intervalId); + $j('#cyclePauseBtn').hide(); + $j('#cyclePlayBtn').show(); +} + +function cycleStart() { + secondsToCycle = $j('#cyclePeriod').val(); + intervalId = setInterval(nextCycleView, 1000); + $j('#cyclePauseBtn').show(); + $j('#cyclePlayBtn').hide(); +} + +function cycleNext() { + monIdx ++; + if (monIdx >= monitorData.length) { + monIdx = 0; + } + if (!monitorData[monIdx]) { + console.log('No monitorData for ' + monIdx); + } + window.location.replace('?view=watch&cycle=true&mid='+monitorData[monIdx].id+'&mode='+mode); +} + +function cyclePrev() { + monIdx --; + if (monIdx < 0) { + monIdx = monitorData.length - 1; + } + if (!monitorData[monIdx]) { + console.log('No monitorData for ' + monIdx); + } + window.location.replace('?view=watch&cycle=true&mid='+monitorData[monIdx].id+'&mode='+mode); +} + +function cyclePeriodChange() { + const cyclePeriodSelect = $j('#cyclePeriod'); + secondsToCycle = cyclePeriodSelect.val(); + setCookie('zmCyclePeriod', secondsToCycle, 3600); +} +function cycleToggle(e) { + sidebar = $j('#sidebar'); + button = $j('#cycleToggle'); + if (sidebar.is(":visible")) { + sidebar.hide(); + setCookie('zmCycleShow', false, 3600); + } else { + sidebar.show(); + setCookie('zmCycleShow', true, 3600); + } + button.toggleClass('btn-secondary'); + button.toggleClass('btn-primary'); +} + +function changeRate(e) { + const newvalue = $j(e).val(); + if (1) { + var data = {}; + data.command = CMD_MAXFPS; + data.maxfps = newvalue; + streamCmdReq(data); + } else { + streamImage = $j('#liveStream'+monitorData[monIdx].id); + const oldsrc = streamImage.attr('src'); + streamImage.attr('src', ''); // stop streaming + console.log(newvalue); + if (newvalue == '0') { + // Unlimited + streamImage.attr('src', oldsrc.replace(/maxfps=\d+/i, 'maxfps=0.00100')); + } else { + streamImage.attr('src', oldsrc.replace(/maxfps=\d+/i, 'maxfps='+newvalue)); + } + } + setCookie('zmWatchRate', newvalue, 3600); +} + // Kick everything off $j(document).ready(initPage); diff --git a/web/skins/classic/views/js/watch.js.php b/web/skins/classic/views/js/watch.js.php index 258a1da73..04a6c0757 100644 --- a/web/skins/classic/views/js/watch.js.php +++ b/web/skins/classic/views/js/watch.js.php @@ -1,33 +1,20 @@ // // Import constants // -var STATE_IDLE = ; -var STATE_PREALARM = ; -var STATE_ALARM = ; -var STATE_ALERT = ; -var STATE_TAPE = ; -var stateStrings = new Array(); -stateStrings[STATE_IDLE] = ""; -stateStrings[STATE_PREALARM] = ""; -stateStrings[STATE_ALARM] = ""; -stateStrings[STATE_ALERT] = ""; -stateStrings[STATE_TAPE] = ""; - -var deleteString = ""; - -var enableAlarmsStr = ""; -var disableAlarmsStr = ""; -var forceAlarmStr = ""; -var cancelForcedAlarmStr = ""; var CMD_NONE = ; var CMD_PAUSE = ; @@ -45,8 +32,7 @@ var CMD_PREV = ; var CMD_NEXT = ; var CMD_SEEK = ; var CMD_QUERY = ; - -var SCALE_BASE = ; +var CMD_MAXFPS = ; var SOUND_ON_ALARM = ; var POPUP_ON_ALARM = ; @@ -54,6 +40,7 @@ var LIST_THUMBS = ; var streamMode = ""; var showMode = ""; +var cycle = ; var connKey = ''; var maxDisplayEvents = ; @@ -67,6 +54,28 @@ var monitorRefresh = 'Refresh() ?>'; var monitorStreamReplayBuffer = StreamReplayBuffer() ?>; var monitorControllable = Controllable()?'true':'false' ?>; +var monIdx = ; +var nextMid = ""; +var mode = ""; + +var monitorData = new Array(); + +monitorData[monitorData.length] = { + 'id': Id() ?>, + 'width': ViewWidth() ?>, + 'height':ViewHeight() ?>, + 'url': 'UrlToIndex() ?>', + 'onclick': function(){window.location.assign( '?view=watch&mid=Id() ?>' );}, + 'type': 'Type() ?>', + 'refresh': 'Refresh() ?>' +}; + + +var SCALE_BASE = ; var scale = ''; var statusRefreshTimeout = ; @@ -75,17 +84,16 @@ var imageRefreshTimeout = ; var canStreamNative = ; -Control(); - if ( $control->CanMoveMap() ) { ?> -var imageControlMode = "moveMap"; -CanMoveRel() ) { ?> -var imageControlMode = "movePseudoMap"; -CanMoveCon() ) { ?> -var imageControlMode = "moveConMap"; - -var imageControlMode = null; - +var imageControlMode = 'Control(); +if ($control->CanMoveMap()) { + echo 'moveMap'; +} else if ($control->CanMoveRel()) { + echo 'movePseudoMap'; +} else if ($control->CanMoveCon()) { + echo 'moveConMap'; +} +?>'; var refreshApplet = ; var appletRefreshTime = ; @@ -93,13 +101,18 @@ var appletRefreshTime = ; var labels = new Array(); Id() ) ) as $row ) { - $labels[$row['Preset']] = $row['Label']; -} - -foreach ( $labels as $index=>$label ) { -?> -labels[] = ''; -Id())) as $row) { + $label = $labels[$row['Preset']] = $row['Label']; + echo 'labels['. validInt($index) .'] = \''.validJsStr($label).'\''; } ?> +var deleteString = ""; +var enableAlarmsStr = ""; +var disableAlarmsStr = ""; +var forceAlarmStr = ""; +var cancelForcedAlarmStr = ""; +var translate = { + "seconds": "", + "Fullscreen": "", + "Exit Fullscreen": "", +}; diff --git a/web/skins/classic/views/js/zone.js b/web/skins/classic/views/js/zone.js index 6282cbf8a..58c286b43 100644 --- a/web/skins/classic/views/js/zone.js +++ b/web/skins/classic/views/js/zone.js @@ -661,6 +661,7 @@ function initPage() { // Start the fps and status updates. give a random delay so that we don't assault the server var delay = Math.round( (Math.random()+0.5)*statusRefreshTimeout ); + monitors[i].setScale('auto'); monitors[i].start(delay); } diff --git a/web/skins/classic/views/js/zone.js.php b/web/skins/classic/views/js/zone.js.php index fc6616327..2ba286d55 100644 --- a/web/skins/classic/views/js/zone.js.php +++ b/web/skins/classic/views/js/zone.js.php @@ -66,6 +66,7 @@ monitorData[monitorData.length] = { 'width': ViewWidth() ?>, 'height':ViewHeight() ?>, 'url': 'UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', + 'url_to_zms': 'UrlToZMS( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', 'type': 'Type() ?>', 'refresh': 'Refresh() ?>' }; @@ -93,20 +94,6 @@ var deleteString = ""; // Imported from watch.js.php and modified for new zone edit view // -var STATE_IDLE = ; -var STATE_PREALARM = ; -var STATE_ALARM = ; -var STATE_ALERT = ; -var STATE_TAPE = ; - -var stateStrings = new Array(); -stateStrings[STATE_IDLE] = ""; -stateStrings[STATE_PREALARM] = ""; -stateStrings[STATE_ALARM] = ""; -stateStrings[STATE_ALERT] = ""; -stateStrings[STATE_TAPE] = ""; - - var CMD_PAUSE = ; var CMD_PLAY = ; var CMD_STOP = ; diff --git a/web/skins/classic/views/js/zones.js b/web/skins/classic/views/js/zones.js index 0ad91942d..67dcb094a 100644 --- a/web/skins/classic/views/js/zones.js +++ b/web/skins/classic/views/js/zones.js @@ -12,6 +12,7 @@ function initPage() { // Start the fps and status updates. give a random delay so that we don't assault the server var delay = Math.round( (Math.random()+0.5)*statusRefreshTimeout ); + monitors[i].setScale('auto'); monitors[i].start(delay); } @@ -31,5 +32,12 @@ function initPage() { }); } +function streamCmdQuit() { + for ( var i = 0, length = monitorData.length; i < length; i++ ) { + monitors[i] = new MonitorStream(monitorData[i]); + monitors[i].stop(); + } +} + window.addEventListener('DOMContentLoaded', initPage); diff --git a/web/skins/classic/views/js/zones.js.php b/web/skins/classic/views/js/zones.js.php index 180a19d81..414231d12 100644 --- a/web/skins/classic/views/js/zones.js.php +++ b/web/skins/classic/views/js/zones.js.php @@ -9,6 +9,7 @@ monitorData[monitorData.length] = { 'width': ViewWidth() ?>, 'height':ViewHeight() ?>, 'url': 'UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', + 'url_to_zms': 'UrlToZMS( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', 'type': 'Type() ?>', 'refresh': 'Refresh() ?>' }; @@ -16,25 +17,10 @@ monitorData[monitorData.length] = { } ?> -var STATE_IDLE = ; -var STATE_PREALARM = ; -var STATE_ALARM = ; -var STATE_ALERT = ; -var STATE_TAPE = ; - -var stateStrings = new Array(); -stateStrings[STATE_IDLE] = ""; -stateStrings[STATE_PREALARM] = ""; -stateStrings[STATE_ALARM] = ""; -stateStrings[STATE_ALERT] = ""; -stateStrings[STATE_TAPE] = ""; - - var CMD_PAUSE = ; var CMD_PLAY = ; var CMD_STOP = ; var CMD_QUERY = ; var CMD_QUIT = ; - var statusRefreshTimeout = ; diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 7b414d3e0..1ddea7143 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -451,7 +451,7 @@ foreach ($tabs as $name=>$value) { switch ($name) { case 'general' : { - if (!$monitor->Id()) { + if (!$monitor->Id() and count($monitors)) { $monitor_ids = array(); foreach ($monitors as $m) { $monitor_ids[] = $m['Id']; } $available_monitor_ids = array_diff(range(min($monitor_ids),max($monitor_ids)), $monitor_ids); @@ -459,13 +459,16 @@ switch ($name) {
-10 Available Ids: - - + + Id() + } # end if ! $monitor->Id() and count($monitors) ?> @@ -475,6 +478,44 @@ switch ($name) { + + + +translate('Unknown')); + foreach ( ZM\Manufacturer::find( null, array('order'=>'lower(Name)')) as $Manufacturer ) { + $manufacturers[$Manufacturer->Id()] = $Manufacturer->Name(); + } + echo htmlSelect('newMonitor[ManufacturerId]', $manufacturers, $monitor->ManufacturerId(), + array('class'=>'chosen','data-on-change-this'=>'ManufacturerId_onchange')); +?> + ManufacturerId() ? ' style="display:none"' : '' ?> + data-on-input-this="Manufacturer_onchange" + /> + + + + + +translate('Unknown')); + foreach ( ZM\Model::find(array('ManufacturerId'=>$monitor->ManufacturerId()), array('order'=>'lower(Name)')) as $Model ) { + $models[$Model->Id()] = $Model->Name(); + } + echo htmlSelect('newMonitor[ModelId]', $models, $monitor->ModelId(), + array('class'=>'chosen', 'data-on-change-this'=>'ModelId_onchange')); +?> + ModelId() ? ' style="display:none"':'' ?> + data-on-input-this="Model_onchange" + /> + + 'lower(Name)')); if (count($Servers)) { @@ -651,6 +692,14 @@ switch ($name) { } ?> + + + + + + + + '1080px', ); - $layouts = ZM\MontageLayout::find(NULL, array('order'=>"lower('Name')")); $layoutsById = array(); foreach ( $layouts as $l ) { @@ -149,7 +149,7 @@ echo getNavBarHTML(); $html .= ' Type() != 'WebSite') ) { + if ((!ZM_WEB_COMPACT_MONTAGE) && ($monitor->Type() != 'WebSite')) { ?> -
 -  fps
+
+ : + +  -  +  fps +
diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 22b164202..25cad40cc 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -44,8 +44,9 @@ $tabs['medband'] = translate('MediumBW'); $tabs['lowband'] = translate('LowBW'); $tabs['users'] = translate('Users'); $tabs['control'] = translate('Control'); +$tabs['privacy'] = translate('Privacy'); -if ( isset($_REQUEST['tab']) ) +if (isset($_REQUEST['tab'])) $tab = validHtmlStr($_REQUEST['tab']); else $tab = 'system'; @@ -53,7 +54,6 @@ else $focusWindow = true; xhtmlHeaders(__FILE__, translate('Options')); - ?> @@ -62,7 +62,7 @@ xhtmlHeaders(__FILE__, translate('Options'));