diff --git a/.gitmodules b/.gitmodules index b8be6d5d..64b19c5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "libavio"] path = libavio url = https://github.com/sr99622/libavio +[submodule "liblivemedia"] + path = liblivemedia + url = https://github.com/sr99622/liblivemedia diff --git a/README.md b/README.md index e95b35b0..d816f58d 100644 --- a/README.md +++ b/README.md @@ -754,7 +754,9 @@ Right clicking over the file will bring up a context menu that can be used to pe --- - +## General Settings + + ### Common Username and Password @@ -768,37 +770,34 @@ A hardware decoder may be selected for the application. Mulitcore CPUs with more Selecting this check box will cause the application to start in full screen mode. The full screen mode can be cancelled with the Escape key. The F12 key will also toggle full screen mode. -### Auto Discovery - -When selected, this option will cause the application to discover cameras automatically when it starts. This holds true whether the application is using Broadcast Discovery or Cached Addresses. Note that if this option is selected and the Broadcast Discovery Option is also selected, the application will poll the network once per minute to find missing or new cameras. +### Auto TIme Sync -### Auto Start +This selection will send a time sync message to each of the cameras once an hour. The camera time is set to the host computer time without regard for time zone. -When selected in combination with the Auto Discovery check box, cameras shown in the list will start automatically when the application starts. This feature will work with either Discovery Broadcast or Cached Adresses. +### Display Refresh Interval -### Auto TIme Sync +Performance on some lower powered systems may be improved by increasing the display refresh interval. -This selection will send a time sync message to each of the cameras once an hour. The camera time is set to the host computer time without regard for time zone. +### Maximum Input Stream Cache Size -### Pre-Alarm Buffer Size +Adjust the maximum number of frames held in the cache before frames are dropped. This is the same cache referred to by the Video Tab of the Camera Panel. -When a camera is recording, this length of media is prepended to the file so that the moments prior to the alarm are preserved. If always recording, or the file length is limited by the system to 15 minutes, this feature will insure that there is a small overlap between adjacent files. +### Start All Cameras / Close All Streams -### Post-Alarm Lag Time +This button will change appearance depending on whether there are streams playing or not. It can be used to batch control cameras to start or stop as a group. It will start all cameras on the Camera List. It will stop all streams, including files if playing. -In the case where a camera is configured to record during alarms, this length of time must pass after the cessation of the alarm before the file recording is turned off. This helps to prevent excessive file creation. +### Show Logs -### Alarm Sounds +This button will show the logs of the application. Many events and errors encountered will be documented here. The log rolls over at 1 MB. The older logs can be managed using the Archive button on the logs display dialog. Note that on Linux, the archive file selection dialog may be slow to open or may require some mouse movement to visualize. -A few default alarm sounds for selection. A system wide volume setting for the alarm volume can be made with the slider. +### Help -### Display Refresh Interval +Shows this file. -Performance on some lower powered systems may be improved by increasing the display refresh interval. -### Maximum Input Stream Cache Size +## Discover Settings -Adjust the maximum number of frames held in the cache before frames are dropped. This is the same cache referred to by the Video Tab of the Camera Panel. + ### Discovery Options @@ -814,6 +813,18 @@ Adjust the maximum number of frames held in the cache before frames are dropped. It is possible to add a camera manually to the address cache by using the Add Camera button. The IP address and ONVIF port are required to connect. The ONVIF port by default is 80. If successful, the camera will be added silently to the camera list. +### Auto Discovery + +When selected, this option will cause the application to discover cameras automatically when it starts. This holds true whether the application is using Broadcast Discovery or Cached Addresses. Note that if this option is selected and the Broadcast Discovery Option is also selected, the application will poll the network once per minute to find missing or new cameras. + +### Auto Start + +When selected in combination with the Auto Discovery check box, cameras shown in the list will start automatically when the application starts. This feature will work with either Discovery Broadcast or Cached Adresses. + +## Storage Settings + + + ### Disk Usage The application has the ability to manage the disk space used by the recorded media files. This setting is recommended as the files can overwhelm the computer and cause the application to crash. Allocating a directory for the camera recordings is done by assigning a directory using the Archive Dir selection widget. The default setting for the Archive Dir is the user's Video directory. It is advised to change this setting if the host computer employs the user's Video directory for other applications. @@ -830,17 +841,39 @@ The application has the ability to manage the disk space used by the recorded me The spin box can be used to limit the application disk usage in GB. Note that the application is conservative in it's estimate of required file size and the actual space occupied by the media files will be a few GB less than the allocated space. -### Start All Cameras / Close All Streams +## Proxy Settings -This button will change appearance depending on whether there are streams playing or not. It can be used to batch control cameras to start or stop as a group. It will start all cameras on the Camera List. It will stop all streams, including files if playing. + -### Show Logs +## Proxy Type -This button will show the logs of the application. Many events and errors encountered will be documented here. The log rolls over at 1 MB. The older logs can be managed using the Archive button on the logs display dialog. Note that on Linux, the archive file selection dialog may be slow to open or may require some mouse movement to visualize. +* Stand Alone -### Help + Default setting, implements a single instance of the progran that connects to the cameras directly. -Shows this file. +* Server + + The application will host a proxy server and allow other instances of the application configured as clients to connect over the local network to the cameras proxied by the server. The connection string required for the clients will be displayed. + +* Client + + The application will act as a client to the proxy server. Use the connection string displayed by the server in the url box. + +## Alarm Settings + + + +### Pre-Alarm Buffer Size + +When a camera is recording, this length of media is prepended to the file so that the moments prior to the alarm are preserved. If always recording, or the file length is limited by the system to 15 minutes, this feature will insure that there is a small overlap between adjacent files. + +### Post-Alarm Lag Time + +In the case where a camera is configured to record during alarms, this length of time must pass after the cessation of the alarm before the file recording is turned off. This helps to prevent excessive file creation. + +### Alarm Sounds + +A few default alarm sounds for selection. A system wide volume setting for the alarm volume can be made with the slider. ---   diff --git a/assets/images/alarm.png b/assets/images/alarm.png new file mode 100644 index 00000000..29ffea58 Binary files /dev/null and b/assets/images/alarm.png differ diff --git a/assets/images/discover.png b/assets/images/discover.png new file mode 100644 index 00000000..cf7516c8 Binary files /dev/null and b/assets/images/discover.png differ diff --git a/assets/images/general.png b/assets/images/general.png new file mode 100644 index 00000000..8bc851d6 Binary files /dev/null and b/assets/images/general.png differ diff --git a/assets/images/proxy.png b/assets/images/proxy.png new file mode 100644 index 00000000..c6cbbbd8 Binary files /dev/null and b/assets/images/proxy.png differ diff --git a/assets/images/storage.png b/assets/images/storage.png new file mode 100644 index 00000000..3bac8f18 Binary files /dev/null and b/assets/images/storage.png differ diff --git a/assets/scripts/build_pkgs b/assets/scripts/build_pkgs index fef78b2b..a3f5cb42 100755 --- a/assets/scripts/build_pkgs +++ b/assets/scripts/build_pkgs @@ -4,9 +4,12 @@ cd libonvif python -m build cd ../libavio python -m build +cd ../liblivemedia +python -m build cd ../onvif-gui python -m build cd .. for FILE in libonvif/dist/*.whl; do pip install $FILE; done for FILE in libavio/dist/*.whl; do pip install $FILE; done +for FILE in liblivemedia/dist/*.whl; do pip install $FILE; done for FILE in onvif-gui/dist/*.whl; do pip install $FILE; done diff --git a/assets/scripts/build_pkgs.bat b/assets/scripts/build_pkgs.bat index ee77da72..cc2b959b 100755 --- a/assets/scripts/build_pkgs.bat +++ b/assets/scripts/build_pkgs.bat @@ -9,9 +9,14 @@ set FFMPEG_INSTALL_DIR=%CD%/ffmpeg set SDL2_INSTALL_DIR=%CD%/sdl python -m build cd .. +cd liblivemedia +set CMAKE_CURRENT_SOURCE_DIR=%CD% +python -m build +cd .. cd onvif-gui python -m build cd .. for /R libonvif\dist %%F in (*.whl) do pip install "%%F" for /R libavio\dist %%F in (*.whl) do pip install "%%F" +for /R liblivemedia\dist %%F in (*.whl) do pip install "%%F" for /R onvif-gui\dist %%F in (*.whl) do pip install "%%F" diff --git a/assets/scripts/clean b/assets/scripts/clean index 4ddead04..a02a8799 100755 --- a/assets/scripts/clean +++ b/assets/scripts/clean @@ -2,32 +2,58 @@ find . -type f -name '._*' -delete cd libonvif -FILE=build -if [ -d "$FILE" ]; then +DIR=build +if [ -d "$DIR" ]; then rm -R build fi -FILE=libonvif.egg-info -if [ -d "$FILE" ]; then +DIR=dist +if [ -d "$DIR" ]; then + rm -R dist +fi +DIR=libonvif.egg-info +if [ -d "$DIR" ]; then rm -R libonvif.egg-info fi cd ../libavio -FILE=build -if [ -d "$FILE" ]; then +DIR=build +if [ -d "$DIR" ]; then rm -R build fi -FILE=avio.egg-info -if [ -d "$FILE" ]; then +DIR=dist +if [ -d "$DIR" ]; then + rm -R dist +fi +DIR=avio.egg-info +if [ -d "$DIR" ]; then rm -R avio.egg-info fi +cd ../liblivemedia +DIR=build +if [ -d "$DIR" ]; then + rm -R build +fi +DIR=dist +if [ -d "$DIR" ]; then + rm -R dist +fi +DIR=liblivemedia.egg-info +if [ -d "$DIR" ]; then + rm -R liblivemedia.egg-info +fi + cd ../onvif-gui -FILE=build -if [ -d "$FILE" ]; then +DIR=build +if [ -d "$DIR" ]; then rm -R build fi -FILE=onvif_gui.egg-info -if [ -d "$FILE" ]; then +DIR=dist +if [ -d "$DIR" ]; then + rm -R dist +fi +DIR=onvif_gui.egg-info +if [ -d "$DIR" ]; then rm -R onvif_gui.egg-info fi diff --git a/assets/scripts/clean.bat b/assets/scripts/clean.bat index e0a25768..7d4d5ea1 100755 --- a/assets/scripts/clean.bat +++ b/assets/scripts/clean.bat @@ -1,22 +1,41 @@ -cd libonvif -if exist build\ ( - rmdir /s /q build -) -if exist libonvif.egg-info\ ( - rmdir /s /q libonvif.egg-info -) -cd ../libavio -if exist build\ ( - rmdir /s /q build -) -if exist avio.egg-info\ ( - rmdir /s /q avio.egg-info -) -cd ../onvif-gui -if exist build\ ( - rmdir /s /q build -) -if exist onvif_gui.egg-info\ ( - rmdir /s /q onvif_gui.egg-info -) -cd .. +cd libonvif +if exist build\ ( + rmdir /s /q build +) +if exist dist\ ( + rmdir /s /q dist +) +if exist libonvif.egg-info\ ( + rmdir /s /q libonvif.egg-info +) +cd ../libavio +if exist build\ ( + rmdir /s /q build +) +if exist dist\ ( + rmdir /s /q dist +) +if exist avio.egg-info\ ( + rmdir /s /q avio.egg-info +) +cd ../liblivemedia +if exist build\ ( + rmdir /s /q build +) +if exist dist\ ( + rmdir /s /q dist +) +if exist liblivemedia.egg-info\ ( + rmdir /s /q liblivemedia.egg-info +) +cd ../onvif-gui +if exist build\ ( + rmdir /s /q build +) +if exist dist\ ( + rmdir /s /q dist +) +if exist onvif_gui.egg-info\ ( + rmdir /s /q onvif_gui.egg-info +) +cd .. diff --git a/assets/scripts/compile b/assets/scripts/compile index 9678c9d4..c89dac8f 100755 --- a/assets/scripts/compile +++ b/assets/scripts/compile @@ -23,6 +23,17 @@ if [ -d "$FILE" ]; then fi pip install -v . +cd ../liblivemedia +FILE=build +if [ -d "$FILE" ]; then + rm -R build +fi +FILE=liblivemedia.egg-info +if [ -d "$FILE" ]; then + rm -R liblivemedia.egg-info +fi +pip install -v . + cd ../onvif-gui FILE=build if [ -d "$FILE" ]; then diff --git a/assets/scripts/compile.bat b/assets/scripts/compile.bat index 1e1c0e43..55cc6aa1 100755 --- a/assets/scripts/compile.bat +++ b/assets/scripts/compile.bat @@ -1,25 +1,33 @@ -cd libonvif -if exist build\ ( - rmdir /s /q build -) -if exist libonvif.egg-info\ ( - rmdir /s /q libonvif.egg-info -) -pip install -v . -cd ../libavio -if exist build\ ( - rmdir /s /q build -) -if exist avio.egg-info\ ( - rmdir /s /q avio.egg-info -) -pip install -v . -cd ../onvif-gui -if exist build\ ( - rmdir /s /q build -) -if exist onvif_gui.egg-info\ ( - rmdir /s /q onvif_gui.egg-info -) -pip install . -cd .. +cd libonvif +if exist build\ ( + rmdir /s /q build +) +if exist libonvif.egg-info\ ( + rmdir /s /q libonvif.egg-info +) +pip install -v . +cd ../libavio +if exist build\ ( + rmdir /s /q build +) +if exist avio.egg-info\ ( + rmdir /s /q avio.egg-info +) +pip install -v . +cd ../liblivemedia +if exist build\ ( + rmdir /s /q build +) +if exist liblivemedia.egg-info\ ( + rmdir /s /q liblivemedia.egg-info +) +pip install -v . +cd ../onvif-gui +if exist build\ ( + rmdir /s /q build +) +if exist onvif_gui.egg-info\ ( + rmdir /s /q onvif_gui.egg-info +) +pip install . +cd .. diff --git a/libavio b/libavio index dd249be9..4aa8f670 160000 --- a/libavio +++ b/libavio @@ -1 +1 @@ -Subproject commit dd249be9bf2600d384ca58b3120eff1145250256 +Subproject commit 4aa8f6705e8911d21e84997666abe080c01ec316 diff --git a/liblivemedia b/liblivemedia new file mode 160000 index 00000000..3bd3a6c5 --- /dev/null +++ b/liblivemedia @@ -0,0 +1 @@ +Subproject commit 3bd3a6c5e4422b3bdfdc787d1e16d82953b75bed diff --git a/libonvif/CMakeLists.txt b/libonvif/CMakeLists.txt index 7a2a840b..0549f0d9 100644 --- a/libonvif/CMakeLists.txt +++ b/libonvif/CMakeLists.txt @@ -21,7 +21,7 @@ cmake_minimum_required(VERSION 3.17) -project(libonvif VERSION 3.1.1) +project(libonvif VERSION 3.2.1) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) diff --git a/libonvif/README.md b/libonvif/README.md index 60d88431..92124378 100644 --- a/libonvif/README.md +++ b/libonvif/README.md @@ -18,3 +18,29 @@ Copyright (c) 2020, 2023, 2024 Stephen Rhodes You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +# libxml2 + +Except where otherwise noted in the source code (e.g. the files dict.c and +list.c, which are covered by a similar licence but with different Copyright +notices) all the files are: + + Copyright (C) 1998-2012 Daniel Veillard. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/libonvif/include/onvif_data.h b/libonvif/include/onvif_data.h index 57c79ea5..1670f71e 100644 --- a/libonvif/include/onvif_data.h +++ b/libonvif/include/onvif_data.h @@ -1,799 +1,819 @@ -/******************************************************************************* -* onvif_data.h -* -* copyright 2023 Stephen Rhodes -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library 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 -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -* -*******************************************************************************/ - -#ifndef ONVIF_DATA_H -#define ONVIF_DATA_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include "onvif.h" - -namespace libonvif -{ - -class Data -{ -public: - std::function filled = nullptr; - std::function getData = nullptr; - std::function discovered = nullptr; - std::function getCredential = nullptr; - std::function setSetting = nullptr; - std::function getSetting = nullptr; - - OnvifData* data; - bool cancelled = false; - std::string alias; - int preset; - int stop_type; - float x, y, z; - bool synchronizeTime = false; - int displayProfile = 0; - - std::vector profiles; - - Data() - { - data = (OnvifData*)calloc(sizeof(OnvifData), 1); - } - - Data(OnvifData* onvif_data) - { - data = onvif_data; - } - - Data(const Data& other) - { - data = (OnvifData*)calloc(sizeof(OnvifData), 1); - copyData(data, other.data); - cancelled = other.cancelled; - alias = other.alias; - preset = other.preset; - stop_type = other.stop_type; - synchronizeTime = other.synchronizeTime; - profiles = other.profiles; - x = other.x; - y = other.y; - z = other.z; - } - - Data(Data&& other) noexcept - { - data = other.data; - other.data = nullptr; - cancelled = other.cancelled; - alias = other.alias; - preset = other.preset; - stop_type = other.stop_type; - synchronizeTime = other.synchronizeTime; - profiles = other.profiles; - x = other.x; - y = other.y; - z = other.z; - } - - Data& operator=(const Data& other) - { - if (!data) data = (OnvifData*)calloc(sizeof(OnvifData), 1); - copyData(data, other.data); - cancelled = other.cancelled; - alias = other.alias; - preset = other.preset; - stop_type = other.stop_type; - synchronizeTime = other.synchronizeTime; - profiles = other.profiles; - x = other.x; - y = other.y; - z = other.z; - return *this; - } - - Data& operator=(Data&& other) noexcept - { - if (data) free(data); - data = other.data; - cancelled = other.cancelled; - alias = other.alias; - other.data = nullptr; - preset = other.preset; - stop_type = other.stop_type; - synchronizeTime = other.synchronizeTime; - profiles = other.profiles; - x = other.x; - y = other.y; - z = other.z; - return *this; - } - - bool operator==(const Data& rhs) - { - if (strcmp(data->xaddrs, rhs.data->xaddrs)) { - return false; - } - else { - return true; - } - } - - bool operator!=(const Data& rhs) - { - if (strcmp(data->xaddrs, rhs.data->xaddrs)) { - return true; - } - else { - return false; - } - } - - bool friend operator==(const Data& lhs, const Data& rhs) - { - if (strcmp(lhs.data->xaddrs, rhs.data->xaddrs)) { - return false; - } - else { - return true; - } - } - - bool friend operator!=(const Data& lhs, const Data& rhs) - { - if (strcmp(lhs.data->xaddrs, rhs.data->xaddrs)) { - return true; - } - else { - return false; - } - } - - ~Data() - { - free(data); - } - - operator OnvifData* () - { - return data; - } - - OnvifData* operator ->() - { - return data; - } - - void startUpdateTime() - { - std::thread thread([&]() { updateTime(); }); - thread.detach(); - } - - void updateTime() - { - std::stringstream str; - if (setSystemDateAndTime(data)) str << data->last_error << " - "; - if (getTimeOffset(data)) str << data->last_error << " - "; - - memset(data->last_error, 0, 1024); - int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); - strncpy(data->last_error, str.str().c_str(), length); - - for (int i = 0; i < profiles.size(); i++) { - profiles[i].data->dst = data->dst; - profiles[i].data->datetimetype = data->datetimetype; - profiles[i].data->time_offset = data->time_offset; - profiles[i].data->ntp_dhcp = data->ntp_dhcp; - strcpy(profiles[i].data->timezone, data->timezone); - strcpy(profiles[i].data->ntp_type, data->ntp_type); - strcpy(profiles[i].data->ntp_addr, data->ntp_addr); - } - } - - void startStop() - { - std::thread thread([&]() { stop(); }); - thread.detach(); - } - - void stop() - { - moveStop(stop_type, data); - } - - void startMove() - { - std::thread thread([&]() { move(); }); - thread.detach(); - } - - void move() - { - continuousMove(x, y, z, data); - } - - void startSet() - { - std::thread thread([&]() { set(); }); - thread.detach(); - } - - void set() - { - char pos[128] = {0}; - sprintf(pos, "%d", preset); - gotoPreset(pos, data); - } - - void startSetGotoPreset() - { - std::thread thread([&]() { setGotoPreset(); }); - thread.detach(); - } - - void setGotoPreset() - { - char pos[128] = {0}; - sprintf(pos, "%d", preset); - setPreset(pos, data); - if (filled) filled(*this); - } - - void startUpdateVideo() - { - std::thread thread([&]() { updateVideo(); }); - thread.detach(); - } - - void updateVideo() - { - std::stringstream str; - if (setVideoEncoderConfiguration(data)) str << data->last_error << " - "; - if (getVideoEncoderConfigurationOptions(data)) str << data->last_error << " - "; - if (getVideoEncoderConfiguration(data)) str << data->last_error << " - "; - - memset(data->last_error, 0, 1024); - int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); - strncpy(data->last_error, str.str().c_str(), length); - - syncProfile(indexForProfile(data->profileToken)); - - if (filled) filled(*this); - } - - void startUpdateAudio() - { - std::thread thread([&] () { updateAudio(); }); - thread.detach(); - } - - void updateAudio() - { - std::stringstream str; - if (setAudioEncoderConfiguration(data)) str << data->last_error << " - "; - if (getAudioEncoderConfigurationOptions(data)) str << data->last_error << " - "; - if (getAudioEncoderConfiguration(data)) str << data->last_error << " - "; - - memset(data->last_error, 0, 1024); - int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); - strncpy(data->last_error, str.str().c_str(), length); - - syncProfile(indexForProfile(data->profileToken)); - - if (filled) filled(*this); - } - - void startUpdateImage() - { - std::thread thread([&]() { updateImage(); }); - thread.detach(); - } - - void updateImage() - { - std::stringstream str; - if (setImagingSettings(data)) str << data->last_error << " - "; - if (getOptions(data)) str << data->last_error << " - "; - if (getImagingSettings(data)) str << data->last_error << " - "; - - memset(data->last_error, 0, 1024); - int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); - strncpy(data->last_error, str.str().c_str(), length); - - for (int i = 0; i < profiles.size(); i++) { - profiles[i].data->brightness = data->brightness; - profiles[i].data->saturation = data->saturation; - profiles[i].data->contrast = data->contrast; - profiles[i].data->sharpness = data->sharpness; - } - - if (filled) filled(*this); - } - - void startUpdateNetwork() - { - std::thread thread([&]() { updateNetwork(); }); - thread.detach(); - } - - void updateNetwork() - { - std::stringstream str; - if (setNetworkInterfaces(data)) str << data->last_error << " - "; - if (setDNS(data)) str << data->last_error << " - "; - if (setNetworkDefaultGateway(data)) str << data->last_error << " - "; - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - if (getNetworkInterfaces(data)) str << data->last_error << " - "; - if (getNetworkDefaultGateway(data)) str << data->last_error << " - "; - if (getDNS(data)) str << data->last_error << " - "; - - memset(data->last_error, 0, 1024); - int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); - strncpy(data->last_error, str.str().c_str(), length); - - syncProfile(indexForProfile(data->profileToken)); - - for (int i = 0; i < profiles.size(); i++) { - if (strcmp(profiles[i].data->profileToken, data->profileToken)) { - strcpy(profiles[i].data->networkInterfaceToken, data->networkInterfaceToken); - strcpy(profiles[i].data->networkInterfaceName, data->networkInterfaceName); - strcpy(profiles[i].data->ip_address_buf, data->ip_address_buf); - strcpy(profiles[i].data->default_gateway_buf, data->default_gateway_buf); - strcpy(profiles[i].data->dns_buf, data->dns_buf); - strcpy(profiles[i].data->mask_buf, data->mask_buf); - profiles[i].data->dhcp_enabled = data->dhcp_enabled; - profiles[i].data->prefix_length = data->prefix_length; - } - } - - if (filled) filled(*this); - } - - void startReboot() - { - std::thread thread([&]() { reboot(); }); - thread.detach(); - } - - void reboot() - { - rebootCamera(data); - } - - void startReset() - { - std::thread thread([&]() { reset(); }); - thread.detach(); - } - - void reset() - { - hardReset(data); - } - - void startSetUser() - { - std::thread thread([&]() { setOnvifUser(); }); - thread.detach(); - } - - void setOnvifUser() - { - /* - if (setUser((char*)new_password.c_str(), onvif_data) == 0) - onvif_data.setPassword(new_password.c_str()); - filled(onvif_data); - */ - } - - void startFill(bool arg) - { - synchronizeTime = arg; - std::thread thread([&]() { fill(); }); - thread.detach(); - } - - void fill() - { - for (int i = 0; i < profiles.size(); i++) { - std::stringstream str; - if (synchronizeTime) { - if (setSystemDateAndTime(profiles[i])) str << profiles[i]->last_error << " - "; - if (getTimeOffset(profiles[i])) str << profiles[i]->last_error << " - "; - } - if (getProfile(profiles[i])) str << profiles[i]->last_error << " - "; - if (getNetworkInterfaces(profiles[i])) str << profiles[i]->last_error << " - "; - if (getNetworkDefaultGateway(profiles[i])) str << profiles[i]->last_error << " - "; - if (getDNS(profiles[i])) str << profiles[i]->last_error << " - "; - if (getVideoEncoderConfiguration(profiles[i])) str << profiles[i]->last_error << " - "; - if (getVideoEncoderConfigurationOptions(profiles[i])) str << profiles[i]->last_error << " - "; - if (getAudioEncoderConfiguration(profiles[i])) str << profiles[i]->last_error << " - "; - if (getAudioEncoderConfigurationOptions(profiles[i])) str << profiles[i]->last_error << " - "; - if (getOptions(profiles[i])) str << profiles[i]->last_error << " - "; - if (getImagingSettings(profiles[i])) str << profiles[i]->last_error << " - "; - - memset(profiles[i]->last_error, 0, 1024); - int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); - strncpy(profiles[i]->last_error, str.str().c_str(), length); - } - - setProfile(displayProfile); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - if (filled) filled(*this); - } - - void startManualFill() - { - std::thread thread([&]() { manual_fill(); }); - thread.detach(); - } - - void manual_fill() - { - bool first_pass = true; - int count = 0; - while (true) { - *this = getCredential(*this); - if (!cancelled) { - - getCapabilities(data); - if (!getTimeOffset(data)) { - time_t rawtime; - struct tm timeinfo; - time(&rawtime); - #ifdef _WIN32 - localtime_s(&timeinfo, &rawtime); - #else - localtime_r(&rawtime, &timeinfo); - #endif - if (timeinfo.tm_isdst) - setTimeOffset(time_offset() - 3600); - } - - if (getDeviceInformation(data) == 0) { - int index = 0; - while (true) { - Data profile(*this); - getProfileToken(profile, index); - if (profile.profile().length() == 0) - - break; - getStreamUri(profile.data); - profiles.push_back(profile); - index++; - } - setProfile(displayProfile); - getData(*this); - break; - } - } - else { - break; - } - } - } - - std::string uri() { - std::stringstream str; - std::string arg(data->stream_uri); - if (arg.length() > 7) - str << arg.substr(0, 7) << data->username << ":" << data->password << "@" << arg.substr(7); - return str.str(); - } - - std::string username() { return data->username; } const - void setUsername(const std::string& arg) { - memset(data->username, 0, 128); - strncpy(data->username, arg.c_str(), arg.length()); - } - - std::string password() { return data->password; } const - void setPassword(const std::string& arg) { - memset(data->password, 0, 128); - strncpy(data->password, arg.c_str(), arg.length()); - } - - bool isValid() const { return data ? true : false; } - std::string xaddrs() { return data->xaddrs; } const - void setXAddrs(const std::string& arg) { strcpy(data->xaddrs, arg.c_str()); } - std::string device_service() { return data->device_service; } const - void setDeviceService(const std::string& arg) { strcpy(data->device_service, arg.c_str()); } - std::string event_service() { return data->event_service; } const - std::string stream_uri() { return data->stream_uri; } const - std::string serial_number() { return data->serial_number; } const - std::string camera_name() { return data->camera_name; } const - void setCameraName(const std::string& arg) { strcpy(data->camera_name, arg.c_str()); } - void setHost(const std::string& arg) { strcpy(data->host, arg.c_str()); } - std::string last_error() { return data->last_error; } const - std::string profile() { return data->profileToken; } const - void clearLastError() { memset(data->last_error, 0, 1024); } - void setLastError(const std::string& arg) { strcpy(data->last_error, arg.c_str()); } - time_t time_offset() { return data->time_offset; } const - void setTimeOffset(time_t arg) { data->time_offset = arg; } - std::string timezone() { return data->timezone; } const - bool dst() { return data->dst; } - - std::string host() { - extractHost(data->xaddrs, data->host); - return data->host; - } const - - void syncProfile(int index) { - if (index < profiles.size()) - copyData(profiles[index].data, data); - } - - void setProfile(int index) { - if (index < profiles.size()) { - copyData(data, profiles[index].data); - displayProfile = index; - } - } - - void clear(int bug) { - clearData(data); - alias = ""; - } - - int indexForProfile(const std::string& profileToken) { - int result = 0; - for (int i = 0; i < profiles.size(); i++) { - if (profileToken == profiles[i].data->profileToken) { - result = i; - break; - } - } - return result; - } - - //VIDEO - std::string resolutions_buf(int arg) { return data->resolutions_buf[arg]; } - int width() { return data->width; } - void setWidth(int arg) { data->width = arg; } - int height() { return data->height; } - void setHeight(int arg) { data->height = arg; } - int frame_rate_max() { return data->frame_rate_max; } - int frame_rate_min() { return data->frame_rate_min; } - int frame_rate() { return data->frame_rate; } - void setFrameRate(int arg) { data->frame_rate = arg; } - int gov_length_max() { return data->gov_length_max; } - int gov_length_min() { return data->gov_length_min; } - int gov_length() { return data->gov_length; } - void setGovLength(int arg) { data->gov_length = arg; } - int bitrate_max() { return data->bitrate_max; } - int bitrate_min() { return data->bitrate_min; } - int bitrate() { return data->bitrate; } - void setBitrate(int arg) { data->bitrate = arg; } - std::string encoding() { return data->encoding; } - - //IMAGE - int brightness_max() { return data->brightness_max; } - int brightness_min() { return data->brightness_min; } - int brightness() { return data->brightness; } - void setBrightness(int arg) { data->brightness = arg; } - int saturation_max() { return data->saturation_max; } - int saturation_min() { return data->saturation_min; } - int saturation() { return data->saturation; } - void setSaturation(int arg) { data->saturation = arg; } - int contrast_max() { return data->contrast_max; } - int contrast_min() { return data->contrast_min; } - int contrast() { return data->contrast; } - void setContrast(int arg) { data->contrast = arg; } - int sharpness_max() { return data->sharpness_max; } - int sharpness_min() { return data->sharpness_min; } - int sharpness() { return data->sharpness; } - void setSharpness(int arg) { data->sharpness = arg; } - - //NETWORK - bool dhcp_enabled() { return data->dhcp_enabled; } - void setDHCPEnabled(bool arg) { data->dhcp_enabled = arg; } - std::string ip_address_buf() { return data->ip_address_buf; } const - void setIPAddressBuf(const std::string& arg) { - memset(data->ip_address_buf, 0, 128); - strncpy(data->ip_address_buf, arg.c_str(), arg.length()); - } - std::string default_gateway_buf() { return data->default_gateway_buf; } const - void setDefaultGatewayBuf(const std::string& arg) { - memset(data->default_gateway_buf, 0, 128); - strncpy(data->default_gateway_buf, arg.c_str(), arg.length()); - } - std::string dns_buf() { return data->dns_buf; } const - void setDNSBuf(const std::string& arg) { - memset(data->dns_buf, 0, 128); - strncpy(data->dns_buf, arg.c_str(), arg.length()); - } - int prefix_length() { return data->prefix_length; } - void setPrefixLength(int arg) { data->prefix_length = arg; } - std::string mask_buf() { - memset(data->mask_buf, 0, 128); - prefix2mask(data->prefix_length, data->mask_buf); - return data->mask_buf; - } const - void setMaskBuf(const std::string& arg) { - data->prefix_length = mask2prefix((char*)arg.c_str()); - } - - //AUDIO - std::vector audio_encoders() { - std::vector result; - for (int i=0; i<3; i++) { - if (strlen(data->audio_encoders[i])) - result.push_back(data->audio_encoders[i]); - } - return result; - } const - std::vector audio_bitrates(int arg) { - std::vector result; - for (int i=0; i<8; i++) { - if (data->audio_bitrates[arg][i]) - result.push_back(data->audio_bitrates[arg][i]); - } - return result; - } const - std::vector audio_sample_rates(int arg) { - std::vector result; - for (int i=0; i<8; i++) { - if (data->audio_sample_rates[arg][i]) - result.push_back(data->audio_sample_rates[arg][i]); - } - return result; - } const - std::string audio_encoding() { return data->audio_encoding; } const - void setAudioEncoding(const std::string& arg) { - memset(data->audio_encoding, 0, sizeof(data->audio_encoding)); - strncpy(data->audio_encoding, arg.c_str(), arg.length()); - } - std::string audio_name() { return data->audio_name; } const - int audio_bitrate() { return data->audio_bitrate; } - void setAudioBitrate(int arg) { data->audio_bitrate = arg; } - int audio_sample_rate() { return data->audio_sample_rate; } - void setAudioSampleRate(int arg) { data->audio_sample_rate = arg; } - std::string audio_session_timeout() { return data->audio_session_timeout; } const - std::string audio_multicast_type() { return data->audio_multicast_type; } const - std::string audio_multicast_address() { return data->audio_multicast_address; } const - int audio_use_count() { return data->audio_use_count; } - int audio_multicast_port() { return data->audio_multicast_port; } - int audio_multicast_TTL() { return data->audio_multicast_TTL; } - bool audio_multicast_auto_start() { return data->audio_multicast_auto_start; } - - //GUI INTERFACE - - /* - Please note that this class is intended to be self contained within the C++ domain. It will not - behave as expected if the calling python program attempts to extend the functionality of the - class by adding member variables in the python domain. This was done so that the profile could - be copied or filled with data by the C++ class exclusively, removing the need for additional - synchronization code in the python domain. - - The effect of this decision is that GUI persistence for profiles must be implemented in this - C++ class directly. The member variables are added to the OnvifData structure in onvif.h and - the copyData and clearData functions in onvif.c. GUI persistence is handled by passing setSetting - and getSetting from the calling python program for writing variable states to disk. - */ - - - bool getDisableVideo() { - std::stringstream str; - str << serial_number() << "/" << profile() << "/DisableVideo"; - return getSetting(str.str(), "0") == "1"; - } - void setDisableVideo(bool arg) { - data->disable_video = arg; - std::stringstream str; - str << serial_number() << "/" << profile() << "/DisableVideo"; - setSetting(str.str(), arg ? "1" : "0"); - } - bool getAnalyzeVideo() { - std::stringstream str; - str << serial_number() << "/" << profile() << "/AnalyzeVideo"; - return getSetting(str.str(), "0") == "1"; - } - void setAnalyzeVideo(bool arg) { - data->analyze_video = arg; - std::stringstream str; - str << serial_number() << "/" << profile() << "/AnalyzeVideo"; - setSetting(str.str(), arg ? "1" : "0"); - } - bool getDisableAudio() { - std::stringstream str; - str << serial_number() << "/" << profile() << "/DisableAudio"; - return getSetting(str.str(), "0") == "1"; - } - void setDisableAudio(bool arg) { - data->disable_audio = arg; - std::stringstream str; - str << serial_number() << "/" << profile() << "/DisableAudio"; - setSetting(str.str(), arg ? "1" : "0"); - } - bool getAnalyzeAudio() { - std::stringstream str; - str << serial_number() << "/" << profile() << "/AnalyzeAudio"; - return getSetting(str.str(), "0") == "1"; - } - void setAnalyzeAudio(bool arg) { - data->analyze_audio = arg; - std::stringstream str; - str << serial_number() << "/" << profile() << "/AnalyzeAudio"; - setSetting(str.str(), arg ? "1" : "0"); - } - bool getSyncAudio() { - std::stringstream str; - str << serial_number() << "/" << profile() << "/SyncAudio"; - return getSetting(str.str(), "0") == "1"; - } - void setSyncAudio(bool arg) { - data->sync_audio = arg; - std::stringstream str; - str << serial_number() << "/" << profile() << "/SyncAudio"; - setSetting(str.str(), arg ? "1" : "0"); - } - bool getHidden() { - std::stringstream str; - str << serial_number() << "/" << profile() << "/Hidden"; - return getSetting(str.str(), "0") == "1"; - } - void setHidden(bool arg) { - data->hidden = arg; - std::stringstream str; - str << serial_number() << "/" << profile() << "/Hidden"; - setSetting(str.str(), arg ? "1" : "0"); - } - int getDesiredAspect() { - std::stringstream str_key, str_val, ratio; - str_key << serial_number() << "/" << profile() << "/DesiredAspect"; - ratio << ((height() == 0) ? 0 : (int)(100 * width() / height())); - str_val << getSetting(str_key.str(), ratio.str()); - int desired_aspect = 0; - str_val >> desired_aspect; - return desired_aspect; - } - void setDesiredAspect(int arg) { - data->desired_aspect = arg; - std::stringstream str_key, str_val; - str_key << serial_number() << "/" << profile() << "/DesiredAspect"; - str_val << arg; - setSetting(str_key.str(), str_val.str()); - } - int getCacheMax() { - std::stringstream str_key, str_val; - str_key << serial_number() << "/" << profile() << "/CacheMax"; - str_val << getSetting(str_key.str(), "100"); - int result = 100; - str_val >> result; - return result; - } - void setCacheMax(int arg) { - data->cache_max = arg; - std::stringstream str_key, str_val; - str_key << serial_number() << "/" << profile() << "/CacheMax"; - str_val << arg; - setSetting(str_key.str(), str_val.str()); - } - -}; - -} - - +/******************************************************************************* +* onvif_data.h +* +* copyright 2023 Stephen Rhodes +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +* +*******************************************************************************/ + +#ifndef ONVIF_DATA_H +#define ONVIF_DATA_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "onvif.h" + +namespace libonvif +{ + +class Data +{ +public: + std::function filled = nullptr; + std::function getData = nullptr; + std::function discovered = nullptr; + std::function getCredential = nullptr; + std::function setSetting = nullptr; + std::function getSetting = nullptr; + std::function getProxyURI; + + OnvifData* data; + bool cancelled = false; + std::string alias; + int preset; + int stop_type; + float x, y, z; + bool synchronizeTime = false; + int displayProfile = 0; + + std::vector profiles; + + Data() + { + data = (OnvifData*)calloc(sizeof(OnvifData), 1); + } + + Data(OnvifData* onvif_data) + { + data = onvif_data; + } + + Data(const Data& other) + { + data = (OnvifData*)calloc(sizeof(OnvifData), 1); + copyData(data, other.data); + cancelled = other.cancelled; + alias = other.alias; + preset = other.preset; + stop_type = other.stop_type; + synchronizeTime = other.synchronizeTime; + profiles = other.profiles; + x = other.x; + y = other.y; + z = other.z; + } + + Data(Data&& other) noexcept + { + data = other.data; + other.data = nullptr; + cancelled = other.cancelled; + alias = other.alias; + preset = other.preset; + stop_type = other.stop_type; + synchronizeTime = other.synchronizeTime; + profiles = other.profiles; + x = other.x; + y = other.y; + z = other.z; + } + + Data& operator=(const Data& other) + { + if (!data) data = (OnvifData*)calloc(sizeof(OnvifData), 1); + copyData(data, other.data); + cancelled = other.cancelled; + alias = other.alias; + preset = other.preset; + stop_type = other.stop_type; + synchronizeTime = other.synchronizeTime; + profiles = other.profiles; + x = other.x; + y = other.y; + z = other.z; + return *this; + } + + Data& operator=(Data&& other) noexcept + { + if (data) free(data); + data = other.data; + cancelled = other.cancelled; + alias = other.alias; + other.data = nullptr; + preset = other.preset; + stop_type = other.stop_type; + synchronizeTime = other.synchronizeTime; + profiles = other.profiles; + x = other.x; + y = other.y; + z = other.z; + return *this; + } + + bool operator==(const Data& rhs) + { + if (strcmp(data->xaddrs, rhs.data->xaddrs)) { + return false; + } + else { + return true; + } + } + + bool operator!=(const Data& rhs) + { + if (strcmp(data->xaddrs, rhs.data->xaddrs)) { + return true; + } + else { + return false; + } + } + + bool friend operator==(const Data& lhs, const Data& rhs) + { + if (strcmp(lhs.data->xaddrs, rhs.data->xaddrs)) { + return false; + } + else { + return true; + } + } + + bool friend operator!=(const Data& lhs, const Data& rhs) + { + if (strcmp(lhs.data->xaddrs, rhs.data->xaddrs)) { + return true; + } + else { + return false; + } + } + + ~Data() + { + free(data); + } + + operator OnvifData* () + { + return data; + } + + OnvifData* operator ->() + { + return data; + } + + void startUpdateTime() + { + std::thread thread([&]() { updateTime(); }); + thread.detach(); + } + + void updateTime() + { + std::stringstream str; + if (setSystemDateAndTime(data)) str << data->last_error << " - "; + if (getTimeOffset(data)) str << data->last_error << " - "; + + memset(data->last_error, 0, 1024); + int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); + strncpy(data->last_error, str.str().c_str(), length); + + for (int i = 0; i < profiles.size(); i++) { + profiles[i].data->dst = data->dst; + profiles[i].data->datetimetype = data->datetimetype; + profiles[i].data->time_offset = data->time_offset; + profiles[i].data->ntp_dhcp = data->ntp_dhcp; + strcpy(profiles[i].data->timezone, data->timezone); + strcpy(profiles[i].data->ntp_type, data->ntp_type); + strcpy(profiles[i].data->ntp_addr, data->ntp_addr); + } + } + + void startStop() + { + std::thread thread([&]() { stop(); }); + thread.detach(); + } + + void stop() + { + moveStop(stop_type, data); + } + + void startMove() + { + std::thread thread([&]() { move(); }); + thread.detach(); + } + + void move() + { + continuousMove(x, y, z, data); + } + + void startSet() + { + std::thread thread([&]() { set(); }); + thread.detach(); + } + + void set() + { + char pos[128] = {0}; + sprintf(pos, "%d", preset); + gotoPreset(pos, data); + } + + void startSetGotoPreset() + { + std::thread thread([&]() { setGotoPreset(); }); + thread.detach(); + } + + void setGotoPreset() + { + char pos[128] = {0}; + sprintf(pos, "%d", preset); + setPreset(pos, data); + if (filled) filled(*this); + } + + void startUpdateVideo() + { + std::thread thread([&]() { updateVideo(); }); + thread.detach(); + } + + void updateVideo() + { + std::stringstream str; + if (setVideoEncoderConfiguration(data)) str << data->last_error << " - "; + if (getVideoEncoderConfigurationOptions(data)) str << data->last_error << " - "; + if (getVideoEncoderConfiguration(data)) str << data->last_error << " - "; + + memset(data->last_error, 0, 1024); + int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); + strncpy(data->last_error, str.str().c_str(), length); + + syncProfile(indexForProfile(data->profileToken)); + + if (filled) filled(*this); + } + + void startUpdateAudio() + { + std::thread thread([&] () { updateAudio(); }); + thread.detach(); + } + + void updateAudio() + { + std::stringstream str; + if (setAudioEncoderConfiguration(data)) str << data->last_error << " - "; + if (getAudioEncoderConfigurationOptions(data)) str << data->last_error << " - "; + if (getAudioEncoderConfiguration(data)) str << data->last_error << " - "; + + memset(data->last_error, 0, 1024); + int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); + strncpy(data->last_error, str.str().c_str(), length); + + syncProfile(indexForProfile(data->profileToken)); + + if (filled) filled(*this); + } + + void startUpdateImage() + { + std::thread thread([&]() { updateImage(); }); + thread.detach(); + } + + void updateImage() + { + std::stringstream str; + if (setImagingSettings(data)) str << data->last_error << " - "; + if (getOptions(data)) str << data->last_error << " - "; + if (getImagingSettings(data)) str << data->last_error << " - "; + + memset(data->last_error, 0, 1024); + int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); + strncpy(data->last_error, str.str().c_str(), length); + + for (int i = 0; i < profiles.size(); i++) { + profiles[i].data->brightness = data->brightness; + profiles[i].data->saturation = data->saturation; + profiles[i].data->contrast = data->contrast; + profiles[i].data->sharpness = data->sharpness; + } + + if (filled) filled(*this); + } + + void startUpdateNetwork() + { + std::thread thread([&]() { updateNetwork(); }); + thread.detach(); + } + + void updateNetwork() + { + std::stringstream str; + if (setNetworkInterfaces(data)) str << data->last_error << " - "; + if (setDNS(data)) str << data->last_error << " - "; + if (setNetworkDefaultGateway(data)) str << data->last_error << " - "; + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + if (getNetworkInterfaces(data)) str << data->last_error << " - "; + if (getNetworkDefaultGateway(data)) str << data->last_error << " - "; + if (getDNS(data)) str << data->last_error << " - "; + + memset(data->last_error, 0, 1024); + int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); + strncpy(data->last_error, str.str().c_str(), length); + + syncProfile(indexForProfile(data->profileToken)); + + for (int i = 0; i < profiles.size(); i++) { + if (strcmp(profiles[i].data->profileToken, data->profileToken)) { + strcpy(profiles[i].data->networkInterfaceToken, data->networkInterfaceToken); + strcpy(profiles[i].data->networkInterfaceName, data->networkInterfaceName); + strcpy(profiles[i].data->ip_address_buf, data->ip_address_buf); + strcpy(profiles[i].data->default_gateway_buf, data->default_gateway_buf); + strcpy(profiles[i].data->dns_buf, data->dns_buf); + strcpy(profiles[i].data->mask_buf, data->mask_buf); + profiles[i].data->dhcp_enabled = data->dhcp_enabled; + profiles[i].data->prefix_length = data->prefix_length; + } + } + + if (filled) filled(*this); + } + + void startReboot() + { + std::thread thread([&]() { reboot(); }); + thread.detach(); + } + + void reboot() + { + rebootCamera(data); + } + + void startReset() + { + std::thread thread([&]() { reset(); }); + thread.detach(); + } + + void reset() + { + hardReset(data); + } + + void startSetUser() + { + std::thread thread([&]() { setOnvifUser(); }); + thread.detach(); + } + + void setOnvifUser() + { + /* + if (setUser((char*)new_password.c_str(), onvif_data) == 0) + onvif_data.setPassword(new_password.c_str()); + filled(onvif_data); + */ + } + + void startFill(bool arg) + { + synchronizeTime = arg; + std::thread thread([&]() { fill(); }); + thread.detach(); + } + + void fill() + { + for (int i = 0; i < profiles.size(); i++) { + std::stringstream str; + if (synchronizeTime) { + if (setSystemDateAndTime(profiles[i])) str << profiles[i]->last_error << " - "; + if (getTimeOffset(profiles[i])) str << profiles[i]->last_error << " - "; + } + if (getProfile(profiles[i])) str << profiles[i]->last_error << " - "; + if (getNetworkInterfaces(profiles[i])) str << profiles[i]->last_error << " - "; + if (getNetworkDefaultGateway(profiles[i])) str << profiles[i]->last_error << " - "; + if (getDNS(profiles[i])) str << profiles[i]->last_error << " - "; + if (getVideoEncoderConfiguration(profiles[i])) str << profiles[i]->last_error << " - "; + if (getVideoEncoderConfigurationOptions(profiles[i])) str << profiles[i]->last_error << " - "; + if (getAudioEncoderConfiguration(profiles[i])) str << profiles[i]->last_error << " - "; + if (getAudioEncoderConfigurationOptions(profiles[i])) str << profiles[i]->last_error << " - "; + if (getOptions(profiles[i])) str << profiles[i]->last_error << " - "; + if (getImagingSettings(profiles[i])) str << profiles[i]->last_error << " - "; + + memset(profiles[i]->last_error, 0, 1024); + int length = std::min(std::max((int)(str.str().length() - 2), 0), 1024); + strncpy(profiles[i]->last_error, str.str().c_str(), length); + } + + setProfile(displayProfile); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + if (filled) filled(*this); + } + + void startManualFill() + { + std::thread thread([&]() { manual_fill(); }); + thread.detach(); + } + + void manual_fill() + { + bool first_pass = true; + int count = 0; + while (true) { + *this = getCredential(*this); + if (!cancelled) { + + getCapabilities(data); + if (!getTimeOffset(data)) { + time_t rawtime; + struct tm timeinfo; + time(&rawtime); + #ifdef _WIN32 + localtime_s(&timeinfo, &rawtime); + #else + localtime_r(&rawtime, &timeinfo); + #endif + if (timeinfo.tm_isdst) + setTimeOffset(time_offset() - 3600); + } + + if (getDeviceInformation(data) == 0) { + int index = 0; + while (true) { + Data profile(*this); + getProfileToken(profile, index); + if (profile.profile().length() == 0) + + break; + getStreamUri(profile.data); + profiles.push_back(profile); + index++; + } + setProfile(displayProfile); + getData(*this); + break; + } + } + else { + break; + } + } + } + + std::string toString() { + std::stringstream str; + str << "camera_name=" << data->camera_name << "\n" + << "stream_uri=" << data->stream_uri << "\n"; + return str.str(); + } + + std::string uri() { + std::stringstream str; + try { + std::string arg(data->stream_uri); + + if (getProxyURI) { + str << getProxyURI(arg); + } + else { + if (arg.length() > 7) + str << arg.substr(0, 7) << data->username << ":" << data->password << "@" << arg.substr(7); + } + } + catch (const std::exception& ex) { + std::cout << "onvif data uri() exception: " << ex.what() << std::endl; + } + + return str.str(); + } + + std::string username() { return data->username; } const + void setUsername(const std::string& arg) { + memset(data->username, 0, 128); + strncpy(data->username, arg.c_str(), arg.length()); + } + + std::string password() { return data->password; } const + void setPassword(const std::string& arg) { + memset(data->password, 0, 128); + strncpy(data->password, arg.c_str(), arg.length()); + } + + bool isValid() const { return data ? true : false; } + std::string xaddrs() { return data->xaddrs; } const + void setXAddrs(const std::string& arg) { strcpy(data->xaddrs, arg.c_str()); } + std::string device_service() { return data->device_service; } const + void setDeviceService(const std::string& arg) { strcpy(data->device_service, arg.c_str()); } + std::string event_service() { return data->event_service; } const + std::string stream_uri() { return data->stream_uri; } const + std::string serial_number() { return data->serial_number; } const + std::string camera_name() { return data->camera_name; } const + void setCameraName(const std::string& arg) { strcpy(data->camera_name, arg.c_str()); } + void setHost(const std::string& arg) { strcpy(data->host, arg.c_str()); } + std::string last_error() { return data->last_error; } const + std::string profile() { return data->profileToken; } const + void clearLastError() { memset(data->last_error, 0, 1024); } + void setLastError(const std::string& arg) { strcpy(data->last_error, arg.c_str()); } + time_t time_offset() { return data->time_offset; } const + void setTimeOffset(time_t arg) { data->time_offset = arg; } + std::string timezone() { return data->timezone; } const + bool dst() { return data->dst; } + + std::string host() { + extractHost(data->xaddrs, data->host); + return data->host; + } const + + void syncProfile(int index) { + if (index < profiles.size()) + copyData(profiles[index].data, data); + } + + void setProfile(int index) { + if (index < profiles.size()) { + copyData(data, profiles[index].data); + displayProfile = index; + } + } + + void clear(int bug) { + clearData(data); + alias = ""; + } + + int indexForProfile(const std::string& profileToken) { + int result = 0; + for (int i = 0; i < profiles.size(); i++) { + if (profileToken == profiles[i].data->profileToken) { + result = i; + break; + } + } + return result; + } + + //VIDEO + std::string resolutions_buf(int arg) { return data->resolutions_buf[arg]; } + int width() { return data->width; } + void setWidth(int arg) { data->width = arg; } + int height() { return data->height; } + void setHeight(int arg) { data->height = arg; } + int frame_rate_max() { return data->frame_rate_max; } + int frame_rate_min() { return data->frame_rate_min; } + int frame_rate() { return data->frame_rate; } + void setFrameRate(int arg) { data->frame_rate = arg; } + int gov_length_max() { return data->gov_length_max; } + int gov_length_min() { return data->gov_length_min; } + int gov_length() { return data->gov_length; } + void setGovLength(int arg) { data->gov_length = arg; } + int bitrate_max() { return data->bitrate_max; } + int bitrate_min() { return data->bitrate_min; } + int bitrate() { return data->bitrate; } + void setBitrate(int arg) { data->bitrate = arg; } + std::string encoding() { return data->encoding; } + + //IMAGE + int brightness_max() { return data->brightness_max; } + int brightness_min() { return data->brightness_min; } + int brightness() { return data->brightness; } + void setBrightness(int arg) { data->brightness = arg; } + int saturation_max() { return data->saturation_max; } + int saturation_min() { return data->saturation_min; } + int saturation() { return data->saturation; } + void setSaturation(int arg) { data->saturation = arg; } + int contrast_max() { return data->contrast_max; } + int contrast_min() { return data->contrast_min; } + int contrast() { return data->contrast; } + void setContrast(int arg) { data->contrast = arg; } + int sharpness_max() { return data->sharpness_max; } + int sharpness_min() { return data->sharpness_min; } + int sharpness() { return data->sharpness; } + void setSharpness(int arg) { data->sharpness = arg; } + + //NETWORK + bool dhcp_enabled() { return data->dhcp_enabled; } + void setDHCPEnabled(bool arg) { data->dhcp_enabled = arg; } + std::string ip_address_buf() { return data->ip_address_buf; } const + void setIPAddressBuf(const std::string& arg) { + memset(data->ip_address_buf, 0, 128); + strncpy(data->ip_address_buf, arg.c_str(), arg.length()); + } + std::string default_gateway_buf() { return data->default_gateway_buf; } const + void setDefaultGatewayBuf(const std::string& arg) { + memset(data->default_gateway_buf, 0, 128); + strncpy(data->default_gateway_buf, arg.c_str(), arg.length()); + } + std::string dns_buf() { return data->dns_buf; } const + void setDNSBuf(const std::string& arg) { + memset(data->dns_buf, 0, 128); + strncpy(data->dns_buf, arg.c_str(), arg.length()); + } + int prefix_length() { return data->prefix_length; } + void setPrefixLength(int arg) { data->prefix_length = arg; } + std::string mask_buf() { + memset(data->mask_buf, 0, 128); + prefix2mask(data->prefix_length, data->mask_buf); + return data->mask_buf; + } const + void setMaskBuf(const std::string& arg) { + data->prefix_length = mask2prefix((char*)arg.c_str()); + } + + //AUDIO + std::vector audio_encoders() { + std::vector result; + for (int i=0; i<3; i++) { + if (strlen(data->audio_encoders[i])) + result.push_back(data->audio_encoders[i]); + } + return result; + } const + std::vector audio_bitrates(int arg) { + std::vector result; + for (int i=0; i<8; i++) { + if (data->audio_bitrates[arg][i]) + result.push_back(data->audio_bitrates[arg][i]); + } + return result; + } const + std::vector audio_sample_rates(int arg) { + std::vector result; + for (int i=0; i<8; i++) { + if (data->audio_sample_rates[arg][i]) + result.push_back(data->audio_sample_rates[arg][i]); + } + return result; + } const + std::string audio_encoding() { return data->audio_encoding; } const + void setAudioEncoding(const std::string& arg) { + memset(data->audio_encoding, 0, sizeof(data->audio_encoding)); + strncpy(data->audio_encoding, arg.c_str(), arg.length()); + } + std::string audio_name() { return data->audio_name; } const + int audio_bitrate() { return data->audio_bitrate; } + void setAudioBitrate(int arg) { data->audio_bitrate = arg; } + int audio_sample_rate() { return data->audio_sample_rate; } + void setAudioSampleRate(int arg) { data->audio_sample_rate = arg; } + std::string audio_session_timeout() { return data->audio_session_timeout; } const + std::string audio_multicast_type() { return data->audio_multicast_type; } const + std::string audio_multicast_address() { return data->audio_multicast_address; } const + int audio_use_count() { return data->audio_use_count; } + int audio_multicast_port() { return data->audio_multicast_port; } + int audio_multicast_TTL() { return data->audio_multicast_TTL; } + bool audio_multicast_auto_start() { return data->audio_multicast_auto_start; } + + //GUI INTERFACE + + /* + Please note that this class is intended to be self contained within the C++ domain. It will not + behave as expected if the calling python program attempts to extend the functionality of the + class by adding member variables in the python domain. This was done so that the profile could + be copied or filled with data by the C++ class exclusively, removing the need for additional + synchronization code in the python domain. + + The effect of this decision is that GUI persistence for profiles must be implemented in this + C++ class directly. The member variables are added to the OnvifData structure in onvif.h and + the copyData and clearData functions in onvif.c. GUI persistence is handled by passing setSetting + and getSetting from the calling python program for writing variable states to disk. + */ + + + bool getDisableVideo() { + std::stringstream str; + str << serial_number() << "/" << profile() << "/DisableVideo"; + return getSetting(str.str(), "0") == "1"; + } + void setDisableVideo(bool arg) { + data->disable_video = arg; + std::stringstream str; + str << serial_number() << "/" << profile() << "/DisableVideo"; + setSetting(str.str(), arg ? "1" : "0"); + } + bool getAnalyzeVideo() { + std::stringstream str; + str << serial_number() << "/" << profile() << "/AnalyzeVideo"; + return getSetting(str.str(), "0") == "1"; + } + void setAnalyzeVideo(bool arg) { + data->analyze_video = arg; + std::stringstream str; + str << serial_number() << "/" << profile() << "/AnalyzeVideo"; + setSetting(str.str(), arg ? "1" : "0"); + } + bool getDisableAudio() { + std::stringstream str; + str << serial_number() << "/" << profile() << "/DisableAudio"; + return getSetting(str.str(), "0") == "1"; + } + void setDisableAudio(bool arg) { + data->disable_audio = arg; + std::stringstream str; + str << serial_number() << "/" << profile() << "/DisableAudio"; + setSetting(str.str(), arg ? "1" : "0"); + } + bool getAnalyzeAudio() { + std::stringstream str; + str << serial_number() << "/" << profile() << "/AnalyzeAudio"; + return getSetting(str.str(), "0") == "1"; + } + void setAnalyzeAudio(bool arg) { + data->analyze_audio = arg; + std::stringstream str; + str << serial_number() << "/" << profile() << "/AnalyzeAudio"; + setSetting(str.str(), arg ? "1" : "0"); + } + bool getSyncAudio() { + std::stringstream str; + str << serial_number() << "/" << profile() << "/SyncAudio"; + return getSetting(str.str(), "0") == "1"; + } + void setSyncAudio(bool arg) { + data->sync_audio = arg; + std::stringstream str; + str << serial_number() << "/" << profile() << "/SyncAudio"; + setSetting(str.str(), arg ? "1" : "0"); + } + bool getHidden() { + std::stringstream str; + str << serial_number() << "/" << profile() << "/Hidden"; + return getSetting(str.str(), "0") == "1"; + } + void setHidden(bool arg) { + data->hidden = arg; + std::stringstream str; + str << serial_number() << "/" << profile() << "/Hidden"; + setSetting(str.str(), arg ? "1" : "0"); + } + int getDesiredAspect() { + std::stringstream str_key, str_val, ratio; + str_key << serial_number() << "/" << profile() << "/DesiredAspect"; + ratio << ((height() == 0) ? 0 : (int)(100 * width() / height())); + str_val << getSetting(str_key.str(), ratio.str()); + int desired_aspect = 0; + str_val >> desired_aspect; + return desired_aspect; + } + void setDesiredAspect(int arg) { + data->desired_aspect = arg; + std::stringstream str_key, str_val; + str_key << serial_number() << "/" << profile() << "/DesiredAspect"; + str_val << arg; + setSetting(str_key.str(), str_val.str()); + } + int getCacheMax() { + std::stringstream str_key, str_val; + str_key << serial_number() << "/" << profile() << "/CacheMax"; + str_val << getSetting(str_key.str(), "100"); + int result = 100; + str_val >> result; + return result; + } + void setCacheMax(int arg) { + data->cache_max = arg; + std::stringstream str_key, str_val; + str_key << serial_number() << "/" << profile() << "/CacheMax"; + str_val << arg; + setSetting(str_key.str(), str_val.str()); + } + +}; + +} + + #endif // ONVIF_DATA_H \ No newline at end of file diff --git a/libonvif/include/session.h b/libonvif/include/session.h index 0348379f..aad4361a 100644 --- a/libonvif/include/session.h +++ b/libonvif/include/session.h @@ -1,136 +1,136 @@ -/******************************************************************************* -* session.h -* -* copyright 2023 Stephen Rhodes -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library 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 -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -* -*******************************************************************************/ - -#ifndef SESSION_H -#define SESSION_H - -#include -#include -#include -#include -#include "onvif.h" - -namespace libonvif -{ - -class Session -{ -public: - Session() - { - session = (OnvifSession*)calloc(sizeof(OnvifSession), 1); - initializeSession(session); - } - - ~Session() - { - closeSession(session); - free(session); - } - - operator OnvifSession* () - { - return session; - } - - void startDiscover() - { - std::thread thread([&]() { discover(); }); - thread.detach(); - } - - void discover() - { - if (interface.length() > 0) { - memset(session->preferred_network_address, 0, 16); - strcpy(session->preferred_network_address, interface.c_str()); - } - int number_of_devices = broadcast(session); - - std::vector devices; - for (int i = 0; i < number_of_devices; i++) { - if (abort) break; - Data data; - if (prepareOnvifData(i, session, data)) { - if (std::find(devices.begin(), devices.end(), data) == devices.end()) { - devices.push_back(data); - while (true) { - data = getCredential(data); - if (!data.cancelled) { - - getCapabilities(data); - if (!getTimeOffset(data)) { - time_t rawtime; - struct tm timeinfo; - time(&rawtime); - #ifdef _WIN32 - localtime_s(&timeinfo, &rawtime); - #else - localtime_r(&rawtime, &timeinfo); - #endif - if (timeinfo.tm_isdst && !data.dst()) - data.setTimeOffset(data.time_offset() - 3600); - } - - if (getDeviceInformation(data) == 0) { - int index = 0; - while (true) { - Data profile(data); - getProfileToken(profile, index); - if (strlen(profile->profileToken) == 0) - break; - getStreamUri(profile); - data.profiles.push_back(profile); - index++; - } - getData(data); - break; - } - } - else { - break; - } - } - } - } - } - - discovered(); - } - - void getActiveInterfaces() { - getActiveNetworkInterfaces(session); - } - - std::string active_interface(int arg) { return session->active_network_interfaces[arg]; } - - std::function discovered = nullptr; - std::function getCredential = nullptr; - std::function getData = nullptr; - - std::string interface; - OnvifSession* session; - bool abort = false; -}; - -} - -#endif // SESSION_H +/******************************************************************************* +* session.h +* +* copyright 2023 Stephen Rhodes +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +* +*******************************************************************************/ + +#ifndef SESSION_H +#define SESSION_H + +#include +#include +#include +#include +#include "onvif.h" + +namespace libonvif +{ + +class Session +{ +public: + Session() + { + session = (OnvifSession*)calloc(sizeof(OnvifSession), 1); + initializeSession(session); + } + + ~Session() + { + closeSession(session); + free(session); + } + + operator OnvifSession* () + { + return session; + } + + void startDiscover() + { + std::thread thread([&]() { discover(); }); + thread.detach(); + } + + void discover() + { + if (interface.length() > 0) { + memset(session->preferred_network_address, 0, 16); + strcpy(session->preferred_network_address, interface.c_str()); + } + int number_of_devices = broadcast(session); + + std::vector devices; + for (int i = 0; i < number_of_devices; i++) { + if (abort) break; + Data data; + if (prepareOnvifData(i, session, data)) { + if (std::find(devices.begin(), devices.end(), data) == devices.end()) { + devices.push_back(data); + while (true) { + data = getCredential(data); + if (!data.cancelled) { + + getCapabilities(data); + if (!getTimeOffset(data)) { + time_t rawtime; + struct tm timeinfo; + time(&rawtime); + #ifdef _WIN32 + localtime_s(&timeinfo, &rawtime); + #else + localtime_r(&rawtime, &timeinfo); + #endif + if (timeinfo.tm_isdst && !data.dst()) + data.setTimeOffset(data.time_offset() - 3600); + } + + if (getDeviceInformation(data) == 0) { + int index = 0; + while (true) { + Data profile(data); + getProfileToken(profile, index); + if (strlen(profile->profileToken) == 0) + break; + getStreamUri(profile); + data.profiles.push_back(profile); + index++; + } + getData(data); + break; + } + } + else { + break; + } + } + } + } + } + + discovered(); + } + + void getActiveInterfaces() { + getActiveNetworkInterfaces(session); + } + + std::string active_interface(int arg) { return session->active_network_interfaces[arg]; } + + std::function discovered = nullptr; + std::function getCredential = nullptr; + std::function getData = nullptr; + + std::string interface; + OnvifSession* session; + bool abort = false; +}; + +} + +#endif // SESSION_H diff --git a/libonvif/pyproject.toml b/libonvif/pyproject.toml index de520f0e..bf0eb76d 100644 --- a/libonvif/pyproject.toml +++ b/libonvif/pyproject.toml @@ -25,7 +25,7 @@ build-backend = "setuptools.build_meta" [project] name = "libonvif" -version = "3.2.0" +version = "3.2.1" authors = [ { name="Stephen Rhodes", email="sr99622@gmail.com" }, ] diff --git a/libonvif/setup.py b/libonvif/setup.py index 97b7d044..893aa8ef 100644 --- a/libonvif/setup.py +++ b/libonvif/setup.py @@ -28,7 +28,7 @@ from setuptools.command.build_ext import build_ext PKG_NAME = "libonvif" -VERSION = "3.2.0" +VERSION = "3.2.1" class CMakeExtension(Extension): def __init__(self, name, sourcedir=""): diff --git a/libonvif/src/onvif.c b/libonvif/src/onvif.c index 1740d805..cd12b54b 100644 --- a/libonvif/src/onvif.c +++ b/libonvif/src/onvif.c @@ -822,7 +822,13 @@ int setVideoEncoderConfiguration(struct OnvifData *onvif_data) { sprintf(use_count_buf, "%d", onvif_data->use_count); sprintf(width_buf, "%d", onvif_data->width); sprintf(height_buf, "%d", onvif_data->height); + sprintf(quality_buf, "%f", onvif_data->quality); + for (int i = 0; i < strlen(quality_buf); i++) { + if (quality_buf[i] == ',') + quality_buf[i] = '.'; + } + sprintf(multicast_port_buf, "%d", onvif_data->multicast_port); sprintf(multicast_ttl_buf, "%d", onvif_data->multicast_ttl); if (onvif_data->autostart) @@ -2713,7 +2719,7 @@ void getActiveNetworkInterfaces(struct OnvifSession* onvif_session) continue; } - if (strcmp(ifa->ifa_name, "lo")) { + if (strcmp(host, "127.0.0.1")) { strcpy(onvif_session->active_network_interfaces[count], host); strcat(onvif_session->active_network_interfaces[count], " - "); strcat(onvif_session->active_network_interfaces[count], ifa->ifa_name); diff --git a/libonvif/src/onvif.cpp b/libonvif/src/onvif.cpp index b719144e..1c90475f 100644 --- a/libonvif/src/onvif.cpp +++ b/libonvif/src/onvif.cpp @@ -166,6 +166,7 @@ PYBIND11_MODULE(libonvif, m) .def("setHidden", &Data::setHidden) .def("getCacheMax", &Data::getCacheMax) .def("setCacheMax", &Data::setCacheMax) + .def("toString", &Data::toString) .def(py::self == py::self) .def_readwrite("profiles", &Data::profiles) .def_readwrite("displayProfile", &Data::displayProfile) @@ -175,6 +176,7 @@ PYBIND11_MODULE(libonvif, m) .def_readwrite("discovered", &Data::discovered) .def_readwrite("getSetting", &Data::getSetting) .def_readwrite("setSetting", &Data::setSetting) + .def_readwrite("getProxyURI", &Data::getProxyURI) .def_readwrite("preset", &Data::preset) .def_readwrite("x", &Data::x) .def_readwrite("y", &Data::y) @@ -183,7 +185,7 @@ PYBIND11_MODULE(libonvif, m) .def_readwrite("alias", &Data::alias) .def_readwrite("cancelled", &Data::cancelled); - m.attr("__version__") = "2.0.10"; + m.attr("__version__") = "3.2.1"; } diff --git a/libonvif/test/onvif-test.cpp b/libonvif/test/onvif-test.cpp index 8716f09a..fa82dd63 100644 --- a/libonvif/test/onvif-test.cpp +++ b/libonvif/test/onvif-test.cpp @@ -1,152 +1,152 @@ -#include -#include -#include -#include -#include -#include -#include -#include "onvif.h" -#ifdef _WIN32 -#include "getopt-win.h" -#else -#include -#endif - -int main(int argc, char **argv) -{ - std::cout << "Looking for cameras on the network..." << std::endl; - - struct OnvifSession *onvif_session = (struct OnvifSession*)calloc(sizeof(struct OnvifSession), 1); - - getActiveNetworkInterfaces(onvif_session); - for (int i = 0; i < 16; i++) { - std::cout << "interface: " << onvif_session->active_network_interfaces[i] << std::endl; - } - - std::string delimiter = " - "; - std::string thingy = onvif_session->active_network_interfaces[0]; - std::string token = thingy.substr(0, thingy.find(delimiter)); - std::cout << "---" << token << "---" << std::endl; - strcpy(onvif_session->preferred_network_address, "10.1.1.1"); - - struct OnvifData *tmp_onvif_data = (struct OnvifData*)calloc(sizeof(struct OnvifData), 1); - struct OnvifData *onvif_data = (struct OnvifData*)calloc(sizeof(struct OnvifData), 1); - - initializeSession(onvif_session); - int n = broadcast(onvif_session); - std::cout << "Found " << n << " cameras" << std::endl; - for (int i = 0; i < n; i++) { - if (prepareOnvifData(i, onvif_session, tmp_onvif_data)) { - char host[128]; - extractHost(tmp_onvif_data->xaddrs, host); - getHostname(tmp_onvif_data); - printf("%s %s(%s)\n",host, - tmp_onvif_data->host_name, - tmp_onvif_data->camera_name); - - if (!strcmp(host, "10.1.1.67")) { - std::cout << "FOUND HOST" << tmp_onvif_data->camera_name << std::endl; - copyData(onvif_data, tmp_onvif_data); - } - } - else { - std::cout << "found invalid xaddrs in device repsonse" << std::endl; - } - } - - closeSession(onvif_session); - free(onvif_session); - free(tmp_onvif_data); - - std::cout << "subject camera - " << onvif_data->camera_name << std::endl; - - strcpy(onvif_data->username, "admin"); - strcpy(onvif_data->password, "admin123"); - if (getDeviceInformation(onvif_data)) - std::cout << "getDeviceInformation failure " << onvif_data->last_error << std::endl; - - if (getCapabilities(onvif_data)) - std::cout << "getCapabilities failure " << onvif_data->last_error << std::endl; - - if (getProfileToken(onvif_data, 0)) - std::cout << "getProfileToken failure " << onvif_data->last_error << std::endl; - - if (getProfile(onvif_data)) - std::cout << "getProfile failure " << onvif_data->last_error << std::endl; - - if (setSystemDateAndTime(onvif_data)) - std::cout << "setSystemDateAndTime failure " << onvif_data->last_error << std::endl; - - if (getStreamUri(onvif_data)) - std::cout << "getStreamUri failure " << onvif_data->last_error << std::endl; - - std::cout << onvif_data->stream_uri << std::endl; - - if(getVideoEncoderConfiguration(onvif_data)) - std::cout << "getVideoEncoderConfiguration failure " << onvif_data->last_error << std::endl; - - std::cout << " Width: " << onvif_data->width << "\n"; - std::cout << " Height: " << onvif_data->height << "\n"; - std::cout << " Frame Rate: " << onvif_data->frame_rate << "\n"; - std::cout << " Gov Length: " << onvif_data->gov_length << "\n"; - std::cout << " Bitrate: " << onvif_data->bitrate << "\n" << std::endl; - - if (getOptions(onvif_data)) - std::cout << "getOptions failure " << onvif_data->last_error << std::endl; - - std::cout << " Min Brightness: " << onvif_data->brightness_min << "\n"; - std::cout << " Max Brightness: " << onvif_data->brightness_max << "\n"; - std::cout << " Min ColorSaturation: " << onvif_data->saturation_min << "\n"; - std::cout << " Max ColorSaturation: " << onvif_data->saturation_max << "\n"; - std::cout << " Min Contrast: " << onvif_data->contrast_min << "\n"; - std::cout << " Max Contrast: " << onvif_data->contrast_max << "\n"; - std::cout << " Min Sharpness: " << onvif_data->sharpness_min << "\n"; - std::cout << " Max Sharpness: " << onvif_data->sharpness_max << "\n" << std::endl; - - if (getImagingSettings(onvif_data)) - std::cout << "getImagingSettings failure" << onvif_data->last_error << std::endl; - - std::cout << " Brightness: " << onvif_data->brightness << "\n"; - std::cout << " Contrast: " << onvif_data->contrast << "\n"; - std::cout << " Saturation: " << onvif_data->saturation << "\n"; - std::cout << " Sharpness: " << onvif_data->sharpness << "\n" << std::endl; - - if (getTimeOffset(onvif_data)) - std::cout << "getTimeOffset failure " << onvif_data->last_error << std::endl; - - std::cout << " Time Offset: " << onvif_data->time_offset << " seconds" << "\n"; - std::cout << " Timezone: " << onvif_data->timezone << "\n"; - std::cout << " DST: " << (onvif_data->dst ? "Yes" : "No") << "\n"; - std::cout << " Time Set By: " << ((onvif_data->datetimetype == 'M') ? "Manual" : "NTP") << "\n"; - - if (getNetworkInterfaces(onvif_data)) - std::cout << "getNetworkInterfaces failure " << onvif_data->last_error << std::endl; - if (getNetworkDefaultGateway(onvif_data)) - std::cout << "getNetworkDefaultGateway failure " << onvif_data->last_error << std::endl; - if (getDNS(onvif_data)) - std::cout << "getDNS failure " << onvif_data->last_error << std::endl; - - std::cout << " IP Address: " << onvif_data->ip_address_buf << "\n"; - std::cout << " Gateway: " << onvif_data->default_gateway_buf << "\n"; - std::cout << " DNS: " << onvif_data->dns_buf << "\n"; - std::cout << " DHCP: " << (onvif_data->dhcp_enabled ? "YES" : "NO") << "\n" << std::endl; - - if (getVideoEncoderConfigurationOptions(onvif_data)) - std::cout << "getVideoEncoderConfigurationOptions failure " << onvif_data->last_error << std::endl; - - std::cout << " Available Resolutions" << std::endl; - for (int i=0; i<16; i++) { - if (strlen(onvif_data->resolutions_buf[i])) - std::cout << " " << onvif_data->resolutions_buf[i] << std::endl; - } - - std::cout << " Min Gov Length: " << onvif_data->gov_length_min << "\n"; - std::cout << " Max Gov Length: " << onvif_data->gov_length_max << "\n"; - std::cout << " Min Frame Rate: " << onvif_data->frame_rate_min << "\n"; - std::cout << " Max Frame Rate: " << onvif_data->frame_rate_max << "\n"; - std::cout << " Min Bit Rate: " << onvif_data->bitrate_min << "\n"; - std::cout << " Max Bit Rate: " << onvif_data->bitrate_max << "\n" << std::endl; - - - free(onvif_data); -} +#include +#include +#include +#include +#include +#include +#include +#include "onvif.h" +#ifdef _WIN32 +#include "getopt-win.h" +#else +#include +#endif + +int main(int argc, char **argv) +{ + std::cout << "Looking for cameras on the network..." << std::endl; + + struct OnvifSession *onvif_session = (struct OnvifSession*)calloc(sizeof(struct OnvifSession), 1); + + getActiveNetworkInterfaces(onvif_session); + for (int i = 0; i < 16; i++) { + std::cout << "interface: " << onvif_session->active_network_interfaces[i] << std::endl; + } + + std::string delimiter = " - "; + std::string thingy = onvif_session->active_network_interfaces[0]; + std::string token = thingy.substr(0, thingy.find(delimiter)); + std::cout << "---" << token << "---" << std::endl; + strcpy(onvif_session->preferred_network_address, "10.1.1.1"); + + struct OnvifData *tmp_onvif_data = (struct OnvifData*)calloc(sizeof(struct OnvifData), 1); + struct OnvifData *onvif_data = (struct OnvifData*)calloc(sizeof(struct OnvifData), 1); + + initializeSession(onvif_session); + int n = broadcast(onvif_session); + std::cout << "Found " << n << " cameras" << std::endl; + for (int i = 0; i < n; i++) { + if (prepareOnvifData(i, onvif_session, tmp_onvif_data)) { + char host[128]; + extractHost(tmp_onvif_data->xaddrs, host); + getHostname(tmp_onvif_data); + printf("%s %s(%s)\n",host, + tmp_onvif_data->host_name, + tmp_onvif_data->camera_name); + + if (!strcmp(host, "10.1.1.67")) { + std::cout << "FOUND HOST" << tmp_onvif_data->camera_name << std::endl; + copyData(onvif_data, tmp_onvif_data); + } + } + else { + std::cout << "found invalid xaddrs in device repsonse" << std::endl; + } + } + + closeSession(onvif_session); + free(onvif_session); + free(tmp_onvif_data); + + std::cout << "subject camera - " << onvif_data->camera_name << std::endl; + + strcpy(onvif_data->username, "admin"); + strcpy(onvif_data->password, "admin123"); + if (getDeviceInformation(onvif_data)) + std::cout << "getDeviceInformation failure " << onvif_data->last_error << std::endl; + + if (getCapabilities(onvif_data)) + std::cout << "getCapabilities failure " << onvif_data->last_error << std::endl; + + if (getProfileToken(onvif_data, 0)) + std::cout << "getProfileToken failure " << onvif_data->last_error << std::endl; + + if (getProfile(onvif_data)) + std::cout << "getProfile failure " << onvif_data->last_error << std::endl; + + if (setSystemDateAndTime(onvif_data)) + std::cout << "setSystemDateAndTime failure " << onvif_data->last_error << std::endl; + + if (getStreamUri(onvif_data)) + std::cout << "getStreamUri failure " << onvif_data->last_error << std::endl; + + std::cout << onvif_data->stream_uri << std::endl; + + if(getVideoEncoderConfiguration(onvif_data)) + std::cout << "getVideoEncoderConfiguration failure " << onvif_data->last_error << std::endl; + + std::cout << " Width: " << onvif_data->width << "\n"; + std::cout << " Height: " << onvif_data->height << "\n"; + std::cout << " Frame Rate: " << onvif_data->frame_rate << "\n"; + std::cout << " Gov Length: " << onvif_data->gov_length << "\n"; + std::cout << " Bitrate: " << onvif_data->bitrate << "\n" << std::endl; + + if (getOptions(onvif_data)) + std::cout << "getOptions failure " << onvif_data->last_error << std::endl; + + std::cout << " Min Brightness: " << onvif_data->brightness_min << "\n"; + std::cout << " Max Brightness: " << onvif_data->brightness_max << "\n"; + std::cout << " Min ColorSaturation: " << onvif_data->saturation_min << "\n"; + std::cout << " Max ColorSaturation: " << onvif_data->saturation_max << "\n"; + std::cout << " Min Contrast: " << onvif_data->contrast_min << "\n"; + std::cout << " Max Contrast: " << onvif_data->contrast_max << "\n"; + std::cout << " Min Sharpness: " << onvif_data->sharpness_min << "\n"; + std::cout << " Max Sharpness: " << onvif_data->sharpness_max << "\n" << std::endl; + + if (getImagingSettings(onvif_data)) + std::cout << "getImagingSettings failure" << onvif_data->last_error << std::endl; + + std::cout << " Brightness: " << onvif_data->brightness << "\n"; + std::cout << " Contrast: " << onvif_data->contrast << "\n"; + std::cout << " Saturation: " << onvif_data->saturation << "\n"; + std::cout << " Sharpness: " << onvif_data->sharpness << "\n" << std::endl; + + if (getTimeOffset(onvif_data)) + std::cout << "getTimeOffset failure " << onvif_data->last_error << std::endl; + + std::cout << " Time Offset: " << onvif_data->time_offset << " seconds" << "\n"; + std::cout << " Timezone: " << onvif_data->timezone << "\n"; + std::cout << " DST: " << (onvif_data->dst ? "Yes" : "No") << "\n"; + std::cout << " Time Set By: " << ((onvif_data->datetimetype == 'M') ? "Manual" : "NTP") << "\n"; + + if (getNetworkInterfaces(onvif_data)) + std::cout << "getNetworkInterfaces failure " << onvif_data->last_error << std::endl; + if (getNetworkDefaultGateway(onvif_data)) + std::cout << "getNetworkDefaultGateway failure " << onvif_data->last_error << std::endl; + if (getDNS(onvif_data)) + std::cout << "getDNS failure " << onvif_data->last_error << std::endl; + + std::cout << " IP Address: " << onvif_data->ip_address_buf << "\n"; + std::cout << " Gateway: " << onvif_data->default_gateway_buf << "\n"; + std::cout << " DNS: " << onvif_data->dns_buf << "\n"; + std::cout << " DHCP: " << (onvif_data->dhcp_enabled ? "YES" : "NO") << "\n" << std::endl; + + if (getVideoEncoderConfigurationOptions(onvif_data)) + std::cout << "getVideoEncoderConfigurationOptions failure " << onvif_data->last_error << std::endl; + + std::cout << " Available Resolutions" << std::endl; + for (int i=0; i<16; i++) { + if (strlen(onvif_data->resolutions_buf[i])) + std::cout << " " << onvif_data->resolutions_buf[i] << std::endl; + } + + std::cout << " Min Gov Length: " << onvif_data->gov_length_min << "\n"; + std::cout << " Max Gov Length: " << onvif_data->gov_length_max << "\n"; + std::cout << " Min Frame Rate: " << onvif_data->frame_rate_min << "\n"; + std::cout << " Max Frame Rate: " << onvif_data->frame_rate_max << "\n"; + std::cout << " Min Bit Rate: " << onvif_data->bitrate_min << "\n"; + std::cout << " Max Bit Rate: " << onvif_data->bitrate_max << "\n" << std::endl; + + + free(onvif_data); +} diff --git a/onvif-gui/gui/__init__.py b/onvif-gui/gui/__init__.py index 15ca685e..91d18a91 100644 --- a/onvif-gui/gui/__init__.py +++ b/onvif-gui/gui/__init__.py @@ -1 +1,3 @@ -from .main import MainWindow \ No newline at end of file +from .main import MainWindow +from .player import Player +from .enums import ProxyType, MediaSource, StreamState \ No newline at end of file diff --git a/onvif-gui/gui/components/target.py b/onvif-gui/gui/components/target.py index a2ef7b03..d410ef32 100644 --- a/onvif-gui/gui/components/target.py +++ b/onvif-gui/gui/components/target.py @@ -1,238 +1,238 @@ -#******************************************************************** -# libonvif/onvif-gui/gui/components/target.py -# -# Copyright (c) 2024 Stephen Rhodes -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#*********************************************************************/ - -from PyQt6.QtWidgets import QDialog, QGridLayout, QListWidget, QListWidgetItem, \ - QDialogButtonBox, QWidget, QLabel, QPushButton, QMessageBox, QSlider, QCheckBox -from PyQt6.QtCore import Qt, pyqtSignal, QObject -from .warningbar import WarningBar, Indicator -from gui.onvif.datastructures import MediaSource -from loguru import logger - -class Target(QListWidgetItem): - def __init__(self, name, id): - super().__init__(name) - self.id = id - -class TargetDialog(QDialog): - def __init__(self, mw): - super().__init__(mw) - self.mw = mw - self.source = MediaSource.CAMERA - - self.targets = { - 0: "person", - 1: "bicycle", - 2: "car", - 3: "motorcycle", - 4: "airplane", - 5: "bus", - 6: "train", - 7: "truck", - 8: "boat", - 14: "bird", - 15: "cat", - 16: "dog", - 17: "horse", - 18: "sheep", - 19: "cow", - 21: "bear" - } - - self.setModal(True) - self.setWindowTitle("Add Target") - self.list = QListWidget() - for key in self.targets: - self.list.addItem(Target(self.targets[key], key)) - - self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Close) - self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.buttonBox.rejected.connect(self.reject) - - lytMain = QGridLayout(self) - lytMain.addWidget(self.list, 0, 0, 1, 2) - lytMain.addWidget(self.buttonBox, 1, 0, 1, 2) - - self.list.setCurrentRow(0) - - def reject(self): - self.hide() - -class TargetListSignals(QObject): - delete = pyqtSignal() - -class TargetList(QListWidget): - def __init__(self, mw): - super().__init__() - self.mw = mw - self.signals = TargetListSignals() - - def keyPressEvent(self, event): - if event.key() == Qt.Key.Key_Delete: - self.signals.delete.emit() - return super().keyPressEvent(event) - - def toString(self): - output = "" - length = self.count() - for i in range(length): - output += str(self.item(i).id) - if i < length - 1: - output += ":" - return output - -class TargetSelector(QWidget): - def __init__(self, mw, module): - super().__init__() - self.mw = mw - self.module = module - - self.lstTargets = TargetList(self.mw) - self.lstTargets.signals.delete.connect(self.btnDeleteTargetClicked) - self.lblTargets = QLabel("Targets") - self.btnAddTarget = QPushButton("+") - self.btnAddTarget.clicked.connect(self.btnAddTargetClicked) - self.btnDeleteTarget = QPushButton("-") - self.btnDeleteTarget.clicked.connect(self.btnDeleteTargetClicked) - self.dlgTarget = TargetDialog(self.mw) - self.dlgTarget.list.itemDoubleClicked.connect(self.onAddItemDoubleClicked) - self.dlgTarget.buttonBox.accepted.connect(self.dlgListAccepted) - - # gui works better if these are on the same panel - self.barLevel = WarningBar() - self.indAlarm = Indicator(self.mw) - self.sldGain = QSlider(Qt.Orientation.Vertical) - self.sldGain.setMinimum(0) - self.sldGain.setMaximum(100) - self.sldGain.setValue(0) - self.sldGain.valueChanged.connect(self.sldGainValueChanged) - self.lblGain = QLabel("0") - - pnlTargets = QWidget() - pnlTargets.setMaximumWidth(200) - lytTargets = QGridLayout(pnlTargets) - - self.chkShowBoxes = QCheckBox("Show Boxes") - self.chkShowBoxes.stateChanged.connect(self.chkShowBoxesStateChanged) - - lytTargets.addWidget(self.lblTargets, 0, 0, 1, 1) - lytTargets.addWidget(self.btnDeleteTarget, 0, 1, 1, 1) - lytTargets.addWidget(self.btnAddTarget, 0, 2, 1, 1) - lytTargets.addWidget(self.lstTargets, 1, 0, 2, 3) - - lytMain = QGridLayout(self) - lytMain.addWidget(pnlTargets, 1, 0, 2, 1) - lytMain.addWidget(self.lblGain, 1, 1, 1, 1, Qt.AlignmentFlag.AlignHCenter) - lytMain.addWidget(self.sldGain, 2, 1, 1, 1, Qt.AlignmentFlag.AlignHCenter) - lytMain.addWidget(QLabel("Limit"), 3, 1, 1, 1, Qt.AlignmentFlag.AlignHCenter) - lytMain.addWidget(self.indAlarm, 1, 2, 1, 1) - lytMain.addWidget(self.barLevel, 2, 2, 1, 1) - lytMain.addWidget(QLabel(), 2, 3, 1, 1) - lytMain.addWidget(self.chkShowBoxes, 3, 0, 1, 1, Qt.AlignmentFlag.AlignCenter) - lytMain.setContentsMargins(0, 0, 0, 0) - - def btnAddTargetClicked(self): - self.dlgTarget.show() - - def btnDeleteTargetClicked(self): - try: - if item := self.lstTargets.currentItem(): - ret = QMessageBox.warning(self, "Delete Target: " + item.text(), "You are about to delete target\n" - "Are you sure you want to continue?", - QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) - if ret != QMessageBox.StandardButton.Ok: - return - self.lstTargets.takeItem(self.lstTargets.row(item)) - - match self.mw.videoConfigure.source: - case MediaSource.CAMERA: - if camera := self.mw.cameraPanel.getCurrentCamera(): - if camera.videoModelSettings: - camera.videoModelSettings.setTargets(self.lstTargets.toString()) - case MediaSource.FILE: - if self.mw.filePanel.videoModelSettings: - self.mw.filePanel.videoModelSettings.setTargets(self.lstTargets.toString()) - except Exception as ex: - logger.error(ex) - - def dlgListAccepted(self): - item = self.dlgTarget.list.item(self.dlgTarget.list.currentRow()) - self.onAddItemDoubleClicked(item) - - def onAddItemDoubleClicked(self, item): - try: - target = Target(item.text(), item.id) - found = False - for i in range(self.lstTargets.count()): - if target.text() == self.lstTargets.item(i).text(): - found = True - break - if not found: - self.lstTargets.addItem(target) - - match self.mw.videoConfigure.source: - case MediaSource.CAMERA: - if camera := self.mw.cameraPanel.getCurrentCamera(): - if camera.videoModelSettings: - camera.videoModelSettings.setTargets(self.lstTargets.toString()) - case MediaSource.FILE: - if self.mw.filePanel.videoModelSettings: - self.mw.filePanel.videoModelSettings.setTargets(self.lstTargets.toString()) - except Exception as ex: - logger.error(ex) - - def setTargets(self, targets): - while self.lstTargets.count() > 0: - self.lstTargets.takeItem(0) - - for t in targets: - self.lstTargets.addItem(Target(self.dlgTarget.targets[t], t)) - - def getTargets(self): - output = [] - for i in range(self.lstTargets.count()): - target = self.lstTargets.item(i) - output.append(target.id) - return output - - def sldGainValueChanged(self, value): - try: - self.lblGain.setText(f'{value}') - match self.mw.videoConfigure.source: - case MediaSource.CAMERA: - if camera := self.mw.cameraPanel.getCurrentCamera(): - if camera.videoModelSettings: - camera.videoModelSettings.setModelOutputLimit(value) - case MediaSource.FILE: - if self.mw.filePanel.videoModelSettings: - self.mw.filePanel.videoModelSettings.setModelOutputLimit(value) - except Exception as ex: - logger.error(ex) - - def chkShowBoxesStateChanged(self, state): - try: - match self.mw.videoConfigure.source: - case MediaSource.CAMERA: - if camera := self.mw.cameraPanel.getCurrentCamera(): - if camera.videoModelSettings: - camera.videoModelSettings.setModelShowBoxes(bool(state)) - case MediaSource.FILE: - if self.mw.filePanel.videoModelSettings: - self.mw.filePanel.videoModelSettings.setModelShowBoxes(bool(state)) - except Exception as ex: - logger.error(ex) +#******************************************************************** +# libonvif/onvif-gui/gui/components/target.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from PyQt6.QtWidgets import QDialog, QGridLayout, QListWidget, QListWidgetItem, \ + QDialogButtonBox, QWidget, QLabel, QPushButton, QMessageBox, QSlider, QCheckBox +from PyQt6.QtCore import Qt, pyqtSignal, QObject +from .warningbar import WarningBar, Indicator +from gui.enums import MediaSource +from loguru import logger + +class Target(QListWidgetItem): + def __init__(self, name, id): + super().__init__(name) + self.id = id + +class TargetDialog(QDialog): + def __init__(self, mw): + super().__init__(mw) + self.mw = mw + self.source = MediaSource.CAMERA + + self.targets = { + 0: "person", + 1: "bicycle", + 2: "car", + 3: "motorcycle", + 4: "airplane", + 5: "bus", + 6: "train", + 7: "truck", + 8: "boat", + 14: "bird", + 15: "cat", + 16: "dog", + 17: "horse", + 18: "sheep", + 19: "cow", + 21: "bear" + } + + self.setModal(True) + self.setWindowTitle("Add Target") + self.list = QListWidget() + for key in self.targets: + self.list.addItem(Target(self.targets[key], key)) + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Close) + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.buttonBox.rejected.connect(self.reject) + + lytMain = QGridLayout(self) + lytMain.addWidget(self.list, 0, 0, 1, 2) + lytMain.addWidget(self.buttonBox, 1, 0, 1, 2) + + self.list.setCurrentRow(0) + + def reject(self): + self.hide() + +class TargetListSignals(QObject): + delete = pyqtSignal() + +class TargetList(QListWidget): + def __init__(self, mw): + super().__init__() + self.mw = mw + self.signals = TargetListSignals() + + def keyPressEvent(self, event): + if event.key() == Qt.Key.Key_Delete: + self.signals.delete.emit() + return super().keyPressEvent(event) + + def toString(self): + output = "" + length = self.count() + for i in range(length): + output += str(self.item(i).id) + if i < length - 1: + output += ":" + return output + +class TargetSelector(QWidget): + def __init__(self, mw, module): + super().__init__() + self.mw = mw + self.module = module + + self.lstTargets = TargetList(self.mw) + self.lstTargets.signals.delete.connect(self.btnDeleteTargetClicked) + self.lblTargets = QLabel("Targets") + self.btnAddTarget = QPushButton("+") + self.btnAddTarget.clicked.connect(self.btnAddTargetClicked) + self.btnDeleteTarget = QPushButton("-") + self.btnDeleteTarget.clicked.connect(self.btnDeleteTargetClicked) + self.dlgTarget = TargetDialog(self.mw) + self.dlgTarget.list.itemDoubleClicked.connect(self.onAddItemDoubleClicked) + self.dlgTarget.buttonBox.accepted.connect(self.dlgListAccepted) + + # gui works better if these are on the same panel + self.barLevel = WarningBar() + self.indAlarm = Indicator(self.mw) + self.sldGain = QSlider(Qt.Orientation.Vertical) + self.sldGain.setMinimum(0) + self.sldGain.setMaximum(100) + self.sldGain.setValue(0) + self.sldGain.valueChanged.connect(self.sldGainValueChanged) + self.lblGain = QLabel("0") + + pnlTargets = QWidget() + pnlTargets.setMaximumWidth(200) + lytTargets = QGridLayout(pnlTargets) + + self.chkShowBoxes = QCheckBox("Show Boxes") + self.chkShowBoxes.stateChanged.connect(self.chkShowBoxesStateChanged) + + lytTargets.addWidget(self.lblTargets, 0, 0, 1, 1) + lytTargets.addWidget(self.btnDeleteTarget, 0, 1, 1, 1) + lytTargets.addWidget(self.btnAddTarget, 0, 2, 1, 1) + lytTargets.addWidget(self.lstTargets, 1, 0, 2, 3) + + lytMain = QGridLayout(self) + lytMain.addWidget(pnlTargets, 1, 0, 2, 1) + lytMain.addWidget(self.lblGain, 1, 1, 1, 1, Qt.AlignmentFlag.AlignHCenter) + lytMain.addWidget(self.sldGain, 2, 1, 1, 1, Qt.AlignmentFlag.AlignHCenter) + lytMain.addWidget(QLabel("Limit"), 3, 1, 1, 1, Qt.AlignmentFlag.AlignHCenter) + lytMain.addWidget(self.indAlarm, 1, 2, 1, 1) + lytMain.addWidget(self.barLevel, 2, 2, 1, 1) + lytMain.addWidget(QLabel(), 2, 3, 1, 1) + lytMain.addWidget(self.chkShowBoxes, 3, 0, 1, 1, Qt.AlignmentFlag.AlignCenter) + lytMain.setContentsMargins(0, 0, 0, 0) + + def btnAddTargetClicked(self): + self.dlgTarget.show() + + def btnDeleteTargetClicked(self): + try: + if item := self.lstTargets.currentItem(): + ret = QMessageBox.warning(self, "Delete Target: " + item.text(), "You are about to delete target\n" + "Are you sure you want to continue?", + QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) + if ret != QMessageBox.StandardButton.Ok: + return + self.lstTargets.takeItem(self.lstTargets.row(item)) + + match self.mw.videoConfigure.source: + case MediaSource.CAMERA: + if camera := self.mw.cameraPanel.getCurrentCamera(): + if camera.videoModelSettings: + camera.videoModelSettings.setTargets(self.lstTargets.toString()) + case MediaSource.FILE: + if self.mw.filePanel.videoModelSettings: + self.mw.filePanel.videoModelSettings.setTargets(self.lstTargets.toString()) + except Exception as ex: + logger.error(ex) + + def dlgListAccepted(self): + item = self.dlgTarget.list.item(self.dlgTarget.list.currentRow()) + self.onAddItemDoubleClicked(item) + + def onAddItemDoubleClicked(self, item): + try: + target = Target(item.text(), item.id) + found = False + for i in range(self.lstTargets.count()): + if target.text() == self.lstTargets.item(i).text(): + found = True + break + if not found: + self.lstTargets.addItem(target) + + match self.mw.videoConfigure.source: + case MediaSource.CAMERA: + if camera := self.mw.cameraPanel.getCurrentCamera(): + if camera.videoModelSettings: + camera.videoModelSettings.setTargets(self.lstTargets.toString()) + case MediaSource.FILE: + if self.mw.filePanel.videoModelSettings: + self.mw.filePanel.videoModelSettings.setTargets(self.lstTargets.toString()) + except Exception as ex: + logger.error(ex) + + def setTargets(self, targets): + while self.lstTargets.count() > 0: + self.lstTargets.takeItem(0) + + for t in targets: + self.lstTargets.addItem(Target(self.dlgTarget.targets[t], t)) + + def getTargets(self): + output = [] + for i in range(self.lstTargets.count()): + target = self.lstTargets.item(i) + output.append(target.id) + return output + + def sldGainValueChanged(self, value): + try: + self.lblGain.setText(f'{value}') + match self.mw.videoConfigure.source: + case MediaSource.CAMERA: + if camera := self.mw.cameraPanel.getCurrentCamera(): + if camera.videoModelSettings: + camera.videoModelSettings.setModelOutputLimit(value) + case MediaSource.FILE: + if self.mw.filePanel.videoModelSettings: + self.mw.filePanel.videoModelSettings.setModelOutputLimit(value) + except Exception as ex: + logger.error(ex) + + def chkShowBoxesStateChanged(self, state): + try: + match self.mw.videoConfigure.source: + case MediaSource.CAMERA: + if camera := self.mw.cameraPanel.getCurrentCamera(): + if camera.videoModelSettings: + camera.videoModelSettings.setModelShowBoxes(bool(state)) + case MediaSource.FILE: + if self.mw.filePanel.videoModelSettings: + self.mw.filePanel.videoModelSettings.setModelShowBoxes(bool(state)) + except Exception as ex: + logger.error(ex) diff --git a/onvif-gui/gui/components/thresholdslider.py b/onvif-gui/gui/components/thresholdslider.py index 432b93bf..1a6e7aed 100644 --- a/onvif-gui/gui/components/thresholdslider.py +++ b/onvif-gui/gui/components/thresholdslider.py @@ -19,7 +19,7 @@ from PyQt6.QtWidgets import QWidget, QSlider, QLabel, QGridLayout from PyQt6.QtCore import Qt -from gui.onvif.datastructures import MediaSource +from gui.enums import MediaSource from loguru import logger class ThresholdSlider(QWidget): diff --git a/onvif-gui/gui/components/warningbar.py b/onvif-gui/gui/components/warningbar.py index 0aaca502..f847f06c 100644 --- a/onvif-gui/gui/components/warningbar.py +++ b/onvif-gui/gui/components/warningbar.py @@ -1,84 +1,84 @@ -#******************************************************************** -# libonvif/onvif-gui/gui/components/warningbar.py -# -# Copyright (c) 2024 Stephen Rhodes -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#*********************************************************************/ - -from PyQt6.QtWidgets import QLabel -from PyQt6.QtGui import QPainter, QColorConstants, QLinearGradient, QColor -from PyQt6.QtCore import QRect, QTimer, pyqtSignal, QObject - -class WarningBar(QLabel): - def __init__(self): - super().__init__() - self.setMaximumWidth(15) - self.setStyleSheet("QLabel { border : 1px solid #808D9E; }") - self.level = 0.0 - self.inverted = False - - def paintEvent(self, event): - marker = max(int(self.height()*(1-self.level)-2), 0) - if self.inverted: - marker = min(int(self.height() * self.level), self.height()-2) - painter = QPainter(self) - gradient = QLinearGradient(0,0,0,100) - gradient.setColorAt(0.0, QColorConstants.Red) - gradient.setColorAt(0.5, QColorConstants.DarkYellow) - gradient.setColorAt(1.0, QColorConstants.DarkGreen) - painter.fillRect(QRect(1, 1, 13, self.height()-2), gradient) - painter.fillRect(QRect(1, 1, 13, marker), QColor("#3B3B3B")) - - def setLevel(self, level): - self.level = level - self.update() - -class IndicatorSignals(QObject): - start = pyqtSignal() - -class Indicator(QLabel): - def __init__(self, mw): - super().__init__() - self.mw = mw - self.setMaximumWidth(15) - self.setMaximumHeight(10) - self.setStyleSheet("QLabel { border : 1px solid #808D9E; }") - self.timer = QTimer() - self.timer.setInterval(self.mw.settingsPanel.spnLagTime.value() * 1000) - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.timeout) - self.signals = IndicatorSignals() - self.signals.start.connect(self.timer.start) - self.state = 0 - - def paintEvent(self, event): - color = QColor("#3B3B3B") - if self.state: - color = QColorConstants.Red - painter = QPainter(self) - painter.fillRect(QRect(1, 1, 13, self.height()-2), color) - - def setState(self, state): - self.state = int(state) - if state: - self.signals.start.emit() - self.update() - - def getState(self): - return self.state - - def timeout(self): - self.setState(0) - +#******************************************************************** +# libonvif/onvif-gui/gui/components/warningbar.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from PyQt6.QtWidgets import QLabel +from PyQt6.QtGui import QPainter, QColorConstants, QLinearGradient, QColor +from PyQt6.QtCore import QRect, QTimer, pyqtSignal, QObject + +class WarningBar(QLabel): + def __init__(self): + super().__init__() + self.setMaximumWidth(15) + self.setStyleSheet("QLabel { border : 1px solid #808D9E; }") + self.level = 0.0 + self.inverted = False + + def paintEvent(self, event): + marker = max(int(self.height()*(1-self.level)-2), 0) + if self.inverted: + marker = min(int(self.height() * self.level), self.height()-2) + painter = QPainter(self) + gradient = QLinearGradient(0,0,0,100) + gradient.setColorAt(0.0, QColorConstants.Red) + gradient.setColorAt(0.5, QColorConstants.DarkYellow) + gradient.setColorAt(1.0, QColorConstants.DarkGreen) + painter.fillRect(QRect(1, 1, 13, self.height()-2), gradient) + painter.fillRect(QRect(1, 1, 13, marker), QColor("#3B3B3B")) + + def setLevel(self, level): + self.level = level + self.update() + +class IndicatorSignals(QObject): + start = pyqtSignal() + +class Indicator(QLabel): + def __init__(self, mw): + super().__init__() + self.mw = mw + self.setMaximumWidth(15) + self.setMaximumHeight(10) + self.setStyleSheet("QLabel { border : 1px solid #808D9E; }") + self.timer = QTimer() + self.timer.setInterval(self.mw.settingsPanel.alarm.spnLagTime.value() * 1000) + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.timeout) + self.signals = IndicatorSignals() + self.signals.start.connect(self.timer.start) + self.state = 0 + + def paintEvent(self, event): + color = QColor("#3B3B3B") + if self.state: + color = QColorConstants.Red + painter = QPainter(self) + painter.fillRect(QRect(1, 1, 13, self.height()-2), color) + + def setState(self, state): + self.state = int(state) + if state: + self.signals.start.emit() + self.update() + + def getState(self): + return self.state + + def timeout(self): + self.setState(0) + diff --git a/onvif-gui/gui/enums.py b/onvif-gui/gui/enums.py new file mode 100644 index 00000000..2a03f8d8 --- /dev/null +++ b/onvif-gui/gui/enums.py @@ -0,0 +1,17 @@ +from enum import Enum + +class ProxyType(Enum): + STAND_ALONE = 0 + SERVER = 1 + CLIENT = 2 + +class StreamState(Enum): + IDLE = 0 + CONNECTING = 1 + CONNECTED = 2 + INVALID = 3 + +class MediaSource(Enum): + CAMERA = 0 + FILE = 1 + diff --git a/onvif-gui/gui/glwidget.py b/onvif-gui/gui/glwidget.py index 0ccf752c..95badfae 100644 --- a/onvif-gui/gui/glwidget.py +++ b/onvif-gui/gui/glwidget.py @@ -22,8 +22,8 @@ from PyQt6.QtCore import QSize, QPointF, QRectF, QTimer, QObject, pyqtSignal import numpy as np from datetime import datetime -from time import sleep -from gui.onvif.datastructures import StreamState +import time +from gui.enums import StreamState from loguru import logger class GLWidgetSignals(QObject): @@ -48,11 +48,12 @@ def __init__(self, mw): self.timer = QTimer() self.timer.timeout.connect(self.timerCallback) - refreshInterval = self.mw.settingsPanel.spnDisplayRefresh.value() + refreshInterval = self.mw.settingsPanel.general.spnDisplayRefresh.value() self.timer.start(refreshInterval) def renderCallback(self, F, player): try : + player.last_render = datetime.now() if player.analyze_video and self.mw.videoConfigure.initialized: F = self.mw.pyVideoCallback(F, player) else: @@ -83,7 +84,7 @@ def renderCallback(self, F, player): self.mw.pm.sizes[player.uri] = QSize(w_s, h_s) while player.rendering: - sleep(0.001) + time.sleep(0.001) self.image_loading = True @@ -112,6 +113,7 @@ def renderCallback(self, F, player): logger.error(f'GLWidget render callback exception: {str(ex)}') def timerCallback(self): + #self.repaint() self.update() def sizeHint(self): @@ -172,7 +174,8 @@ def paintGL(self): try: painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) - + painter.fillRect(self.rect(), QColorConstants.Black) + if self.model_loading: rectSpinner = QRectF(0, 0, 40, 40) rectSpinner.moveCenter(QPointF(self.rect().center())) @@ -181,13 +184,18 @@ def paintGL(self): for player in self.mw.pm.players: - camera = self.mw.cameraPanel.getCamera(player.uri) + if player.isCameraStream() and player.running and player.last_render: + interval = datetime.now() - player.last_render + if interval.total_seconds() > 5: + logger.debug(f'Lost signal for {self.mw.getCameraName(player.uri)}') + player.requestShutdown() + self.mw.playMedia(player.uri) if player.pipe_output_start_time: interval = datetime.now() - player.pipe_output_start_time if interval.total_seconds() > self.mw.STD_FILE_DURATION: - d = self.mw.settingsPanel.dirArchive.txtDirectory.text() - if self.mw.settingsPanel.chkManageDiskUsage.isChecked(): + d = self.mw.settingsPanel.storage.dirArchive.txtDirectory.text() + if self.mw.settingsPanel.storage.chkManageDiskUsage.isChecked(): player.manageDirectory(d) filename = player.getPipeOutFilename(d) @@ -197,6 +205,8 @@ def paintGL(self): if player.disable_video or player.hidden: continue + camera = self.mw.cameraPanel.getCamera(player.uri) + if player.image is None: rect = self.mw.pm.displayRect(player.uri, self.size()) rectSpinner = QRectF(0, 0, 40, 40) @@ -215,7 +225,7 @@ def paintGL(self): continue while self.image_loading: - sleep(0.001) + time.sleep(0.001) player.rendering = True @@ -241,6 +251,9 @@ def paintGL(self): if camera.isAlarming(): painter.drawImage(rectBlinker, QImage("image:alarm_plain.png")) + if not player.isCameraStream() and player.alarm_state: + painter.drawImage(rectBlinker, QImage("image:alarm_plain.png")) + if self.isFocusedURI(player.uri): painter.setPen(QColorConstants.White) painter.drawRect(rect.adjusted(1, 1, -2, -2)) diff --git a/onvif-gui/gui/main.py b/onvif-gui/gui/main.py index fe0dfcac..6c0d7d19 100644 --- a/onvif-gui/gui/main.py +++ b/onvif-gui/gui/main.py @@ -39,237 +39,19 @@ from pathlib import Path from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QSplitter, \ QTabWidget, QMessageBox -from PyQt6.QtCore import pyqtSignal, QObject, QSettings, QDir, QSize, QTimer, QFile, Qt +from PyQt6.QtCore import pyqtSignal, QObject, QSettings, QDir, QSize, QTimer, Qt from PyQt6.QtGui import QIcon -from gui.panels import CameraPanel, FilePanel, SettingsPanel, VideoPanel, AudioPanel +from gui.panels import CameraPanel, FilePanel, SettingsPanel, VideoPanel, \ + AudioPanel +from gui.enums import ProxyType from gui.glwidget import GLWidget from gui.manager import Manager +from gui.player import Player from gui.onvif import StreamState -from collections import deque -import shutil import avio +import liblivemedia -VERSION = "2.1.1" - -class PipeManager(): - def __init__(self, mw, uri): - self.mw = mw - self.uri = uri - -class PlayerSignals(QObject): - start = pyqtSignal() - stop = pyqtSignal() - -class Player(avio.Player): - def __init__(self, uri, mw): - super().__init__(uri) - self.mw = mw - self.signals = PlayerSignals() - self.image = None - self.rendering = False - self.desired_aspect = 0 - self.systemTabSettings = None - self.analyze_video = False - self.analyze_audio = False - self.videoModelSettings = None - self.audioModelSettings = None - self.detection_count = deque() - self.last_image = None - self.zombie_counter = 0 - - self.boxes = [] - self.labels = [] - self.scores = [] - - self.save_image_filename = None - self.pipe_output_start_time = None - self.estimated_file_size = 0 - self.packet_drop_frame_counter = 0 - - self.timer = QTimer() - self.timer.setInterval(self.mw.settingsPanel.spnLagTime.value() * 1000) - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.timeout) - self.signals.start.connect(self.timer.start) - self.signals.stop.connect(self.timer.stop) - - self.alarm_state = 0 - self.last_alarm_state = 0 - self.file_progress = 0.0 - - def requestShutdown(self): - self.setAlarmState(0) - self.analyze_video = False - self.analyze_audio = False - self.request_reconnect = False - self.running = False - - def setAlarmState(self, state): - self.alarm_state = int(state) - - record_enable = self.systemTabSettings.record_enable if self.systemTabSettings else False - record_alarm = self.systemTabSettings.record_alarm if self.systemTabSettings else False - camera = self.mw.cameraPanel.getCamera(self.uri) - manual_recording = camera.manual_recording if camera else False - profile = camera.getRecordProfile() if camera else None - player = self.mw.pm.getPlayer(profile.uri()) if profile else None - - if state: - self.signals.start.emit() - if record_enable and record_alarm: - if player: - if not player.isRecording(): - d = self.mw.settingsPanel.dirArchive.txtDirectory.text() - if self.mw.settingsPanel.chkManageDiskUsage.isChecked(): - player.manageDirectory(d) - filename = player.getPipeOutFilename(d) - if filename: - player.toggleRecording(filename) - self.mw.cameraPanel.syncGUI() - else: - self.signals.stop.emit() - if record_alarm and not manual_recording: - if player: - if player.isRecording(): - player.toggleRecording("") - self.mw.cameraPanel.syncGUI() - - def timeout(self): - self.setAlarmState(0) - - def getPipeOutFilename(self, d): - filename = None - camera = self.mw.cameraPanel.getCamera(self.uri) - if camera: - d = self.mw.settingsPanel.dirArchive.txtDirectory.text() - root = d + "/" + camera.text() - Path(root).mkdir(parents=True, exist_ok=True) - self.pipe_output_start_time = datetime.now() - filename = '{0:%Y%m%d%H%M%S}'.format(self.pipe_output_start_time) - filename = root + "/" + filename + ".mp4" - self.setMetaData("title", camera.text()) - return filename - - def estimateFileSize(self): - # duration is in seconds, cameras report bitrate in kbps, result in bytes - result = 0 - bitrate = 0 - profile = self.mw.cameraPanel.getProfile(self.uri) - if profile: - audio_bitrate = min(profile.audio_bitrate(), 128) - video_bitrate = min(profile.bitrate(), 16384) - bitrate = video_bitrate + audio_bitrate - result = (bitrate * 1000 / 8) * self.mw.STD_FILE_DURATION - self.estimated_file_size = result - return result - - def getCommittedSize(self): - committed = 0 - for player in self.mw.pm.players: - if player.isRecording(): - committed += player.estimateFileSize() - player.pipeBytesWritten() - return committed - - def getDirectorySize(self, d): - total_size = 0 - for dirpath, dirnames, filenames in os.walk(d): - for f in filenames: - fp = os.path.join(dirpath, f) - if not os.path.islink(fp): - try: - total_size += os.path.getsize(fp) - except FileNotFoundError: - pass - - dir_size = "{:.2f}".format(total_size / 1000000000) - self.mw.settingsPanel.grpDiskUsage.setTitle(f'Disk Usage (currently {dir_size} GB)') - return total_size - - def getOldestFile(self, d): - oldest_file = None - oldest_time = None - for dirpath, dirnames, filenames in os.walk(d): - for f in filenames: - fp = os.path.join(dirpath, f) - if not os.path.islink(fp): - - stem = Path(fp).stem - if len(stem) == 14 and stem.isnumeric(): - try: - if oldest_file is None: - oldest_file = fp - oldest_time = os.path.getmtime(fp) - else: - file_time = os.path.getmtime(fp) - if file_time < oldest_time: - oldest_file = fp - oldest_time = file_time - except FileNotFoundError: - pass - return oldest_file - - def getMaximumDirectorySize(self, d): - estimated_file_size = self.estimateFileSize() - space_committed = self.getCommittedSize() - allowed_space = min(self.mw.settingsPanel.spnDiskLimit.value() * 1000000000, shutil.disk_usage(d)[2]) - return allowed_space - (space_committed + estimated_file_size) - - def manageDirectory(self, d): - while self.getDirectorySize(d) > self.getMaximumDirectorySize(d): - oldest_file = self.getOldestFile(d) - if oldest_file: - QFile.remove(oldest_file) - #logger.debug(f'File has been deleted by auto process: {oldest_file}') - else: - logger.debug("Unable to find the oldest file for deletion during disk management") - break - - def handleAlarm(self, state): - if self.analyze_video or self.analyze_audio: - if state: - self.setAlarmState(1) - if self.alarm_state: - if self.isCameraStream(): - if self.systemTabSettings.sound_alarm_enable: - filename = f'{self.mw.getLocation()}/gui/resources/{self.mw.settingsPanel.cmbSoundFiles.currentText()}' - if self.systemTabSettings.sound_alarm_once: - if self.alarm_state != self.last_alarm_state: - self.mw.playMedia(filename, True) - if self.systemTabSettings.sound_alarm_loop: - p = self.mw.pm.getPlayer(filename) - if not p: - self.mw.playMedia(filename, True) - self.last_alarm_state = self.alarm_state - else: - self.setAlarmState(0) - - def getFrameRate(self): - frame_rate = self.getVideoFrameRate() - if frame_rate <= 0: - profile = self.mw.cameraPanel.getProfile(self.uri) - if profile: - frame_rate = profile.frame_rate() - return frame_rate - - def processModelOutput(self): - - while self.rendering: - sleep(0.001) - - sum = 0 - - if len(self.detection_count) > self.videoModelSettings.sampleSize - 1 and len(self.detection_count): - self.detection_count.popleft() - - if len(self.boxes): - self.detection_count.append(1) - else: - self.detection_count.append(0) - - for count in self.detection_count: - sum += count - - return sum +VERSION = "2.2.4" class TimerSignals(QObject): timeoutPlayer = pyqtSignal(str) @@ -346,11 +128,14 @@ def __init__(self, clear_settings=False): self.splitKey = "MainWindow/split" self.collapsedKey = "MainWindow/collapsed" + self.signals = MainWindowSignals() + self.pm = Manager(self) self.timers = {} self.audioPlayer = None - self.signals = MainWindowSignals() + self.proxies = {} + self.proxy = None self.settingsPanel = SettingsPanel(self) self.signals.started.connect(self.settingsPanel.onMediaStarted) @@ -370,7 +155,7 @@ def __init__(self, clear_settings=False): self.signals.error.connect(self.onError) self.signals.reconnect.connect(self.startReconnectTimer) self.signals.stopReconnect.connect(self.stopReconnectTimer) - + self.tab = QTabWidget() self.tab.addTab(self.cameraPanel, "Cameras") self.tab.addTab(self.filePanel, "Files") @@ -396,7 +181,7 @@ def __init__(self, clear_settings=False): self.setGeometry(rect) self.discoverTimer = None - if self.settingsPanel.chkAutoDiscover.isChecked(): + if self.settingsPanel.discover.chkAutoDiscover.isChecked(): self.cameraPanel.btnDiscoverClicked() self.videoWorkerHook = None @@ -495,16 +280,12 @@ def playMedia(self, uri, alarm_sound=False): logger.debug(f'Attempt to create player with null uri') return - if existing := self.pm.getPlayer(uri): - logger.debug(f'Duplicate media uri from {self.getCameraName(uri)}') - existing_terminated = False - if not existing.running: - existing.zombie_counter += 1 - if existing.zombie_counter > 60: - logger.debug(f'Removing zombie player {self.getCameraName(uri)}') - self.pm.removePlayer(uri) - existing_terminated = True - if not existing_terminated: + count = 0 + while self.pm.getPlayer(uri) is not None: + sleep(0.01) + count += 1 + if count > 300: + logger.debug(f'Duplicate media uri from {self.getCameraName(uri)} is blocking launch of new player') return player = Player(uri, self) @@ -519,15 +300,16 @@ def playMedia(self, uri, alarm_sound=False): player.infoCallback = lambda msg, uri : self.infoCallback(msg, uri) player.getAudioStatus = lambda : self.getAudioStatus() player.setAudioStatus = lambda status : self.setAudioStatus(status) - player.hw_device_type = self.settingsPanel.getDecoder() + player.hw_device_type = self.settingsPanel.general.getDecoder() + player.audio_driver_index = self.settingsPanel.general.cmbAudioDriver.currentIndex() if player.isCameraStream(): if profile := self.cameraPanel.getProfile(uri): - player.vpq_size = self.settingsPanel.spnCacheMax.value() - player.apq_size = self.settingsPanel.spnCacheMax.value() + player.vpq_size = self.settingsPanel.general.spnCacheMax.value() + player.apq_size = self.settingsPanel.general.spnCacheMax.value() if profile.audio_encoding() == "AAC" and profile.audio_sample_rate() and profile.frame_rate(): player.apq_size = int(player.vpq_size * profile.audio_sample_rate() / profile.frame_rate()) - player.buffer_size_in_seconds = self.settings.value(self.settingsPanel.bufferSizeKey, 10) + player.buffer_size_in_seconds = self.settings.value(self.settingsPanel.alarm.bufferSizeKey, 10) player.onvif_frame_rate.num = profile.frame_rate() player.onvif_frame_rate.den = 1 player.disable_audio = profile.getDisableAudio() @@ -548,7 +330,7 @@ def playMedia(self, uri, alarm_sound=False): if alarm_sound: player.disable_video = True player.setMute(False) - player.setVolume(self.settingsPanel.sldAlarmVolume.value()) + player.setVolume(self.settingsPanel.alarm.sldAlarmVolume.value()) player.analyze_audio = False else: player.setVolume(self.filePanel.getVolume()) @@ -583,30 +365,56 @@ def showEvent(self, event): if not splitterState: self.splitterMoved(0, 0) - if self.settingsPanel.chkStartFullScreen.isChecked(): + if self.settingsPanel.general.chkStartFullScreen.isChecked(): self.showFullScreen() super().showEvent(event) - def closeEvent(self, event): + def startAllCameras(self): + try: + lstCamera = self.cameraPanel.lstCamera + if lstCamera: + cameras = [lstCamera.item(x) for x in range(lstCamera.count())] + for camera in cameras: + self.cameraPanel.setCurrentCamera(camera.uri()) + self.cameraPanel.onItemDoubleClicked(camera) + except Exception as ex: + logger.error(f'start all cameras error {ex}') + + def closeAllStreams(self): try: - self.cameraPanel.closeEvent() for player in self.pm.players: player.requestShutdown() for timer in self.timers.values(): timer.stop() + self.pm.auto_start_mode = False + lstCamera = self.cameraPanel.lstCamera + if lstCamera: + cameras = [lstCamera.item(x) for x in range(lstCamera.count())] + for camera in cameras: + camera.setIconIdle() count = 0 while len(self.pm.players): sleep(0.1) count += 1 - if count > 200: + if count > 20: logger.debug("not all players closed within the allotted time, flushing player manager") self.pm.players.clear() break self.pm.ordinals.clear() self.pm.sizes.clear() + self.cameraPanel.syncGUI() + if self.settingsPanel: + if self.settingsPanel.general: + self.settingsPanel.general.btnCloseAll.setText("Start All Cameras") + except Exception as ex: + logger.error(f'close all streams error {ex}') + + def closeEvent(self, event): + try: + self.closeAllStreams() self.settings.setValue(self.geometryKey, self.geometry()) super().closeEvent(event) @@ -615,7 +423,7 @@ def closeEvent(self, event): def mediaPlayingStarted(self, uri): if self.isCameraStreamURI(uri): - logger.debug(f'camera stream opened {self.getCameraName(uri)}') + logger.debug(f'camera stream opened {self.getCameraName(uri)} : {uri}') if self.pm.auto_start_mode: finished = True @@ -641,8 +449,8 @@ def mediaPlayingStarted(self, uri): if camera.profiles[camera.displayProfileIndex()].uri() == uri: record = True if record: - d = self.settingsPanel.dirArchive.txtDirectory.text() - if self.settingsPanel.chkManageDiskUsage.isChecked(): + d = self.settingsPanel.storage.dirArchive.txtDirectory.text() + if self.settingsPanel.storage.chkManageDiskUsage.isChecked(): player.manageDirectory(d) filename = player.getPipeOutFilename(d) if filename: @@ -702,9 +510,14 @@ def infoCallback(self, msg, uri): else: name = f'File: {uri}' - if msg.startswith("Output file creation failure:") or msg.startswith("Record to file close error:"): + if msg.startswith("Output file creation failure") or \ + msg.startswith("Record to file close error") or \ + msg.startswith("SDL_OpenAudioDevice exception"): logger.error(f'{name}, Message: {msg}') + if msg.startswith("Using SDL audio driver"): + logger.debug(msg) + else: print(f'{name}, Message: {msg}') @@ -728,25 +541,36 @@ def errorCallback(self, msg, uri, reconnect): logger.debug(f'Error from camera: {camera_name} : {msg}, attempting to re-connect') else: name = "" + last_msg = "" if self.isCameraStreamURI(uri): - player = self.pm.getPlayer(uri) - self.pm.removePlayer(uri) - self.pm.removeKeys(uri) + if player := self.pm.getPlayer(uri): + player.requestShutdown() + last_msg = player.last_msg + player.last_msg = msg + + if camera := self.cameraPanel.getCamera(uri): + if c_uri := camera.companionURI(uri): + if c_player := self.pm.getPlayer(c_uri): + c_player.requestShutdown() - camera = self.cameraPanel.getCamera(uri) - if camera: name = f'Camera: {self.getCameraName(uri)}' camera.setIconIdle() self.cameraPanel.syncGUI() self.cameraPanel.setTabsEnabled(True) - self.signals.error.emit(msg) + + if msg != last_msg: + self.signals.error.emit(msg) + else: - name = f'File: {uri}' - self.pm.removePlayer(uri) - self.pm.removeKeys(uri) - self.filePanel.control.btnPlay.setStyleSheet(self.filePanel.control.getButtonStyle("play")) - self.signals.error.emit(msg) + self.closeAllStreams() + #sleep(0.5) + #self.filePanel.control.btnPlay.setStyleSheet(self.filePanel.control.getButtonStyle("play")) + #sleep(0.5) + #self.signals.error.emit(msg) + #sleep(0.5) + #self.pm.removePlayer(uri) + logger.error(f'{name}, Error: {msg}') def mediaProgress(self, pct, uri): @@ -852,6 +676,39 @@ def getLogFilename(self): log_dir += os.path.sep + "logs" + os.path.sep + "onvif-gui" + os.path.sep + datestamp return log_dir + os.path.sep + source + "_" + timestamp + ".csv" + def startProxyServer(self): + try: + self.proxy = liblivemedia.ProxyServer() + self.proxy.init(554) + self.proxy.startLoop() + + except Exception as ex: + logger.error(f'Error starting proxy server {str(ex)}') + + def stopProxyServer(self): + if self.proxy: + self.proxy.stopLoop() + + def getProxyURI(self, arg): + match self.settingsPanel.proxy.proxyType: + case ProxyType.CLIENT: + return self.proxies[arg] + case ProxyType.SERVER: + return self.proxy.getProxyURI(arg) + + def addCameraProxy(self, camera): + match self.settingsPanel.proxy.proxyType: + case ProxyType.SERVER: + for profile in camera.profiles: + key = f'{camera.serial_number()}/{profile.profile()}' + existing_uri = self.proxy.getProxyURI(profile.stream_uri()) + if not len(existing_uri): + self.proxy.addURI(profile.stream_uri(), key, camera.onvif_data.username(), camera.onvif_data.password()) + case ProxyType.CLIENT: + for profile in camera.profiles: + server = self.settingsPanel.proxy.txtRemote.text() + self.proxies[profile.stream_uri()] = f'{server}{camera.serial_number()}/{profile.profile()}' + def style(self): blDefault = "#5B5B5B" bmDefault = "#4B4B4B" diff --git a/onvif-gui/gui/manager.py b/onvif-gui/gui/manager.py index bb49c159..b2f936a1 100644 --- a/onvif-gui/gui/manager.py +++ b/onvif-gui/gui/manager.py @@ -1,281 +1,281 @@ -#/******************************************************************** -# libonvif/onvif-gui/gui/manager.py -# -# Copyright (c) 2024 Stephen Rhodes -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#*********************************************************************/ - -from time import sleep -from loguru import logger -from PyQt6.QtCore import QRectF, QSize, Qt, QSizeF, QPointF - -class Manager(): - def __init__(self, mw): - self.players = [] - self.ordinals = {} - self.sizes = {} - self.start_lock = False - self.remove_lock = False - self.mw = mw - self.auto_start_mode = False - - def startPlayer(self, player): - while self.start_lock: - sleep(0.001) - self.start_lock = True - - if not player.disable_video: - if not player.uri in self.ordinals.keys(): - ordinal = self.getOrdinal() - if player.isCameraStream(): - camera = self.mw.cameraPanel.getCamera(player.uri) - if camera: - if self.auto_start_mode and camera.ordinal > -1: - duplicate = False - keys = self.ordinals.keys() - for key in keys: - c = self.mw.cameraPanel.getCamera(key) - if camera.ordinal == self.ordinals[key] and camera.serial_number() != c.serial_number(): - duplicate = True - if duplicate: - camera.ordinal = ordinal - else: - ordinal = camera.ordinal - else: - comp_uri = camera.companionURI(player.uri) - if comp_uri: - if comp_uri in self.ordinals.keys(): - ordinal = self.ordinals[comp_uri] - camera.setOrdinal(ordinal) - - self.ordinals[player.uri] = ordinal - - self.players.append(player) - player.start() - self.start_lock = False - - def getUniqueOrdinals(self): - result = [] - values = self.ordinals.values() - for value in values: - if value not in result: - result.append(value) - return result - - def getOrdinal(self): - ordinal = -1 - - values = self.getUniqueOrdinals() - for i in range(len(values)): - if not i in values: - ordinal = i - break - - if ordinal == -1: - ordinal = len(values) - - return ordinal - - def getPlayer(self, uri): - result = None - if uri: - for player in self.players: - if player.uri == uri: - result = player - break - return result - - def getPlayerByOrdinal(self, ordinal): - result = None - for key, value in self.ordinals.items(): - if value == ordinal: - result = self.getPlayer(key) - break - return result - - def getCurrentPlayer(self): - return self.getPlayer(self.mw.glWidget.focused_uri) - - def getStreamPairProfiles(self, uri): - result = [] - if uri: - camera = self.mw.cameraPanel.getCamera(uri) - if camera: - displayProfile = camera.getDisplayProfile() - if displayProfile: - result.append(displayProfile) - if camera.displayProfileIndex() != camera.recordProfileIndex(): - recordProfile = camera.getRecordProfile() - if recordProfile: - result.append(recordProfile) - - return result - - def getStreamPairURIs(self, uri): - result = [] - profiles = self.getStreamPairProfiles(uri) - for profile in profiles: - result.append(profile.uri()) - return result - - def getStreamPairPlayers(self, uri): - result = [] - profiles = self.getStreamPairProfiles(uri) - for profile in profiles: - player = self.getPlayer(profile.uri()) - if player: - result.append(player) - return result - - def getStreamPairTimers(self, uri): - result = [] - profiles = self.getStreamPairProfiles(uri) - for profile in profiles: - timer = self.mw.timers.get(profile.uri(), None) - if timer: - result.append(timer) - return result - - def removeKeys(self, uri): - if uri in self.ordinals.keys(): - del self.ordinals[uri] - if uri in self.sizes.keys(): - del self.sizes[uri] - - def removePlayer(self, uri): - for player in self.players: - if player.uri == uri: - while player.rendering: - sleep(0.001) - - if not player.request_reconnect: - self.removeKeys(uri) - - self.players.remove(player) - - def playerShutdownWait(self, uri): - player = self.getPlayer(uri) - if player: - player.requestShutdown() - count = 0 - while self.getPlayer(uri): - sleep(0.01) - count += 1 - if count > 200: - logger.error(f'Player did not complete shut down during allocated time interval: {uri}') - break - - def getMostCommonAspectRatio(self): - ratio_counter = {} - for size in self.sizes.values(): - ratio = round(1000 * size.width() / size.height()) - if not ratio in ratio_counter.keys(): - ratio_counter[ratio] = 1 - else: - ratio_counter[ratio] += 1 - - highest_count_key = -1 - if ratio_counter: - keys = list(ratio_counter.keys()) - if len(keys): - highest_count_key = keys[0] - highest_count = ratio_counter[highest_count_key] - for key in keys: - if ratio_counter[key] > highest_count: - highest_count = ratio_counter[key] - highest_count_key = key - - return highest_count_key - - def computeRowsCols(self, size_canvas, aspect_ratio): - - num_cells = len(self.getUniqueOrdinals()) - - if self.auto_start_mode: - num_cells = len(self.mw.cameraPanel.cached_serial_numbers) - - valid_layouts = [] - for i in range(1, num_cells+1): - for j in range(num_cells, 0, -1): - if ((i * j) >= num_cells): - if (((i-1)*j) < num_cells) and ((i*(j-1)) < num_cells): - valid_layouts.append(QSize(i, j)) - - index = -1 - min_ratio = 0 - first_pass = True - for i, layout in enumerate(valid_layouts): - composite = (aspect_ratio * layout.height()) / layout.width() - ratio = (size_canvas.width() / size_canvas.height()) / composite - optimize = abs(1 - ratio) - if first_pass: - first_pass = False - min_ratio = optimize - index = i - else: - if optimize < min_ratio: - min_ratio = optimize - index = i - - if index == -1: - return 0, 0 - - return valid_layouts[index].width(), valid_layouts[index].height() - - def displayRect(self, uri, canvas_size): - ar = self.getMostCommonAspectRatio() - num_rows, num_cols = self.computeRowsCols(canvas_size, ar / 1000) - if num_cols == 0: - return QRectF(QPointF(0, 0), QSizeF(canvas_size)) - - ordinal = -1 - if uri in self.ordinals.keys(): - ordinal = self.ordinals[uri] - else: - return QRectF(QPointF(0, 0), QSizeF(canvas_size)) - - if ordinal > num_rows * num_cols - 1: - ordinal = self.getOrdinal() - uris = self.getStreamPairURIs(uri) - for u in uris: - if u in self.ordinals.keys(): - self.ordinals[u] = ordinal - - col = ordinal % num_cols - row = int(ordinal / num_cols) - - composite_size = QSizeF() - if num_rows: - composite_size = QSizeF(num_cols * ar / 1000, num_rows) - composite_size.scale(QSizeF(canvas_size), Qt.AspectRatioMode.KeepAspectRatio) - - cell_width = composite_size.width() / num_cols - cell_height = composite_size.height() / num_rows - - image_size = QSizeF(ar, 1000) - if uri in self.sizes.keys(): - image_size = QSizeF(self.sizes[uri]) - - image_size.scale(cell_width, cell_height, Qt.AspectRatioMode.KeepAspectRatio) - w = image_size.width() - h = image_size.height() - - x_offset = (canvas_size.width() - composite_size.width() + (cell_width - w)) / 2 - y_offset = (canvas_size.height() - composite_size.height() + (cell_height - h)) / 2 - - x = (col * cell_width) + x_offset - y = (row * cell_height) + y_offset - - return QRectF(x, y, w, h) +#/******************************************************************** +# libonvif/onvif-gui/gui/manager.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from time import sleep +from loguru import logger +from PyQt6.QtCore import QRectF, QSize, Qt, QSizeF, QPointF + +class Manager(): + def __init__(self, mw): + self.players = [] + self.ordinals = {} + self.sizes = {} + self.start_lock = False + self.remove_lock = False + self.mw = mw + self.auto_start_mode = False + + def startPlayer(self, player): + while self.start_lock: + sleep(0.001) + self.start_lock = True + + if not player.disable_video: + if not player.uri in self.ordinals.keys(): + ordinal = self.getOrdinal() + if player.isCameraStream(): + camera = self.mw.cameraPanel.getCamera(player.uri) + if camera: + if self.auto_start_mode and camera.ordinal > -1: + duplicate = False + keys = self.ordinals.keys() + for key in keys: + c = self.mw.cameraPanel.getCamera(key) + if camera.ordinal == self.ordinals[key] and camera.serial_number() != c.serial_number(): + duplicate = True + if duplicate: + camera.ordinal = ordinal + else: + ordinal = camera.ordinal + else: + comp_uri = camera.companionURI(player.uri) + if comp_uri: + if comp_uri in self.ordinals.keys(): + ordinal = self.ordinals[comp_uri] + camera.setOrdinal(ordinal) + + self.ordinals[player.uri] = ordinal + + self.players.append(player) + player.start() + self.start_lock = False + + def getUniqueOrdinals(self): + result = [] + values = self.ordinals.values() + for value in values: + if value not in result: + result.append(value) + return result + + def getOrdinal(self): + ordinal = -1 + + values = self.getUniqueOrdinals() + for i in range(len(values)): + if not i in values: + ordinal = i + break + + if ordinal == -1: + ordinal = len(values) + + return ordinal + + def getPlayer(self, uri): + result = None + if uri: + for player in self.players: + if player.uri == uri: + result = player + break + return result + + def getPlayerByOrdinal(self, ordinal): + result = None + for key, value in self.ordinals.items(): + if value == ordinal: + result = self.getPlayer(key) + break + return result + + def getCurrentPlayer(self): + return self.getPlayer(self.mw.glWidget.focused_uri) + + def getStreamPairProfiles(self, uri): + result = [] + if uri: + camera = self.mw.cameraPanel.getCamera(uri) + if camera: + displayProfile = camera.getDisplayProfile() + if displayProfile: + result.append(displayProfile) + if camera.displayProfileIndex() != camera.recordProfileIndex(): + recordProfile = camera.getRecordProfile() + if recordProfile: + result.append(recordProfile) + + return result + + def getStreamPairURIs(self, uri): + result = [] + profiles = self.getStreamPairProfiles(uri) + for profile in profiles: + result.append(profile.uri()) + return result + + def getStreamPairPlayers(self, uri): + result = [] + profiles = self.getStreamPairProfiles(uri) + for profile in profiles: + player = self.getPlayer(profile.uri()) + if player: + result.append(player) + return result + + def getStreamPairTimers(self, uri): + result = [] + profiles = self.getStreamPairProfiles(uri) + for profile in profiles: + timer = self.mw.timers.get(profile.uri(), None) + if timer: + result.append(timer) + return result + + def removeKeys(self, uri): + if uri in self.ordinals.keys(): + del self.ordinals[uri] + if uri in self.sizes.keys(): + del self.sizes[uri] + + def removePlayer(self, uri): + for player in self.players: + if player.uri == uri: + while player.rendering: + sleep(0.001) + + if not player.request_reconnect: + self.removeKeys(uri) + + self.players.remove(player) + + def playerShutdownWait(self, uri): + player = self.getPlayer(uri) + if player: + player.requestShutdown() + count = 0 + while self.getPlayer(uri): + sleep(0.01) + count += 1 + if count > 200: + logger.error(f'Player did not complete shut down during allocated time interval: {uri}') + break + + def getMostCommonAspectRatio(self): + ratio_counter = {} + for size in self.sizes.values(): + ratio = round(1000 * size.width() / size.height()) + if not ratio in ratio_counter.keys(): + ratio_counter[ratio] = 1 + else: + ratio_counter[ratio] += 1 + + highest_count_key = -1 + if ratio_counter: + keys = list(ratio_counter.keys()) + if len(keys): + highest_count_key = keys[0] + highest_count = ratio_counter[highest_count_key] + for key in keys: + if ratio_counter[key] > highest_count: + highest_count = ratio_counter[key] + highest_count_key = key + + return highest_count_key + + def computeRowsCols(self, size_canvas, aspect_ratio): + + num_cells = len(self.getUniqueOrdinals()) + + if self.auto_start_mode: + num_cells = len(self.mw.cameraPanel.cached_serial_numbers) + + valid_layouts = [] + for i in range(1, num_cells+1): + for j in range(num_cells, 0, -1): + if ((i * j) >= num_cells): + if (((i-1)*j) < num_cells) and ((i*(j-1)) < num_cells): + valid_layouts.append(QSize(i, j)) + + index = -1 + min_ratio = 0 + first_pass = True + for i, layout in enumerate(valid_layouts): + composite = (aspect_ratio * layout.height()) / layout.width() + ratio = (size_canvas.width() / size_canvas.height()) / composite + optimize = abs(1 - ratio) + if first_pass: + first_pass = False + min_ratio = optimize + index = i + else: + if optimize < min_ratio: + min_ratio = optimize + index = i + + if index == -1: + return 0, 0 + + return valid_layouts[index].width(), valid_layouts[index].height() + + def displayRect(self, uri, canvas_size): + ar = self.getMostCommonAspectRatio() + num_rows, num_cols = self.computeRowsCols(canvas_size, ar / 1000) + if num_cols == 0: + return QRectF(QPointF(0, 0), QSizeF(canvas_size)) + + ordinal = -1 + if uri in self.ordinals.keys(): + ordinal = self.ordinals[uri] + else: + return QRectF(QPointF(0, 0), QSizeF(canvas_size)) + + if ordinal > num_rows * num_cols - 1: + ordinal = self.getOrdinal() + uris = self.getStreamPairURIs(uri) + for u in uris: + if u in self.ordinals.keys(): + self.ordinals[u] = ordinal + + col = ordinal % num_cols + row = int(ordinal / num_cols) + + composite_size = QSizeF() + if num_rows: + composite_size = QSizeF(num_cols * ar / 1000, num_rows) + composite_size.scale(QSizeF(canvas_size), Qt.AspectRatioMode.KeepAspectRatio) + + cell_width = composite_size.width() / num_cols + cell_height = composite_size.height() / num_rows + + image_size = QSizeF(ar, 1000) + if uri in self.sizes.keys(): + image_size = QSizeF(self.sizes[uri]) + + image_size.scale(cell_width, cell_height, Qt.AspectRatioMode.KeepAspectRatio) + w = image_size.width() + h = image_size.height() + + x_offset = (canvas_size.width() - composite_size.width() + (cell_width - w)) / 2 + y_offset = (canvas_size.height() - composite_size.height() + (cell_height - h)) / 2 + + x = (col * cell_width) + x_offset + y = (row * cell_height) + y_offset + + return QRectF(x, y, w, h) diff --git a/onvif-gui/gui/onvif/datastructures.py b/onvif-gui/gui/onvif/datastructures.py index 6d1d5b53..6147332d 100644 --- a/onvif-gui/gui/onvif/datastructures.py +++ b/onvif-gui/gui/onvif/datastructures.py @@ -1,281 +1,286 @@ -#/******************************************************************** -# libonvif/onvif-gui/gui/onvif/datastructures.py -# -# Copyright (c) 2023 Stephen Rhodes -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#*********************************************************************/ - -from time import sleep -from enum import auto, Enum -from PyQt6.QtWidgets import QListWidgetItem -from PyQt6.QtCore import Qt, pyqtSignal, QObject, QTimer -from PyQt6.QtGui import QIcon, QColor -import libonvif as onvif -from gui.onvif.systemtab import SystemTabSettings - -class StreamState(Enum): - IDLE = auto() - CONNECTING = auto() - CONNECTED = auto() - INVALID = auto() - -class MediaSource(Enum): - CAMERA = auto() - FILE = auto() - -class SessionSignals(QObject): - finished = pyqtSignal() - -class Session(onvif.Session): - def __init__(self, cp, interface): - super().__init__() - self.cp = cp - self.interface = interface - self.signals = SessionSignals() - self.discovered = lambda : self.finish() - self.getCredential = lambda D : self.cp.getCredential(D) - self.getData = lambda D : self.cp.getData(D) - self.timer = QTimer() - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.timeout) - self.signals.finished.connect(self.timer.stop) - self.active = False - - def start(self): - self.active = True - self.startDiscover() - self.timer.start(10000) - - def finish(self): - self.active = False - self.signals.finished.emit() - self.cp.discovered() - - def timeout(self): - self.cp.discoveryTimeout() - -class Camera(QListWidgetItem): - def __init__(self, onvif_data, mw): - super().__init__(onvif_data.alias) - self.onvif_data = onvif_data - self.mw = mw - self.icnIdle = QIcon("image:idle_lo.png") - self.icnOn = QIcon("image:record.png") - self.icnRecord = QIcon("image:recording_hi.png") - self.defaultForeground = self.foreground() - self.filled = False - self.last_msg = "" - - onvif_data.setSetting = self.setSetting - onvif_data.getSetting = self.getSetting - for profile in onvif_data.profiles: - profile.setSetting = self.setSetting - profile.getSetting = self.getSetting - self.profiles = onvif_data.profiles - - self.videoModelSettings = None - self.audioModelSettings = None - self.systemTabSettings = SystemTabSettings(self.mw, self) - self.manual_recording = False - self.ordinalKey = f'{self.serial_number()}/Ordinal' - self.ordinal = self.getOrdinal() - self.volumeKey = f'{self.serial_number()}/Volume' - self.volume = self.getVolume() - self.muteKey = f'{self.serial_number()}/Mute' - self.mute = self.getMute() - - def getSetting(self, key, default_value): - return str(self.mw.settings.value(key, default_value)) - - def setSetting(self, key, value): - self.mw.settings.setValue(key, value) - - def uri(self): - return self.onvif_data.uri() - - def serial_number(self): - return self.onvif_data.serial_number() - - def name(self): - return self.onvif_data.alias - - def xaddrs(self): - return self.onvif_data.xaddrs() - - def hasAudio(self): - return bool(self.onvif_data.audio_bitrate()) - - def setOrdinal(self, value): - self.ordinal = value - self.mw.settings.setValue(self.ordinalKey, value) - - def getOrdinal(self): - return int(self.mw.settings.value(self.ordinalKey, -1)) - - def getMute(self): - return bool(int(self.mw.settings.value(self.muteKey, 0))) - - def setMute(self, state): - self.mute = bool(state) - self.mw.settings.setValue(self.muteKey, int(state)) - - def getVolume(self): - return int(self.mw.settings.value(self.volumeKey, 80)) - - def setVolume(self, volume): - self.volume = volume - self.mw.settings.setValue(self.volumeKey, volume) - - def isRunning(self): - result = False - players = self.mw.pm.getStreamPairPlayers(self.uri()) - if len(players): - result = True - return result - - def isRecording(self): - result = False - players = self.mw.pm.getStreamPairPlayers(self.uri()) - for player in players: - if player.isRecording(): - result = True - return result - - def isAlarming(self): - result = False - players = self.mw.pm.getStreamPairPlayers(self.uri()) - for player in players: - if player.alarm_state: - result = True - return result - - def isFocus(self): - result = False - for profile in self.profiles: - if profile.uri() == self.mw.glWidget.focused_uri: - result = True - return result - - def editing(self): - return self.flags() & Qt.ItemFlag.ItemIsEditable - - def setIconIdle(self): - if not self.flags() & Qt.ItemFlag.ItemIsEditable: - self.setIcon(self.icnIdle) - - def setIconOn(self): - if not self.flags() & Qt.ItemFlag.ItemIsEditable: - self.setIcon(self.icnOn) - - def setIconRecord(self): - if not self.flags() & Qt.ItemFlag.ItemIsEditable: - self.setIcon(self.icnRecord) - - def dimForeground(self): - self.setForeground(QColor("#808D9E")) - - def restoreForeground(self): - self.setForeground(self.defaultForeground) - - def isCurrent(self): - result = False - current_camera = self.mw.cameraPanel.getCurrentCamera() - if current_camera: - if current_camera.serial_number() == self.serial_number(): - result = True - return result - - def getStreamState(self, index): - result = StreamState.INVALID - profile = self.profiles[index] - if profile: - player = self.mw.pm.getPlayer(profile.uri()) - if player: - if player.image: - result = StreamState.CONNECTED - else: - result = StreamState.CONNECTING - else: - result = StreamState.IDLE - - timer = self.mw.timers.get(profile.uri(), None) - if timer: - if timer.isActive(): - result = StreamState.CONNECTING - return result - - def profileName(self, uri): - result = "" - for profile in self.profiles: - if profile.uri() == uri: - result = profile.profile() - return result - - def recordProfileIndex(self): - return self.systemTabSettings.record_profile - - def displayProfileIndex(self): - return self.onvif_data.displayProfile - - def setDisplayProfile(self, index): - self.onvif_data.setProfile(index) - self.mw.settings.setValue(f'{self.serial_number()}/DisplayProfile', index) - for i, profile in enumerate(self.profiles): - if i == index: - profile.setHidden(False) - else: - profile.setHidden(True) - - def getDisplayProfileSetting(self): - return int(self.mw.settings.value(f'{self.serial_number()}/DisplayProfile', self.displayProfileIndex())) - - def getProfile(self, uri): - result = None - for profile in self.profiles: - if profile.uri() == uri: - result = profile - break - return result - - def getRecordProfile(self): - result = None - if len(self.profiles) > self.recordProfileIndex(): - result = self.profiles[self.recordProfileIndex()] - return result - - def isRecordProfile(self, uri): - result = False - recordProfile = self.getRecordProfile() - if recordProfile: - if uri == recordProfile.uri(): - result = True - return result - - def getDisplayProfile(self): - result = None - if len(self.profiles) > self.displayProfileIndex(): - result = self.profiles[self.displayProfileIndex()] - return result - - def companionURI(self, uri): - result = None - recordProfile = self.getRecordProfile() - displayProfile = self.getDisplayProfile() - if recordProfile and displayProfile: - if uri == recordProfile.uri(): - result = displayProfile.uri() - if uri == displayProfile.uri(): - result = recordProfile.uri() - return result +#/******************************************************************** +# libonvif/onvif-gui/gui/onvif/datastructures.py +# +# Copyright (c) 2023 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from time import sleep +from PyQt6.QtWidgets import QListWidgetItem +from PyQt6.QtCore import Qt, pyqtSignal, QObject, QTimer +from PyQt6.QtGui import QIcon, QColor +import libonvif as onvif +from gui.onvif.systemtab import SystemTabSettings +from gui.enums import ProxyType, MediaSource, StreamState + +class SessionSignals(QObject): + finished = pyqtSignal() + +class Session(onvif.Session): + def __init__(self, cp, interface): + super().__init__() + self.cp = cp + self.interface = interface + self.signals = SessionSignals() + self.discovered = lambda : self.finish() + self.getCredential = lambda D : self.cp.getCredential(D) + self.getData = lambda D : self.cp.getData(D) + self.timer = QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.timeout) + self.signals.finished.connect(self.timer.stop) + self.active = False + + def start(self): + self.active = True + self.startDiscover() + self.timer.start(10000) + + def finish(self): + self.active = False + self.signals.finished.emit() + self.cp.discovered() + + def timeout(self): + self.cp.discoveryTimeout() + +class Camera(QListWidgetItem): + def __init__(self, onvif_data, mw): + super().__init__(onvif_data.alias) + self.onvif_data = onvif_data + self.mw = mw + self.icnIdle = QIcon("image:idle_lo.png") + self.icnOn = QIcon("image:record.png") + self.icnRecord = QIcon("image:recording_hi.png") + self.defaultForeground = self.foreground() + self.filled = False + self.last_msg = "" + + onvif_data.setSetting = self.setSetting + onvif_data.getSetting = self.getSetting + if mw.settingsPanel.proxy.proxyType != ProxyType.STAND_ALONE: + onvif_data.getProxyURI = self.mw.getProxyURI + + for profile in onvif_data.profiles: + profile.setSetting = self.setSetting + profile.getSetting = self.getSetting + if mw.settingsPanel.proxy.proxyType != ProxyType.STAND_ALONE: + profile.getProxyURI = self.mw.getProxyURI + + self.profiles = onvif_data.profiles + + self.videoModelSettings = None + self.audioModelSettings = None + self.systemTabSettings = SystemTabSettings(self.mw, self) + self.manual_recording = False + self.ordinalKey = f'{self.serial_number()}/Ordinal' + self.ordinal = self.getOrdinal() + self.volumeKey = f'{self.serial_number()}/Volume' + self.volume = self.getVolume() + self.muteKey = f'{self.serial_number()}/Mute' + self.mute = self.getMute() + + ''' + def getProxyURI(self, arg): + match self.mw.settingsPanel.proxy.proxyType: + case ProxyType.CLIENT: + return self.mw.proxies[arg] + case ProxyType.SERVER: + return self.mw.proxy.getProxyURI(arg) + ''' + + def getSetting(self, key, default_value): + return str(self.mw.settings.value(key, default_value)) + + def setSetting(self, key, value): + self.mw.settings.setValue(key, value) + + def uri(self): + return self.onvif_data.uri() + + def serial_number(self): + return self.onvif_data.serial_number() + + def name(self): + return self.onvif_data.alias + + def xaddrs(self): + return self.onvif_data.xaddrs() + + def hasAudio(self): + return bool(self.onvif_data.audio_bitrate()) + + def setOrdinal(self, value): + self.ordinal = value + self.mw.settings.setValue(self.ordinalKey, value) + + def getOrdinal(self): + return int(self.mw.settings.value(self.ordinalKey, -1)) + + def getMute(self): + return bool(int(self.mw.settings.value(self.muteKey, 0))) + + def setMute(self, state): + self.mute = bool(state) + self.mw.settings.setValue(self.muteKey, int(state)) + + def getVolume(self): + return int(self.mw.settings.value(self.volumeKey, 80)) + + def setVolume(self, volume): + self.volume = volume + self.mw.settings.setValue(self.volumeKey, volume) + + def isRunning(self): + result = False + players = self.mw.pm.getStreamPairPlayers(self.uri()) + if len(players): + result = True + return result + + def isRecording(self): + result = False + players = self.mw.pm.getStreamPairPlayers(self.uri()) + for player in players: + if player.isRecording(): + result = True + return result + + def isAlarming(self): + result = False + players = self.mw.pm.getStreamPairPlayers(self.uri()) + for player in players: + if player.alarm_state: + result = True + return result + + def isFocus(self): + result = False + for profile in self.profiles: + if profile.uri() == self.mw.glWidget.focused_uri: + result = True + return result + + def editing(self): + return self.flags() & Qt.ItemFlag.ItemIsEditable + + def setIconIdle(self): + if not self.flags() & Qt.ItemFlag.ItemIsEditable: + self.setIcon(self.icnIdle) + + def setIconOn(self): + if not self.flags() & Qt.ItemFlag.ItemIsEditable: + self.setIcon(self.icnOn) + + def setIconRecord(self): + if not self.flags() & Qt.ItemFlag.ItemIsEditable: + self.setIcon(self.icnRecord) + + def dimForeground(self): + self.setForeground(QColor("#808D9E")) + + def restoreForeground(self): + self.setForeground(self.defaultForeground) + + def isCurrent(self): + result = False + current_camera = self.mw.cameraPanel.getCurrentCamera() + if current_camera: + if current_camera.serial_number() == self.serial_number(): + result = True + return result + + def getStreamState(self, index): + result = StreamState.INVALID + profile = self.profiles[index] + if profile: + player = self.mw.pm.getPlayer(profile.uri()) + if player: + if player.image: + result = StreamState.CONNECTED + else: + result = StreamState.CONNECTING + else: + result = StreamState.IDLE + + timer = self.mw.timers.get(profile.uri(), None) + if timer: + if timer.isActive(): + result = StreamState.CONNECTING + return result + + def profileName(self, uri): + result = "" + for profile in self.profiles: + if profile.uri() == uri: + result = profile.profile() + return result + + def recordProfileIndex(self): + return self.systemTabSettings.record_profile + + def displayProfileIndex(self): + return self.onvif_data.displayProfile + + def setDisplayProfile(self, index): + self.onvif_data.setProfile(index) + self.mw.settings.setValue(f'{self.serial_number()}/DisplayProfile', index) + for i, profile in enumerate(self.profiles): + if i == index: + profile.setHidden(False) + else: + profile.setHidden(True) + + def getDisplayProfileSetting(self): + return int(self.mw.settings.value(f'{self.serial_number()}/DisplayProfile', self.displayProfileIndex())) + + def getProfile(self, uri): + result = None + for profile in self.profiles: + if profile.uri() == uri: + result = profile + break + return result + + def getRecordProfile(self): + result = None + if len(self.profiles) > self.recordProfileIndex(): + result = self.profiles[self.recordProfileIndex()] + return result + + def isRecordProfile(self, uri): + result = False + recordProfile = self.getRecordProfile() + if recordProfile: + if uri == recordProfile.uri(): + result = True + return result + + def getDisplayProfile(self): + result = None + if len(self.profiles) > self.displayProfileIndex(): + result = self.profiles[self.displayProfileIndex()] + return result + + def companionURI(self, uri): + result = None + recordProfile = self.getRecordProfile() + displayProfile = self.getDisplayProfile() + if recordProfile and displayProfile: + if uri == recordProfile.uri(): + result = displayProfile.uri() + if uri == displayProfile.uri(): + result = recordProfile.uri() + return result diff --git a/onvif-gui/gui/onvif/systemtab.py b/onvif-gui/gui/onvif/systemtab.py index 25b64aa7..b0ee7533 100644 --- a/onvif-gui/gui/onvif/systemtab.py +++ b/onvif-gui/gui/onvif/systemtab.py @@ -1,306 +1,306 @@ -#/******************************************************************** -# libonvif/onvif-gui/gui/onvif/systemtab.py -# -# Copyright (c) 2023 Stephen Rhodes -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -#*********************************************************************/ - -from PyQt6.QtWidgets import QGridLayout, QWidget, QPushButton, QGroupBox, \ - QMessageBox, QRadioButton, QComboBox, QLabel -from PyQt6.QtCore import Qt -import datetime -import pathlib -import webbrowser - -class SystemTabSettings(): - def __init__(self, mw, camera): - self.camera = camera - self.mw = mw - - self.record_enable = self.getRecordAlarmEnabled() - self.record_always = self.getRecordAlways() - self.record_alarm = self.getRecordOnAlarm() - self.sound_alarm_enable = self.getSoundAlarmEnabled() - self.sound_alarm_once = self.getSoundAlarmOnce() - self.sound_alarm_loop = self.getSoundAlarmLoop() - self.record_profile = self.getRecordProfile() - - def managePlayers(self): - record = False - if self.record_enable: - if self.record_always or (self.record_alarm and self.camera.isAlarming()): - record = True - if record: - profile = self.camera.getRecordProfile() - if profile: - player = self.mw.pm.getPlayer(profile.uri()) - if player: - if not player.isRecording(): - d = self.mw.settingsPanel.dirArchive.txtDirectory.text() - filename = player.getPipeOutFilename(d) - if filename: - player.toggleRecording(filename) - else: - players = self.mw.pm.getStreamPairPlayers(self.camera.uri()) - for player in players: - if player.isRecording(): - player.toggleRecording("") - - self.mw.cameraPanel.syncGUI() - - def getRecordProfile(self): - key = f'{self.camera.serial_number()}/RecordProfile' - return int(self.mw.settings.value(key, 0)) - - def setRecordProfile(self, ordinal): - self.record_profile = ordinal - key = f'{self.camera.serial_number()}/RecordProfile' - self.mw.settings.setValue(key, ordinal) - - def getRecordAlarmEnabled(self): - key = f'{self.camera.serial_number()}/RecordAlarmEnabled' - return bool(int(self.mw.settings.value(key, 0))) - - def setRecordAlarmEnabled(self, state): - self.record_enable = bool(state) - key = f'{self.camera.serial_number()}/RecordAlarmEnabled' - self.managePlayers() - self.mw.settings.setValue(key, int(state)) - - def getRecordAlways(self): - key = f'{self.camera.serial_number()}/RecordAlways' - return bool(int(self.mw.settings.value(key, 0))) - - def setRecordAlways(self, state): - self.record_always = bool(state) - key = f'{self.camera.serial_number()}/RecordAlways' - self.mw.settings.setValue(key, int(state)) - self.managePlayers() - - def getRecordOnAlarm(self): - key = f'{self.camera.serial_number()}/RecordOnAlarm' - return bool(int(self.mw.settings.value(key, 1))) - - def setRecordOnAlarm(self, state): - self.record_alarm = bool(state) - key = f'{self.camera.serial_number()}/RecordOnAlarm' - self.mw.settings.setValue(key, int(state)) - - def getSoundAlarmEnabled(self): - key = f'{self.camera.serial_number()}/SoundAlarmEnabled' - return bool(int(self.mw.settings.value(key, 0))) - - def setSoundAlarmEnabled(self, state): - self.sound_alarm_enable = bool(state) - key = f'{self.camera.serial_number()}/SoundAlarmEnabled' - self.mw.settings.setValue(key, int(state)) - - def getSoundAlarmOnce(self): - key = f'{self.camera.serial_number()}/SoundAlarmOnce' - return bool(int(self.mw.settings.value(key, 0))) - - def setSoundAlarmOnce(self, state): - self.sound_alarm_once = bool(state) - key = f'{self.camera.serial_number()}/SoundAlarmOnce' - self.mw.settings.setValue(key, int(state)) - - def getSoundAlarmLoop(self): - key = f'{self.camera.serial_number()}/SoundAlarmLoop' - return bool(int(self.mw.settings.value(key, 1))) - - def setSoundAlarmLoop(self, state): - self.sound_alarm_loop = bool(state) - key = f'{self.camera.serial_number()}/SoundAlarmLoop' - self.mw.settings.setValue(key, int(state)) - -class SystemTab(QWidget): - def __init__(self, cp): - super().__init__() - self.cp = cp - - self.radRecordAlways = QRadioButton("Always") - self.radRecordAlways.clicked.connect(self.radRecordAlwaysClicked) - self.radRecordAlways.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.radRecordOnAlarm = QRadioButton("Alarms") - self.radRecordOnAlarm.clicked.connect(self.radRecordOnAlarmClicked) - self.radRecordOnAlarm.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.grpRecord = QGroupBox("Record") - self.grpRecord.setCheckable(True) - self.grpRecord.clicked.connect(self.grpRecordClicked) - self.grpRecord.setFocusPolicy(Qt.FocusPolicy.NoFocus) - lytGroup = QGridLayout(self.grpRecord) - lytGroup.addWidget(self.radRecordAlways, 0, 0, 1, 1) - lytGroup.addWidget(self.radRecordOnAlarm, 1, 0, 1, 1) - - self.radSoundOnce = QRadioButton("Once") - self.radSoundOnce.clicked.connect(self.radSoundOnceClicked) - self.radSoundOnce.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.radSoundLoop = QRadioButton("Loop") - self.radSoundLoop.clicked.connect(self.radSoundLoopClicked) - self.radSoundLoop.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.grpSounds = QGroupBox("Sounds") - self.grpSounds.setCheckable(True) - self.grpSounds.clicked.connect(self.grpSoundsClicked) - self.grpSounds.setFocusPolicy(Qt.FocusPolicy.NoFocus) - lytGroupSounds = QGridLayout(self.grpSounds) - lytGroupSounds.addWidget(self.radSoundOnce, 0, 0, 1, 1) - lytGroupSounds.addWidget(self.radSoundLoop, 1, 0, 1, 1) - - self.cmbRecordProfile = QComboBox() - self.cmbRecordProfile.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.cmbRecordProfile.currentIndexChanged.connect(self.cmbRecordProfileChanged) - self.lblRecordProfile = QLabel(" Record ") - pnlRecordProfile = QWidget() - lytRecordProfile = QGridLayout(pnlRecordProfile) - lytRecordProfile.addWidget(self.lblRecordProfile, 0, 0, 1, 1) - lytRecordProfile.addWidget(self.cmbRecordProfile, 0, 1, 1, 1) - lytRecordProfile.setColumnStretch(1, 5) - lytRecordProfile.setContentsMargins(0, 0, 0, 0) - - self.btnReboot = QPushButton("Reboot") - self.btnReboot.clicked.connect(self.btnRebootClicked) - self.btnReboot.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.btnSyncTime = QPushButton("Sync Time") - self.btnSyncTime.clicked.connect(self.btnSyncTimeClicked) - self.btnSyncTime.setFocusPolicy(Qt.FocusPolicy.NoFocus) - self.btnBrowser = QPushButton("Browser") - self.btnBrowser.clicked.connect(self.btnBrowserClicked) - self.btnBrowser.setFocusPolicy(Qt.FocusPolicy.NoFocus) - - self.btnSnapshot = QPushButton("JPEG") - self.btnSnapshot.clicked.connect(self.btnSnapshotClicked) - self.btnSnapshot.setFocusPolicy(Qt.FocusPolicy.NoFocus) - - pnlButton = QWidget() - lytButton = QGridLayout(pnlButton) - lytButton.addWidget(self.btnReboot, 0, 0, 1, 1) - lytButton.addWidget(self.btnSyncTime, 1, 0, 1, 1) - lytButton.addWidget(self.btnBrowser, 2, 0, 1, 1) - lytButton.addWidget(self.btnSnapshot, 3, 0, 1, 1) - lytButton.setContentsMargins(6, 0, 6, 0) - - lytMain = QGridLayout(self) - lytMain.addWidget(self.grpRecord, 0, 0, 1, 1) - lytMain.addWidget(self.grpSounds, 0, 1, 1, 1) - lytMain.addWidget(pnlRecordProfile, 1, 0, 1, 2) - lytMain.addWidget(pnlButton, 0, 2, 2, 1) - - def grpRecordClicked(self, state): - camera = self.cp.getCurrentCamera() - if camera: - camera.systemTabSettings.setRecordAlarmEnabled(state) - - def radRecordAlwaysClicked(self, state): - camera = self.cp.getCurrentCamera() - if camera: - camera.systemTabSettings.setRecordAlways(state) - camera.systemTabSettings.setRecordOnAlarm(not state) - - def radRecordOnAlarmClicked(self, state): - camera = self.cp.getCurrentCamera() - if camera: - camera.systemTabSettings.setRecordOnAlarm(state) - camera.systemTabSettings.setRecordAlways(not state) - - def grpSoundsClicked(self, state): - camera = self.cp.getCurrentCamera() - if camera: - camera.systemTabSettings.setSoundAlarmEnabled(state) - - def radSoundOnceClicked(self, state): - camera = self.cp.getCurrentCamera() - if camera: - camera.systemTabSettings.setSoundAlarmOnce(state) - camera.systemTabSettings.setSoundAlarmLoop(not state) - - def radSoundLoopClicked(self, state): - camera = self.cp.getCurrentCamera() - if camera: - camera.systemTabSettings.setSoundAlarmLoop(state) - camera.systemTabSettings.setSoundAlarmOnce(not state) - - def cmbRecordProfileChanged(self, index): - camera = self.cp.getCurrentCamera() - if camera: - players = self.cp.mw.pm.getStreamPairPlayers(camera.uri()) - camera.systemTabSettings.setRecordProfile(index) - if len(players): - for player in players: - self.cp.mw.pm.playerShutdownWait(player.uri) - self.cp.onItemDoubleClicked(camera) - - def fill(self, onvif_data): - self.cmbRecordProfile.disconnect() - self.cmbRecordProfile.clear() - for profile in onvif_data.profiles: - self.cmbRecordProfile.addItem(profile.profile()) - - camera = self.cp.getCurrentCamera() - if camera: - self.cmbRecordProfile.setCurrentIndex(camera.systemTabSettings.getRecordProfile()) - - self.cmbRecordProfile.currentIndexChanged.connect(self.cmbRecordProfileChanged) - self.syncGUI() - self.setEnabled(True) - - def syncGUI(self): - camera = self.cp.getCurrentCamera() - if camera: - self.grpRecord.setChecked(camera.systemTabSettings.record_enable) - if camera.systemTabSettings.record_always: - self.radRecordAlways.setChecked(True) - self.radRecordOnAlarm.setChecked(False) - if camera.systemTabSettings.record_alarm: - self.radRecordOnAlarm.setChecked(True) - self.radRecordAlways.setChecked(False) - self.grpSounds.setChecked(camera.systemTabSettings.sound_alarm_enable) - if camera.systemTabSettings.sound_alarm_once: - self.radSoundOnce.setChecked(True) - self.radSoundLoop.setChecked(False) - if camera.systemTabSettings.sound_alarm_loop: - self.radSoundLoop.setChecked(True) - self.radSoundOnce.setChecked(False) - - self.cp.btnRecord.setEnabled(not (self.grpRecord.isChecked() and self.radRecordAlways.isChecked())) - if camera.isRecording(): - self.cp.btnRecord.setStyleSheet(self.cp.getButtonStyle("recording")) - else: - self.cp.btnRecord.setStyleSheet(self.cp.getButtonStyle("record")) - - def btnRebootClicked(self): - camera = self.cp.getCurrentCamera() - if camera: - result = QMessageBox.question(self, "Warning", f'{camera.name()}: Please confirm reboot') - if result == QMessageBox.StandardButton.Yes: - camera.onvif_data.startReboot() - - def btnSyncTimeClicked(self): - camera = self.cp.getCurrentCamera() - if camera: - camera.onvif_data.startUpdateTime() - - def btnBrowserClicked(self): - camera = self.cp.lstCamera.currentItem() - if camera: - host = "http://" + camera.onvif_data.host() - webbrowser.get().open(host) - - def btnSnapshotClicked(self): - if player := self.cp.getCurrentPlayer(): - root = self.cp.mw.settingsPanel.dirPictures.txtDirectory.text() + "/" + self.cp.getCamera(player.uri).text() - pathlib.Path(root).mkdir(parents=True, exist_ok=True) - filename = '{0:%Y%m%d%H%M%S.jpg}'.format(datetime.datetime.now()) - filename = root + "/" + filename - player.save_image_filename = filename +#/******************************************************************** +# libonvif/onvif-gui/gui/onvif/systemtab.py +# +# Copyright (c) 2023 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from PyQt6.QtWidgets import QGridLayout, QWidget, QPushButton, QGroupBox, \ + QMessageBox, QRadioButton, QComboBox, QLabel +from PyQt6.QtCore import Qt +import datetime +import pathlib +import webbrowser + +class SystemTabSettings(): + def __init__(self, mw, camera): + self.camera = camera + self.mw = mw + + self.record_enable = self.getRecordAlarmEnabled() + self.record_always = self.getRecordAlways() + self.record_alarm = self.getRecordOnAlarm() + self.sound_alarm_enable = self.getSoundAlarmEnabled() + self.sound_alarm_once = self.getSoundAlarmOnce() + self.sound_alarm_loop = self.getSoundAlarmLoop() + self.record_profile = self.getRecordProfile() + + def managePlayers(self): + record = False + if self.record_enable: + if self.record_always or (self.record_alarm and self.camera.isAlarming()): + record = True + if record: + profile = self.camera.getRecordProfile() + if profile: + player = self.mw.pm.getPlayer(profile.uri()) + if player: + if not player.isRecording(): + d = self.mw.settingsPanel.storage.dirArchive.txtDirectory.text() + filename = player.getPipeOutFilename(d) + if filename: + player.toggleRecording(filename) + else: + players = self.mw.pm.getStreamPairPlayers(self.camera.uri()) + for player in players: + if player.isRecording(): + player.toggleRecording("") + + self.mw.cameraPanel.syncGUI() + + def getRecordProfile(self): + key = f'{self.camera.serial_number()}/RecordProfile' + return int(self.mw.settings.value(key, 0)) + + def setRecordProfile(self, ordinal): + self.record_profile = ordinal + key = f'{self.camera.serial_number()}/RecordProfile' + self.mw.settings.setValue(key, ordinal) + + def getRecordAlarmEnabled(self): + key = f'{self.camera.serial_number()}/RecordAlarmEnabled' + return bool(int(self.mw.settings.value(key, 0))) + + def setRecordAlarmEnabled(self, state): + self.record_enable = bool(state) + key = f'{self.camera.serial_number()}/RecordAlarmEnabled' + self.managePlayers() + self.mw.settings.setValue(key, int(state)) + + def getRecordAlways(self): + key = f'{self.camera.serial_number()}/RecordAlways' + return bool(int(self.mw.settings.value(key, 0))) + + def setRecordAlways(self, state): + self.record_always = bool(state) + key = f'{self.camera.serial_number()}/RecordAlways' + self.mw.settings.setValue(key, int(state)) + self.managePlayers() + + def getRecordOnAlarm(self): + key = f'{self.camera.serial_number()}/RecordOnAlarm' + return bool(int(self.mw.settings.value(key, 1))) + + def setRecordOnAlarm(self, state): + self.record_alarm = bool(state) + key = f'{self.camera.serial_number()}/RecordOnAlarm' + self.mw.settings.setValue(key, int(state)) + + def getSoundAlarmEnabled(self): + key = f'{self.camera.serial_number()}/SoundAlarmEnabled' + return bool(int(self.mw.settings.value(key, 0))) + + def setSoundAlarmEnabled(self, state): + self.sound_alarm_enable = bool(state) + key = f'{self.camera.serial_number()}/SoundAlarmEnabled' + self.mw.settings.setValue(key, int(state)) + + def getSoundAlarmOnce(self): + key = f'{self.camera.serial_number()}/SoundAlarmOnce' + return bool(int(self.mw.settings.value(key, 0))) + + def setSoundAlarmOnce(self, state): + self.sound_alarm_once = bool(state) + key = f'{self.camera.serial_number()}/SoundAlarmOnce' + self.mw.settings.setValue(key, int(state)) + + def getSoundAlarmLoop(self): + key = f'{self.camera.serial_number()}/SoundAlarmLoop' + return bool(int(self.mw.settings.value(key, 1))) + + def setSoundAlarmLoop(self, state): + self.sound_alarm_loop = bool(state) + key = f'{self.camera.serial_number()}/SoundAlarmLoop' + self.mw.settings.setValue(key, int(state)) + +class SystemTab(QWidget): + def __init__(self, cp): + super().__init__() + self.cp = cp + + self.radRecordAlways = QRadioButton("Always") + self.radRecordAlways.clicked.connect(self.radRecordAlwaysClicked) + self.radRecordAlways.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.radRecordOnAlarm = QRadioButton("Alarms") + self.radRecordOnAlarm.clicked.connect(self.radRecordOnAlarmClicked) + self.radRecordOnAlarm.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.grpRecord = QGroupBox("Record") + self.grpRecord.setCheckable(True) + self.grpRecord.clicked.connect(self.grpRecordClicked) + self.grpRecord.setFocusPolicy(Qt.FocusPolicy.NoFocus) + lytGroup = QGridLayout(self.grpRecord) + lytGroup.addWidget(self.radRecordAlways, 0, 0, 1, 1) + lytGroup.addWidget(self.radRecordOnAlarm, 1, 0, 1, 1) + + self.radSoundOnce = QRadioButton("Once") + self.radSoundOnce.clicked.connect(self.radSoundOnceClicked) + self.radSoundOnce.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.radSoundLoop = QRadioButton("Loop") + self.radSoundLoop.clicked.connect(self.radSoundLoopClicked) + self.radSoundLoop.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.grpSounds = QGroupBox("Sounds") + self.grpSounds.setCheckable(True) + self.grpSounds.clicked.connect(self.grpSoundsClicked) + self.grpSounds.setFocusPolicy(Qt.FocusPolicy.NoFocus) + lytGroupSounds = QGridLayout(self.grpSounds) + lytGroupSounds.addWidget(self.radSoundOnce, 0, 0, 1, 1) + lytGroupSounds.addWidget(self.radSoundLoop, 1, 0, 1, 1) + + self.cmbRecordProfile = QComboBox() + self.cmbRecordProfile.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.cmbRecordProfile.currentIndexChanged.connect(self.cmbRecordProfileChanged) + self.lblRecordProfile = QLabel(" Record ") + pnlRecordProfile = QWidget() + lytRecordProfile = QGridLayout(pnlRecordProfile) + lytRecordProfile.addWidget(self.lblRecordProfile, 0, 0, 1, 1) + lytRecordProfile.addWidget(self.cmbRecordProfile, 0, 1, 1, 1) + lytRecordProfile.setColumnStretch(1, 5) + lytRecordProfile.setContentsMargins(0, 0, 0, 0) + + self.btnReboot = QPushButton("Reboot") + self.btnReboot.clicked.connect(self.btnRebootClicked) + self.btnReboot.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.btnSyncTime = QPushButton("Sync Time") + self.btnSyncTime.clicked.connect(self.btnSyncTimeClicked) + self.btnSyncTime.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.btnBrowser = QPushButton("Browser") + self.btnBrowser.clicked.connect(self.btnBrowserClicked) + self.btnBrowser.setFocusPolicy(Qt.FocusPolicy.NoFocus) + + self.btnSnapshot = QPushButton("JPEG") + self.btnSnapshot.clicked.connect(self.btnSnapshotClicked) + self.btnSnapshot.setFocusPolicy(Qt.FocusPolicy.NoFocus) + + pnlButton = QWidget() + lytButton = QGridLayout(pnlButton) + lytButton.addWidget(self.btnReboot, 0, 0, 1, 1) + lytButton.addWidget(self.btnSyncTime, 1, 0, 1, 1) + lytButton.addWidget(self.btnBrowser, 2, 0, 1, 1) + lytButton.addWidget(self.btnSnapshot, 3, 0, 1, 1) + lytButton.setContentsMargins(6, 0, 6, 0) + + lytMain = QGridLayout(self) + lytMain.addWidget(self.grpRecord, 0, 0, 1, 1) + lytMain.addWidget(self.grpSounds, 0, 1, 1, 1) + lytMain.addWidget(pnlRecordProfile, 1, 0, 1, 2) + lytMain.addWidget(pnlButton, 0, 2, 2, 1) + + def grpRecordClicked(self, state): + camera = self.cp.getCurrentCamera() + if camera: + camera.systemTabSettings.setRecordAlarmEnabled(state) + + def radRecordAlwaysClicked(self, state): + camera = self.cp.getCurrentCamera() + if camera: + camera.systemTabSettings.setRecordAlways(state) + camera.systemTabSettings.setRecordOnAlarm(not state) + + def radRecordOnAlarmClicked(self, state): + camera = self.cp.getCurrentCamera() + if camera: + camera.systemTabSettings.setRecordOnAlarm(state) + camera.systemTabSettings.setRecordAlways(not state) + + def grpSoundsClicked(self, state): + camera = self.cp.getCurrentCamera() + if camera: + camera.systemTabSettings.setSoundAlarmEnabled(state) + + def radSoundOnceClicked(self, state): + camera = self.cp.getCurrentCamera() + if camera: + camera.systemTabSettings.setSoundAlarmOnce(state) + camera.systemTabSettings.setSoundAlarmLoop(not state) + + def radSoundLoopClicked(self, state): + camera = self.cp.getCurrentCamera() + if camera: + camera.systemTabSettings.setSoundAlarmLoop(state) + camera.systemTabSettings.setSoundAlarmOnce(not state) + + def cmbRecordProfileChanged(self, index): + camera = self.cp.getCurrentCamera() + if camera: + players = self.cp.mw.pm.getStreamPairPlayers(camera.uri()) + camera.systemTabSettings.setRecordProfile(index) + if len(players): + for player in players: + self.cp.mw.pm.playerShutdownWait(player.uri) + self.cp.onItemDoubleClicked(camera) + + def fill(self, onvif_data): + self.cmbRecordProfile.disconnect() + self.cmbRecordProfile.clear() + for profile in onvif_data.profiles: + self.cmbRecordProfile.addItem(profile.profile()) + + camera = self.cp.getCurrentCamera() + if camera: + self.cmbRecordProfile.setCurrentIndex(camera.systemTabSettings.getRecordProfile()) + + self.cmbRecordProfile.currentIndexChanged.connect(self.cmbRecordProfileChanged) + self.syncGUI() + self.setEnabled(True) + + def syncGUI(self): + camera = self.cp.getCurrentCamera() + if camera: + self.grpRecord.setChecked(camera.systemTabSettings.record_enable) + if camera.systemTabSettings.record_always: + self.radRecordAlways.setChecked(True) + self.radRecordOnAlarm.setChecked(False) + if camera.systemTabSettings.record_alarm: + self.radRecordOnAlarm.setChecked(True) + self.radRecordAlways.setChecked(False) + self.grpSounds.setChecked(camera.systemTabSettings.sound_alarm_enable) + if camera.systemTabSettings.sound_alarm_once: + self.radSoundOnce.setChecked(True) + self.radSoundLoop.setChecked(False) + if camera.systemTabSettings.sound_alarm_loop: + self.radSoundLoop.setChecked(True) + self.radSoundOnce.setChecked(False) + + self.cp.btnRecord.setEnabled(not (self.grpRecord.isChecked() and self.radRecordAlways.isChecked())) + if camera.isRecording(): + self.cp.btnRecord.setStyleSheet(self.cp.getButtonStyle("recording")) + else: + self.cp.btnRecord.setStyleSheet(self.cp.getButtonStyle("record")) + + def btnRebootClicked(self): + camera = self.cp.getCurrentCamera() + if camera: + result = QMessageBox.question(self, "Warning", f'{camera.name()}: Please confirm reboot') + if result == QMessageBox.StandardButton.Yes: + camera.onvif_data.startReboot() + + def btnSyncTimeClicked(self): + camera = self.cp.getCurrentCamera() + if camera: + camera.onvif_data.startUpdateTime() + + def btnBrowserClicked(self): + camera = self.cp.lstCamera.currentItem() + if camera: + host = "http://" + camera.onvif_data.host() + webbrowser.get().open(host) + + def btnSnapshotClicked(self): + if player := self.cp.getCurrentPlayer(): + root = self.cp.mw.settingsPanel.storage.dirPictures.txtDirectory.text() + "/" + self.cp.getCamera(player.uri).text() + pathlib.Path(root).mkdir(parents=True, exist_ok=True) + filename = '{0:%Y%m%d%H%M%S.jpg}'.format(datetime.datetime.now()) + filename = root + "/" + filename + player.save_image_filename = filename diff --git a/onvif-gui/gui/panels/__init__.py b/onvif-gui/gui/panels/__init__.py index 605f0a71..552e38db 100644 --- a/onvif-gui/gui/panels/__init__.py +++ b/onvif-gui/gui/panels/__init__.py @@ -2,4 +2,4 @@ from .filepanel import FilePanel from .videopanel import VideoPanel from .settingspanel import SettingsPanel -from .audiopanel import AudioPanel \ No newline at end of file +from .audiopanel import AudioPanel diff --git a/onvif-gui/gui/panels/audiopanel.py b/onvif-gui/gui/panels/audiopanel.py index a1224d0e..d658de9e 100644 --- a/onvif-gui/gui/panels/audiopanel.py +++ b/onvif-gui/gui/panels/audiopanel.py @@ -20,7 +20,7 @@ from PyQt6.QtWidgets import QGridLayout, QWidget, QCheckBox, \ QLabel, QComboBox, QVBoxLayout from PyQt6.QtCore import Qt -from gui.onvif.datastructures import MediaSource +from gui.enums import MediaSource class AudioPanel(QWidget): def __init__(self, mw): diff --git a/onvif-gui/gui/panels/camerapanel.py b/onvif-gui/gui/panels/camerapanel.py index 2cf30db1..2c9b90f1 100644 --- a/onvif-gui/gui/panels/camerapanel.py +++ b/onvif-gui/gui/panels/camerapanel.py @@ -141,10 +141,11 @@ def __init__(self, mw): self.dlgLogin = LoginDialog(self) self.fillers = [] self.fill_first_pass = True + self.sync_lock = False self.cameras_awaiting_authentication = [] self.autoTimeSyncer = None - self.enableAutoTimeSync(self.mw.settingsPanel.chkAutoTimeSync.isChecked()) + self.enableAutoTimeSync(self.mw.settingsPanel.general.chkAutoTimeSync.isChecked()) self.cached_serial_numbers = [] @@ -162,23 +163,33 @@ def __init__(self, mw): self.sldVolume.setEnabled(False) self.btnStop = QPushButton() + self.btnStop.setMinimumWidth(40) + self.btnStop.setMaximumHeight(20) self.btnStop.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.btnStop.clicked.connect(self.btnStopClicked) self.btnRecord = QPushButton() + self.btnRecord.setMinimumWidth(40) + self.btnRecord.setMaximumHeight(20) self.btnRecord.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.btnRecord.clicked.connect(self.btnRecordClicked) self.btnMute = QPushButton() + self.btnMute.setMinimumWidth(40) + self.btnMute.setMaximumHeight(20) self.btnMute.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.btnMute.clicked.connect(self.btnMuteClicked) self.btnDiscover = QPushButton() + self.btnDiscover.setMinimumWidth(40) + self.btnDiscover.setMaximumHeight(20) self.btnDiscover.setStyleSheet(self.getButtonStyle("discover")) self.btnDiscover.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.btnDiscover.clicked.connect(self.btnDiscoverClicked) self.btnApply = QPushButton() + self.btnApply.setMinimumWidth(40) + self.btnApply.setMaximumHeight(20) self.btnApply.setStyleSheet(self.getButtonStyle("apply")) self.btnApply.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.btnApply.clicked.connect(self.btnApplyClicked) @@ -250,16 +261,16 @@ def onMenuInfo(self): self.lstCamera.info() def btnDiscoverClicked(self): - if self.mw.settingsPanel.radDiscover.isChecked(): + if self.mw.settingsPanel.discover.radDiscover.isChecked(): logger.debug("Using broadcast discovery") interfaces = [] self.sessions.clear() - if self.mw.settingsPanel.chkScanAllNetworks.isChecked(): - for i in range(self.mw.settingsPanel.cmbInterfaces.count()): - interfaces.append(self.mw.settingsPanel.cmbInterfaces.itemText(i).split(" - ")[0]) + if self.mw.settingsPanel.discover.chkScanAllNetworks.isChecked(): + for i in range(self.mw.settingsPanel.discover.cmbInterfaces.count()): + interfaces.append(self.mw.settingsPanel.discover.cmbInterfaces.itemText(i).split(" - ")[0]) else: - interfaces.append(self.mw.settingsPanel.cmbInterfaces.currentText().split(" - ")[0]) + interfaces.append(self.mw.settingsPanel.discover.cmbInterfaces.currentText().split(" - ")[0]) for interface in interfaces: session = Session(self, interface) @@ -271,7 +282,7 @@ def btnDiscoverClicked(self): else: logger.debug("Using cached camera addresses for discovery") self.fillers.clear() - tmp = self.mw.settings.value(self.mw.settingsPanel.cameraListKey) + tmp = self.mw.settings.value(self.mw.settingsPanel.discover.cameraListKey) if tmp: numbers = tmp.strip().split("\n") for serial_number in numbers: @@ -312,9 +323,9 @@ def getCredential(self, onvif_data): onvif_data.cancelled = True return onvif_data - if len(self.mw.settingsPanel.txtPassword.text()) > 0 and len(onvif_data.last_error()) == 0: - onvif_data.setUsername(self.mw.settingsPanel.txtUsername.text()) - onvif_data.setPassword(self.mw.settingsPanel.txtPassword.text()) + if len(self.mw.settingsPanel.general.txtPassword.text()) > 0 and len(onvif_data.last_error()) == 0: + onvif_data.setUsername(self.mw.settingsPanel.general.txtUsername.text()) + onvif_data.setPassword(self.mw.settingsPanel.general.txtPassword.text()) else: if onvif_data.last_error().startswith("Network error, unable to connect"): logger.debug(f'Unable to connect with {onvif_data.xaddrs()}') @@ -365,13 +376,15 @@ def getData(self, onvif_data): camera = Camera(onvif_data, self.mw) camera.setIconIdle() camera.dimForeground() + self.mw.addCameraProxy(camera) + self.lstCamera.addItem(camera) self.lstCamera.sortItems() camera.setDisplayProfile(camera.getDisplayProfileSetting()) self.saveCameraList() logger.debug(f'Discovery completed for Camera: {onvif_data.alias}, Serial Number: {onvif_data.serial_number()}, Stream URI: {onvif_data.stream_uri()}, xaddrs: {onvif_data.xaddrs()}') - synchronizeTime = self.mw.settingsPanel.chkAutoTimeSync.isChecked() + synchronizeTime = self.mw.settingsPanel.general.chkAutoTimeSync.isChecked() if not self.closing: onvif_data.startFill(synchronizeTime) @@ -395,12 +408,12 @@ def filled(self, onvif_data): camera.filled = True # auto start after fill, recording needs onvif frame rate - if self.mw.settingsPanel.chkAutoStart.isChecked(): + if self.mw.settingsPanel.discover.chkAutoStart.isChecked(): if self.fill_first_pass: self.fill_first_pass = False if bool(int(self.mw.settings.value(self.mw.collapsedKey, 0))): self.signals.collapseSplitter.emit() - if self.mw.settingsPanel.radCached.isChecked(): + if self.mw.settingsPanel.discover.radCached.isChecked(): self.mw.pm.auto_start_mode = True if not camera.isRunning(): @@ -417,7 +430,7 @@ def saveCameraList(self): cameras = [self.mw.cameraPanel.lstCamera.item(x) for x in range(self.mw.cameraPanel.lstCamera.count())] for camera in cameras: serial_numbers += camera.serial_number() + "\n" - self.mw.settings.setValue(self.mw.settingsPanel.cameraListKey, serial_numbers) + self.mw.settings.setValue(self.mw.settingsPanel.discover.cameraListKey, serial_numbers) def onCurrentItemChanged(self, current, previous): if current: @@ -450,8 +463,8 @@ def onItemDoubleClicked(self, camera): self.mw.signals.stopReconnect.emit(timer.uri) for player in players: player.requestShutdown() - for profile in profiles: - self.mw.pm.removeKeys(profile.uri()) + #for profile in profiles: + # self.mw.pm.removeKeys(profile.uri()) camera.setIconIdle() else: if len(players): @@ -534,14 +547,14 @@ def btnRecordClicked(self): if camera: camera.manual_recording = False else: - d = self.mw.settingsPanel.dirArchive.txtDirectory.text() + d = self.mw.settingsPanel.storage.dirArchive.txtDirectory.text() root = d + "/" + self.getCamera(player.uri).text() pathlib.Path(root).mkdir(parents=True, exist_ok=True) player.pipe_output_start_time = datetime.now() filename = '{0:%Y%m%d%H%M%S}'.format(player.pipe_output_start_time) filename = root + "/" + filename + ".mp4" player.setMetaData("title", self.getCamera(player.uri).text()) - if self.mw.settingsPanel.chkManageDiskUsage.isChecked(): + if self.mw.settingsPanel.storage.chkManageDiskUsage.isChecked(): player.manageDirectory(d) player.toggleRecording(filename) if camera: @@ -577,13 +590,18 @@ def onMediaStopped(self, uri): self.syncGUI() def syncGUI(self): + + while (self.sync_lock): + sleep(0.001) + + self.sync_lock = True if camera := self.getCurrentCamera(): self.btnStop.setEnabled(True) if player := self.mw.pm.getPlayer(camera.uri()): self.btnStop.setStyleSheet(self.getButtonStyle("stop")) - ps = player.systemTabSettings - self.btnRecord.setEnabled(not (ps.record_enable and ps.record_always)) + if ps := player.systemTabSettings: + self.btnRecord.setEnabled(not (ps.record_enable and ps.record_always)) if player.hasAudio() and not player.disable_audio: self.btnMute.setEnabled(True) @@ -644,6 +662,11 @@ def syncGUI(self): self.btnStop.setStyleSheet(self.getButtonStyle("play")) self.btnStop.setEnabled(False) + #print("btn width", self.btnStop.width()) + #print("btn height", self.btnStop.height()) + self.sync_lock = False + + def getButtonStyle(self, name): strStyle = "QPushButton { image : url(image:%1.png); } \ QPushButton:hover { image : url(image:%1_hi.png); } \ diff --git a/onvif-gui/gui/panels/options/__init__.py b/onvif-gui/gui/panels/options/__init__.py new file mode 100644 index 00000000..9dff794e --- /dev/null +++ b/onvif-gui/gui/panels/options/__init__.py @@ -0,0 +1,5 @@ +from .discover import DiscoverOptions +from .general import GeneralOptions +from .storage import StorageOptions +from .alarm import AlarmOptions +from .proxy import ProxyOptions \ No newline at end of file diff --git a/onvif-gui/gui/panels/options/alarm.py b/onvif-gui/gui/panels/options/alarm.py new file mode 100644 index 00000000..3bf41c7e --- /dev/null +++ b/onvif-gui/gui/panels/options/alarm.py @@ -0,0 +1,94 @@ +#/******************************************************************** +# libonvif/onvif-gui/gui/panels/options/alarm.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +import os +from PyQt6.QtWidgets import QSpinBox, QGridLayout, QWidget, \ + QLabel, QComboBox, QSlider +from PyQt6.QtCore import Qt + +class AlarmOptions(QWidget): + def __init__(self, mw): + super().__init__() + self.mw = mw + + self.bufferSizeKey = "settings/bufferSize" + self.lagTimeKey = "settings/lagTime" + self.alarmSoundFileKey = "settings/alarmSoundFile" + self.alarmSoundVolumeKey = "settings/alarmSoundVolume" + + self.spnBufferSize = QSpinBox() + self.spnBufferSize.setMinimum(1) + self.spnBufferSize.setMaximum(60) + self.spnBufferSize.setMaximumWidth(80) + self.spnBufferSize.setValue(int(self.mw.settings.value(self.bufferSizeKey, 10))) + self.spnBufferSize.valueChanged.connect(self.spnBufferSizeChanged) + lblBufferSize = QLabel("Pre-Alarm Buffer Size (in seconds)") + + self.spnLagTime = QSpinBox() + self.spnLagTime.setMinimum(1) + self.spnLagTime.setMaximum(60) + self.spnLagTime.setMaximumWidth(80) + self.spnLagTime.setValue(int(self.mw.settings.value(self.lagTimeKey, 5))) + self.spnLagTime.valueChanged.connect(self.spnLagTimeChanged) + lblLagTime = QLabel("Post-Alarm Lag Time (in seconds)") + + self.cmbSoundFiles = QComboBox() + d = f'{self.mw.getLocation()}/gui/resources' + sounds = [f for f in os.listdir(d) if os.path.isfile(os.path.join(d, f)) and f.endswith(".mp3")] + self.cmbSoundFiles.addItems(sounds) + self.cmbSoundFiles.currentTextChanged.connect(self.cmbSoundFilesChanged) + self.cmbSoundFiles.setCurrentText(self.mw.settings.value(self.alarmSoundFileKey, "drops.mp3")) + lblSoundFiles = QLabel("Alarm Sounds") + self.sldAlarmVolume = QSlider(Qt.Orientation.Horizontal) + self.sldAlarmVolume.setValue(int(self.mw.settings.value(self.alarmSoundVolumeKey, 80))) + self.sldAlarmVolume.valueChanged.connect(self.sldAlarmVolumeChanged) + + pnlSoundFile = QWidget() + lytSoundFile = QGridLayout(pnlSoundFile) + lytSoundFile.addWidget(lblSoundFiles, 0, 0, 1, 1) + lytSoundFile.addWidget(self.cmbSoundFiles, 0, 1, 1, 1) + lytSoundFile.addWidget(self.sldAlarmVolume, 0, 2, 1, 1) + lytSoundFile.setColumnStretch(1, 10) + + pnlBuffer = QWidget() + lytBuffer = QGridLayout(pnlBuffer) + lytBuffer.addWidget(lblBufferSize, 1, 0, 1, 3) + lytBuffer.addWidget(self.spnBufferSize, 1, 3, 1, 1) + lytBuffer.addWidget(lblLagTime, 2, 0, 1, 3) + lytBuffer.addWidget(self.spnLagTime, 2, 3, 1, 1) + lytBuffer.addWidget(pnlSoundFile, 3, 0, 1, 4) + lytBuffer.setContentsMargins(0, 0, 0, 0) + + lytMain = QGridLayout(self) + lytMain.addWidget(pnlBuffer, 0, 0, 1, 1) + lytMain.addWidget(QLabel(), 1, 0, 1, 1) + lytMain.setRowStretch(1, 10) + + def spnBufferSizeChanged(self, i): + self.mw.settings.setValue(self.bufferSizeKey, i) + + def spnLagTimeChanged(self, i): + self.mw.settings.setValue(self.lagTimeKey, i) + + def cmbSoundFilesChanged(self, value): + self.mw.settings.setValue(self.alarmSoundFileKey, value) + + def sldAlarmVolumeChanged(self, value): + self.mw.settings.setValue(self.alarmSoundVolumeKey, value) + diff --git a/onvif-gui/gui/panels/options/discover.py b/onvif-gui/gui/panels/options/discover.py new file mode 100644 index 00000000..c81a7c4f --- /dev/null +++ b/onvif-gui/gui/panels/options/discover.py @@ -0,0 +1,171 @@ +#/******************************************************************** +# libonvif/onvif-gui/gui/panels/options/discover.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from PyQt6.QtWidgets import QLineEdit, QGridLayout, QWidget, QCheckBox, \ + QLabel, QComboBox, QPushButton, QDialog, QDialogButtonBox, \ + QRadioButton, QGroupBox +from PyQt6.QtCore import Qt, QRegularExpression +from PyQt6.QtGui import QRegularExpressionValidator +from loguru import logger +import libonvif as onvif + +class AddCameraDialog(QDialog): + def __init__(self, mw): + super().__init__(mw) + self.mw = mw + + ipRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" + ipRegex = QRegularExpression("^" + ipRange + "\\." + ipRange + "\\." + ipRange + "\\." + ipRange + "$") + ipValidator = QRegularExpressionValidator(ipRegex, self) + + self.setWindowTitle("Add Camera") + self.txtIPAddress = QLineEdit() + self.txtIPAddress.setValidator(ipValidator) + self.lblIPAddress = QLabel("IP Address") + self.txtOnvifPort = QLineEdit() + self.lblOnvifPort = QLabel("Onvif Port") + + buttonBox = QDialogButtonBox( \ + QDialogButtonBox.StandardButton.Ok | \ + QDialogButtonBox.StandardButton.Cancel) + + lytMain = QGridLayout(self) + lytMain.addWidget(self.lblIPAddress, 1, 0, 1, 1) + lytMain.addWidget(self.txtIPAddress, 1, 1, 1, 1) + lytMain.addWidget(self.lblOnvifPort, 2, 0, 1, 1) + lytMain.addWidget(self.txtOnvifPort, 2, 1, 1, 1) + lytMain.addWidget(buttonBox, 5, 0, 1, 2) + + buttonBox.accepted.connect(self.accept) + buttonBox.rejected.connect(self.reject) + + self.txtIPAddress.setFocus() + +class DiscoverOptions(QWidget): + def __init__(self, mw): + super().__init__() + self.mw = mw + + self.interfaceKey = "settings/interface" + self.autoDiscoverKey = "settings/autoDiscover" + self.discoveryTypeKey = "settings/discoveryType" + self.autoStartKey = "settings/autoStart" + self.scanAllKey = "settings/scanAll" + self.cameraListKey = "settings/cameraList" + + + self.grpDiscoverType = QGroupBox("Set Camera Discovery Method") + self.radDiscover = QRadioButton("Discover Broadcast", self.grpDiscoverType ) + self.radDiscover.setChecked(int(self.mw.settings.value(self.discoveryTypeKey, 1))) + self.radDiscover.toggled.connect(self.radDiscoverToggled) + self.radCached = QRadioButton("Cached Addresses", self.grpDiscoverType ) + self.radCached.setChecked(not self.radDiscover.isChecked()) + lytDiscoverType = QGridLayout(self.grpDiscoverType ) + lytDiscoverType.addWidget(self.radDiscover, 0, 0, 1, 1) + lytDiscoverType.addWidget(self.radCached, 0, 1, 1, 1) + + self.chkScanAllNetworks = QCheckBox("Scan All Networks During Discovery") + self.chkScanAllNetworks.setChecked(int(mw.settings.value(self.scanAllKey, 1))) + self.chkScanAllNetworks.stateChanged.connect(self.scanAllNetworksChecked) + self.cmbInterfaces = QComboBox() + intf = self.mw.settings.value(self.interfaceKey, "") + self.lblInterfaces = QLabel("Network") + session = onvif.Session() + session.getActiveInterfaces() + i = 0 + while len(session.active_interface(i)) > 0 and i < 16: + self.cmbInterfaces.addItem(session.active_interface(i)) + i += 1 + if len(intf) > 0: + self.cmbInterfaces.setCurrentText(intf) + self.cmbInterfaces.currentTextChanged.connect(self.cmbInterfacesChanged) + self.cmbInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) + self.lblInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) + + self.btnAddCamera = QPushButton("Add Camera") + self.btnAddCamera.clicked.connect(self.btnAddCameraClicked) + + self.chkAutoDiscover = QCheckBox("Auto Discovery") + self.chkAutoDiscover.setChecked(bool(int(mw.settings.value(self.autoDiscoverKey, 0)))) + self.chkAutoDiscover.stateChanged.connect(self.autoDiscoverChecked) + + self.chkAutoStart = QCheckBox("Auto Start") + self.chkAutoStart.setChecked(bool(int(mw.settings.value(self.autoStartKey, 0)))) + self.chkAutoStart.stateChanged.connect(self.autoStartChecked) + + pnlInterface = QGroupBox("Discovery Options") + lytInterface = QGridLayout(pnlInterface) + lytInterface.addWidget(self.grpDiscoverType, 0, 0, 1, 2) + lytInterface.addWidget(self.chkScanAllNetworks, 2, 0, 1, 2) + lytInterface.addWidget(self.lblInterfaces, 4, 0, 1, 1) + lytInterface.addWidget(self.cmbInterfaces, 4, 1, 1, 1) + lytInterface.addWidget(self.btnAddCamera, 5, 0, 1, 2, Qt.AlignmentFlag.AlignCenter) + lytInterface.setColumnStretch(1, 10) + lytInterface.setContentsMargins(10, 10, 10, 10) + + lytMain = QGridLayout(self) + lytMain.addWidget(pnlInterface, 0, 0, 1, 2) + lytMain.addWidget(QLabel(), 1, 0, 1, 2) + lytMain.addWidget(self.chkAutoDiscover, 2, 0, 1, 1) + lytMain.addWidget(self.chkAutoStart, 2, 1, 1, 1) + lytMain.addWidget(QLabel(), 3, 0, 1, 2) + lytMain.setRowStretch(3, 10) + + self.radDiscoverToggled(self.radDiscover.isChecked()) + + def radDiscoverToggled(self, checked): + self.chkScanAllNetworks.setEnabled(checked) + if self.chkScanAllNetworks.isChecked(): + self.lblInterfaces.setEnabled(False) + self.cmbInterfaces.setEnabled(False) + else: + self.lblInterfaces.setEnabled(checked) + self.cmbInterfaces.setEnabled(checked) + self.mw.settings.setValue(self.discoveryTypeKey, int(checked)) + + def scanAllNetworksChecked(self, state): + self.mw.settings.setValue(self.scanAllKey, state) + self.cmbInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) + self.lblInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) + + def cmbInterfacesChanged(self, network): + self.mw.settings.setValue(self.interfaceKey, network) + + def btnAddCameraClicked(self): + dlg = AddCameraDialog(self.mw) + if dlg.exec(): + ip_address = dlg.txtIPAddress.text() + onvif_port = dlg.txtOnvifPort.text() + if not len(onvif_port): + onvif_port = "80" + xaddrs = f'http://{ip_address}:{onvif_port}/onvif/device_service' + logger.debug(f'Attempting to add camera manually using xaddrs: {xaddrs}') + data = onvif.Data() + data.getData = self.mw.cameraPanel.getData + data.getCredential = self.mw.cameraPanel.getCredential + data.setXAddrs(xaddrs) + data.setDeviceService(xaddrs) + data.manual_fill() + + def autoDiscoverChecked(self, state): + self.mw.settings.setValue(self.autoDiscoverKey, state) + + def autoStartChecked(self, state): + self.mw.settings.setValue(self.autoStartKey, state) + diff --git a/onvif-gui/gui/panels/options/general.py b/onvif-gui/gui/panels/options/general.py new file mode 100644 index 00000000..6c204138 --- /dev/null +++ b/onvif-gui/gui/panels/options/general.py @@ -0,0 +1,321 @@ +#/******************************************************************** +# libonvif/onvif-gui/gui/panels/options/general.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +import os +import sys +from PyQt6.QtWidgets import QMessageBox, QLineEdit, QSpinBox, \ + QGridLayout, QWidget, QCheckBox, QLabel, QComboBox, QPushButton, \ + QDialog, QTextEdit, QMessageBox, QFileDialog +from PyQt6.QtCore import QFile, QRect +from PyQt6.QtGui import QTextCursor, QTextOption +from loguru import logger +import avio +import webbrowser +import platform +from gui.player import Player + +class LogText(QTextEdit): + def __init__(self, parent): + super().__init__(parent) + self.setWordWrapMode(QTextOption.WrapMode.NoWrap) + + def scrollToBottom(self): + self.moveCursor(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.MoveAnchor) + self.ensureCursorVisible() + +class LogDialog(QDialog): + def __init__(self, mw): + super().__init__(mw) + self.mw = mw + self.geometryKey = "LogDialog/geometry" + rect = self.mw.settings.value(self.geometryKey, QRect(0, 0, 480, 640)) + if rect is not None: + if rect.width() and rect.height(): + self.setGeometry(rect) + + self.editor = LogText(self) + self.editor.setReadOnly(True) + self.editor.setFontFamily("courier") + + self.lblSize = QLabel("Log File Size:") + self.btnArchive = QPushButton("Archive") + self.btnArchive.clicked.connect(self.btnArchiveClicked) + self.btnClear = QPushButton(" Clear ") + self.btnClear.clicked.connect(self.btnClearClicked) + + pnlButton = QWidget() + lytButton = QGridLayout(pnlButton) + lytButton.addWidget(self.btnArchive, 0, 1, 1, 1) + lytButton.addWidget(self.btnClear, 0, 2, 1, 1) + lytButton.setContentsMargins(0, 0, 0, 0) + + panel = QWidget() + lytPanel = QGridLayout(panel) + lytPanel.addWidget(self.lblSize, 0, 0, 1, 1) + lytPanel.addWidget(pnlButton, 0, 1, 1, 1) + lytPanel.addWidget(QLabel(), 0, 2, 1, 1) + lytPanel.setColumnStretch(2, 10) + + lyt = QGridLayout(self) + lyt.addWidget(self.editor, 0, 0, 1, 1) + lyt.addWidget(panel, 1, 0, 1, 1) + lyt.setRowStretch(0, 10) + + def closeEvent(self, e): + self.mw.settings.setValue(self.geometryKey, self.geometry()) + + def readLogFile(self, path): + data = "" + if os.path.isfile(path): + with open(path, 'r') as file: + data = file.read() + self.setWindowTitle(path) + self.editor.setText(data) + y = "{:.2f}".format(os.stat(path).st_size/1000000) + self.lblSize.setText(f'Log File Size: {y} MB ') + self.editor.scrollToBottom() + + def btnArchiveClicked(self): + path = None + if platform.system() == "Linux": + path = QFileDialog.getOpenFileName(self, "Select File", self.windowTitle(), options=QFileDialog.Option.DontUseNativeDialog)[0] + else: + path = QFileDialog.getOpenFileName(self, "Select File", self.windowTitle())[0] + if path: + if len(path) > 0: + self.readLogFile(path) + + def btnClearClicked(self): + filename = self.windowTitle() + ret = QMessageBox.warning(self, "Deleting File", + f'\n{filename}\n\n' + "You are about to delete this file.\n" + "Are you sure you want to continue?", + QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) + + if ret == QMessageBox.StandardButton.Ok: + if filename == self.mw.settingsPanel.getLogFilename(): + ret = QMessageBox.warning(self, "Deleting Current Log", + "You are about to delete the current log.\n" + "Are you sure you want to continue?", + QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) + if ret == QMessageBox.StandardButton.Ok: + QFile.remove(filename) + logger.add(filename) + logger.debug("Created new log file") + self.readLogFile(filename) + else: + QFile.remove(filename) + self.readLogFile(self.mw.settingsPanel.getLogFilename()) + QMessageBox.information(self, "Current Log Displayed", "The current log has been loaded into the display", QMessageBox.StandardButton.Ok) + +class GeneralOptions(QWidget): + def __init__(self, mw): + super().__init__() + self.mw = mw + + self.usernameKey = "settings/username" + self.passwordKey = "settings/password" + self.decoderKey = "settings/decoder" + self.bufferSizeKey = "settings/bufferSize" + self.lagTimeKey = "settings/lagTime" + self.startFullScreenKey = "settings/startFullScreen" + self.autoTimeSyncKey = "settings/autoTimeSync" + self.alarmSoundFileKey = "settings/alarmSoundFile" + self.alarmSoundVolumeKey = "settings/alarmSoundVolume" + self.displayRefreshKey = "settings/displayRefresh" + self.cacheMaxSizeKey = "settings/cacheMaxSize" + self.audioDriverIndexKey = "settings/audioDriverIndex" + + decoders = ["NONE"] + if sys.platform == "win32": + decoders += ["CUDA", "DXVA2", "D3D11VA"] + if sys.platform == "linux": + decoders += ["CUDA", "VAAPI", "VDPAU"] + + p = Player("", self) + audioDrivers = p.getAudioDrivers() + + self.dlgLog = None + + self.txtUsername = QLineEdit() + self.txtUsername.setText(mw.settings.value(self.usernameKey, "")) + self.txtUsername.textChanged.connect(self.usernameChanged) + lblUsername = QLabel("Common Username") + + self.txtPassword = QLineEdit() + self.txtPassword.setText(mw.settings.value(self.passwordKey, "")) + self.txtPassword.textChanged.connect(self.passwordChanged) + lblPassword = QLabel("Common Password") + + self.cmbDecoder = QComboBox() + self.cmbDecoder.addItems(decoders) + self.cmbDecoder.setCurrentText(mw.settings.value(self.decoderKey, "NONE")) + self.cmbDecoder.currentTextChanged.connect(self.cmbDecoderChanged) + lblDecoders = QLabel("Hardware Decoder") + + self.cmbAudioDriver = QComboBox() + self.cmbAudioDriver.addItems(audioDrivers) + self.cmbAudioDriver.setCurrentIndex(int(mw.settings.value(self.audioDriverIndexKey, 0))) + self.cmbAudioDriver.currentIndexChanged.connect(self.cmbAudioDriverChanged) + lblAudioDrivers = QLabel("Audio Driver") + + self.chkStartFullScreen = QCheckBox("Start Full Screen") + self.chkStartFullScreen.setChecked(bool(int(mw.settings.value(self.startFullScreenKey, 0)))) + self.chkStartFullScreen.stateChanged.connect(self.startFullScreenChecked) + + self.chkAutoTimeSync = QCheckBox("Auto Time Sync") + self.chkAutoTimeSync.setChecked(bool(int(mw.settings.value(self.autoTimeSyncKey, 0)))) + self.chkAutoTimeSync.stateChanged.connect(self.autoTimeSyncChecked) + + pnlChecks = QWidget() + lytChecks = QGridLayout(pnlChecks) + lytChecks.addWidget(self.chkStartFullScreen, 0, 0, 1, 1) + lytChecks.addWidget(self.chkAutoTimeSync, 0, 1, 1, 1) + + self.spnDisplayRefresh = QSpinBox() + self.spnDisplayRefresh.setMinimum(1) + self.spnDisplayRefresh.setMaximum(1000) + self.spnDisplayRefresh.setMaximumWidth(80) + refresh = 10 + if sys.platform == "win32": + refresh = 20 + self.spnDisplayRefresh.setValue(int(self.mw.settings.value(self.displayRefreshKey, refresh))) + self.spnDisplayRefresh.valueChanged.connect(self.spnDisplayRefreshChanged) + lblDisplayRefresh = QLabel("Display Refresh Interval (in milliseconds)") + + self.spnCacheMax = QSpinBox() + self.spnCacheMax.setMaximum(200) + self.spnCacheMax.setValue(100) + self.spnCacheMax.setMaximumWidth(80) + self.spnCacheMax.setValue(int(self.mw.settings.value(self.cacheMaxSizeKey, 100))) + self.spnCacheMax.valueChanged.connect(self.spnCacheMaxChanged) + lblCacheMax = QLabel("Maximum Input Stream Cache Size") + + self.btnCloseAll = QPushButton("Start All Cameras") + self.btnCloseAll.clicked.connect(self.btnCloseAllClicked) + + self.btnShowLogs = QPushButton("Show Logs") + self.btnShowLogs.clicked.connect(self.btnShowLogsClicked) + + self.btnHelp = QPushButton("Help") + self.btnHelp.clicked.connect(self.btnHelpClicked) + + pnlBuffer = QWidget() + lytBuffer = QGridLayout(pnlBuffer) + lytBuffer.addWidget(lblDisplayRefresh, 4, 0, 1, 3) + lytBuffer.addWidget(self.spnDisplayRefresh, 4, 3, 1, 1) + lytBuffer.addWidget(lblCacheMax, 5, 0, 1, 3) + lytBuffer.addWidget(self.spnCacheMax, 5, 3, 1, 1) + lytBuffer.setContentsMargins(0, 0, 0, 0) + + pnlButtons = QWidget() + lytButtons = QGridLayout(pnlButtons) + lytButtons.addWidget(self.btnCloseAll, 0, 0, 1, 1) + lytButtons.addWidget(self.btnShowLogs, 0, 1, 1, 1) + lytButtons.addWidget(self.btnHelp, 0, 2, 1, 1) + + lytMain = QGridLayout(self) + lytMain.addWidget(lblUsername, 1, 0, 1, 1) + lytMain.addWidget(self.txtUsername, 1, 1, 1, 1) + lytMain.addWidget(lblPassword, 2, 0, 1, 1) + lytMain.addWidget(self.txtPassword, 2, 1, 1, 1) + lytMain.addWidget(lblDecoders, 3, 0, 1, 1) + lytMain.addWidget(self.cmbDecoder, 3, 1, 1, 1) + lytMain.addWidget(lblAudioDrivers, 4, 0, 1, 1) + lytMain.addWidget(self.cmbAudioDriver, 4, 1, 1, 1) + lytMain.addWidget(pnlChecks, 5, 0, 1, 3) + lytMain.addWidget(pnlBuffer, 6, 0, 1, 3) + lytMain.addWidget(pnlButtons, 7, 0, 1, 3) + lytMain.addWidget(QLabel(), 8, 0, 1, 3) + lytMain.setRowStretch(8, 10) + + def usernameChanged(self, username): + self.mw.settings.setValue(self.usernameKey, username) + + def passwordChanged(self, password): + self.mw.settings.setValue(self.passwordKey, password) + + def cmbDecoderChanged(self, decoder): + self.mw.settings.setValue(self.decoderKey, decoder) + + def cmbAudioDriverChanged(self, index): + self.mw.settings.setValue(self.audioDriverIndexKey, index) + if self.mw.audioStatus != avio.AudioStatus.UNINITIALIZED: + QMessageBox.warning(self.mw, "Application Restart Required", "It is necessary to re-start Onvif GUI in order to enable this change") + + def autoDiscoverChecked(self, state): + self.mw.settings.setValue(self.autoDiscoverKey, state) + + def startFullScreenChecked(self, state): + self.mw.settings.setValue(self.startFullScreenKey, state) + + def autoTimeSyncChecked(self, state): + self.mw.settings.setValue(self.autoTimeSyncKey, state) + self.mw.cameraPanel.enableAutoTimeSync(state) + + def spnDisplayRefreshChanged(self, i): + self.mw.settings.setValue(self.displayRefreshKey, i) + self.mw.glWidget.timer.setInterval(i) + + def spnCacheMaxChanged(self, i): + self.mw.settings.setValue(self.cacheMaxSizeKey, i) + + def cmbInterfacesChanged(self, network): + self.mw.settings.setValue(self.interfaceKey, network) + + def getDecoder(self): + result = avio.AV_HWDEVICE_TYPE_NONE + if self.cmbDecoder.currentText() == "CUDA": + result = avio.AV_HWDEVICE_TYPE_CUDA + if self.cmbDecoder.currentText() == "VAAPI": + result = avio.AV_HWDEVICE_TYPE_VAAPI + if self.cmbDecoder.currentText() == "VDPAU": + result = avio.AV_HWDEVICE_TYPE_VDPAU + if self.cmbDecoder.currentText() == "DXVA2": + result = avio.AV_HWDEVICE_TYPE_DXVA2 + if self.cmbDecoder.currentText() == "D3D11VA": + result = avio.AV_HWDEVICE_TYPE_D3D11VA + return result + + def btnCloseAllClicked(self): + if self.btnCloseAll.text() == "Close All Streams": + self.mw.closeAllStreams() + else: + self.mw.startAllCameras() + + def getLogFilename(self): + filename = "" + if sys.platform == "win32": + filename = os.environ['HOMEPATH'] + "/.cache/onvif-gui/logs.txt" + else: + filename = os.environ['HOME'] + "/.cache/onvif-gui/logs.txt" + return filename + + def btnShowLogsClicked(self): + filename = self.getLogFilename() + if not self.dlgLog: + self.dlgLog = LogDialog(self.mw) + self.dlgLog.readLogFile(filename) + self.dlgLog.exec() + + def btnHelpClicked(self): + result = webbrowser.get().open("https://github.com/sr99622/libonvif#readme-ov-file") + if not result: + webbrowser.get().open("https://github.com/sr99622/libonvif") diff --git a/onvif-gui/gui/panels/options/proxy.py b/onvif-gui/gui/panels/options/proxy.py new file mode 100644 index 00000000..ae8e0ff4 --- /dev/null +++ b/onvif-gui/gui/panels/options/proxy.py @@ -0,0 +1,129 @@ +#/******************************************************************** +# libonvif/onvif-gui/gui/panels/options/proxy.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +from PyQt6.QtWidgets import QMessageBox, QLineEdit, \ + QGridLayout, QWidget, QLabel, QMessageBox, QRadioButton, \ + QGroupBox +from PyQt6.QtCore import Qt + +from gui.enums import ProxyType + +class ProxyOptions(QWidget): + def __init__(self, mw): + super().__init__() + self.mw = mw + + self.proxyTypeKey = "settings/proxyType" + self.proxyRemoteKey = "settings/proxyRemote" + + self.grpProxyType = QGroupBox("Select Proxy Type") + self.radStandAlone = QRadioButton("Stand Alone", self.grpProxyType) + self.radServer = QRadioButton("Server", self.grpProxyType) + self.radClient = QRadioButton("Client", self.grpProxyType) + + self.lblServer = QLabel() + self.lblConnect = QLabel("Connect url for clients") + self.lblConnect.setEnabled(False) + + self.proxyRemote = self.mw.settings.value(self.proxyRemoteKey) + + self.txtRemote = QLineEdit() + self.txtRemote.setText(self.proxyRemote) + self.txtRemote.textEdited.connect(self.txtRemoteEdited) + self.txtRemote.setEnabled(False) + self.lblRemote = QLabel("Enter connect url from server") + self.lblRemote.setEnabled(False) + + self.proxyType = (self.mw.settings.value(self.proxyTypeKey, ProxyType.STAND_ALONE)) + + match self.proxyType: + case ProxyType.STAND_ALONE: + self.radStandAlone.setChecked(True) + self.radStandAloneToggled(True) + case ProxyType.SERVER: + self.radServer.setChecked(True) + self.radServerToggled(True) + case ProxyType.CLIENT: + self.radClient.setChecked(True) + self.radClientToggled(True) + + self.radStandAlone.toggled.connect(self.radStandAloneToggled) + self.radServer.toggled.connect(self.radServerToggled) + self.radClient.toggled.connect(self.radClientToggled) + + lytProxyType = QGridLayout(self.grpProxyType) + lytProxyType.addWidget(self.radStandAlone, 0, 0, 1, 1) + lytProxyType.addWidget(self.radServer, 1, 0, 1, 1) + lytProxyType.addWidget(self.lblConnect, 1, 1, 1, 1, Qt.AlignmentFlag.AlignRight) + lytProxyType.addWidget(self.lblServer, 2, 0, 1, 2, Qt.AlignmentFlag.AlignRight) + lytProxyType.addWidget(self.radClient, 3, 0, 1, 1) + lytProxyType.addWidget(self.lblRemote, 3, 1, 1, 1, Qt.AlignmentFlag.AlignRight) + lytProxyType.addWidget(self.txtRemote, 4, 0, 1, 2) + lytProxyType.setColumnStretch(2, 10) + + lytMain = QGridLayout(self) + lytMain.addWidget(self.grpProxyType, 0, 0, 1, 1) + lytMain.addWidget(QLabel(), 1, 0, 1, 1) + lytMain.setRowStretch(1, 10) + + def setProxyType(self, type): + if not hasattr(self.mw, "cameraPanel"): + return + + if len(self.mw.pm.players): + QMessageBox.information(self.mw, "Closing Streams", "All current streams will be closed") + self.mw.closeAllStreams() + + self.proxyType = type + self.mw.settings.setValue(self.proxyTypeKey, type) + + getProxyURI = None + if type != ProxyType.STAND_ALONE: + getProxyURI = self.mw.getProxyURI + + if lstCamera := self.mw.cameraPanel.lstCamera: + cameras = [lstCamera.item(x) for x in range(lstCamera.count())] + for camera in cameras: + self.mw.addCameraProxy(camera) + camera.onvif_data.getProxyURI = getProxyURI + for profile in camera.profiles: + profile.getProxyURI = getProxyURI + + def radStandAloneToggled(self, checked): + if checked: + self.setProxyType(ProxyType.STAND_ALONE) + + def radServerToggled(self, checked): + self.lblConnect.setEnabled(checked) + if checked: + self.setProxyType(ProxyType.SERVER) + self.mw.startProxyServer() + self.lblServer.setText(self.mw.proxy.getRootURI()) + else: + self.mw.stopProxyServer() + self.lblServer.setText("") + + def radClientToggled(self, checked): + self.lblRemote.setEnabled(checked) + self.txtRemote.setEnabled(checked) + if checked: + self.setProxyType(ProxyType.CLIENT) + + def txtRemoteEdited(self, arg): + self.mw.settings.setValue(self.proxyRemoteKey, arg) \ No newline at end of file diff --git a/onvif-gui/gui/panels/options/storage.py b/onvif-gui/gui/panels/options/storage.py new file mode 100644 index 00000000..5b303290 --- /dev/null +++ b/onvif-gui/gui/panels/options/storage.py @@ -0,0 +1,124 @@ +#/******************************************************************** +# libonvif/onvif-gui/gui/panels/options/storage.py +# +# Copyright (c) 2024 Stephen Rhodes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#*********************************************************************/ + +import os +from PyQt6.QtWidgets import QMessageBox, QSpinBox, \ + QGridLayout, QWidget, QCheckBox, QLabel, QMessageBox, QGroupBox +from PyQt6.QtCore import QStandardPaths +from loguru import logger +from gui.components import DirectorySelector +import shutil + +class StorageOptions(QWidget): + def __init__(self, mw): + super().__init__() + self.mw = mw + + self.archiveKey = "settings/archive" + self.pictureKey = "settings/picture" + self.diskLimitKey = "settings/diskLimit" + self.mangageDiskUsagekey = "settings/manageDiskUsage" + + video_dirs = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.MoviesLocation) + self.dirArchive = DirectorySelector(mw, self.archiveKey, "Archive Dir", video_dirs[0]) + self.dirArchive.signals.dirChanged.connect(self.dirArchiveChanged) + + picture_dirs = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.PicturesLocation) + self.dirPictures = DirectorySelector(mw, self.pictureKey, "Picture Dir", picture_dirs[0]) + self.dirPictures.signals.dirChanged.connect(self.dirPicturesChanged) + + dir_size = "{:.2f}".format(self.getDirectorySize(self.dirArchive.text()) / 1000000000) + self.grpDiskUsage = QGroupBox(f'Disk Usage (currently {dir_size} GB)') + self.spnDiskLimit = QSpinBox() + max_size = int(self.getMaximumDirectorySize()) + self.spnDiskLimit.setMaximum(max_size) + disk_limit = min(int(self.mw.settings.value(self.diskLimitKey, 100)), max_size) + self.spnDiskLimit.setValue(disk_limit) + self.spnDiskLimit.valueChanged.connect(self.spnDiskLimitChanged) + + lbl = f'Auto Manage (max {max_size} GB)' + self.chkManageDiskUsage = QCheckBox(lbl) + self.chkManageDiskUsage.setChecked(bool(int(self.mw.settings.value(self.mangageDiskUsagekey, 0)))) + self.chkManageDiskUsage.clicked.connect(self.chkManageDiskUsageChanged) + + lytDiskUsage = QGridLayout(self.grpDiskUsage) + lytDiskUsage.addWidget(self.chkManageDiskUsage, 0, 0, 1, 1) + lytDiskUsage.addWidget(self.spnDiskLimit, 0, 2, 1, 1) + lytDiskUsage.addWidget(QLabel("GB"), 0, 3, 1, 1) + lytDiskUsage.addWidget(self.dirArchive, 1, 0, 1, 4) + lytDiskUsage.addWidget(self.dirPictures, 2, 0, 1, 4) + + lytMain = QGridLayout(self) + lytMain.addWidget(self.grpDiskUsage, 0, 0, 1, 1) + lytMain.addWidget(QLabel(), 1, 0, 1, 1) + lytMain.setRowStretch(1, 10) + + def spnDiskLimitChanged(self, value): + self.mw.settings.setValue(self.diskLimitKey, value) + + def chkManageDiskUsageChanged(self): + if self.chkManageDiskUsage.isChecked(): + ret = QMessageBox.warning(self, "** WARNING **", + "You are giving full control of the archive directory to this program. " + "Any files contained within this directory or its sub-directories are subject to deletion. " + "You should only enable this feature if you are sure that this is ok.\n\n" + "Are you sure you want to continue?", + QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) + if ret == QMessageBox.StandardButton.Cancel: + self.chkManageDiskUsage.setChecked(False) + self.mw.settings.setValue(self.mangageDiskUsagekey, int(self.chkManageDiskUsage.isChecked())) + + def dirArchiveChanged(self, path): + logger.debug(f'Video archive directory changed to {path}') + self.mw.settings.setValue(self.archiveKey, path) + max_size = int(self.getMaximumDirectorySize()) + self.spnDiskLimit.setMaximum(max_size) + lbl = f'Auto Manage (max {max_size} GB)' + self.chkManageDiskUsage.setText(lbl) + disk_limit = min(int(self.mw.settings.value(self.diskLimitKey, 100)), max_size) + self.spnDiskLimit.setValue(disk_limit) + self.chkManageDiskUsageChanged() + + def dirPicturesChanged(self, path): + logger.debug(f'Picture directory changed to {path}') + self.mw.settings.setValue(self.pictureKey, path) + + def getMaximumDirectorySize(self): + # compute disk space available for archive directory in GB + d = self.dirArchive.txtDirectory.text() + d_size = 0 + for dirpath, dirnames, filenames in os.walk(d): + for f in filenames: + fp = os.path.join(dirpath, f) + if not os.path.islink(fp): + d_size += os.path.getsize(fp) + total, used, free = shutil.disk_usage(d) + max_available = (free + d_size - 10000000000) / 1000000000 + return max_available + + def getDirectorySize(self, d): + total_size = 0 + for dirpath, dirnames, filenames in os.walk(d): + for f in filenames: + fp = os.path.join(dirpath, f) + if not os.path.islink(fp): + total_size += os.path.getsize(fp) + + return total_size + diff --git a/onvif-gui/gui/panels/settingspanel.py b/onvif-gui/gui/panels/settingspanel.py index c6808333..1e314eb9 100644 --- a/onvif-gui/gui/panels/settingspanel.py +++ b/onvif-gui/gui/panels/settingspanel.py @@ -17,605 +17,37 @@ # #*********************************************************************/ -import os -import sys -from PyQt6.QtWidgets import QMessageBox, QLineEdit, QSpinBox, \ - QGridLayout, QWidget, QCheckBox, QLabel, QComboBox, QPushButton, \ - QDialog, QTextEdit, QMessageBox, QDialogButtonBox, QRadioButton, \ - QGroupBox, QSlider, QFileDialog -from PyQt6.QtCore import Qt, QStandardPaths, QFile, QRegularExpression, QRect -from PyQt6.QtGui import QRegularExpressionValidator, QTextCursor, QTextOption -from loguru import logger -from gui.components import DirectorySelector -import libonvif as onvif -import avio -import shutil -from time import sleep -import webbrowser -import platform +from PyQt6.QtWidgets import QGridLayout, QWidget, QTabWidget -class AddCameraDialog(QDialog): - def __init__(self, mw): - super().__init__(mw) - self.mw = mw - - ipRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" - ipRegex = QRegularExpression("^" + ipRange + "\\." + ipRange + "\\." + ipRange + "\\." + ipRange + "$") - ipValidator = QRegularExpressionValidator(ipRegex, self) - - self.setWindowTitle("Add Camera") - self.txtIPAddress = QLineEdit() - self.txtIPAddress.setValidator(ipValidator) - self.lblIPAddress = QLabel("IP Address") - self.txtOnvifPort = QLineEdit() - self.lblOnvifPort = QLabel("Onvif Port") - - buttonBox = QDialogButtonBox( \ - QDialogButtonBox.StandardButton.Ok | \ - QDialogButtonBox.StandardButton.Cancel) - - lytMain = QGridLayout(self) - lytMain.addWidget(self.lblIPAddress, 1, 0, 1, 1) - lytMain.addWidget(self.txtIPAddress, 1, 1, 1, 1) - lytMain.addWidget(self.lblOnvifPort, 2, 0, 1, 1) - lytMain.addWidget(self.txtOnvifPort, 2, 1, 1, 1) - lytMain.addWidget(buttonBox, 5, 0, 1, 2) - - buttonBox.accepted.connect(self.accept) - buttonBox.rejected.connect(self.reject) - - self.txtIPAddress.setFocus() - -class LogText(QTextEdit): - def __init__(self, parent): - super().__init__(parent) - self.setWordWrapMode(QTextOption.WrapMode.NoWrap) - - def scrollToBottom(self): - self.moveCursor(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.MoveAnchor) - self.ensureCursorVisible() - -class LogDialog(QDialog): - def __init__(self, mw): - super().__init__(mw) - self.mw = mw - self.geometryKey = "LogDialog/geometry" - rect = self.mw.settings.value(self.geometryKey, QRect(0, 0, 480, 640)) - if rect is not None: - if rect.width() and rect.height(): - self.setGeometry(rect) - - self.editor = LogText(self) - self.editor.setReadOnly(True) - self.editor.setFontFamily("courier") - - self.lblSize = QLabel("Log File Size:") - self.btnArchive = QPushButton("Archive") - self.btnArchive.clicked.connect(self.btnArchiveClicked) - self.btnClear = QPushButton(" Clear ") - self.btnClear.clicked.connect(self.btnClearClicked) - - pnlButton = QWidget() - lytButton = QGridLayout(pnlButton) - lytButton.addWidget(self.btnArchive, 0, 1, 1, 1) - lytButton.addWidget(self.btnClear, 0, 2, 1, 1) - lytButton.setContentsMargins(0, 0, 0, 0) - - panel = QWidget() - lytPanel = QGridLayout(panel) - lytPanel.addWidget(self.lblSize, 0, 0, 1, 1) - lytPanel.addWidget(pnlButton, 0, 1, 1, 1) - lytPanel.addWidget(QLabel(), 0, 2, 1, 1) - lytPanel.setColumnStretch(2, 10) - - lyt = QGridLayout(self) - lyt.addWidget(self.editor, 0, 0, 1, 1) - lyt.addWidget(panel, 1, 0, 1, 1) - lyt.setRowStretch(0, 10) - - def closeEvent(self, e): - self.mw.settings.setValue(self.geometryKey, self.geometry()) - - def readLogFile(self, path): - data = "" - if os.path.isfile(path): - with open(path, 'r') as file: - data = file.read() - self.setWindowTitle(path) - self.editor.setText(data) - y = "{:.2f}".format(os.stat(path).st_size/1000000) - self.lblSize.setText(f'Log File Size: {y} MB ') - self.editor.scrollToBottom() - - def btnArchiveClicked(self): - path = None - if platform.system() == "Linux": - path = QFileDialog.getOpenFileName(self, "Select File", self.windowTitle(), options=QFileDialog.Option.DontUseNativeDialog)[0] - else: - path = QFileDialog.getOpenFileName(self, "Select File", self.windowTitle())[0] - if path: - if len(path) > 0: - self.readLogFile(path) - - def btnClearClicked(self): - filename = self.windowTitle() - ret = QMessageBox.warning(self, "Deleting File", - f'\n{filename}\n\n' - "You are about to delete this file.\n" - "Are you sure you want to continue?", - QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) - - if ret == QMessageBox.StandardButton.Ok: - if filename == self.mw.settingsPanel.getLogFilename(): - ret = QMessageBox.warning(self, "Deleting Current Log", - "You are about to delete the current log.\n" - "Are you sure you want to continue?", - QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) - if ret == QMessageBox.StandardButton.Ok: - QFile.remove(filename) - logger.add(filename) - logger.debug("Created new log file") - self.readLogFile(filename) - else: - QFile.remove(filename) - self.readLogFile(self.mw.settingsPanel.getLogFilename()) - QMessageBox.information(self, "Current Log Displayed", "The current log has been loaded into the display", QMessageBox.StandardButton.Ok) +from gui.panels.options import DiscoverOptions, GeneralOptions, StorageOptions, \ + AlarmOptions, ProxyOptions class SettingsPanel(QWidget): def __init__(self, mw): super().__init__() self.mw = mw - self.usernameKey = "settings/username" - self.passwordKey = "settings/password" - self.decoderKey = "settings/decoder" - self.bufferSizeKey = "settings/bufferSize" - self.lagTimeKey = "settings/lagTime" - self.interfaceKey = "settings/interface" - self.autoDiscoverKey = "settings/autoDiscover" - self.startFullScreenKey = "settings/startFullScreen" - self.autoTimeSyncKey = "settings/autoTimeSync" - self.autoStartKey = "settings/autoStart" - self.archiveKey = "settings/archive" - self.pictureKey = "settings/picture" - self.scanAllKey = "settings/scanAll" - self.cameraListKey = "settings/cameraList" - self.discoveryTypeKey = "settings/discoveryType" - self.alarmSoundFileKey = "settings/alarmSoundFile" - self.alarmSoundVolumeKey = "settings/alarmSoundVolume" - self.diskLimitKey = "settings/diskLimit" - self.mangageDiskUsagekey = "settings/manageDiskUsage" - self.displayRefreshKey = "settings/displayRefresh" - self.cacheMaxSizeKey = "settings/cacheMaxSize" - - decoders = ["NONE"] - if sys.platform == "win32": - decoders += ["CUDA", "DXVA2", "D3D11VA"] - if sys.platform == "linux": - decoders += ["CUDA", "VAAPI", "VDPAU"] - - self.dlgLog = None - - self.txtUsername = QLineEdit() - self.txtUsername.setText(mw.settings.value(self.usernameKey, "")) - self.txtUsername.textChanged.connect(self.usernameChanged) - lblUsername = QLabel("Common Username") - - self.txtPassword = QLineEdit() - self.txtPassword.setText(mw.settings.value(self.passwordKey, "")) - self.txtPassword.textChanged.connect(self.passwordChanged) - lblPassword = QLabel("Common Password") - - self.cmbDecoder = QComboBox() - self.cmbDecoder.addItems(decoders) - self.cmbDecoder.setCurrentText(mw.settings.value(self.decoderKey, "NONE")) - self.cmbDecoder.currentTextChanged.connect(self.cmbDecoderChanged) - lblDecoders = QLabel("Hardware Decoder") - - self.chkStartFullScreen = QCheckBox("Start Full Screen") - self.chkStartFullScreen.setChecked(bool(int(mw.settings.value(self.startFullScreenKey, 0)))) - self.chkStartFullScreen.stateChanged.connect(self.startFullScreenChecked) - - self.chkAutoDiscover = QCheckBox("Auto Discovery") - self.chkAutoDiscover.setChecked(bool(int(mw.settings.value(self.autoDiscoverKey, 0)))) - self.chkAutoDiscover.stateChanged.connect(self.autoDiscoverChecked) - - self.chkAutoTimeSync = QCheckBox("Auto Time Sync") - self.chkAutoTimeSync.setChecked(bool(int(mw.settings.value(self.autoTimeSyncKey, 0)))) - self.chkAutoTimeSync.stateChanged.connect(self.autoTimeSyncChecked) - - self.chkAutoStart = QCheckBox("Auto Start") - self.chkAutoStart.setChecked(bool(int(mw.settings.value(self.autoStartKey, 0)))) - self.chkAutoStart.stateChanged.connect(self.autoStartChecked) - - pnlChecks = QWidget() - lytChecks = QGridLayout(pnlChecks) - lytChecks.addWidget(self.chkStartFullScreen, 0, 0, 1, 1) - lytChecks.addWidget(self.chkAutoDiscover, 0, 1, 1, 1) - lytChecks.addWidget(self.chkAutoStart, 1, 0, 1, 1) - lytChecks.addWidget(self.chkAutoTimeSync, 1, 1, 1, 1) - - self.spnBufferSize = QSpinBox() - self.spnBufferSize.setMinimum(1) - self.spnBufferSize.setMaximum(60) - self.spnBufferSize.setMaximumWidth(80) - self.spnBufferSize.setValue(int(self.mw.settings.value(self.bufferSizeKey, 10))) - self.spnBufferSize.valueChanged.connect(self.spnBufferSizeChanged) - lblBufferSize = QLabel("Pre-Alarm Buffer Size (in seconds)") - - self.spnLagTime = QSpinBox() - self.spnLagTime.setMinimum(1) - self.spnLagTime.setMaximum(60) - self.spnLagTime.setMaximumWidth(80) - self.spnLagTime.setValue(int(self.mw.settings.value(self.lagTimeKey, 5))) - self.spnLagTime.valueChanged.connect(self.spnLagTimeChanged) - lblLagTime = QLabel("Post-Alarm Lag Time (in seconds)") - - self.spnDisplayRefresh = QSpinBox() - self.spnDisplayRefresh.setMinimum(1) - self.spnDisplayRefresh.setMaximum(1000) - self.spnDisplayRefresh.setMaximumWidth(80) - refresh = 10 - if sys.platform == "win32": - refresh = 20 - self.spnDisplayRefresh.setValue(int(self.mw.settings.value(self.displayRefreshKey, refresh))) - self.spnDisplayRefresh.valueChanged.connect(self.spnDisplayRefreshChanged) - lblDisplayRefresh = QLabel("Display Refresh Interval (in milliseconds)") - - self.spnCacheMax = QSpinBox() - self.spnCacheMax.setMaximum(200) - self.spnCacheMax.setValue(100) - self.spnCacheMax.setMaximumWidth(80) - self.spnCacheMax.setValue(int(self.mw.settings.value(self.cacheMaxSizeKey, 100))) - self.spnCacheMax.valueChanged.connect(self.spnCacheMaxChanged) - lblCacheMax = QLabel("Maximum Input Stream Cache Size") - - self.cmbSoundFiles = QComboBox() - d = f'{self.mw.getLocation()}/gui/resources' - sounds = [f for f in os.listdir(d) if os.path.isfile(os.path.join(d, f)) and f.endswith(".mp3")] - self.cmbSoundFiles.addItems(sounds) - self.cmbSoundFiles.currentTextChanged.connect(self.cmbSoundFilesChanged) - self.cmbSoundFiles.setCurrentText(self.mw.settings.value(self.alarmSoundFileKey, "drops.mp3")) - lblSoundFiles = QLabel("Alarm Sounds") - self.sldAlarmVolume = QSlider(Qt.Orientation.Horizontal) - self.sldAlarmVolume.setValue(int(self.mw.settings.value(self.alarmSoundVolumeKey, 80))) - self.sldAlarmVolume.valueChanged.connect(self.sldAlarmVolumeChanged) - - pnlSoundFile = QWidget() - lytSoundFile = QGridLayout(pnlSoundFile) - lytSoundFile.addWidget(lblSoundFiles, 0, 0, 1, 1) - lytSoundFile.addWidget(self.cmbSoundFiles, 0, 1, 1, 1) - lytSoundFile.addWidget(self.sldAlarmVolume, 0, 2, 1, 1) - lytSoundFile.setColumnStretch(1, 10) - - pnlBuffer = QWidget() - lytBuffer = QGridLayout(pnlBuffer) - lytBuffer.addWidget(lblBufferSize, 1, 0, 1, 3) - lytBuffer.addWidget(self.spnBufferSize, 1, 3, 1, 1) - lytBuffer.addWidget(lblLagTime, 2, 0, 1, 3) - lytBuffer.addWidget(self.spnLagTime, 2, 3, 1, 1) - lytBuffer.addWidget(pnlSoundFile, 3, 0, 1, 4) - lytBuffer.addWidget(lblDisplayRefresh, 4, 0, 1, 3) - lytBuffer.addWidget(self.spnDisplayRefresh, 4, 3, 1, 1) - lytBuffer.addWidget(lblCacheMax, 5, 0, 1, 3) - lytBuffer.addWidget(self.spnCacheMax, 5, 3, 1, 1) - lytBuffer.setContentsMargins(0, 0, 0, 0) - - self.grpDiscoverType = QGroupBox("Set Camera Discovery Method") - self.radDiscover = QRadioButton("Discover Broadcast", self.grpDiscoverType ) - self.radDiscover.setChecked(int(self.mw.settings.value(self.discoveryTypeKey, 1))) - self.radDiscover.toggled.connect(self.radDiscoverToggled) - self.radCached = QRadioButton("Cached Addresses", self.grpDiscoverType ) - self.radCached.setChecked(not self.radDiscover.isChecked()) - lytDiscoverType = QGridLayout(self.grpDiscoverType ) - lytDiscoverType.addWidget(self.radDiscover, 0, 0, 1, 1) - lytDiscoverType.addWidget(self.radCached, 0, 1, 1, 1) - - self.chkScanAllNetworks = QCheckBox("Scan All Networks During Discovery") - self.chkScanAllNetworks.setChecked(int(mw.settings.value(self.scanAllKey, 1))) - self.chkScanAllNetworks.stateChanged.connect(self.scanAllNetworksChecked) - self.cmbInterfaces = QComboBox() - intf = self.mw.settings.value(self.interfaceKey, "") - self.lblInterfaces = QLabel("Network") - session = onvif.Session() - session.getActiveInterfaces() - i = 0 - while len(session.active_interface(i)) > 0 and i < 16: - self.cmbInterfaces.addItem(session.active_interface(i)) - i += 1 - if len(intf) > 0: - self.cmbInterfaces.setCurrentText(intf) - self.cmbInterfaces.currentTextChanged.connect(self.cmbInterfacesChanged) - self.cmbInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) - self.lblInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) - - self.btnAddCamera = QPushButton("Add Camera") - self.btnAddCamera.clicked.connect(self.btnAddCameraClicked) - - pnlInterface = QGroupBox("Discovery Options") - lytInterface = QGridLayout(pnlInterface) - lytInterface.addWidget(self.grpDiscoverType, 0, 0, 1, 2) - lytInterface.addWidget(self.chkScanAllNetworks, 2, 0, 1, 2) - lytInterface.addWidget(self.lblInterfaces, 4, 0, 1, 1) - lytInterface.addWidget(self.cmbInterfaces, 4, 1, 1, 1) - lytInterface.addWidget(self.btnAddCamera, 5, 0, 1, 2, Qt.AlignmentFlag.AlignCenter) - lytInterface.setColumnStretch(1, 10) - lytInterface.setContentsMargins(10, 10, 10, 10) - self.radDiscoverToggled(self.radDiscover.isChecked()) + self.tab = QTabWidget() - video_dirs = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.MoviesLocation) - self.dirArchive = DirectorySelector(mw, self.archiveKey, "Archive Dir", video_dirs[0]) - self.dirArchive.signals.dirChanged.connect(self.dirArchiveChanged) + self.general = GeneralOptions(mw) + self.discover = DiscoverOptions(mw) + self.storage = StorageOptions(mw) + self.proxy = ProxyOptions(mw) + self.alarm = AlarmOptions(mw) - picture_dirs = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.PicturesLocation) - self.dirPictures = DirectorySelector(mw, self.pictureKey, "Picture Dir", picture_dirs[0]) - self.dirPictures.signals.dirChanged.connect(self.dirPicturesChanged) - - dir_size = "{:.2f}".format(self.getDirectorySize(self.dirArchive.text()) / 1000000000) - self.grpDiskUsage = QGroupBox(f'Disk Usage (currently {dir_size} GB)') - self.spnDiskLimit = QSpinBox() - max_size = int(self.getMaximumDirectorySize()) - self.spnDiskLimit.setMaximum(max_size) - disk_limit = min(int(self.mw.settings.value(self.diskLimitKey, 100)), max_size) - self.spnDiskLimit.setValue(disk_limit) - self.spnDiskLimit.valueChanged.connect(self.spnDiskLimitChanged) - - lbl = f'Auto Manage (max {max_size} GB)' - self.chkManageDiskUsage = QCheckBox(lbl) - self.chkManageDiskUsage.setChecked(bool(int(self.mw.settings.value(self.mangageDiskUsagekey, 0)))) - self.chkManageDiskUsage.clicked.connect(self.chkManageDiskUsageChanged) - - lytDiskUsage = QGridLayout(self.grpDiskUsage) - lytDiskUsage.addWidget(self.chkManageDiskUsage, 0, 0, 1, 1) - lytDiskUsage.addWidget(self.spnDiskLimit, 0, 2, 1, 1) - lytDiskUsage.addWidget(QLabel("GB"), 0, 3, 1, 1) - lytDiskUsage.addWidget(self.dirArchive, 1, 0, 1, 4) - lytDiskUsage.addWidget(self.dirPictures, 2, 0, 1, 4) - - self.btnCloseAll = QPushButton("Start All Cameras") - self.btnCloseAll.clicked.connect(self.btnCloseAllClicked) - - self.btnShowLogs = QPushButton("Show Logs") - self.btnShowLogs.clicked.connect(self.btnShowLogsClicked) - - self.btnHelp = QPushButton("Help") - self.btnHelp.clicked.connect(self.btnHelpClicked) - - pnlButtons = QWidget() - lytButtons = QGridLayout(pnlButtons) - lytButtons.addWidget(self.btnCloseAll, 0, 0, 1, 1) - lytButtons.addWidget(self.btnShowLogs, 0, 1, 1, 1) - lytButtons.addWidget(self.btnHelp, 0, 2, 1, 1) - - self.lblSpacer = QLabel("") + self.tab.addTab(self.general, "General") + self.tab.addTab(self.discover, "Discover") + self.tab.addTab(self.storage, "Storage") + self.tab.addTab(self.proxy, "Proxy") + self.tab.addTab(self.alarm, "Alarm") lytMain = QGridLayout(self) - lytMain.addWidget(lblUsername, 1, 0, 1, 1) - lytMain.addWidget(self.txtUsername, 1, 1, 1, 1) - lytMain.addWidget(lblPassword, 2, 0, 1, 1) - lytMain.addWidget(self.txtPassword, 2, 1, 1, 1) - lytMain.addWidget(lblDecoders, 3, 0, 1, 1) - lytMain.addWidget(self.cmbDecoder, 3, 1, 1, 1) - lytMain.addWidget(pnlChecks, 4, 0, 1, 3) - lytMain.addWidget(pnlBuffer, 5, 0, 1, 3) - lytMain.addWidget(pnlInterface, 6, 0, 1, 3) - lytMain.addWidget(self.grpDiskUsage, 7, 0, 1, 3) - lytMain.addWidget(pnlButtons, 8, 0, 1, 3) - lytMain.addWidget(self.lblSpacer, 9, 0, 1, 3) - lytMain.setRowStretch(9, 10) - - def showEvent(self, event): - self.lblSpacer.setFocus() - return super().showEvent(event) - - def usernameChanged(self, username): - self.mw.settings.setValue(self.usernameKey, username) - - def passwordChanged(self, password): - self.mw.settings.setValue(self.passwordKey, password) - - def cmbDecoderChanged(self, decoder): - self.mw.settings.setValue(self.decoderKey, decoder) - - def autoDiscoverChecked(self, state): - self.mw.settings.setValue(self.autoDiscoverKey, state) - - def startFullScreenChecked(self, state): - self.mw.settings.setValue(self.startFullScreenKey, state) - - def autoTimeSyncChecked(self, state): - self.mw.settings.setValue(self.autoTimeSyncKey, state) - self.mw.cameraPanel.enableAutoTimeSync(state) - - def autoStartChecked(self, state): - self.mw.settings.setValue(self.autoStartKey, state) - - def spnDisplayRefreshChanged(self, i): - self.mw.settings.setValue(self.displayRefreshKey, i) - self.mw.glWidget.timer.setInterval(i) - - def spnCacheMaxChanged(self, i): - self.mw.settings.setValue(self.cacheMaxSizeKey, i) - - def spnBufferSizeChanged(self, i): - self.mw.settings.setValue(self.bufferSizeKey, i) - - def spnLagTimeChanged(self, i): - self.mw.settings.setValue(self.lagTimeKey, i) - - def cmbInterfacesChanged(self, network): - self.mw.settings.setValue(self.interfaceKey, network) + lytMain.addWidget(self.tab, 0, 0, 1, 1) def onMediaStarted(self): if len(self.mw.pm.players): - self.btnCloseAll.setText("Close All Streams") + self.general.btnCloseAll.setText("Close All Streams") def onMediaStopped(self): if not len(self.mw.pm.players): - self.btnCloseAll.setText("Start All Cameras") - - - def btnCloseAllClicked(self): - try: - if self.btnCloseAll.text() == "Close All Streams": - for player in self.mw.pm.players: - player.requestShutdown() - for timer in self.mw.timers.values(): - timer.stop() - self.mw.pm.auto_start_mode = False - lstCamera = self.mw.cameraPanel.lstCamera - if lstCamera: - cameras = [lstCamera.item(x) for x in range(lstCamera.count())] - for camera in cameras: - camera.setIconIdle() - - count = 0 - while len(self.mw.pm.players): - sleep(0.1) - count += 1 - if count > 200: - logger.debug("not all players closed within the allotted time, flushing player manager") - self.mw.pm.players.clear() - break - - self.mw.pm.ordinals.clear() - self.mw.pm.sizes.clear() - self.mw.cameraPanel.syncGUI() - else: - lstCamera = self.mw.cameraPanel.lstCamera - if lstCamera: - cameras = [lstCamera.item(x) for x in range(lstCamera.count())] - for camera in cameras: - self.mw.cameraPanel.setCurrentCamera(camera.uri()) - self.mw.cameraPanel.onItemDoubleClicked(camera) - except Exception as ex: - logger.error(ex) - - def scanAllNetworksChecked(self, state): - self.mw.settings.setValue(self.scanAllKey, state) - self.cmbInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) - self.lblInterfaces.setEnabled(not self.chkScanAllNetworks.isChecked()) - - def getLogFilename(self): - filename = "" - if sys.platform == "win32": - filename = os.environ['HOMEPATH'] + "/.cache/onvif-gui/logs.txt" - else: - filename = os.environ['HOME'] + "/.cache/onvif-gui/logs.txt" - return filename - - def btnShowLogsClicked(self): - filename = self.getLogFilename() - if not self.dlgLog: - self.dlgLog = LogDialog(self.mw) - self.dlgLog.readLogFile(filename) - self.dlgLog.exec() - - def btnHelpClicked(self): - result = webbrowser.get().open("https://github.com/sr99622/libonvif#readme-ov-file") - if not result: - webbrowser.get().open("https://github.com/sr99622/libonvif") - - def radDiscoverToggled(self, checked): - self.chkScanAllNetworks.setEnabled(checked) - if self.chkScanAllNetworks.isChecked(): - self.lblInterfaces.setEnabled(False) - self.cmbInterfaces.setEnabled(False) - else: - self.lblInterfaces.setEnabled(checked) - self.cmbInterfaces.setEnabled(checked) - self.mw.settings.setValue(self.discoveryTypeKey, int(checked)) - - def radEntireDiskToggled(self, checked): - self.spnDiskLimit.setEnabled(not checked) - self.mw.settings.setValue(self.entireDiskKey, int(checked)) - - def spnDiskLimitChanged(self, value): - self.mw.settings.setValue(self.diskLimitKey, value) - - def cmbSoundFilesChanged(self, value): - self.mw.settings.setValue(self.alarmSoundFileKey, value) - - def sldAlarmVolumeChanged(self, value): - self.mw.settings.setValue(self.alarmSoundVolumeKey, value) - - def getDecoder(self): - result = avio.AV_HWDEVICE_TYPE_NONE - if self.cmbDecoder.currentText() == "CUDA": - result = avio.AV_HWDEVICE_TYPE_CUDA - if self.cmbDecoder.currentText() == "VAAPI": - result = avio.AV_HWDEVICE_TYPE_VAAPI - if self.cmbDecoder.currentText() == "VDPAU": - result = avio.AV_HWDEVICE_TYPE_VDPAU - if self.cmbDecoder.currentText() == "DXVA2": - result = avio.AV_HWDEVICE_TYPE_DXVA2 - if self.cmbDecoder.currentText() == "D3D11VA": - result = avio.AV_HWDEVICE_TYPE_D3D11VA - return result - - def btnAddCameraClicked(self): - dlg = AddCameraDialog(self.mw) - if dlg.exec(): - ip_address = dlg.txtIPAddress.text() - onvif_port = dlg.txtOnvifPort.text() - if not len(onvif_port): - onvif_port = "80" - xaddrs = f'http://{ip_address}:{onvif_port}/onvif/device_service' - logger.debug(f'Attempting to add camera manually using xaddrs: {xaddrs}') - data = onvif.Data() - data.getData = self.mw.cameraPanel.getData - data.getCredential = self.mw.cameraPanel.getCredential - data.setXAddrs(xaddrs) - data.setDeviceService(xaddrs) - data.manual_fill() - - def chkManageDiskUsageChanged(self): - if self.chkManageDiskUsage.isChecked(): - ret = QMessageBox.warning(self, "** WARNING **", - "You are giving full control of the archive directory to this program. " - "Any files contained within this directory or its sub-directories are subject to deletion. " - "You should only enable this feature if you are sure that this is ok.\n\n" - "Are you sure you want to continue?", - QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) - if ret == QMessageBox.StandardButton.Cancel: - self.chkManageDiskUsage.setChecked(False) - self.mw.settings.setValue(self.mangageDiskUsagekey, int(self.chkManageDiskUsage.isChecked())) - - def dirArchiveChanged(self, path): - logger.debug(f'Video archive directory changed to {path}') - self.mw.settings.setValue(self.archiveKey, path) - max_size = int(self.getMaximumDirectorySize()) - self.spnDiskLimit.setMaximum(max_size) - lbl = f'Auto Manage (max {max_size} GB)' - self.chkManageDiskUsage.setText(lbl) - disk_limit = min(int(self.mw.settings.value(self.diskLimitKey, 100)), max_size) - self.spnDiskLimit.setValue(disk_limit) - self.chkManageDiskUsageChanged() - - def dirPicturesChanged(self, path): - logger.debug(f'Picture directory changed to {path}') - self.mw.settings.setValue(self.pictureKey, path) - - def getMaximumDirectorySize(self): - # compute disk space available for archive directory in GB - d = self.dirArchive.txtDirectory.text() - d_size = 0 - for dirpath, dirnames, filenames in os.walk(d): - for f in filenames: - fp = os.path.join(dirpath, f) - if not os.path.islink(fp): - d_size += os.path.getsize(fp) - total, used, free = shutil.disk_usage(d) - max_available = (free + d_size - 10000000000) / 1000000000 - return max_available - - def getDirectorySize(self, d): - total_size = 0 - for dirpath, dirnames, filenames in os.walk(d): - for f in filenames: - fp = os.path.join(dirpath, f) - if not os.path.islink(fp): - total_size += os.path.getsize(fp) - - return total_size - + self.general.btnCloseAll.setText("Start All Cameras") diff --git a/onvif-gui/gui/panels/videopanel.py b/onvif-gui/gui/panels/videopanel.py index 6f75aa56..5c3adb55 100644 --- a/onvif-gui/gui/panels/videopanel.py +++ b/onvif-gui/gui/panels/videopanel.py @@ -21,7 +21,7 @@ from PyQt6.QtWidgets import QGridLayout, QWidget, QCheckBox, \ QLabel, QComboBox, QVBoxLayout from PyQt6.QtCore import Qt -from gui.onvif.datastructures import MediaSource +from gui.enums import MediaSource class VideoPanel(QWidget): def __init__(self, mw): diff --git a/onvif-gui/gui/player.py b/onvif-gui/gui/player.py new file mode 100644 index 00000000..5b67f150 --- /dev/null +++ b/onvif-gui/gui/player.py @@ -0,0 +1,231 @@ +import os +from time import sleep +from datetime import datetime +from pathlib import Path +from PyQt6.QtCore import pyqtSignal, QObject, QTimer, QFile +from collections import deque +import shutil +import avio +from loguru import logger + +class PlayerSignals(QObject): + start = pyqtSignal() + stop = pyqtSignal() + +class Player(avio.Player): + def __init__(self, uri, mw): + super().__init__(uri) + self.mw = mw + self.signals = PlayerSignals() + self.image = None + self.rendering = False + self.desired_aspect = 0 + self.systemTabSettings = None + self.analyze_video = False + self.analyze_audio = False + self.videoModelSettings = None + self.audioModelSettings = None + self.detection_count = deque() + self.last_image = None + self.last_render = None + self.timer = None + + self.boxes = [] + self.labels = [] + self.scores = [] + + self.save_image_filename = None + self.pipe_output_start_time = None + self.estimated_file_size = 0 + self.packet_drop_frame_counter = 0 + self.last_msg = "" + + self.alarm_state = 0 + self.last_alarm_state = 0 + self.file_progress = 0.0 + + if (len(uri)): + self.timer = QTimer() + self.timer.setInterval(self.mw.settingsPanel.alarm.spnLagTime.value() * 1000) + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.timeout) + self.signals.start.connect(self.timer.start) + self.signals.stop.connect(self.timer.stop) + + def requestShutdown(self): + self.setAlarmState(0) + self.analyze_video = False + self.analyze_audio = False + self.request_reconnect = False + self.running = False + + def setAlarmState(self, state): + self.alarm_state = int(state) + + record_enable = self.systemTabSettings.record_enable if self.systemTabSettings else False + record_alarm = self.systemTabSettings.record_alarm if self.systemTabSettings else False + if camera := self.mw.cameraPanel.getCamera(self.uri): + manual_recording = camera.manual_recording if camera else False + profile = camera.getRecordProfile() if camera else None + player = self.mw.pm.getPlayer(profile.uri()) if profile else None + + if state: + self.signals.start.emit() + if record_enable and record_alarm: + if player: + if not player.isRecording(): + d = self.mw.settingsPanel.storage.dirArchive.txtDirectory.text() + if self.mw.settingsPanel.storage.chkManageDiskUsage.isChecked(): + player.manageDirectory(d) + filename = player.getPipeOutFilename(d) + if filename: + player.toggleRecording(filename) + if current_camera := self.mw.cameraPanel.getCurrentCamera(): + if camera.serial_number() == current_camera.serial_number(): + self.mw.cameraPanel.syncGUI() + else: + self.signals.stop.emit() + if record_alarm and not manual_recording: + if player: + if player.isRecording(): + player.toggleRecording("") + if current_camera := self.mw.cameraPanel.getCurrentCamera(): + if camera.serial_number() == current_camera.serial_number(): + self.mw.cameraPanel.syncGUI() + + def timeout(self): + self.setAlarmState(0) + + def getPipeOutFilename(self, d): + filename = None + camera = self.mw.cameraPanel.getCamera(self.uri) + if camera: + d = self.mw.settingsPanel.storage.dirArchive.txtDirectory.text() + root = d + "/" + camera.text() + Path(root).mkdir(parents=True, exist_ok=True) + self.pipe_output_start_time = datetime.now() + filename = '{0:%Y%m%d%H%M%S}'.format(self.pipe_output_start_time) + filename = root + "/" + filename + ".mp4" + self.setMetaData("title", camera.text()) + return filename + + def estimateFileSize(self): + # duration is in seconds, cameras report bitrate in kbps, result in bytes + result = 0 + bitrate = 0 + profile = self.mw.cameraPanel.getProfile(self.uri) + if profile: + audio_bitrate = min(profile.audio_bitrate(), 128) + video_bitrate = min(profile.bitrate(), 16384) + bitrate = video_bitrate + audio_bitrate + result = (bitrate * 1000 / 8) * self.mw.STD_FILE_DURATION + self.estimated_file_size = result + return result + + def getCommittedSize(self): + committed = 0 + for player in self.mw.pm.players: + if player.isRecording(): + committed += player.estimateFileSize() - player.pipeBytesWritten() + return committed + + def getDirectorySize(self, d): + total_size = 0 + for dirpath, dirnames, filenames in os.walk(d): + for f in filenames: + fp = os.path.join(dirpath, f) + if not os.path.islink(fp): + try: + total_size += os.path.getsize(fp) + except FileNotFoundError: + pass + + dir_size = "{:.2f}".format(total_size / 1000000000) + self.mw.settingsPanel.storage.grpDiskUsage.setTitle(f'Disk Usage (currently {dir_size} GB)') + return total_size + + def getOldestFile(self, d): + oldest_file = None + oldest_time = None + for dirpath, dirnames, filenames in os.walk(d): + for f in filenames: + fp = os.path.join(dirpath, f) + if not os.path.islink(fp): + + stem = Path(fp).stem + if len(stem) == 14 and stem.isnumeric(): + try: + if oldest_file is None: + oldest_file = fp + oldest_time = os.path.getmtime(fp) + else: + file_time = os.path.getmtime(fp) + if file_time < oldest_time: + oldest_file = fp + oldest_time = file_time + except FileNotFoundError: + pass + return oldest_file + + def getMaximumDirectorySize(self, d): + estimated_file_size = self.estimateFileSize() + space_committed = self.getCommittedSize() + allowed_space = min(self.mw.settingsPanel.storage.spnDiskLimit.value() * 1000000000, shutil.disk_usage(d)[2]) + return allowed_space - (space_committed + estimated_file_size) + + def manageDirectory(self, d): + while self.getDirectorySize(d) > self.getMaximumDirectorySize(d): + oldest_file = self.getOldestFile(d) + if oldest_file: + QFile.remove(oldest_file) + #logger.debug(f'File has been deleted by auto process: {oldest_file}') + else: + logger.debug("Unable to find the oldest file for deletion during disk management") + break + + def handleAlarm(self, state): + if self.analyze_video or self.analyze_audio: + if state: + self.setAlarmState(1) + if self.alarm_state: + if self.isCameraStream(): + if self.systemTabSettings.sound_alarm_enable: + filename = f'{self.mw.getLocation()}/gui/resources/{self.mw.settingsPanel.alarm.cmbSoundFiles.currentText()}' + if self.systemTabSettings.sound_alarm_once: + if self.alarm_state != self.last_alarm_state: + self.mw.playMedia(filename, True) + if self.systemTabSettings.sound_alarm_loop: + p = self.mw.pm.getPlayer(filename) + if not p: + self.mw.playMedia(filename, True) + self.last_alarm_state = self.alarm_state + else: + self.setAlarmState(0) + + def getFrameRate(self): + frame_rate = self.getVideoFrameRate() + if frame_rate <= 0: + profile = self.mw.cameraPanel.getProfile(self.uri) + if profile: + frame_rate = profile.frame_rate() + return frame_rate + + def processModelOutput(self): + + while self.rendering: + sleep(0.001) + + sum = 0 + + if len(self.detection_count) > self.videoModelSettings.sampleSize - 1 and len(self.detection_count): + self.detection_count.popleft() + + if len(self.boxes): + self.detection_count.append(1) + else: + self.detection_count.append(0) + + for count in self.detection_count: + sum += count + + return sum diff --git a/onvif-gui/modules/audio/sample.py b/onvif-gui/modules/audio/sample.py index aeb2a3ed..6e270c57 100644 --- a/onvif-gui/modules/audio/sample.py +++ b/onvif-gui/modules/audio/sample.py @@ -27,7 +27,7 @@ from PyQt6.QtGui import QPainter, QColorConstants, QColor from PyQt6.QtCore import QPointF, Qt, QRectF from gui.components import WarningBar, Indicator -from gui.onvif.datastructures import MediaSource +from gui.enums import MediaSource MODULE_NAME = "sample" diff --git a/onvif-gui/modules/video/motion.py b/onvif-gui/modules/video/motion.py index cff9b4dd..87f90612 100644 --- a/onvif-gui/modules/video/motion.py +++ b/onvif-gui/modules/video/motion.py @@ -25,7 +25,7 @@ from PyQt6.QtCore import Qt from loguru import logger from gui.components import WarningBar, Indicator -from gui.onvif.datastructures import MediaSource +from gui.enums import MediaSource MODULE_NAME = "motion" diff --git a/onvif-gui/modules/video/yolox.py b/onvif-gui/modules/video/yolox.py index 454a604d..b0eba38a 100644 --- a/onvif-gui/modules/video/yolox.py +++ b/onvif-gui/modules/video/yolox.py @@ -27,7 +27,7 @@ import numpy as np from pathlib import Path from gui.components import ComboSelector, FileSelector, ThresholdSlider, TargetSelector - from gui.onvif.datastructures import MediaSource + from gui.enums import MediaSource from PyQt6.QtWidgets import QWidget, QGridLayout, QLabel, QCheckBox, QMessageBox, \ QGroupBox, QDialog, QSpinBox from PyQt6.QtCore import Qt, QSize, QObject, pyqtSignal @@ -499,20 +499,25 @@ def postprocess(self, outputs, player): alarmState = result >= player.videoModelSettings.limit if result else False player.handleAlarm(alarmState) + show_alarm = False if camera := self.mw.cameraPanel.getCamera(player.uri): if camera.isFocus(): + show_alarm = True + if not player.isCameraStream(): + show_alarm = True + + if show_alarm: + level = 0 + if player.videoModelSettings.limit: + level = result / player.videoModelSettings.limit + else: + if result: + level = 1.0 - level = 0 - if player.videoModelSettings.limit: - level = result / player.videoModelSettings.limit - else: - if result: - level = 1.0 - - self.mw.videoConfigure.selTargets.barLevel.setLevel(level) + self.mw.videoConfigure.selTargets.barLevel.setLevel(level) - if alarmState: - self.mw.videoConfigure.selTargets.indAlarm.setState(1) + if alarmState: + self.mw.videoConfigure.selTargets.indAlarm.setState(1) def callback(self, infer_request, player): try: diff --git a/onvif-gui/pyproject.toml b/onvif-gui/pyproject.toml index c0fcb855..1e0e4ccd 100644 --- a/onvif-gui/pyproject.toml +++ b/onvif-gui/pyproject.toml @@ -19,7 +19,7 @@ [project] name = "onvif-gui" -version = "2.1.1" +version = "2.2.4" dynamic = ["gui-scripts"] description = "A client gui for Onvif" readme = "README.md" @@ -38,11 +38,8 @@ classifiers = [ ] dependencies = [ - 'libonvif==3.2.0', 'avio==3.2.0', 'numpy', 'loguru', 'opencv-python', - 'PyQt6-Qt6==6.6.1; platform_system != "Darwin"', - 'pyqt6==6.6.1; platform_system != "Darwin"', - 'PyQt6-Qt6; platform_system == "Darwin"', - 'pyqt6; platform_system == "Darwin"' + 'libonvif==3.2.1', 'avio==3.2.1', 'liblivemedia==1.0.1', 'numpy', 'loguru', + 'opencv-python', 'PyQt6-Qt6', 'pyqt6' ] [project.urls] diff --git a/onvif-gui/setup.py b/onvif-gui/setup.py index 0b2fe086..03d12ef0 100644 --- a/onvif-gui/setup.py +++ b/onvif-gui/setup.py @@ -24,7 +24,7 @@ setup( name="onvif-gui", - version="2.1.1", + version="2.2.4", author="Stephen Rhodes", author_email="sr99622@gmail.com", description="GUI program for onvif",