// // ZoneMinder Event Class Implementation, $Date$, $Revision$ // Copyright (C) 2001-2008 Philip Coombes // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // #include #include #include #include #include #include #include #include #include #include #include "zm.h" #include "zm_db.h" #include "zm_time.h" #include "zm_mpeg.h" #include "zm_signal.h" #include "zm_event.h" #include "zm_monitor.h" // sendfile tricks extern "C" { #include "zm_sendfile.h" } #include "zmf.h" #if HAVE_SYS_SENDFILE_H #include #endif //#define USE_PREPARED_SQL 1 bool Event::initialised = false; char Event::capture_file_format[PATH_MAX]; char Event::analyse_file_format[PATH_MAX]; char Event::general_file_format[PATH_MAX]; char Event::video_file_format[PATH_MAX]; int Event::pre_alarm_count = 0; Event::PreAlarmData Event::pre_alarm_data[MAX_PRE_ALARM_FRAMES] = { { 0 } }; Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap, bool p_videoEvent ) : monitor( p_monitor ), start_time( p_start_time ), cause( p_cause ), noteSetMap( p_noteSetMap ), videoEvent( p_videoEvent ), videowriter( NULL ) { if ( !initialised ) Initialise(); std::string notes; createNotes( notes ); bool untimedEvent = false; if ( !start_time.tv_sec ) { untimedEvent = true; gettimeofday( &start_time, 0 ); } Storage * storage = monitor->getStorage(); static char sql[ZM_SQL_MED_BUFSIZ]; struct tm *stime = localtime( &start_time.tv_sec ); snprintf( sql, sizeof(sql), "insert into Events ( MonitorId, StorageId, Name, StartTime, Width, Height, Cause, Notes, Orientation, Videoed ) values ( %d, %d, 'New Event', from_unixtime( %ld ), %d, %d, '%s', '%s', %d, %d )", monitor->Id(), storage->Id(), start_time.tv_sec, monitor->Width(), monitor->Height(), cause.c_str(), notes.c_str(), monitor->getOrientation(), videoEvent ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't insert event: %s. sql was (%s)", mysql_error( &dbconn ), sql ); exit( mysql_errno( &dbconn ) ); } id = mysql_insert_id( &dbconn ); if ( untimedEvent ) { Warning( "Event %d has zero time, setting to current", id ); } end_time.tv_sec = 0; frames = 0; alarm_frames = 0; tot_score = 0; max_score = 0; char id_file[PATH_MAX]; struct stat statbuf; if ( config.use_deep_storage ) { char *path_ptr = path; path_ptr += snprintf( path_ptr, sizeof(path), "%s/%d", storage->Path(), monitor->Id() ); int dt_parts[6]; dt_parts[0] = stime->tm_year-100; dt_parts[1] = stime->tm_mon+1; dt_parts[2] = stime->tm_mday; dt_parts[3] = stime->tm_hour; dt_parts[4] = stime->tm_min; dt_parts[5] = stime->tm_sec; char date_path[PATH_MAX] = ""; char time_path[PATH_MAX] = ""; char *time_path_ptr = time_path; for ( unsigned int i = 0; i < sizeof(dt_parts)/sizeof(*dt_parts); i++ ) { path_ptr += snprintf( path_ptr, sizeof(path)-(path_ptr-path), "/%02d", dt_parts[i] ); errno = 0; // Do we really need to stat it? Perhaps we could do that on error, instead if ( stat( path, &statbuf ) ) { if ( errno == ENOENT || errno == ENOTDIR ) { if ( mkdir( path, 0755 ) ) { // FIXME This should not be fatal. Should probably move to a different storage area. Fatal( "Can't mkdir %s: %s", path, strerror(errno)); } } else { Warning( "Error stat'ing %s, may be fatal. error is %s", path, strerror(errno)); } } if ( i == 2 ) strncpy( date_path, path, sizeof(date_path) ); else if ( i >= 3 ) time_path_ptr += snprintf( time_path_ptr, sizeof(time_path)-(time_path_ptr-time_path), "%s%02d", i>3?"/":"", dt_parts[i] ); } // Create event id symlink snprintf( id_file, sizeof(id_file), "%s/.%d", date_path, id ); if ( symlink( time_path, id_file ) < 0 ) Fatal( "Can't symlink %s -> %s: %s", id_file, path, strerror(errno)); // Create empty id tag file } else { snprintf( path, sizeof(path), "%s/%d/%d", storage->Path(), monitor->Id(), id ); errno = 0; stat( path, &statbuf ); if ( errno == ENOENT || errno == ENOTDIR ) { if ( mkdir( path, 0755 ) ) { Error( "Can't mkdir %s: %s", path, strerror(errno)); } } } // deep storage or not // Create empty id tag file snprintf( id_file, sizeof(id_file), "%s/.%d", path, id ); if ( FILE *id_fp = fopen( id_file, "w" ) ) fclose( id_fp ); else Fatal( "Can't fopen %s: %s", id_file, strerror(errno)); last_db_frame = 0; video_name[0] = 0; /* Save as video */ if ( monitor->GetOptVideoWriter() != 0 ) { int nRet; snprintf( video_name, sizeof(video_name), "%d-%s", id, "video.mp4" ); snprintf( video_file, sizeof(video_file), video_file_format, path, video_name ); /* X264 MP4 video writer */ if(monitor->GetOptVideoWriter() == 1) { #if ZM_HAVE_VIDEOWRITER_X264MP4 videowriter = new X264MP4Writer(video_file, monitor->Width(), monitor->Height(), monitor->Colours(), monitor->SubpixelOrder(), monitor->GetOptEncoderParams()); #else videowriter = NULL; Error("ZoneMinder was not compiled with the X264 MP4 video writer, check dependencies (x264 and mp4v2)"); #endif } if(videowriter != NULL) { /* Open the video stream */ nRet = videowriter->Open(); if(nRet != 0) { Error("Failed opening video stream"); delete videowriter; videowriter = NULL; } snprintf( timecodes_name, sizeof(timecodes_name), "%d-%s", id, "video.timecodes" ); snprintf( timecodes_file, sizeof(timecodes_file), video_file_format, path, timecodes_name ); /* Create timecodes file */ timecodes_fd = fopen(timecodes_file, "wb"); if(timecodes_fd == NULL) { Error("Failed creating timecodes file"); } } } else { /* No video object */ videowriter = NULL; } } Event::~Event() { static char sql[ZM_SQL_MED_BUFSIZ]; struct DeltaTimeval delta_time; DELTA_TIMEVAL( delta_time, end_time, start_time, DT_PREC_2 ); if ( frames > last_db_frame ) { Debug( 1, "Adding closing frame %d to DB", frames ); snprintf( sql, sizeof(sql), "insert into Frames ( EventId, FrameId, TimeStamp, Delta ) values ( %d, %d, from_unixtime( %ld ), %s%ld.%02ld )", id, frames, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't insert frame: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } } /* Close the video file */ if ( videowriter != NULL ) { int nRet; nRet = videowriter->Close(); if(nRet != 0) { Error("Failed closing video stream"); } delete videowriter; videowriter = NULL; /* Close the timecodes file */ fclose(timecodes_fd); timecodes_fd = NULL; } snprintf( sql, sizeof(sql), "update Events set Name='%s%d', EndTime = from_unixtime( %ld ), Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d, DefaultVideo = '%s' where Id = %d", monitor->EventPrefix(), id, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, video_name, id ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't update event: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } } void Event::createNotes( std::string ¬es ) { notes.clear(); for ( StringSetMap::const_iterator mapIter = noteSetMap.begin(); mapIter != noteSetMap.end(); mapIter++ ) { notes += mapIter->first; notes += ": "; const StringSet &stringSet = mapIter->second; for ( StringSet::const_iterator setIter = stringSet.begin(); setIter != stringSet.end(); setIter++ ) { if ( setIter != stringSet.begin() ) notes += ", "; notes += *setIter; } } } int Event::sd = -1; bool Event::OpenFrameSocket( int monitor_id ) { if ( sd > 0 ) { close( sd ); } sd = socket( AF_UNIX, SOCK_STREAM, 0); if ( sd < 0 ) { Error( "Can't create socket: %s", strerror(errno) ); return( false ); } int socket_buffer_size = config.frame_socket_size; if ( socket_buffer_size > 0 ) { if ( setsockopt( sd, SOL_SOCKET, SO_SNDBUF, &socket_buffer_size, sizeof(socket_buffer_size) ) < 0 ) { Error( "Can't get socket buffer size to %d, error = %s", socket_buffer_size, strerror(errno) ); close( sd ); sd = -1; return( false ); } } int flags; if ( (flags = fcntl( sd, F_GETFL )) < 0 ) { Error( "Can't get socket flags, error = %s", strerror(errno) ); close( sd ); sd = -1; return( false ); } flags |= O_NONBLOCK; if ( fcntl( sd, F_SETFL, flags ) < 0 ) { Error( "Can't set socket flags, error = %s", strerror(errno) ); close( sd ); sd = -1; return( false ); } char sock_path[PATH_MAX] = ""; snprintf( sock_path, sizeof(sock_path), "%s/zmf-%d.sock", config.path_socks, monitor_id ); struct sockaddr_un addr; strncpy( addr.sun_path, sock_path, sizeof(addr.sun_path) ); addr.sun_family = AF_UNIX; if ( connect( sd, (struct sockaddr *)&addr, strlen(addr.sun_path)+sizeof(addr.sun_family)+1) < 0 ) { Warning( "Can't connect to frame server: %s", strerror(errno) ); close( sd ); sd = -1; return( false ); } Debug( 1, "Opened connection to frame server" ); return( true ); } bool Event::ValidateFrameSocket( int monitor_id ) { if ( sd < 0 ) { return( OpenFrameSocket( monitor_id ) ); } return( true ); } bool Event::SendFrameImage( const Image *image, bool alarm_frame ) { if ( !ValidateFrameSocket( monitor->Id() ) ) { return( false ); } static int jpg_buffer_size = 0; static unsigned char jpg_buffer[ZM_MAX_IMAGE_SIZE]; image->EncodeJpeg( jpg_buffer, &jpg_buffer_size, (alarm_frame&&(config.jpeg_alarm_file_quality>config.jpeg_file_quality))?config.jpeg_alarm_file_quality:config.jpeg_file_quality ); static FrameHeader frame_header; frame_header.event_id = id; if ( config.use_deep_storage ) frame_header.event_time = start_time.tv_sec; frame_header.frame_id = frames; frame_header.alarm_frame = alarm_frame; frame_header.image_length = jpg_buffer_size; struct iovec iovecs[2]; iovecs[0].iov_base = &frame_header; iovecs[0].iov_len = sizeof(frame_header); iovecs[1].iov_base = jpg_buffer; iovecs[1].iov_len = jpg_buffer_size; ssize_t writev_size = sizeof(frame_header)+jpg_buffer_size; ssize_t writev_result = writev( sd, iovecs, sizeof(iovecs)/sizeof(*iovecs)); if ( writev_result != writev_size ) { if ( writev_result < 0 ) { if ( errno == EAGAIN ) { Warning( "Blocking write detected" ); } else { Error( "Can't write frame: %s", strerror(errno) ); close( sd ); sd = -1; } } else { Error( "Incomplete frame write: %zd of %zd bytes written", writev_result, writev_size ); close( sd ); sd = -1; } return( false ); } Debug( 1, "Wrote frame image, %d bytes", jpg_buffer_size ); return( true ); } bool Event::WriteFrameImage( Image *image, struct timeval timestamp, const char *event_file, bool alarm_frame ) { Image* ImgToWrite; Image* ts_image = NULL; if ( config.timestamp_on_capture ) // stash the image we plan to use in another pointer regardless if timestamped. { ts_image = new Image(*image); monitor->TimestampImage( ts_image, ×tamp ); ImgToWrite=ts_image; } else ImgToWrite=image; if ( !config.opt_frame_server || !SendFrameImage(ImgToWrite, alarm_frame) ) { int thisquality = ( alarm_frame && (config.jpeg_alarm_file_quality > config.jpeg_file_quality) ) ? config.jpeg_alarm_file_quality : 0 ; // quality to use, zero is default ImgToWrite->WriteJpeg( event_file, thisquality, (monitor->Exif() ? timestamp : (timeval){0,0}) ); // exif is only timestamp at present this switches on or off for write } if(ts_image) delete(ts_image); // clean up if used. return( true ); } bool Event::WriteFrameVideo( const Image *image, const struct timeval timestamp, VideoWriter* videow ) { const Image* frameimg = image; Image ts_image; /* Checking for invalid parameters */ if ( videow == NULL ) { Error("NULL Video object"); return false; } /* If the image does not contain a timestamp, add the timestamp */ if (!config.timestamp_on_capture) { ts_image = *image; monitor->TimestampImage( &ts_image, ×tamp ); frameimg = &ts_image; } /* Calculate delta time */ struct DeltaTimeval delta_time3; DELTA_TIMEVAL( delta_time3, timestamp, start_time, DT_PREC_3 ); unsigned int timeMS = (delta_time3.sec * delta_time3.prec) + delta_time3.fsec; /* Encode and write the frame */ if(videowriter->Encode(frameimg, timeMS) != 0) { Error("Failed encoding video frame"); } /* Add the frame to the timecodes file */ fprintf(timecodes_fd, "%u\n", timeMS); return( true ); } void Event::updateNotes( const StringSetMap &newNoteSetMap ) { bool update = false; //Info( "Checking notes, %d <> %d", noteSetMap.size(), newNoteSetMap.size() ); if ( newNoteSetMap.size() > 0 ) { if ( noteSetMap.size() == 0 ) { noteSetMap = newNoteSetMap; update = true; } else { for ( StringSetMap::const_iterator newNoteSetMapIter = newNoteSetMap.begin(); newNoteSetMapIter != newNoteSetMap.end(); newNoteSetMapIter++ ) { const std::string &newNoteGroup = newNoteSetMapIter->first; const StringSet &newNoteSet = newNoteSetMapIter->second; //Info( "Got %d new strings", newNoteSet.size() ); if ( newNoteSet.size() > 0 ) { StringSetMap::iterator noteSetMapIter = noteSetMap.find( newNoteGroup ); if ( noteSetMapIter == noteSetMap.end() ) { //Info( "Can't find note group %s, copying %d strings", newNoteGroup.c_str(), newNoteSet.size() ); noteSetMap.insert( StringSetMap::value_type( newNoteGroup, newNoteSet ) ); update = true; } else { StringSet ¬eSet = noteSetMapIter->second; //Info( "Found note group %s, got %d strings", newNoteGroup.c_str(), newNoteSet.size() ); for ( StringSet::const_iterator newNoteSetIter = newNoteSet.begin(); newNoteSetIter != newNoteSet.end(); newNoteSetIter++ ) { const std::string &newNote = *newNoteSetIter; StringSet::iterator noteSetIter = noteSet.find( newNote ); if ( noteSetIter == noteSet.end() ) { noteSet.insert( newNote ); update = true; } } } } } // end for } // end if ( noteSetMap.size() == 0 } // end if newNoteSetupMap.size() > 0 if ( update ) { std::string notes; createNotes( notes ); Debug( 2, "Updating notes for event %d, '%s'", id, notes.c_str() ); static char sql[ZM_SQL_MED_BUFSIZ]; #if USE_PREPARED_SQL static MYSQL_STMT *stmt = 0; char notesStr[ZM_SQL_MED_BUFSIZ] = ""; unsigned long notesLen = 0; if ( !stmt ) { const char *sql = "update Events set Notes = ? where Id = ?"; stmt = mysql_stmt_init( &dbconn ); if ( mysql_stmt_prepare( stmt, sql, strlen(sql) ) ) { Fatal( "Unable to prepare sql '%s': %s", sql, mysql_stmt_error(stmt) ); } /* Get the parameter count from the statement */ if ( mysql_stmt_param_count( stmt ) != 2 ) { Fatal( "Unexpected parameter count %ld in sql '%s'", mysql_stmt_param_count( stmt ), sql ); } MYSQL_BIND bind[2]; memset(bind, 0, sizeof(bind)); /* STRING PARAM */ bind[0].buffer_type = MYSQL_TYPE_STRING; bind[0].buffer = (char *)notesStr; bind[0].buffer_length = sizeof(notesStr); bind[0].is_null = 0; bind[0].length = ¬esLen; bind[1].buffer_type= MYSQL_TYPE_LONG; bind[1].buffer= (char *)&id; bind[1].is_null= 0; bind[1].length= 0; /* Bind the buffers */ if ( mysql_stmt_bind_param( stmt, bind ) ) { Fatal( "Unable to bind sql '%s': %s", sql, mysql_stmt_error(stmt) ); } } strncpy( notesStr, notes.c_str(), sizeof(notesStr) ); notesLen = notes.length(); if ( mysql_stmt_execute( stmt ) ) { Fatal( "Unable to execute sql '%s': %s", sql, mysql_stmt_error(stmt) ); } #else static char escapedNotes[ZM_SQL_MED_BUFSIZ]; mysql_real_escape_string( &dbconn, escapedNotes, notes.c_str(), notes.length() ); snprintf( sql, sizeof(sql), "update Events set Notes = '%s' where Id = %d", escapedNotes, id ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't insert event: %s", mysql_error( &dbconn ) ); } #endif } } void Event::AddFrames( int n_frames, Image **images, struct timeval **timestamps ) { for (int i = 0; i < n_frames; i += ZM_SQL_BATCH_SIZE) { AddFramesInternal(n_frames, i, images, timestamps); } } void Event::AddFramesInternal( int n_frames, int start_frame, Image **images, struct timeval **timestamps ) { static char sql[ZM_SQL_LGE_BUFSIZ]; strncpy( sql, "insert into Frames ( EventId, FrameId, TimeStamp, Delta ) values ", sizeof(sql) ); int frameCount = 0; for ( int i = start_frame; i < n_frames && i - start_frame < ZM_SQL_BATCH_SIZE; i++ ) { if ( !timestamps[i]->tv_sec ) { Debug( 1, "Not adding pre-capture frame %d, zero timestamp", i ); continue; } frames++; static char event_file[PATH_MAX]; snprintf( event_file, sizeof(event_file), capture_file_format, path, frames ); if ( monitor->GetOptSaveJPEGs() & 4) { //If this is the first frame, we should add a thumbnail to the event directory if(frames == 10){ char snapshot_file[PATH_MAX]; snprintf( snapshot_file, sizeof(snapshot_file), "%s/snapshot.jpg", path ); WriteFrameImage( images[i], *(timestamps[i]), snapshot_file ); } } if ( monitor->GetOptSaveJPEGs() & 1) { Debug( 1, "Writing pre-capture frame %d", frames ); WriteFrameImage( images[i], *(timestamps[i]), event_file ); } if ( videowriter != NULL ) { WriteFrameVideo( images[i], *(timestamps[i]), videowriter ); } struct DeltaTimeval delta_time; DELTA_TIMEVAL( delta_time, *(timestamps[i]), start_time, DT_PREC_2 ); int sql_len = strlen(sql); snprintf( sql+sql_len, sizeof(sql)-sql_len, "( %d, %d, from_unixtime(%ld), %s%ld.%02ld ), ", id, frames, timestamps[i]->tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec ); frameCount++; } if ( frameCount ) { Debug( 1, "Adding %d/%d frames to DB", frameCount, n_frames ); *(sql+strlen(sql)-2) = '\0'; if ( mysql_query( &dbconn, sql ) ) { Error( "Can't insert frames: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } last_db_frame = frames; } else { Debug( 1, "No valid pre-capture frames to add" ); } } void Event::AddFrame( Image *image, struct timeval timestamp, int score, Image *alarm_image ) { if ( !timestamp.tv_sec ) { Debug( 1, "Not adding new frame, zero timestamp" ); return; } frames++; static char event_file[PATH_MAX]; snprintf( event_file, sizeof(event_file), capture_file_format, path, frames ); if ( monitor->GetOptSaveJPEGs() & 4) { //If this is the first frame, we should add a thumbnail to the event directory if(frames == 10){ char snapshot_file[PATH_MAX]; snprintf( snapshot_file, sizeof(snapshot_file), "%s/snapshot.jpg", path ); WriteFrameImage( image, timestamp, snapshot_file ); } } if( monitor->GetOptSaveJPEGs() & 1) { Debug( 1, "Writing capture frame %d", frames ); WriteFrameImage( image, timestamp, event_file ); } if ( videowriter != NULL ) { WriteFrameVideo( image, timestamp, videowriter ); } struct DeltaTimeval delta_time; DELTA_TIMEVAL( delta_time, timestamp, start_time, DT_PREC_2 ); const char *frame_type = score>0?"Alarm":(score<0?"Bulk":"Normal"); if ( score < 0 ) score = 0; bool db_frame = (strcmp(frame_type,"Bulk") != 0) || ((frames%config.bulk_frame_interval)==0) || !frames; if ( db_frame ) { Debug( 1, "Adding frame %d of type \"%s\" to DB", frames, frame_type ); static char sql[ZM_SQL_MED_BUFSIZ]; snprintf( sql, sizeof(sql), "insert into Frames ( EventId, FrameId, Type, TimeStamp, Delta, Score ) values ( %d, %d, '%s', from_unixtime( %ld ), %s%ld.%02ld, %d )", id, frames, frame_type, timestamp.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, score ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't insert frame: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } last_db_frame = frames; // We are writing a Bulk frame if ( !strcmp( frame_type,"Bulk") ) { snprintf( sql, sizeof(sql), "update Events set Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d where Id = %d", delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, id ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't update event: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } } } end_time = timestamp; // We are writing an Alarm frame if ( !strcmp( frame_type,"Alarm") ) { alarm_frames++; tot_score += score; if ( score > (int)max_score ) max_score = score; if ( alarm_image ) { snprintf( event_file, sizeof(event_file), analyse_file_format, path, frames ); Debug( 1, "Writing analysis frame %d", frames ); if ( monitor->GetOptSaveJPEGs() & 2) { WriteFrameImage( alarm_image, timestamp, event_file, true ); } } } /* This makes viewing the diagnostic images impossible because it keeps deleting them if ( config.record_diag_images ) { char diag_glob[PATH_MAX] = ""; snprintf( diag_glob, sizeof(diag_glob), "%s/%d/diag-*.jpg", config.dir_events, monitor->Id() ); glob_t pglob; int glob_status = glob( diag_glob, 0, 0, &pglob ); if ( glob_status != 0 ) { if ( glob_status < 0 ) { Error( "Can't glob '%s': %s", diag_glob, strerror(errno) ); } else { Debug( 1, "Can't glob '%s': %d", diag_glob, glob_status ); } } else { char new_diag_path[PATH_MAX] = ""; for ( int i = 0; i < pglob.gl_pathc; i++ ) { char *diag_path = pglob.gl_pathv[i]; char *diag_file = strstr( diag_path, "diag-" ); if ( diag_file ) { snprintf( new_diag_path, sizeof(new_diag_path), general_file_format, path, frames, diag_file ); if ( rename( diag_path, new_diag_path ) < 0 ) { Error( "Can't rename '%s' to '%s': %s", diag_path, new_diag_path, strerror(errno) ); } } } } globfree( &pglob ); } */ } bool EventStream::loadInitialEventData( int monitor_id, time_t event_time ) { static char sql[ZM_SQL_SML_BUFSIZ]; snprintf( sql, sizeof(sql), "select Id from Events where MonitorId = %d and unix_timestamp( EndTime ) > %ld order by Id asc limit 1", monitor_id, event_time ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't run query: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } MYSQL_RES *result = mysql_store_result( &dbconn ); if ( !result ) { Error( "Can't use query result: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } MYSQL_ROW dbrow = mysql_fetch_row( result ); if ( mysql_errno( &dbconn ) ) { Error( "Can't fetch row: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } int init_event_id = atoi( dbrow[0] ); mysql_free_result( result ); loadEventData( init_event_id ); if ( event_time ) { curr_stream_time = event_time; curr_frame_id = 1; if ( event_time >= event_data->start_time ) { for (unsigned int i = 0; i < event_data->frame_count; i++ ) { //Info( "eft %d > et %d", event_data->frames[i].timestamp, event_time ); if ( event_data->frames[i].timestamp >= event_time ) { curr_frame_id = i+1; Debug( 3, "Set cst:%.2f", curr_stream_time ); Debug( 3, "Set cfid:%d", curr_frame_id ); break; } } Debug( 3, "Skipping %ld frames", event_data->frame_count ); } } return( true ); } bool EventStream::loadInitialEventData( int init_event_id, unsigned int init_frame_id ) { loadEventData( init_event_id ); if ( init_frame_id ) { curr_stream_time = event_data->frames[init_frame_id-1].timestamp; curr_frame_id = init_frame_id; } else { curr_stream_time = event_data->start_time; } return( true ); } bool EventStream::loadEventData( int event_id ) { static char sql[ZM_SQL_MED_BUFSIZ]; snprintf( sql, sizeof(sql), "select M.Id, M.Name, E.StorageId, E.Frames, unix_timestamp( StartTime ) as StartTimestamp, (SELECT max(Delta)-min(Delta) FROM Frames WHERE EventId=E.Id) as Duration,E.DefaultVideo from Events as E inner join Monitors as M on E.MonitorId = M.Id where E.Id = %d", event_id ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't run query: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } MYSQL_RES *result = mysql_store_result( &dbconn ); if ( !result ) { Error( "Can't use query result: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } if ( !mysql_num_rows( result ) ) { Fatal( "Unable to load event %d, not found in DB", event_id ); } MYSQL_ROW dbrow = mysql_fetch_row( result ); if ( mysql_errno( &dbconn ) ) { Error( "Can't fetch row: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } delete event_data; event_data = new EventData; event_data->event_id = event_id; event_data->monitor_id = atoi( dbrow[0] ); event_data->storage_id = dbrow[2] ? atoi( dbrow[2] ) : 0; event_data->start_time = atoi(dbrow[4]); Storage * storage = new Storage( event_data->storage_id ); const char *storage_path = storage->Path(); if ( config.use_deep_storage ) { struct tm *event_time = localtime( &event_data->start_time ); if ( storage_path[0] == '/' ) snprintf( event_data->path, sizeof(event_data->path), "%s/%ld/%02d/%02d/%02d/%02d/%02d/%02d", storage_path, event_data->monitor_id, event_time->tm_year-100, event_time->tm_mon+1, event_time->tm_mday, event_time->tm_hour, event_time->tm_min, event_time->tm_sec ); else snprintf( event_data->path, sizeof(event_data->path), "%s/%s/%ld/%02d/%02d/%02d/%02d/%02d/%02d", staticConfig.PATH_WEB.c_str(), storage_path, event_data->monitor_id, event_time->tm_year-100, event_time->tm_mon+1, event_time->tm_mday, event_time->tm_hour, event_time->tm_min, event_time->tm_sec ); } else { if ( storage_path[0] == '/' ) snprintf( event_data->path, sizeof(event_data->path), "%s/%ld/%ld", storage_path, event_data->monitor_id, event_data->event_id ); else snprintf( event_data->path, sizeof(event_data->path), "%s/%s/%ld/%ld", staticConfig.PATH_WEB.c_str(), storage_path, event_data->monitor_id, event_data->event_id ); } event_data->frame_count = dbrow[3] == NULL ? 0 : atoi(dbrow[3]); event_data->duration = atof(dbrow[5]); strncpy( event_data->video_file, dbrow[6], sizeof( event_data->video_file )-1 ); updateFrameRate( (double)event_data->frame_count/event_data->duration ); mysql_free_result( result ); snprintf( sql, sizeof(sql), "select FrameId, unix_timestamp( `TimeStamp` ), Delta from Frames where EventId = %d order by FrameId asc", event_id ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't run query: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } result = mysql_store_result( &dbconn ); if ( !result ) { Error( "Can't use query result: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } event_data->n_frames = mysql_num_rows( result ); event_data->frames = new FrameData[event_data->frame_count]; int id, last_id = 0; time_t timestamp, last_timestamp = event_data->start_time; double delta, last_delta = 0.0; while ( ( dbrow = mysql_fetch_row( result ) ) ) { id = atoi(dbrow[0]); timestamp = atoi(dbrow[1]); delta = atof(dbrow[2]); int id_diff = id - last_id; double frame_delta = (delta-last_delta)/id_diff; if ( id_diff > 1 ) { for ( int i = last_id+1; i < id; i++ ) { event_data->frames[i-1].timestamp = (time_t)(last_timestamp + ((i-last_id)*frame_delta)); event_data->frames[i-1].offset = (time_t)(event_data->frames[i-1].timestamp-event_data->start_time); event_data->frames[i-1].delta = frame_delta; event_data->frames[i-1].in_db = false; } } event_data->frames[id-1].timestamp = timestamp; event_data->frames[id-1].offset = (time_t)(event_data->frames[id-1].timestamp-event_data->start_time); event_data->frames[id-1].delta = id>1?frame_delta:0.0; event_data->frames[id-1].in_db = true; last_id = id; last_delta = delta; last_timestamp = timestamp; } if ( mysql_errno( &dbconn ) ) { Error( "Can't fetch row: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } //for ( int i = 0; i < 250; i++ ) //{ //Info( "%d -> %d @ %f (%d)", i+1, event_data->frames[i].timestamp, event_data->frames[i].delta, event_data->frames[i].in_db ); //} mysql_free_result( result ); if ( forceEventChange || mode == MODE_ALL_GAPLESS ) { if ( replay_rate > 0 ) curr_stream_time = event_data->frames[0].timestamp; else curr_stream_time = event_data->frames[event_data->frame_count-1].timestamp; } Debug( 2, "Event:%ld, Frames:%ld, Duration: %.2f", event_data->event_id, event_data->frame_count, event_data->duration ); return( true ); } void EventStream::processCommand( const CmdMsg *msg ) { Debug( 2, "Got message, type %d, msg %d", msg->msg_type, msg->msg_data[0] ) // Check for incoming command switch( (MsgCommand)msg->msg_data[0] ) { case CMD_PAUSE : { Debug( 1, "Got PAUSE command" ); // Set paused flag paused = true; replay_rate = ZM_RATE_BASE; last_frame_sent = TV_2_FLOAT( now ); break; } case CMD_PLAY : { Debug( 1, "Got PLAY command" ); if ( paused ) { // Clear paused flag paused = false; } // If we are in single event mode and at the last frame, replay the current event if ( (mode == MODE_SINGLE) && ((unsigned int)curr_frame_id == event_data->frame_count) ) curr_frame_id = 1; replay_rate = ZM_RATE_BASE; break; } case CMD_VARPLAY : { Debug( 1, "Got VARPLAY command" ); if ( paused ) { // Clear paused flag paused = false; } replay_rate = ntohs(((unsigned char)msg->msg_data[2]<<8)|(unsigned char)msg->msg_data[1])-32768; break; } case CMD_STOP : { Debug( 1, "Got STOP command" ); // Clear paused flag paused = false; break; } case CMD_FASTFWD : { Debug( 1, "Got FAST FWD command" ); if ( paused ) { // Clear paused flag paused = false; } // Set play rate switch ( replay_rate ) { case 2 * ZM_RATE_BASE : replay_rate = 5 * ZM_RATE_BASE; break; case 5 * ZM_RATE_BASE : replay_rate = 10 * ZM_RATE_BASE; break; case 10 * ZM_RATE_BASE : replay_rate = 25 * ZM_RATE_BASE; break; case 25 * ZM_RATE_BASE : case 50 * ZM_RATE_BASE : replay_rate = 50 * ZM_RATE_BASE; break; default : replay_rate = 2 * ZM_RATE_BASE; break; } break; } case CMD_SLOWFWD : { Debug( 1, "Got SLOW FWD command" ); // Set paused flag paused = true; // Set play rate replay_rate = ZM_RATE_BASE; // Set step step = 1; break; } case CMD_SLOWREV : { Debug( 1, "Got SLOW REV command" ); // Set paused flag paused = true; // Set play rate replay_rate = ZM_RATE_BASE; // Set step step = -1; break; } case CMD_FASTREV : { Debug( 1, "Got FAST REV command" ); if ( paused ) { // Clear paused flag paused = false; } // Set play rate switch ( replay_rate ) { case -2 * ZM_RATE_BASE : replay_rate = -5 * ZM_RATE_BASE; break; case -5 * ZM_RATE_BASE : replay_rate = -10 * ZM_RATE_BASE; break; case -10 * ZM_RATE_BASE : replay_rate = -25 * ZM_RATE_BASE; break; case -25 * ZM_RATE_BASE : case -50 * ZM_RATE_BASE : replay_rate = -50 * ZM_RATE_BASE; break; default : replay_rate = -2 * ZM_RATE_BASE; break; } break; } case CMD_ZOOMIN : { x = ((unsigned char)msg->msg_data[1]<<8)|(unsigned char)msg->msg_data[2]; y = ((unsigned char)msg->msg_data[3]<<8)|(unsigned char)msg->msg_data[4]; Debug( 1, "Got ZOOM IN command, to %d,%d", x, y ); switch ( zoom ) { case 100: zoom = 150; break; case 150: zoom = 200; break; case 200: zoom = 300; break; case 300: zoom = 400; break; case 400: default : zoom = 500; break; } break; } case CMD_ZOOMOUT : { Debug( 1, "Got ZOOM OUT command" ); switch ( zoom ) { case 500: zoom = 400; break; case 400: zoom = 300; break; case 300: zoom = 200; break; case 200: zoom = 150; break; case 150: default : zoom = 100; break; } break; } case CMD_PAN : { x = ((unsigned char)msg->msg_data[1]<<8)|(unsigned char)msg->msg_data[2]; y = ((unsigned char)msg->msg_data[3]<<8)|(unsigned char)msg->msg_data[4]; Debug( 1, "Got PAN command, to %d,%d", x, y ); break; } case CMD_SCALE : { scale = ((unsigned char)msg->msg_data[1]<<8)|(unsigned char)msg->msg_data[2]; Debug( 1, "Got SCALE command, to %d", scale ); break; } case CMD_PREV : { Debug( 1, "Got PREV command" ); if ( replay_rate >= 0 ) curr_frame_id = 0; else curr_frame_id = event_data->frame_count+1; paused = false; forceEventChange = true; break; } case CMD_NEXT : { Debug( 1, "Got NEXT command" ); if ( replay_rate >= 0 ) curr_frame_id = event_data->frame_count+1; else curr_frame_id = 0; paused = false; forceEventChange = true; break; } case CMD_SEEK : { int offset = ((unsigned char)msg->msg_data[1]<<24)|((unsigned char)msg->msg_data[2]<<16)|((unsigned char)msg->msg_data[3]<<8)|(unsigned char)msg->msg_data[4]; curr_frame_id = (int)(event_data->frame_count*offset/event_data->duration); Debug( 1, "Got SEEK command, to %d (new cfid: %d)", offset, curr_frame_id ); break; } case CMD_QUERY : { Debug( 1, "Got QUERY command, sending STATUS" ); break; } case CMD_QUIT : { Info ("User initiated exit - CMD_QUIT"); break; } default : { // Do nothing, for now } } struct { int event; int progress; int rate; int zoom; bool paused; } status_data; status_data.event = event_data->event_id; status_data.progress = (int)event_data->frames[curr_frame_id-1].offset; status_data.rate = replay_rate; status_data.zoom = zoom; status_data.paused = paused; Debug( 2, "E:%d, P:%d, p:%d R:%d, Z:%d", status_data.event, status_data.paused, status_data.progress, status_data.rate, status_data.zoom ); DataMsg status_msg; status_msg.msg_type = MSG_DATA_EVENT; memcpy( &status_msg.msg_data, &status_data, sizeof(status_data) ); if ( sendto( sd, &status_msg, sizeof(status_msg), MSG_DONTWAIT, (sockaddr *)&rem_addr, sizeof(rem_addr) ) < 0 ) { //if ( errno != EAGAIN ) { Error( "Can't sendto on sd %d: %s", sd, strerror(errno) ); exit( -1 ); } } // quit after sending a status, if this was a quit request if ((MsgCommand)msg->msg_data[0]==CMD_QUIT) exit(0); updateFrameRate( (double)event_data->frame_count/event_data->duration ); } void EventStream::checkEventLoaded() { bool reload_event = false; static char sql[ZM_SQL_SML_BUFSIZ]; if ( curr_frame_id <= 0 ) { snprintf( sql, sizeof(sql), "select Id from Events where MonitorId = %ld and Id < %ld order by Id desc limit 1", event_data->monitor_id, event_data->event_id ); reload_event = true; } else if ( (unsigned int)curr_frame_id > event_data->frame_count ) { snprintf( sql, sizeof(sql), "select Id from Events where MonitorId = %ld and Id > %ld order by Id asc limit 1", event_data->monitor_id, event_data->event_id ); reload_event = true; } if ( reload_event ) { if ( forceEventChange || mode != MODE_SINGLE ) { //Info( "SQL:%s", sql ); if ( mysql_query( &dbconn, sql ) ) { Error( "Can't run query: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } MYSQL_RES *result = mysql_store_result( &dbconn ); if ( !result ) { Error( "Can't use query result: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } MYSQL_ROW dbrow = mysql_fetch_row( result ); if ( mysql_errno( &dbconn ) ) { Error( "Can't fetch row: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } if ( dbrow ) { int event_id = atoi(dbrow[0]); Debug( 1, "Loading new event %d", event_id ); loadEventData( event_id ); Debug( 2, "Current frame id = %d", curr_frame_id ); if ( replay_rate < 0 ) curr_frame_id = event_data->frame_count; else curr_frame_id = 1; Debug( 2, "New frame id = %d", curr_frame_id ); } else { if ( curr_frame_id <= 0 ) curr_frame_id = 1; else curr_frame_id = event_data->frame_count; paused = true; } mysql_free_result( result ); forceEventChange = false; } else { if ( curr_frame_id <= 0 ) curr_frame_id = 1; else curr_frame_id = event_data->frame_count; paused = true; } } } Image * EventStream::getImage( ) { Event::Initialise(); static char filepath[PATH_MAX]; Debug( 2, "EventStream::getImage path(%s) frame(%d)", event_data->path, curr_frame_id ); snprintf( filepath, sizeof(filepath), Event::capture_file_format, event_data->path, curr_frame_id ); Debug( 2, "EventStream::getImage path(%s) ", filepath, curr_frame_id ); Image *image = new Image( filepath ); return image; } bool EventStream::sendFrame( int delta_us ) { Debug( 2, "Sending frame %d", curr_frame_id ); static char filepath[PATH_MAX]; static struct stat filestat; FILE *fdj = NULL; if ( monitor->GetOptSaveJPEGs() & 1) { snprintf( filepath, sizeof(filepath), Event::capture_file_format, event_data->path, curr_frame_id ); } else if ( monitor->GetOptSaveJPEGs() & 2 ) { snprintf( filepath, sizeof(filepath), Event::analyse_file_format, event_data->path, curr_frame_id ); } else { Fatal("JPEGS not saved.zms is not capable of streaming jpegs from mp4 yet"); return false; } #if HAVE_LIBAVCODEC if ( type == STREAM_MPEG ) { Image image( filepath ); Image *send_image = prepareImage( &image ); if ( !vid_stream ) { vid_stream = new VideoStream( "pipe:", format, bitrate, effective_fps, send_image->Colours(), send_image->SubpixelOrder(), send_image->Width(), send_image->Height() ); fprintf( stdout, "Content-type: %s\r\n\r\n", vid_stream->MimeType() ); vid_stream->OpenStream(); } /* double pts = */ vid_stream->EncodeFrame( send_image->Buffer(), send_image->Size(), config.mpeg_timed_frames, delta_us*1000 ); } else #endif // HAVE_LIBAVCODEC { static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; int img_buffer_size = 0; uint8_t *img_buffer = temp_img_buffer; bool send_raw = ((scale>=ZM_SCALE_BASE)&&(zoom==ZM_SCALE_BASE)); fprintf( stdout, "--ZoneMinderFrame\r\n" ); if ( type != STREAM_JPEG ) send_raw = false; if ( send_raw ) { fdj = fopen( filepath, "rb" ); if ( !fdj ) { Error( "Can't open %s: %s", filepath, strerror(errno) ); return( false ); } #if HAVE_SENDFILE if( fstat(fileno(fdj),&filestat) < 0 ) { Error( "Failed getting information about file %s: %s", filepath, strerror(errno) ); return( false ); } #else img_buffer_size = fread( img_buffer, 1, sizeof(temp_img_buffer), fdj ); #endif } else { Image image( filepath ); Image *send_image = prepareImage( &image ); switch( type ) { case STREAM_JPEG : send_image->EncodeJpeg( img_buffer, &img_buffer_size ); break; case STREAM_ZIP : #if HAVE_ZLIB_H unsigned long zip_buffer_size; send_image->Zip( img_buffer, &zip_buffer_size ); img_buffer_size = zip_buffer_size; break; #else Error("zlib is required for zipped images. Falling back to raw image"); type = STREAM_RAW; #endif // HAVE_ZLIB_H case STREAM_RAW : img_buffer = (uint8_t*)(send_image->Buffer()); img_buffer_size = send_image->Size(); break; default: Fatal( "Unexpected frame type %d", type ); break; } } switch( type ) { case STREAM_JPEG : fprintf( stdout, "Content-Type: image/jpeg\r\n" ); break; case STREAM_RAW : fprintf( stdout, "Content-Type: image/x-rgb\r\n" ); break; case STREAM_ZIP : fprintf( stdout, "Content-Type: image/x-rgbz\r\n" ); break; default : Fatal( "Unexpected frame type %d", type ); break; } if(send_raw) { #if HAVE_SENDFILE fprintf( stdout, "Content-Length: %d\r\n\r\n", (int)filestat.st_size ); if(zm_sendfile(fileno(stdout), fileno(fdj), 0, (int)filestat.st_size) != (int)filestat.st_size) { /* sendfile() failed, use standard way instead */ img_buffer_size = fread( img_buffer, 1, sizeof(temp_img_buffer), fdj ); if ( fwrite( img_buffer, img_buffer_size, 1, stdout ) != 1 ) { Error("Unable to send raw frame %u: %s",curr_frame_id,strerror(errno)); return( false ); } } #else fprintf( stdout, "Content-Length: %d\r\n\r\n", img_buffer_size ); if ( fwrite( img_buffer, img_buffer_size, 1, stdout ) != 1 ) { Error("Unable to send raw frame %u: %s",curr_frame_id,strerror(errno)); return( false ); } #endif fclose(fdj); /* Close the file handle */ } else { fprintf( stdout, "Content-Length: %d\r\n\r\n", img_buffer_size ); if ( fwrite( img_buffer, img_buffer_size, 1, stdout ) != 1 ) { Error( "Unable to send stream frame: %s", strerror(errno) ); return( false ); } } fprintf( stdout, "\r\n\r\n" ); fflush( stdout ); } last_frame_sent = TV_2_FLOAT( now ); return( true ); } void EventStream::runStream() { Event::Initialise(); openComms(); checkInitialised(); updateFrameRate( (double)event_data->frame_count/event_data->duration ); if ( type == STREAM_JPEG ) fprintf( stdout, "Content-Type: multipart/x-mixed-replace;boundary=ZoneMinderFrame\r\n\r\n" ); if ( !event_data ) { sendTextFrame( "No event data found" ); exit( 0 ); } unsigned int delta_us = 0; while( !zm_terminate ) { gettimeofday( &now, NULL ); while(checkCommandQueue()); if ( step != 0 ) curr_frame_id += step; checkEventLoaded(); // Get current frame data FrameData *frame_data = &event_data->frames[curr_frame_id-1]; //Info( "cst:%.2f", curr_stream_time ); //Info( "cfid:%d", curr_frame_id ); //Info( "fdt:%d", frame_data->timestamp ); if ( !paused ) { bool in_event = true; double time_to_event = 0; if ( replay_rate > 0 ) { time_to_event = event_data->frames[0].timestamp - curr_stream_time; if ( time_to_event > 0 ) in_event = false; } else if ( replay_rate < 0 ) { time_to_event = curr_stream_time - event_data->frames[event_data->frame_count-1].timestamp; if ( time_to_event > 0 ) in_event = false; } if ( !in_event ) { double actual_delta_time = TV_2_FLOAT( now ) - last_frame_sent; if ( actual_delta_time > 1 ) { static char frame_text[64]; snprintf( frame_text, sizeof(frame_text), "Time to next event = %d seconds", (int)time_to_event ); if ( !sendTextFrame( frame_text ) ) zm_terminate = true; } //else //{ usleep( STREAM_PAUSE_WAIT ); //curr_stream_time += (replay_rate>0?1:-1) * ((1.0L * replay_rate * STREAM_PAUSE_WAIT)/(ZM_RATE_BASE * 1000000)); curr_stream_time += (1.0L * replay_rate * STREAM_PAUSE_WAIT)/(ZM_RATE_BASE * 1000000); //} continue; } } // Figure out if we should send this frame bool send_frame = false; if ( !paused ) { // If we are streaming and this frame is due to be sent if ( ((curr_frame_id-1)%frame_mod) == 0 ) { delta_us = (unsigned int)(frame_data->delta * 1000000); // if effective > base we should speed up frame delivery delta_us = (unsigned int)((delta_us * base_fps)/effective_fps); // but must not exceed maxfps delta_us = max(delta_us, 1000000 / maxfps); send_frame = true; } } else if ( step != 0 ) { // We are paused and are just stepping forward or backward one frame step = 0; send_frame = true; } else { // We are paused, and doing nothing double actual_delta_time = TV_2_FLOAT( now ) - last_frame_sent; if ( actual_delta_time > MAX_STREAM_DELAY ) { // Send keepalive Debug( 2, "Sending keepalive frame" ); send_frame = true; } } if ( send_frame ) if ( !sendFrame( delta_us ) ) zm_terminate = true; curr_stream_time = frame_data->timestamp; if ( !paused ) { curr_frame_id += replay_rate>0?1:-1; if ( send_frame && type != STREAM_MPEG ) { Debug( 3, "dUs: %d", delta_us ); usleep( delta_us ); } } else { usleep( (unsigned long)((1000000 * ZM_RATE_BASE)/((base_fps?base_fps:1)*abs(replay_rate*2))) ); } } #if HAVE_LIBAVCODEC if ( type == STREAM_MPEG ) delete vid_stream; #endif // HAVE_LIBAVCODEC closeComms(); }