diff --git a/CMakeLists.txt b/CMakeLists.txt index 83e9d812b..794b41fa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,7 @@ set(ZM_MYSQL_ENGINE "InnoDB" CACHE STRING "MySQL engine to use with database, de set(ZM_NO_MMAP "OFF" CACHE BOOL "Set to ON to not use mmap shared memory. Shouldn't be enabled unless you experience problems with the shared memory. default: OFF") set(ZM_NO_FFMPEG "OFF" CACHE BOOL "Set to ON to skip ffmpeg checks and force building ZM without ffmpeg. default: OFF") set(ZM_NO_LIBVLC "OFF" CACHE BOOL "Set to ON to skip libvlc checks and force building ZM without libvlc. default: OFF") +set(ZM_NO_CURL "OFF" CACHE BOOL "Set to ON to skip cURL checks and force building ZM without cURL. default: OFF") set(ZM_NO_X10 "OFF" CACHE BOOL "Set to ON to build ZoneMinder without X10 support. default: OFF") set(ZM_PERL_SUBPREFIX "${CMAKE_INSTALL_LIBDIR}/perl5" CACHE PATH "Use a different directory for the zm perl modules. NOTE: This is a subprefix, e.g. lib will be turned into /lib, default: /perl5") set(ZM_PERL_USE_PATH "${CMAKE_INSTALL_PREFIX}/${ZM_PERL_SUBPREFIX}" CACHE PATH "Override the include path for zm perl modules. Useful if you are moving the perl modules without using the ZM_PERL_SUBPREFIX option. default: /") @@ -105,6 +106,22 @@ else(ZLIB_FOUND) set(optlibsnotfound "${optlibsnotfound} zlib") endif(ZLIB_FOUND) +# Do not check for cURL if ZM_NO_CURL is on +if(NOT ZM_NO_CURL) + # cURL + find_package(CURL) + if(CURL_FOUND) + set(HAVE_LIBCURL 1) + list(APPEND ZM_BIN_LIBS ${CURL_LIBRARIES}) + include_directories(${CURL_INCLUDE_DIRS}) + set(CMAKE_REQUIRED_INCLUDES ${CURL_INCLUDE_DIRS}) + check_include_file("curl/curl.h" HAVE_CURL_CURL_H) + set(optlibsfound "${optlibsfound} cURL") + else(CURL_FOUND) + set(optlibsnotfound "${optlibsnotfound} cURL") + endif(CURL_FOUND) +endif(NOT ZM_NO_CURL) + # jpeg find_package(JPEG) if(JPEG_FOUND) diff --git a/configure.ac b/configure.ac index 6e7451426..b32b7c12e 100644 --- a/configure.ac +++ b/configure.ac @@ -278,6 +278,7 @@ AC_CHECK_LIB(swscale,sws_scale,,,-lswscale) AC_CHECK_LIB(vlc,libvlc_new,,AC_MSG_WARN(libvlc.a may be required for streaming)) AC_CHECK_LIB(bz2,BZ2_bzCompress,,AC_MSG_WARN(zm requires libbz2.a for recent versions of ffmpeg)) AC_CHECK_LIB(z,compress,,) +AC_CHECK_LIB(curl,curl_global_init,,) # Checks for header files. AC_FUNC_ALLOCA @@ -315,6 +316,7 @@ AC_CHECK_HEADERS(sys/shm.h,,,) fi AC_CHECK_HEADERS(zlib.h,,,) AC_CHECK_HEADERS(vlc/vlc.h,,,) +AC_CHECK_HEADERS(curl/curl.h,,,) if test "$ZM_SSL_LIB" == "openssl"; then AC_CHECK_DECLS(MD5,,AC_MSG_ERROR([zm requires openssl/md5.h - use ZM_SSL_LIB option to select gnutls instead]),[#include diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 05353cb7f..c7a5c597b 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -63,7 +63,7 @@ DROP TABLE IF EXISTS `Controls`; CREATE TABLE `Controls` ( `Id` int(10) unsigned NOT NULL auto_increment, `Name` varchar(64) NOT NULL default '', - `Type` enum('Local','Remote','Ffmpeg','Libvlc') NOT NULL default 'Local', + `Type` enum('Local','Remote','Ffmpeg','Libvlc','cURL') NOT NULL default 'Local', `Protocol` varchar(64) default NULL, `CanWake` tinyint(3) unsigned NOT NULL default '0', `CanSleep` tinyint(3) unsigned NOT NULL default '0', @@ -282,7 +282,7 @@ DROP TABLE IF EXISTS `MonitorPresets`; CREATE TABLE `MonitorPresets` ( `Id` int(10) unsigned NOT NULL auto_increment, `Name` varchar(64) NOT NULL default '', - `Type` enum('Local','Remote','File','Ffmpeg','Libvlc') NOT NULL default 'Local', + `Type` enum('Local','Remote','File','Ffmpeg','Libvlc','cURL') NOT NULL default 'Local', `Device` tinytext, `Channel` tinytext, `Format` int(10) unsigned default NULL, @@ -313,7 +313,7 @@ DROP TABLE IF EXISTS `Monitors`; CREATE TABLE `Monitors` ( `Id` int(10) unsigned NOT NULL auto_increment, `Name` varchar(64) NOT NULL default '', - `Type` enum('Local','Remote','File','Ffmpeg','Libvlc') NOT NULL default 'Local', + `Type` enum('Local','Remote','File','Ffmpeg','Libvlc','cURL') NOT NULL default 'Local', `Function` enum('None','Monitor','Modect','Record','Mocord','Nodect') NOT NULL default 'Monitor', `Enabled` tinyint(3) unsigned NOT NULL default '1', `LinkedMonitors` varchar(255) NOT NULL default '', @@ -327,6 +327,8 @@ CREATE TABLE `Monitors` ( `Port` varchar(8) NOT NULL default '', `SubPath` varchar(64) NOT NULL default '', `Path` varchar(255) NOT NULL default '', + `User` varchar(64) NOT NULL default '', + `Pass` varchar(64) NOT NULL default '', `Width` smallint(5) unsigned NOT NULL default '0', `Height` smallint(5) unsigned NOT NULL default '0', `Colours` tinyint(3) unsigned NOT NULL default '1', diff --git a/db/zm_update-1.26.6.sql b/db/zm_update-1.26.6.sql index 4b667d8a8..37c77fca4 100644 --- a/db/zm_update-1.26.6.sql +++ b/db/zm_update-1.26.6.sql @@ -3,9 +3,15 @@ -- -- --- Add Libvlc monitor type +-- Add Libvlc and cURL monitor types -- -ALTER TABLE Controls modify column Type enum('Local','Remote','Ffmpeg','Libvlc') NOT NULL default 'Local'; -ALTER TABLE MonitorPresets modify column Type enum('Local','Remote','File','Ffmpeg','Libvlc') NOT NULL default 'Local'; -ALTER TABLE Monitors modify column Type enum('Local','Remote','File','Ffmpeg','Libvlc') NOT NULL default 'Local'; +ALTER TABLE Controls modify column Type enum('Local','Remote','Ffmpeg','Libvlc','cURL') NOT NULL default 'Local'; +ALTER TABLE MonitorPresets modify column Type enum('Local','Remote','File','Ffmpeg','Libvlc','cURL') NOT NULL default 'Local'; +ALTER TABLE Monitors modify column Type enum('Local','Remote','File','Ffmpeg','Libvlc','cURL') NOT NULL default 'Local'; + +-- +-- Add required fields for cURL authenication +-- +ALTER TABLE `Monitors` ADD `User` VARCHAR(32) NOT NULL AFTER `SubPath`; +ALTER TABLE `Monitors` ADD `Pass` VARCHAR(32) NOT NULL AFTER `User`; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0d895c4e2..28a07a87d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) # Group together all the source files that are used by all the binaries (zmc, zma, zmu, zms etc) -set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_camera.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_zone.cpp) +set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_camera.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_zone.cpp) # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES}) diff --git a/src/Makefile.am b/src/Makefile.am index 64c9163cb..9feaa15d5 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -26,6 +26,7 @@ zm_SOURCES = \ zm_comms.cpp \ zm_config.cpp \ zm_coord.cpp \ + zm_curl_camera.cpp \ zm.cpp \ zm_db.cpp \ zm_logger.cpp \ @@ -76,6 +77,7 @@ noinst_HEADERS = \ zm_config_defines.h \ zm_config.h \ zm_coord.h \ + zm_curl_camera.h \ zm_db.h \ zm_logger.h \ zm_event.h \ diff --git a/src/zm_camera.h b/src/zm_camera.h index aff4b8926..bddb66fc8 100644 --- a/src/zm_camera.h +++ b/src/zm_camera.h @@ -32,7 +32,7 @@ class Camera { protected: - typedef enum { LOCAL_SRC, REMOTE_SRC, FILE_SRC, FFMPEG_SRC, LIBVLC_SRC } SourceType; + typedef enum { LOCAL_SRC, REMOTE_SRC, FILE_SRC, FFMPEG_SRC, LIBVLC_SRC, CURL_SRC } SourceType; int id; SourceType type; @@ -59,6 +59,7 @@ public: bool IsFile() const { return( type == FILE_SRC ); } bool IsFfmpeg() const { return( type == FFMPEG_SRC ); } bool IsLibvlc() const { return( type == LIBVLC_SRC ); } + bool IscURL() const { return( type == CURL_SRC ); } unsigned int Width() const { return( width ); } unsigned int Height() const { return( height ); } unsigned int Colours() const { return( colours ); } diff --git a/src/zm_curl_camera.cpp b/src/zm_curl_camera.cpp new file mode 100644 index 000000000..c873c9834 --- /dev/null +++ b/src/zm_curl_camera.cpp @@ -0,0 +1,563 @@ +// +// ZoneMinder cURL Camera Class Implementation, $Date: 2009-01-16 12:18:50 +0000 (Fri, 16 Jan 2009) $, $Revision: 2713 $ +// Copyright (C) 2001-2008 Philip Coombes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "zm.h" +#include "zm_curl_camera.h" + +#if HAVE_LIBCURL + +#define CURL_MAXRETRY 5 +#define CURL_BUFFER_INITIAL_SIZE 65536 + +const char* content_length_match = "Content-Length:"; +const char* content_type_match = "Content-Type:"; +size_t content_length_match_len; +size_t content_type_match_len; + +cURLCamera::cURLCamera( int p_id, const std::string &p_path, const std::string &p_user, const std::string &p_pass, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture ) : + Camera( p_id, CURL_SRC, p_width, p_height, p_colours, ZM_SUBPIX_ORDER_DEFAULT_FOR_COLOUR(p_colours), p_brightness, p_contrast, p_hue, p_colour, p_capture ), + mPath( p_path ), mUser( p_user ), mPass ( p_pass ), bTerminate( false ), bReset( false ), mode ( MODE_UNSET ) +{ + + if ( capture ) + { + Initialise(); + } +} + +cURLCamera::~cURLCamera() +{ + if ( capture ) + { + + Terminate(); + } +} + +void cURLCamera::Initialise() +{ + content_length_match_len = strlen(content_length_match); + content_type_match_len = strlen(content_type_match); + + databuffer.expand(CURL_BUFFER_INITIAL_SIZE); + + /* cURL initialization */ + cRet = curl_global_init(CURL_GLOBAL_ALL); + if(cRet != CURLE_OK) { + Fatal("libcurl initialization failed: ", curl_easy_strerror(cRet)); + } + + Debug(2,"libcurl version: %s",curl_version()); + + /* Create the shared data mutex */ + nRet = pthread_mutex_init(&shareddata_mutex, NULL); + if(nRet != 0) { + Fatal("Shared data mutex creation failed: %s",strerror(nRet)); + } + /* Create the data available condition variable */ + nRet = pthread_cond_init(&data_available_cond, NULL); + if(nRet != 0) { + Fatal("Data available condition variable creation failed: %s",strerror(nRet)); + } + /* Create the request complete condition variable */ + nRet = pthread_cond_init(&request_complete_cond, NULL); + if(nRet != 0) { + Fatal("Request complete condition variable creation failed: %s",strerror(nRet)); + } + + /* Create the thread */ + nRet = pthread_create(&thread, NULL, thread_func_dispatcher, this); + if(nRet != 0) { + Fatal("Thread creation failed: %s",strerror(nRet)); + } +} + +void cURLCamera::Terminate() +{ + /* Signal the thread to terminate */ + bTerminate = true; + + /* Wait for thread termination */ + pthread_join(thread, NULL); + + /* Destroy condition variables */ + pthread_cond_destroy(&request_complete_cond); + pthread_cond_destroy(&data_available_cond); + + /* Destroy mutex */ + pthread_mutex_destroy(&shareddata_mutex); + + /* cURL cleanup */ + curl_global_cleanup(); + +} + +int cURLCamera::PrimeCapture() +{ + //Info( "Priming capture from %s", mPath.c_str() ); + return 0; +} + +int cURLCamera::PreCapture() +{ + // Nothing to do here + return( 0 ); +} + +int cURLCamera::Capture( Image &image ) +{ + bool frameComplete = false; + uint8_t* directbuffer; + + /* MODE_STREAM specific variables */ + bool SubHeadersParsingComplete = false; + unsigned int frame_content_length = 0; + std::string frame_content_type; + bool need_more_data = false; + + /* Request a writeable buffer of the target image */ + directbuffer = image.WriteBuffer(width, height, colours, subpixelorder); + if(directbuffer == NULL) { + Error("Failed requesting writeable buffer for the captured image"); + return (-1); + } + + /* Grab the mutex to ensure exclusive access to the shared data */ + lock(); + + while (!frameComplete) { + + /* If the work thread did a reset, reset our local variables */ + if(bReset) { + SubHeadersParsingComplete = false; + frame_content_length = 0; + frame_content_type.clear(); + need_more_data = false; + bReset = false; + } + + if(mode == MODE_UNSET) { + /* Don't have a mode yet. Sleep while waiting for data */ + nRet = pthread_cond_wait(&data_available_cond,&shareddata_mutex); + if(nRet != 0) { + Error("Failed waiting for available data condition variable: %s",strerror(nRet)); + return -20; + } + } + + if(mode == MODE_STREAM) { + + /* Subheader parsing */ + while(!SubHeadersParsingComplete && !need_more_data) { + + size_t crlf_start, crlf_end, crlf_size; + std::string subheader; + + /* Check if the buffer contains something */ + if(databuffer.empty()) { + /* Empty buffer, wait for data */ + need_more_data = true; + break; + } + + /* Find crlf start */ + crlf_start = memcspn(databuffer,"\r\n",databuffer.size()); + if(crlf_start == databuffer.size()) { + /* Not found, wait for more data */ + need_more_data = true; + break; + } + + /* See if we have enough data for determining crlf length */ + if(databuffer.size() < crlf_start+5) { + /* Need more data */ + need_more_data = true; + break; + } + + /* Find crlf end and calculate crlf size */ + crlf_end = memspn(((const char*)databuffer.head())+crlf_start,"\r\n",5); + crlf_size = (crlf_start + crlf_end) - crlf_start; + + /* Is this the end of a previous stream? (This is just before the boundary) */ + if(crlf_start == 0) { + databuffer.consume(crlf_size); + continue; + } + + /* Check for invalid CRLF size */ + if(crlf_size > 4) { + Error("Invalid CRLF length"); + } + + /* Check if the crlf is \n\n or \r\n\r\n (marks end of headers, this is the last header) */ + if( (crlf_size == 2 && memcmp(((const char*)databuffer.head())+crlf_start,"\n\n",2) == 0) || (crlf_size == 4 && memcmp(((const char*)databuffer.head())+crlf_start,"\r\n\r\n",4) == 0) ) { + /* This is the last header */ + SubHeadersParsingComplete = true; + } + + /* Copy the subheader, excluding the crlf */ + subheader.assign(databuffer, crlf_start); + + /* Advance the buffer past this one */ + databuffer.consume(crlf_start+crlf_size); + + Debug(7,"Got subheader: %s",subheader.c_str()); + + /* Find where the data in this header starts */ + size_t subheader_data_start = subheader.rfind(' '); + if(subheader_data_start == std::string::npos) { + subheader_data_start = subheader.find(':'); + } + + /* Extract the data into a string */ + std::string subheader_data = subheader.substr(subheader_data_start+1, std::string::npos); + + Debug(8,"Got subheader data: %s",subheader_data.c_str()); + + /* Check the header */ + if(subheader.compare(0,content_length_match_len,content_length_match,content_length_match_len) == 0) { + /* Found the content-length header */ + frame_content_length = atoi(subheader_data.c_str()); + Debug(6,"Got content-length subheader: %d",frame_content_length); + } else if(subheader.compare(0,content_type_match_len,content_type_match,content_type_match_len) == 0) { + /* Found the content-type header */ + frame_content_type = subheader_data; + Debug(6,"Got content-type subheader: %s",frame_content_type.c_str()); + } + + } + + /* Attempt to extract the frame */ + if(!need_more_data) { + if(!SubHeadersParsingComplete) { + /* We haven't parsed all headers yet */ + need_more_data = true; + } else if(frame_content_length <= 0) { + /* Invalid frame */ + Error("Invalid frame: invalid content length"); + } else if(frame_content_type != "image/jpeg") { + /* Unsupported frame type */ + Error("Unsupported frame: %s",frame_content_type.c_str()); + } else if(frame_content_length > databuffer.size()) { + /* Incomplete frame, wait for more data */ + need_more_data = true; + } else { + /* All good. decode the image */ + image.DecodeJpeg(databuffer.extract(frame_content_length), frame_content_length, colours, subpixelorder); + frameComplete = true; + } + } + + /* Attempt to get more data */ + if(need_more_data) { + nRet = pthread_cond_wait(&data_available_cond,&shareddata_mutex); + if(nRet != 0) { + Error("Failed waiting for available data condition variable: %s",strerror(nRet)); + return -18; + } + need_more_data = false; + } + + } else if(mode == MODE_SINGLE) { + /* Check if we have anything */ + if (!single_offsets.empty()) { + if( (single_offsets.front() > 0) && (databuffer.size() >= single_offsets.front()) ) { + /* Extract frame */ + image.DecodeJpeg(databuffer.extract(single_offsets.front()), single_offsets.front(), colours, subpixelorder); + single_offsets.pop_front(); + frameComplete = true; + } else { + /* This shouldn't happen */ + Error("Internal error. Attempting recovery"); + databuffer.consume(single_offsets.front()); + single_offsets.pop_front(); + } + } else { + /* Don't have a frame yet, wait for the request complete condition variable */ + nRet = pthread_cond_wait(&request_complete_cond,&shareddata_mutex); + if(nRet != 0) { + Error("Failed waiting for request complete condition variable: %s",strerror(nRet)); + return -19; + } + } + } else { + /* Failed to match content-type */ + Fatal("Unable to match Content-Type. Check URL, username and password"); + } /* mode */ + + } /* frameComplete loop */ + + /* Release the mutex */ + unlock(); + + if(!frameComplete) + return -1; + + return 0; +} + +int cURLCamera::PostCapture() +{ + // Nothing to do here + return( 0 ); +} + +size_t cURLCamera::data_callback(void *buffer, size_t size, size_t nmemb, void *userdata) +{ + lock(); + + /* Append the data we just received to our buffer */ + databuffer.append((const char*)buffer, size*nmemb); + + /* Signal data available */ + nRet = pthread_cond_signal(&data_available_cond); + if(nRet != 0) { + Error("Failed signaling data available condition variable: %s",strerror(nRet)); + return -16; + } + + unlock(); + + /* Return bytes processed */ + return size*nmemb; +} + + + +size_t cURLCamera::header_callback( void *buffer, size_t size, size_t nmemb, void *userdata) +{ + std::string header; + header.assign((const char*)buffer, size*nmemb); + + Debug(4,"Got header: %s",header.c_str()); + + /* Check Content-Type header */ + if(strncasecmp(header.c_str(),content_type_match,content_type_match_len) == 0) { + size_t pos = header.find(';'); + if(pos != std::string::npos) { + header.erase(pos, std::string::npos); + } + + pos = header.rfind(' '); + if(pos == std::string::npos) { + pos = header.find(':'); + } + + std::string content_type = header.substr(pos+1, std::string::npos); + Debug(6,"Content-Type is: %s",content_type.c_str()); + + lock(); + + const char* multipart_match = "multipart/x-mixed-replace"; + const char* image_jpeg_match = "image/jpeg"; + if(content_type.compare(0, strlen(multipart_match), multipart_match) == 0) { + Debug(7,"Content type matched as multipart/x-mixed-replace"); + mode = MODE_STREAM; + } else if(content_type.compare(0, strlen(image_jpeg_match), image_jpeg_match) == 0) { + Debug(7,"Content type matched as image/jpeg"); + mode = MODE_SINGLE; + } + + unlock(); + } + + /* Return bytes processed */ + return size*nmemb; +} + +void* cURLCamera::thread_func() +{ + int tRet; + double dSize; + + c = curl_easy_init(); + if(c == NULL) { + Fatal("Failed getting easy handle from libcurl"); + } + + /* Set URL */ + cRet = curl_easy_setopt(c, CURLOPT_URL, mPath.c_str()); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl URL: %s", curl_easy_strerror(cRet)); + + /* Header callback */ + cRet = curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, &header_callback_dispatcher); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl header callback function: %s", curl_easy_strerror(cRet)); + cRet = curl_easy_setopt(c, CURLOPT_HEADERDATA, this); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl header callback object: %s", curl_easy_strerror(cRet)); + + /* Data callback */ + cRet = curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, &data_callback_dispatcher); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl data callback function: %s", curl_easy_strerror(cRet)); + cRet = curl_easy_setopt(c, CURLOPT_WRITEDATA, this); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl data callback object: %s", curl_easy_strerror(cRet)); + + /* Progress callback */ + cRet = curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0); + if(cRet != CURLE_OK) + Fatal("Failed enabling libcurl progress callback function: %s", curl_easy_strerror(cRet)); + cRet = curl_easy_setopt(c, CURLOPT_PROGRESSFUNCTION, &progress_callback_dispatcher); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl progress callback function: %s", curl_easy_strerror(cRet)); + cRet = curl_easy_setopt(c, CURLOPT_PROGRESSDATA, this); + if(cRet != CURLE_OK) + Fatal("Failed setting libcurl progress callback object: %s", curl_easy_strerror(cRet)); + + /* Set username and password */ + if(!mUser.empty()) { + cRet = curl_easy_setopt(c, CURLOPT_USERNAME, mUser.c_str()); + if(cRet != CURLE_OK) + Error("Failed setting username: %s", curl_easy_strerror(cRet)); + } + if(!mPass.empty()) { + cRet = curl_easy_setopt(c, CURLOPT_PASSWORD, mPass.c_str()); + if(cRet != CURLE_OK) + Error("Failed setting password: %s", curl_easy_strerror(cRet)); + } + + /* Authenication preference */ + cRet = curl_easy_setopt(c, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + if(cRet != CURLE_OK) + Warning("Failed setting libcurl acceptable http authenication methods: %s", curl_easy_strerror(cRet)); + + + /* Work loop */ + for(int attempt=1;attempt<=CURL_MAXRETRY;attempt++) { + tRet = 0; + while(!bTerminate) { + /* Do the work */ + cRet = curl_easy_perform(c); + + if(mode == MODE_SINGLE) { + if(cRet != CURLE_OK) { + break; + } + /* Attempt to get the size of the file */ + cRet = curl_easy_getinfo(c, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &dSize); + if(cRet != CURLE_OK) { + break; + } + /* We need to lock for the offsets array and the condition variable */ + lock(); + /* Push the size into our offsets array */ + if(dSize > 0) { + single_offsets.push_back(dSize); + } else { + Fatal("Unable to get the size of the image"); + } + /* Signal the request complete condition variable */ + tRet = pthread_cond_signal(&request_complete_cond); + if(tRet != 0) { + Error("Failed signaling request completed condition variable: %s",strerror(tRet)); + } + /* Unlock */ + unlock(); + + } else if (mode == MODE_STREAM) { + break; + } + } + + /* Return value checking */ + if(cRet == CURLE_ABORTED_BY_CALLBACK || bTerminate) { + /* Aborted */ + break; + } else if (cRet != CURLE_OK) { + /* Some error */ + Error("cURL Request failed: %s",curl_easy_strerror(cRet)); + if(attempt < CURL_MAXRETRY) { + Error("Retrying.. Attempt %d of %d",attempt,CURL_MAXRETRY); + /* Do a reset */ + lock(); + databuffer.clear(); + single_offsets.clear(); + mode = MODE_UNSET; + bReset = true; + unlock(); + } + tRet = -50; + } + } + + /* Cleanup */ + curl_easy_cleanup(c); + c = NULL; + + return (void*)tRet; +} + +int cURLCamera::lock() { + int nRet; + + /* Lock shared data */ + nRet = pthread_mutex_lock(&shareddata_mutex); + if(nRet != 0) { + Error("Failed locking shared data mutex: %s",strerror(nRet)); + } + return nRet; +} + +int cURLCamera::unlock() { + int nRet; + + /* Unlock shared data */ + nRet = pthread_mutex_unlock(&shareddata_mutex); + if(nRet != 0) { + Error("Failed unlocking shared data mutex: %s",strerror(nRet)); + } + return nRet; +} + +int cURLCamera::progress_callback(void *userdata, double dltotal, double dlnow, double ultotal, double ulnow) +{ + /* Signal the curl thread to terminate */ + if(bTerminate) + return -10; + + return 0; +} + +/* These functions call the functions in the class for the correct object */ +size_t data_callback_dispatcher(void *buffer, size_t size, size_t nmemb, void *userdata) +{ + return ((cURLCamera*)userdata)->data_callback(buffer,size,nmemb,userdata); +} + +size_t header_callback_dispatcher(void *buffer, size_t size, size_t nmemb, void *userdata) +{ + return ((cURLCamera*)userdata)->header_callback(buffer,size,nmemb,userdata); +} + +int progress_callback_dispatcher(void *userdata, double dltotal, double dlnow, double ultotal, double ulnow) +{ + return ((cURLCamera*)userdata)->progress_callback(userdata,dltotal,dlnow,ultotal,ulnow); +} + +void* thread_func_dispatcher(void* object) { + return ((cURLCamera*)object)->thread_func(); +} + + + +#endif // HAVE_LIBCURL diff --git a/src/zm_curl_camera.h b/src/zm_curl_camera.h new file mode 100644 index 000000000..1cf413454 --- /dev/null +++ b/src/zm_curl_camera.h @@ -0,0 +1,105 @@ +// +// ZoneMinder cURL Class Interface, $Date: 2008-07-25 10:33:23 +0100 (Fri, 25 Jul 2008) $, $Revision: 2611 $ +// Copyright (C) 2001-2008 Philip Coombes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef ZM_CURL_CAMERA_H +#define ZM_CURL_CAMERA_H + +#if HAVE_LIBCURL + +#include "zm_camera.h" +#include "zm_ffmpeg.h" +#include "zm_buffer.h" +#include "zm_regexp.h" +#include "zm_utils.h" +#include "zm_signal.h" +#include +#include + +#if HAVE_CURL_CURL_H +#include +#endif + +// +// Class representing 'remote' cameras, i.e. those which are +// accessed over a network connection. +// +class cURLCamera : public Camera +{ +protected: + typedef enum {MODE_UNSET, MODE_SINGLE, MODE_STREAM} mode_t; + + std::string mPath; + std::string mUser; + std::string mPass; + + /* cURL object(s) */ + CURL* c; + + /* Shared data */ + volatile bool bTerminate; + volatile bool bReset; + volatile mode_t mode; + Buffer databuffer; + std::deque single_offsets; + + /* pthread objects */ + pthread_t thread; + pthread_mutex_t shareddata_mutex; + pthread_cond_t data_available_cond; + pthread_cond_t request_complete_cond; + +public: + cURLCamera( int p_id, const std::string &path, const std::string &username, const std::string &password, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture ); + ~cURLCamera(); + + const std::string &Path() const { return( mPath ); } + const std::string &Username() const { return( mUser ); } + const std::string &Password() const { return( mPass ); } + + void Initialise(); + void Terminate(); + + int PrimeCapture(); + int PreCapture(); + int Capture( Image &image ); + int PostCapture(); + + size_t data_callback(void *buffer, size_t size, size_t nmemb, void *userdata); + size_t header_callback(void *buffer, size_t size, size_t nmemb, void *userdata); + int progress_callback(void *userdata, double dltotal, double dlnow, double ultotal, double ulnow); + int debug_callback(CURL* handle, curl_infotype type, char* str, size_t strsize, void* data); + void* thread_func(); + int lock(); + int unlock(); + +private: + int nRet; + CURLcode cRet; + +}; + +/* Dispatchers */ +size_t header_callback_dispatcher(void *buffer, size_t size, size_t nmemb, void *userdata); +size_t data_callback_dispatcher(void *buffer, size_t size, size_t nmemb, void *userdata); +int progress_callback_dispatcher(void *userdata, double dltotal, double dlnow, double ultotal, double ulnow); +void* thread_func_dispatcher(void* object); + +#endif // HAVE_LIBCURL + +#endif // ZM_CURL_CAMERA_H diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 78def1112..02bc2b8cd 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -43,6 +43,9 @@ #if HAVE_LIBVLC #include "zm_libvlc_camera.h" #endif // HAVE_LIBVLC +#if HAVE_LIBCURL +#include "zm_curl_camera.h" +#endif // HAVE_LIBCURL #if ZM_MEM_MAPPED #include @@ -2422,7 +2425,7 @@ int Monitor::LoadFfmpegMonitors( const char *file, Monitor **&monitors, Purpose Monitor *Monitor::Load( int id, bool load_zones, Purpose purpose ) { static char sql[ZM_SQL_MED_BUFSIZ]; - snprintf( sql, sizeof(sql), "select Id, Name, Type, Function+0, Enabled, LinkedMonitors, Device, Channel, Format, Protocol, Method, Host, Port, Path, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, SignalCheckColour from Monitors where Id = %d", id ); + snprintf( sql, sizeof(sql), "select Id, Name, Type, Function+0, Enabled, LinkedMonitors, Device, Channel, Format, Protocol, Method, Host, Port, Path, User, Pass, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, SignalCheckColour from Monitors where Id = %d", id ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't run query: %s", mysql_error( &dbconn ) ); @@ -2458,6 +2461,8 @@ Monitor *Monitor::Load( int id, bool load_zones, Purpose purpose ) std::string host = dbrow[col]; col++; std::string port = dbrow[col]; col++; std::string path = dbrow[col]; col++; + std::string user = dbrow[col]; col++; + std::string pass = dbrow[col]; col++; int width = atoi(dbrow[col]); col++; int height = atoi(dbrow[col]); col++; @@ -2626,6 +2631,27 @@ Monitor *Monitor::Load( int id, bool load_zones, Purpose purpose ) #else // HAVE_LIBVLC Fatal( "You must have vlc libraries installed to use vlc cameras for monitor %d", id ); #endif // HAVE_LIBVLC + } + else if ( type == "cURL" ) + { +#if HAVE_LIBCURL + camera = new cURLCamera( + id, + path.c_str(), + user.c_str(), + pass.c_str(), + cam_width, + cam_height, + colours, + brightness, + contrast, + hue, + colour, + purpose==CAPTURE + ); +#else // HAVE_LIBCURL + Fatal( "You must have libcurl installed to use ffmpeg cameras for monitor %d", id ); +#endif // HAVE_LIBCURL } else { diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index c95a9c4c8..e351cea25 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -313,6 +313,8 @@ foreach( $displayMonitors as $monitor ) $shortpath = $domain ? $domain : preg_replace( '/^.*\//', '', $monitor['Path'] ); ?> '.$shortpath.'', canEdit( 'Monitors' ) ) ?> + + '.preg_replace( '/^.*\//', '', $monitor['Path'] ).'', canEdit( 'Monitors' ) ) ?>   diff --git a/web/skins/classic/views/controlcap.php b/web/skins/classic/views/controlcap.php index f516b42ec..eab3e15ad 100644 --- a/web/skins/classic/views/controlcap.php +++ b/web/skins/classic/views/controlcap.php @@ -345,7 +345,7 @@ switch ( $tab ) ?> $SLANG['Local'], 'Remote'=>$SLANG['Remote'], 'Ffmpeg'=>$SLANG['Ffmpeg'], 'Libvlc'=>$SLANG['Libvlc'] ); + $types = array( 'Local'=>$SLANG['Local'], 'Remote'=>$SLANG['Remote'], 'Ffmpeg'=>$SLANG['Ffmpeg'], 'Libvlc'=>$SLANG['Libvlc'], 'cURL'=>"cURL"); ?> diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 13a92dde6..b4be02abb 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -64,6 +64,8 @@ else 'Host' => "", 'Path' => "", 'Port' => "80", + 'User' => "", + 'Pass' => "", 'Colours' => 3, 'Palette' => 0, 'Width' => "320", @@ -176,6 +178,7 @@ $sourceTypes = array( 'File' => $SLANG['File'], 'Ffmpeg' => $SLANG['Ffmpeg'], 'Libvlc' => $SLANG['Libvlc'], + 'cURL' => "cURL (HTTP only)" ); if ( !ZM_HAS_V4L ) unset($sourceTypes['Local']); @@ -503,10 +506,12 @@ if ( $tab != 'source' || ($newMonitor['Type'] != 'Local' && $newMonitor['Type'] + + + + + + diff --git a/zoneminder-config.cmake b/zoneminder-config.cmake index e7e47b265..90759c1b6 100644 --- a/zoneminder-config.cmake +++ b/zoneminder-config.cmake @@ -21,6 +21,8 @@ /* Library checks and their header files */ #cmakedefine HAVE_LIBZLIB 1 #cmakedefine HAVE_ZLIB_H 1 +#cmakedefine HAVE_LIBCURL 1 +#cmakedefine HAVE_CURL_CURL_H 1 #cmakedefine HAVE_LIBJPEG 1 #cmakedefine HAVE_JPEGLIB_H 1 #cmakedefine HAVE_LIBOPENSSL 1