mirror of
https://github.com/Motion-Project/motion.git
synced 2026-01-14 09:48:55 -05:00
This commit includes: 1. Consolidate functions into appropriate modules 2. Rename modules to reflect contents (v4l2, bktr, common) 3. Combines v4l2 and bktr so they can be used together 4. Implements functional prefixes 5. Uses HAVE_V4L2, HAVE_BKTR instead of WITHOUT_ 6. Include pthread_np.h for FreeBSD
3500 lines
124 KiB
C
3500 lines
124 KiB
C
/* motion.c
|
|
*
|
|
* Detect changes in a video stream.
|
|
* Copyright 2000 by Jeroen Vreeken (pe1rxq@amsat.org)
|
|
* This software is distributed under the GNU public license version 2
|
|
* See also the file 'COPYING'.
|
|
*
|
|
*/
|
|
#include "motion.h"
|
|
#include "ffmpeg.h"
|
|
#include "video_common.h"
|
|
#include "video_loopback.h"
|
|
#include "conf.h"
|
|
#include "alg.h"
|
|
#include "track.h"
|
|
#include "event.h"
|
|
#include "picture.h"
|
|
#include "rotate.h"
|
|
|
|
#define IMAGE_BUFFER_FLUSH ((unsigned int)-1)
|
|
|
|
/**
|
|
* tls_key_threadnr
|
|
*
|
|
* TLS key for storing thread number in thread-local storage.
|
|
*/
|
|
pthread_key_t tls_key_threadnr;
|
|
|
|
/**
|
|
* global_lock
|
|
*
|
|
* Protects any global variables (like 'threads_running') during updates,
|
|
* to prevent problems with multiple threads updating at the same time.
|
|
*/
|
|
//pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
pthread_mutex_t global_lock;
|
|
|
|
/**
|
|
* cnt_list
|
|
*
|
|
* List of context structures, one for each main Motion thread.
|
|
*/
|
|
struct context **cnt_list = NULL;
|
|
|
|
/**
|
|
* threads_running
|
|
*
|
|
* Keeps track of number of Motion threads currently running. Also used
|
|
* by 'main' to know when all threads have exited.
|
|
*/
|
|
volatile int threads_running = 0;
|
|
|
|
/* Set this when we want main to end or restart */
|
|
volatile unsigned int finish = 0;
|
|
|
|
/* Log file used instead of stderr and syslog */
|
|
FILE *ptr_logfile = NULL;
|
|
|
|
/**
|
|
* restart
|
|
*
|
|
* Differentiates between a quit and a restart. When all threads have
|
|
* finished running, 'main' checks if 'restart' is true and if so starts
|
|
* up again (instead of just quitting).
|
|
*/
|
|
unsigned int restart = 0;
|
|
|
|
/**
|
|
* image_ring_resize
|
|
*
|
|
* This routine is called from motion_loop to resize the image precapture ringbuffer
|
|
* NOTE: This function clears all images in the old ring buffer
|
|
|
|
* Parameters:
|
|
*
|
|
* cnt Pointer to the motion context structure
|
|
* new_size The new size of the ring buffer
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void image_ring_resize(struct context *cnt, int new_size)
|
|
{
|
|
/*
|
|
* Only resize if :
|
|
* Not in an event and
|
|
* decreasing at last position in new buffer
|
|
* increasing at last position in old buffer
|
|
* e.g. at end of smallest buffer
|
|
*/
|
|
if (cnt->event_nr != cnt->prev_event) {
|
|
int smallest;
|
|
|
|
if (new_size < cnt->imgs.image_ring_size) /* Decreasing */
|
|
smallest = new_size;
|
|
else /* Increasing */
|
|
smallest = cnt->imgs.image_ring_size;
|
|
|
|
if (cnt->imgs.image_ring_in == smallest - 1 || smallest == 0) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Resizing pre_capture buffer to %d items",
|
|
new_size);
|
|
|
|
/* Create memory for new ring buffer */
|
|
struct image_data *tmp;
|
|
tmp = mymalloc(new_size * sizeof(struct image_data));
|
|
|
|
/*
|
|
* Copy all information from old to new
|
|
* Smallest is 0 at initial init
|
|
*/
|
|
if (smallest > 0)
|
|
memcpy(tmp, cnt->imgs.image_ring, sizeof(struct image_data) * smallest);
|
|
|
|
|
|
/* In the new buffers, allocate image memory */
|
|
{
|
|
int i;
|
|
for(i = smallest; i < new_size; i++) {
|
|
tmp[i].image = mymalloc(cnt->imgs.size);
|
|
memset(tmp[i].image, 0x80, cnt->imgs.size); /* initialize to grey */
|
|
}
|
|
}
|
|
|
|
/* Free the old ring */
|
|
free(cnt->imgs.image_ring);
|
|
|
|
/* Point to the new ring */
|
|
cnt->imgs.image_ring = tmp;
|
|
cnt->current_image = NULL;
|
|
|
|
cnt->imgs.image_ring_size = new_size;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* image_ring_destroy
|
|
*
|
|
* This routine is called when we want to free the ring
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt Pointer to the motion context structure
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void image_ring_destroy(struct context *cnt)
|
|
{
|
|
int i;
|
|
|
|
/* Exit if don't have any ring */
|
|
if (cnt->imgs.image_ring == NULL)
|
|
return;
|
|
|
|
/* Free all image buffers */
|
|
for (i = 0; i < cnt->imgs.image_ring_size; i++)
|
|
free(cnt->imgs.image_ring[i].image);
|
|
|
|
|
|
/* Free the ring */
|
|
free(cnt->imgs.image_ring);
|
|
|
|
cnt->imgs.image_ring = NULL;
|
|
cnt->current_image = NULL;
|
|
cnt->imgs.image_ring_size = 0;
|
|
}
|
|
|
|
/**
|
|
* image_save_as_preview
|
|
*
|
|
* This routine is called when we detect motion and want to save an image in the preview buffer
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt Pointer to the motion context structure
|
|
* img Pointer to the image_data structure we want to set as preview image
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void image_save_as_preview(struct context *cnt, struct image_data *img)
|
|
{
|
|
void * image;
|
|
/* Save preview image pointer */
|
|
image = cnt->imgs.preview_image.image;
|
|
/* Copy all info */
|
|
memcpy(&cnt->imgs.preview_image.image, img, sizeof(struct image_data));
|
|
/* restore image pointer */
|
|
cnt->imgs.preview_image.image = image;
|
|
|
|
/* Copy image */
|
|
memcpy(cnt->imgs.preview_image.image, img->image, cnt->imgs.size);
|
|
|
|
/*
|
|
* If we set output_all to yes and during the event
|
|
* there is no image with motion, diffs is 0, we are not going to save the preview event
|
|
*/
|
|
if (cnt->imgs.preview_image.diffs == 0)
|
|
cnt->imgs.preview_image.diffs = 1;
|
|
|
|
/* draw locate box here when mode = LOCATE_PREVIEW */
|
|
if (cnt->locate_motion_mode == LOCATE_PREVIEW) {
|
|
|
|
if (cnt->locate_motion_style == LOCATE_BOX) {
|
|
alg_draw_location(&img->location, &cnt->imgs, cnt->imgs.width, cnt->imgs.preview_image.image,
|
|
LOCATE_BOX, LOCATE_NORMAL, cnt->process_thisframe);
|
|
} else if (cnt->locate_motion_style == LOCATE_REDBOX) {
|
|
alg_draw_red_location(&img->location, &cnt->imgs, cnt->imgs.width, cnt->imgs.preview_image.image,
|
|
LOCATE_REDBOX, LOCATE_NORMAL, cnt->process_thisframe);
|
|
} else if (cnt->locate_motion_style == LOCATE_CROSS) {
|
|
alg_draw_location(&img->location, &cnt->imgs, cnt->imgs.width, cnt->imgs.preview_image.image,
|
|
LOCATE_CROSS, LOCATE_NORMAL, cnt->process_thisframe);
|
|
} else if (cnt->locate_motion_style == LOCATE_REDCROSS) {
|
|
alg_draw_red_location(&img->location, &cnt->imgs, cnt->imgs.width, cnt->imgs.preview_image.image,
|
|
LOCATE_REDCROSS, LOCATE_NORMAL, cnt->process_thisframe);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* context_init
|
|
*
|
|
* Initializes a context struct with the default values for all the
|
|
* variables.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - the context struct to destroy
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void context_init(struct context *cnt)
|
|
{
|
|
/*
|
|
* We first clear the entire structure to zero, then fill in any
|
|
* values which have non-zero default values. Note that this
|
|
* assumes that a NULL address pointer has a value of binary 0
|
|
* (this is also assumed at other places within the code, i.e.
|
|
* there are instances of "if (ptr)"). Just for possible future
|
|
* changes to this assumption, any pointers which are intended
|
|
* to be initialised to NULL are listed within a comment.
|
|
*/
|
|
|
|
memset(cnt, 0, sizeof(struct context));
|
|
cnt->noise = 255;
|
|
cnt->lastrate = 25;
|
|
|
|
memcpy(&cnt->track, &track_template, sizeof(struct trackoptions));
|
|
cnt->pipe = -1;
|
|
cnt->mpipe = -1;
|
|
|
|
}
|
|
|
|
/**
|
|
* context_destroy
|
|
*
|
|
* Destroys a context struct by freeing allocated memory, calling the
|
|
* appropriate cleanup functions and finally freeing the struct itself.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - the context struct to destroy
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void context_destroy(struct context *cnt)
|
|
{
|
|
unsigned int j;
|
|
|
|
/* Free memory allocated for config parameters */
|
|
for (j = 0; config_params[j].param_name != NULL; j++) {
|
|
if (config_params[j].copy == copy_string) {
|
|
void **val;
|
|
val = (void *)((char *)cnt+(int)config_params[j].conf_value);
|
|
if (*val) {
|
|
free(*val);
|
|
*val = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
free(cnt);
|
|
}
|
|
|
|
/**
|
|
* sig_handler
|
|
*
|
|
* Our SIGNAL-Handler. We need this to handle alarms and external signals.
|
|
*/
|
|
static void sig_handler(int signo)
|
|
{
|
|
int i;
|
|
|
|
switch(signo) {
|
|
case SIGALRM:
|
|
/*
|
|
* Somebody (maybe we ourself) wants us to make a snapshot
|
|
* This feature triggers snapshots on ALL threads that have
|
|
* snapshot_interval different from 0.
|
|
*/
|
|
if (cnt_list) {
|
|
i = -1;
|
|
while (cnt_list[++i]) {
|
|
if (cnt_list[i]->conf.snapshot_interval)
|
|
cnt_list[i]->snapshot = 1;
|
|
|
|
}
|
|
}
|
|
break;
|
|
case SIGUSR1:
|
|
/*
|
|
* Ouch! We have been hit from the outside! Someone wants us to
|
|
* make a movie!
|
|
*/
|
|
if (cnt_list) {
|
|
i = -1;
|
|
while (cnt_list[++i])
|
|
cnt_list[i]->makemovie = 1;
|
|
}
|
|
break;
|
|
case SIGHUP:
|
|
restart = 1;
|
|
/*
|
|
* Fall through, as the value of 'restart' is the only difference
|
|
* between SIGHUP and the ones below.
|
|
*/
|
|
case SIGINT:
|
|
case SIGQUIT:
|
|
case SIGTERM:
|
|
/*
|
|
* Somebody wants us to quit! We should better finish the actual
|
|
* movie and end up!
|
|
*/
|
|
if (cnt_list) {
|
|
i = -1;
|
|
while (cnt_list[++i]) {
|
|
cnt_list[i]->makemovie = 1;
|
|
cnt_list[i]->finish = 1;
|
|
cnt_list[i]->webcontrol_finish = 1;
|
|
/*
|
|
* Don't restart thread when it ends,
|
|
* all threads restarts if global restart is set
|
|
*/
|
|
cnt_list[i]->restart = 0;
|
|
}
|
|
}
|
|
/*
|
|
* Set flag we want to quit main check threads loop
|
|
* if restart is set (above) we start up again
|
|
*/
|
|
finish = 1;
|
|
break;
|
|
case SIGSEGV:
|
|
exit(0);
|
|
case SIGVTALRM:
|
|
printf("SIGVTALRM went off\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* sigchild_handler
|
|
*
|
|
* This function is a POSIX compliant replacement of the commonly used
|
|
* signal(SIGCHLD, SIG_IGN).
|
|
*/
|
|
static void sigchild_handler(int signo ATTRIBUTE_UNUSED)
|
|
{
|
|
#ifdef WNOHANG
|
|
while (waitpid(-1, NULL, WNOHANG) > 0) {};
|
|
#endif /* WNOHANG */
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* setup_signals
|
|
*
|
|
* Attaches handlers to a number of signals that Motion need to catch.
|
|
*
|
|
* Parameters: sigaction structs for signals in general and SIGCHLD.
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void setup_signals(struct sigaction *sig_handler_action, struct sigaction *sigchild_action)
|
|
{
|
|
#ifdef SA_NOCLDWAIT
|
|
sigchild_action->sa_flags = SA_NOCLDWAIT;
|
|
#else
|
|
sigchild_action->sa_flags = 0;
|
|
#endif
|
|
sigchild_action->sa_handler = sigchild_handler;
|
|
sigemptyset(&sigchild_action->sa_mask);
|
|
#ifdef SA_RESTART
|
|
sig_handler_action->sa_flags = SA_RESTART;
|
|
#else
|
|
sig_handler_action->sa_flags = 0;
|
|
#endif
|
|
sig_handler_action->sa_handler = sig_handler;
|
|
sigemptyset(&sig_handler_action->sa_mask);
|
|
|
|
/* Enable automatic zombie reaping */
|
|
sigaction(SIGCHLD, sigchild_action, NULL);
|
|
sigaction(SIGPIPE, sigchild_action, NULL);
|
|
sigaction(SIGALRM, sig_handler_action, NULL);
|
|
sigaction(SIGHUP, sig_handler_action, NULL);
|
|
sigaction(SIGINT, sig_handler_action, NULL);
|
|
sigaction(SIGQUIT, sig_handler_action, NULL);
|
|
sigaction(SIGTERM, sig_handler_action, NULL);
|
|
sigaction(SIGUSR1, sig_handler_action, NULL);
|
|
|
|
/* use SIGVTALRM as a way to break out of the ioctl, don't restart */
|
|
sig_handler_action->sa_flags = 0;
|
|
sigaction(SIGVTALRM, sig_handler_action, NULL);
|
|
}
|
|
|
|
static void setup_signals_BSD(struct context *cnt){
|
|
#ifdef __OpenBSD__
|
|
/*
|
|
* FIXMARK
|
|
* Fixes zombie issue on OpenBSD 4.6
|
|
*/
|
|
struct sigaction sig_handler_action;
|
|
struct sigaction sigchild_action;
|
|
setup_signals(&sig_handler_action, &sigchild_action);
|
|
#else
|
|
/* Kill compiler warnings */
|
|
cnt->log_level = cnt->log_level;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* motion_remove_pid
|
|
*
|
|
* This function remove the process id file ( pid file ) before motion exit.
|
|
*/
|
|
static void motion_remove_pid(void)
|
|
{
|
|
if ((cnt_list[0]->daemon) && (cnt_list[0]->conf.pid_file) && (restart == 0)) {
|
|
if (!unlink(cnt_list[0]->conf.pid_file))
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Removed process id file (pid file).");
|
|
else
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Error removing pid file");
|
|
}
|
|
|
|
if (ptr_logfile) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Closing logfile (%s).",
|
|
cnt_list[0]->conf.log_file);
|
|
myfclose(ptr_logfile);
|
|
set_log_mode(LOGMODE_SYSLOG);
|
|
ptr_logfile = NULL;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* motion_detected
|
|
*
|
|
* Called from 'motion_loop' when motion is detected
|
|
* Can be called when no motion if emulate_motion is set!
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - current thread's context struct
|
|
* dev - video device file descriptor
|
|
* img - pointer to the captured image_data with detected motion
|
|
*/
|
|
static void motion_detected(struct context *cnt, int dev, struct image_data *img)
|
|
{
|
|
struct config *conf = &cnt->conf;
|
|
struct images *imgs = &cnt->imgs;
|
|
struct coord *location = &img->location;
|
|
|
|
/* Draw location */
|
|
if (cnt->locate_motion_mode == LOCATE_ON) {
|
|
|
|
if (cnt->locate_motion_style == LOCATE_BOX) {
|
|
alg_draw_location(location, imgs, imgs->width, img->image, LOCATE_BOX,
|
|
LOCATE_BOTH, cnt->process_thisframe);
|
|
} else if (cnt->locate_motion_style == LOCATE_REDBOX) {
|
|
alg_draw_red_location(location, imgs, imgs->width, img->image, LOCATE_REDBOX,
|
|
LOCATE_BOTH, cnt->process_thisframe);
|
|
} else if (cnt->locate_motion_style == LOCATE_CROSS) {
|
|
alg_draw_location(location, imgs, imgs->width, img->image, LOCATE_CROSS,
|
|
LOCATE_BOTH, cnt->process_thisframe);
|
|
} else if (cnt->locate_motion_style == LOCATE_REDCROSS) {
|
|
alg_draw_red_location(location, imgs, imgs->width, img->image, LOCATE_REDCROSS,
|
|
LOCATE_BOTH, cnt->process_thisframe);
|
|
}
|
|
}
|
|
|
|
/* Calculate how centric motion is if configured preview center*/
|
|
if (cnt->new_img & NEWIMG_CENTER) {
|
|
unsigned int distX = abs((imgs->width / 2) - location->x);
|
|
unsigned int distY = abs((imgs->height / 2) - location->y);
|
|
|
|
img->cent_dist = distX * distX + distY * distY;
|
|
}
|
|
|
|
|
|
/* Do things only if we have got minimum_motion_frames */
|
|
if (img->flags & IMAGE_TRIGGER) {
|
|
/* Take action if this is a new event and we have a trigger image */
|
|
if (cnt->event_nr != cnt->prev_event) {
|
|
/*
|
|
* Reset prev_event number to current event and save event time
|
|
* in both time_t and struct tm format.
|
|
*/
|
|
cnt->prev_event = cnt->event_nr;
|
|
cnt->eventtime = img->timestamp_tv.tv_sec;
|
|
localtime_r(&cnt->eventtime, cnt->eventtime_tm);
|
|
|
|
/*
|
|
* Since this is a new event we create the event_text_string used for
|
|
* the %C conversion specifier. We may already need it for
|
|
* on_motion_detected_commend so it must be done now.
|
|
*/
|
|
mystrftime(cnt, cnt->text_event_string, sizeof(cnt->text_event_string),
|
|
cnt->conf.text_event, &img->timestamp_tv, NULL, 0);
|
|
|
|
/* EVENT_FIRSTMOTION triggers on_event_start_command and event_ffmpeg_newfile */
|
|
event(cnt, EVENT_FIRSTMOTION, img->image, NULL, NULL,
|
|
&cnt->imgs.image_ring[cnt->imgs.image_ring_out].timestamp_tv);
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion detected - starting event %d",
|
|
cnt->event_nr);
|
|
|
|
/* always save first motion frame as preview-shot, may be changed to an other one later */
|
|
if (cnt->new_img & (NEWIMG_FIRST | NEWIMG_BEST | NEWIMG_CENTER))
|
|
image_save_as_preview(cnt, img);
|
|
|
|
}
|
|
|
|
/* EVENT_MOTION triggers event_beep and on_motion_detected_command */
|
|
event(cnt, EVENT_MOTION, NULL, NULL, NULL, &img->timestamp_tv);
|
|
}
|
|
|
|
/* Limit framerate */
|
|
if (img->shot < conf->frame_limit) {
|
|
/*
|
|
* If config option stream_motion is enabled, send the latest motion detected image
|
|
* to the stream but only if it is not the first shot within a second. This is to
|
|
* avoid double frames since we already have sent a frame to the stream.
|
|
* We also disable this in setup_mode.
|
|
*/
|
|
if (conf->stream_motion && !conf->setup_mode && img->shot != 1)
|
|
event(cnt, EVENT_STREAM, img->image, NULL, NULL, &img->timestamp_tv);
|
|
|
|
/*
|
|
* Save motion jpeg, if configured
|
|
* Output the image_out (motion) picture.
|
|
*/
|
|
if (conf->motion_img)
|
|
event(cnt, EVENT_IMAGEM_DETECTED, NULL, NULL, NULL, &img->timestamp_tv);
|
|
}
|
|
|
|
/* if track enabled and auto track on */
|
|
if (cnt->track.type && cnt->track.active)
|
|
cnt->moved = track_move(cnt, dev, location, imgs, 0);
|
|
|
|
}
|
|
|
|
/**
|
|
* process_image_ring
|
|
*
|
|
* Called from 'motion_loop' to save images / send images to movie
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - current thread's context struct
|
|
* max_images - Max number of images to process
|
|
* Set to IMAGE_BUFFER_FLUSH to send/save all images in buffer
|
|
*/
|
|
|
|
static void process_image_ring(struct context *cnt, unsigned int max_images)
|
|
{
|
|
/*
|
|
* We are going to send an event, in the events there is still
|
|
* some code that use cnt->current_image
|
|
* so set it temporary to our image
|
|
*/
|
|
struct image_data *saved_current_image = cnt->current_image;
|
|
|
|
/* If image is flaged to be saved and not saved yet, process it */
|
|
do {
|
|
/* Check if we should save/send this image, breakout if not */
|
|
if ((cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags & (IMAGE_SAVE | IMAGE_SAVED)) != IMAGE_SAVE)
|
|
break;
|
|
|
|
/* Set inte global cotext that we are working with this image */
|
|
cnt->current_image = &cnt->imgs.image_ring[cnt->imgs.image_ring_out];
|
|
|
|
if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].shot < cnt->conf.frame_limit) {
|
|
if (cnt->log_level >= DBG) {
|
|
char tmp[32];
|
|
const char *t;
|
|
|
|
if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags & IMAGE_TRIGGER)
|
|
t = "Trigger";
|
|
else if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags & IMAGE_MOTION)
|
|
t = "Motion";
|
|
else if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags & IMAGE_PRECAP)
|
|
t = "Precap";
|
|
else if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags & IMAGE_POSTCAP)
|
|
t = "Postcap";
|
|
else
|
|
t = "Other";
|
|
|
|
mystrftime(cnt, tmp, sizeof(tmp), "%H%M%S-%q",
|
|
&cnt->imgs.image_ring[cnt->imgs.image_ring_out].timestamp_tv, NULL, 0);
|
|
draw_text(cnt->imgs.image_ring[cnt->imgs.image_ring_out].image, 10, 20,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
draw_text(cnt->imgs.image_ring[cnt->imgs.image_ring_out].image, 10, 30,
|
|
cnt->imgs.width, t, cnt->conf.text_double);
|
|
}
|
|
|
|
/* Output the picture to jpegs and ffmpeg */
|
|
event(cnt, EVENT_IMAGE_DETECTED,
|
|
cnt->imgs.image_ring[cnt->imgs.image_ring_out].image, NULL, NULL,
|
|
&cnt->imgs.image_ring[cnt->imgs.image_ring_out].timestamp_tv);
|
|
|
|
/*
|
|
* Check if we must add any "filler" frames into movie to keep up fps
|
|
* Only if we are recording videos ( ffmpeg or extenal pipe )
|
|
* While the overall elapsed time might be correct, if there are
|
|
* many duplicated frames, say 10 fps, 5 duplicated, the video will
|
|
* look like it is frozen every second for half a second.
|
|
*/
|
|
if (!cnt->conf.ffmpeg_duplicate_frames) {
|
|
/* don't duplicate frames */
|
|
} else if ((cnt->imgs.image_ring[cnt->imgs.image_ring_out].shot == 0) &&
|
|
(cnt->ffmpeg_output || (cnt->conf.useextpipe && cnt->extpipe))) {
|
|
/*
|
|
* movie_last_shoot is -1 when file is created,
|
|
* we don't know how many frames there is in first sec
|
|
*/
|
|
if (cnt->movie_last_shot >= 0) {
|
|
if (cnt_list[0]->log_level >= DBG) {
|
|
int frames = cnt->movie_fps - (cnt->movie_last_shot + 1);
|
|
if (frames > 0) {
|
|
char tmp[25];
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: Added %d fillerframes into movie",
|
|
frames);
|
|
sprintf(tmp, "Fillerframes %d", frames);
|
|
draw_text(cnt->imgs.image_ring[cnt->imgs.image_ring_out].image, 10, 40,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
}
|
|
}
|
|
/* Check how many frames it was last sec */
|
|
while ((cnt->movie_last_shot + 1) < cnt->movie_fps) {
|
|
/* Add a filler frame into encoder */
|
|
event(cnt, EVENT_FFMPEG_PUT,
|
|
cnt->imgs.image_ring[cnt->imgs.image_ring_out].image, NULL, NULL,
|
|
&cnt->imgs.image_ring[cnt->imgs.image_ring_out].timestamp_tv);
|
|
|
|
cnt->movie_last_shot++;
|
|
}
|
|
}
|
|
cnt->movie_last_shot = 0;
|
|
} else if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].shot != (cnt->movie_last_shot + 1)) {
|
|
/* We are out of sync! Propably we got motion - no motion - motion */
|
|
cnt->movie_last_shot = -1;
|
|
}
|
|
|
|
/*
|
|
* Save last shot added to movie
|
|
* only when we not are within first sec
|
|
*/
|
|
if (cnt->movie_last_shot >= 0)
|
|
cnt->movie_last_shot = cnt->imgs.image_ring[cnt->imgs.image_ring_out].shot;
|
|
}
|
|
|
|
/* Mark the image as saved */
|
|
cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags |= IMAGE_SAVED;
|
|
|
|
/* Store it as a preview image, only if it have motion */
|
|
if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].flags & IMAGE_MOTION) {
|
|
/* Check for most significant preview-shot when output_pictures=best */
|
|
if (cnt->new_img & NEWIMG_BEST) {
|
|
if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].diffs > cnt->imgs.preview_image.diffs) {
|
|
image_save_as_preview(cnt, &cnt->imgs.image_ring[cnt->imgs.image_ring_out]);
|
|
}
|
|
}
|
|
/* Check for most significant preview-shot when output_pictures=center */
|
|
if (cnt->new_img & NEWIMG_CENTER) {
|
|
if (cnt->imgs.image_ring[cnt->imgs.image_ring_out].cent_dist < cnt->imgs.preview_image.cent_dist) {
|
|
image_save_as_preview(cnt, &cnt->imgs.image_ring[cnt->imgs.image_ring_out]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Increment to image after last sended */
|
|
if (++cnt->imgs.image_ring_out >= cnt->imgs.image_ring_size)
|
|
cnt->imgs.image_ring_out = 0;
|
|
|
|
if (max_images != IMAGE_BUFFER_FLUSH) {
|
|
max_images--;
|
|
/* breakout if we have done max_images */
|
|
if (max_images == 0)
|
|
break;
|
|
}
|
|
|
|
/* loop until out and in is same e.g. buffer empty */
|
|
} while (cnt->imgs.image_ring_out != cnt->imgs.image_ring_in);
|
|
|
|
/* restore global context values */
|
|
cnt->current_image = saved_current_image;
|
|
}
|
|
|
|
static int init_camera_type(struct context *cnt){
|
|
|
|
cnt->camera_type = CAMERA_TYPE_UNKNOWN;
|
|
|
|
#ifdef HAVE_MMAL
|
|
if (cnt->conf.mmalcam_name) {
|
|
cnt->camera_type = CAMERA_TYPE_MMAL;
|
|
return 0;
|
|
}
|
|
#endif // HAVE_MMAL
|
|
|
|
if (cnt->conf.netcam_url) {
|
|
cnt->camera_type = CAMERA_TYPE_NETCAM;
|
|
return 0;
|
|
}
|
|
|
|
#ifdef HAVE_V4L2
|
|
if (strncmp(cnt->conf.video_device,"/dev/video",10) == 0) {
|
|
cnt->camera_type = CAMERA_TYPE_V4L2;
|
|
return 0;
|
|
}
|
|
#endif // HAVE_V4L2
|
|
|
|
#ifdef HAVE_BKTR
|
|
if (strncmp(cnt->conf.video_device,"/dev/bktr",9) == 0) {
|
|
cnt->camera_type = CAMERA_TYPE_BKTR;
|
|
return 0;
|
|
}
|
|
#endif // HAVE_BKTR
|
|
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Unable to determine camera type (MMAL, Netcam, V4L2, BKTR)");
|
|
return -1;
|
|
|
|
}
|
|
|
|
static void init_mask_privacy(struct context *cnt){
|
|
|
|
int indxrow;
|
|
int indxcol;
|
|
int start_cr, start_cb;
|
|
|
|
FILE *picture;
|
|
|
|
/* Load the privacy file if any */
|
|
if (cnt->conf.mask_privacy) {
|
|
if ((picture = myfopen(cnt->conf.mask_privacy, "r"))) {
|
|
/*
|
|
* NOTE: The mask is expected to have the output dimensions. I.e., the mask
|
|
* applies to the already rotated image, not the capture image. Thus, use
|
|
* width and height from imgs.
|
|
*/
|
|
cnt->imgs.mask_privacy = get_pgm(picture, cnt->imgs.width, cnt->imgs.height);
|
|
myfclose(picture);
|
|
} else {
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Error opening mask file %s",
|
|
cnt->conf.mask_privacy);
|
|
/*
|
|
* Try to write an empty mask file to make it easier
|
|
* for the user to edit it
|
|
*/
|
|
put_fixed_mask(cnt, cnt->conf.mask_privacy);
|
|
}
|
|
|
|
if (!cnt->imgs.mask_privacy) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Failed to read mask privacy image. Mask privacy feature disabled.");
|
|
} else {
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: Mask privacy file \"%s\" loaded.", cnt->conf.mask_privacy);
|
|
start_cr = (cnt->imgs.height * cnt->imgs.width);
|
|
start_cb = start_cr + ((cnt->imgs.height * cnt->imgs.width)/4);
|
|
|
|
for (indxrow = 0; indxrow < cnt->imgs.height; indxrow++) {
|
|
for (indxcol = 0; indxcol < cnt->imgs.width; indxcol++) {
|
|
if ( cnt->imgs.mask_privacy[indxcol + (indxrow * cnt->imgs.width)] == 0xff) {
|
|
if ((indxcol % 2 == 0) && (indxrow % 2 == 0) ){
|
|
cnt->imgs.mask_privacy[ start_cr + (indxcol/2) + ((indxrow * cnt->imgs.width)/4)] = 0xff;
|
|
cnt->imgs.mask_privacy[ start_cb + (indxcol/2) + ((indxrow * cnt->imgs.width)/4)] = 0xff;
|
|
}
|
|
} else{
|
|
cnt->imgs.mask_privacy[indxcol + (indxrow * cnt->imgs.width)] = 0x00;
|
|
if ((indxcol % 2 == 0) && (indxrow % 2 == 0) ){
|
|
cnt->imgs.mask_privacy[ start_cr + (indxcol/2) + ((indxrow * cnt->imgs.width)/4)] = 0x00;
|
|
cnt->imgs.mask_privacy[ start_cb + (indxcol/2) + ((indxrow * cnt->imgs.width)/4)] = 0x00;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
cnt->imgs.mask_privacy = NULL;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* motion_init
|
|
*
|
|
* This routine is called from motion_loop (the main thread of the program) to do
|
|
* all of the initialisation required before starting the actual run.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt Pointer to the motion context structure
|
|
*
|
|
* Returns: 0 OK
|
|
* -1 Fatal error, open loopback error
|
|
* -2 Fatal error, open SQL database error
|
|
* -3 Fatal error, image dimensions are not modulo 8
|
|
*/
|
|
static int motion_init(struct context *cnt)
|
|
{
|
|
FILE *picture;
|
|
int indx;
|
|
|
|
char tname[16];
|
|
snprintf(tname, sizeof(tname), "ml%d%s%s",
|
|
cnt->threadnr,
|
|
cnt->conf.camera_name ? ":" : "",
|
|
cnt->conf.camera_name ? cnt->conf.camera_name : "");
|
|
MOTION_PTHREAD_SETNAME(tname);
|
|
|
|
/* Store thread number in TLS. */
|
|
pthread_setspecific(tls_key_threadnr, (void *)((unsigned long)cnt->threadnr));
|
|
|
|
cnt->currenttime_tm = mymalloc(sizeof(struct tm));
|
|
cnt->eventtime_tm = mymalloc(sizeof(struct tm));
|
|
/* Init frame time */
|
|
cnt->currenttime = time(NULL);
|
|
localtime_r(&cnt->currenttime, cnt->currenttime_tm);
|
|
|
|
cnt->smartmask_speed = 0;
|
|
|
|
/*
|
|
* We initialize cnt->event_nr to 1 and cnt->prev_event to 0 (not really needed) so
|
|
* that certain code below does not run until motion has been detected the first time
|
|
*/
|
|
cnt->event_nr = 1;
|
|
cnt->prev_event = 0;
|
|
cnt->lightswitch_framecounter = 0;
|
|
cnt->detecting_motion = 0;
|
|
cnt->makemovie = 0;
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Camera %d started: motion detection %s",
|
|
cnt->conf.camera_id, cnt->pause ? "Disabled":"Enabled");
|
|
|
|
if (!cnt->conf.filepath)
|
|
cnt->conf.filepath = mystrdup(".");
|
|
|
|
if (init_camera_type(cnt) != 0 ) return -3;
|
|
|
|
/* set the device settings */
|
|
cnt->video_dev = vid_start(cnt);
|
|
|
|
/*
|
|
* We failed to get an initial image from a camera
|
|
* So we need to guess height and width based on the config
|
|
* file options.
|
|
*/
|
|
if (cnt->video_dev == -1) {
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO, "%s: Could not fetch initial image from camera "
|
|
"Motion continues using width and height from config file(s)");
|
|
cnt->imgs.width = cnt->conf.width;
|
|
cnt->imgs.height = cnt->conf.height;
|
|
cnt->imgs.size = cnt->conf.width * cnt->conf.height * 3 / 2;
|
|
cnt->imgs.motionsize = cnt->conf.width * cnt->conf.height;
|
|
cnt->imgs.type = VIDEO_PALETTE_YUV420P;
|
|
} else if (cnt->video_dev == -2) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Could not fetch initial image from camera ");
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Motion only supports width and height modulo 8");
|
|
return -3;
|
|
}
|
|
|
|
image_ring_resize(cnt, 1); /* Create a initial precapture ring buffer with 1 frame */
|
|
|
|
cnt->imgs.ref = mymalloc(cnt->imgs.size);
|
|
cnt->imgs.out = mymalloc(cnt->imgs.size);
|
|
|
|
/* contains the moving objects of ref. frame */
|
|
cnt->imgs.ref_dyn = mymalloc(cnt->imgs.motionsize * sizeof(*cnt->imgs.ref_dyn));
|
|
cnt->imgs.image_virgin = mymalloc(cnt->imgs.size);
|
|
cnt->imgs.smartmask = mymalloc(cnt->imgs.motionsize);
|
|
cnt->imgs.smartmask_final = mymalloc(cnt->imgs.motionsize);
|
|
cnt->imgs.smartmask_buffer = mymalloc(cnt->imgs.motionsize * sizeof(*cnt->imgs.smartmask_buffer));
|
|
cnt->imgs.labels = mymalloc(cnt->imgs.motionsize * sizeof(*cnt->imgs.labels));
|
|
cnt->imgs.labelsize = mymalloc((cnt->imgs.motionsize/2+1) * sizeof(*cnt->imgs.labelsize));
|
|
|
|
/* Set output picture type */
|
|
if (!strcmp(cnt->conf.picture_type, "ppm"))
|
|
cnt->imgs.picture_type = IMAGE_TYPE_PPM;
|
|
else if (!strcmp(cnt->conf.picture_type, "webp")) {
|
|
#ifdef HAVE_WEBP
|
|
cnt->imgs.picture_type = IMAGE_TYPE_WEBP;
|
|
#else
|
|
/* Fallback to jpeg if webp was selected in the config file, but the support for it was not compiled in */
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: webp image format is not available, failing back to jpeg");
|
|
cnt->imgs.picture_type = IMAGE_TYPE_JPEG;
|
|
#endif /* HAVE_WEBP */
|
|
}
|
|
else
|
|
cnt->imgs.picture_type = IMAGE_TYPE_JPEG;
|
|
|
|
/* allocate buffer here for preview buffer */
|
|
cnt->imgs.preview_image.image = mymalloc(cnt->imgs.size);
|
|
|
|
/*
|
|
* Allocate a buffer for temp. usage in some places
|
|
* Only despeckle & bayer2rgb24() for now for now...
|
|
*/
|
|
cnt->imgs.common_buffer = mymalloc(3 * cnt->imgs.width * cnt->imgs.height);
|
|
|
|
/*
|
|
* Now is a good time to init rotation data. Since vid_start has been
|
|
* called, we know that we have imgs.width and imgs.height. When capturing
|
|
* from a V4L device, these are copied from the corresponding conf values
|
|
* in vid_start. When capturing from a netcam, they get set in netcam_start,
|
|
* which is called from vid_start.
|
|
*
|
|
* rotate_init will set cap_width and cap_height in cnt->rotate_data.
|
|
*/
|
|
rotate_init(cnt); /* rotate_deinit is called in main */
|
|
|
|
/* Capture first image, or we will get an alarm on start */
|
|
if (cnt->video_dev > 0) {
|
|
int i;
|
|
|
|
for (i = 0; i < 5; i++) {
|
|
if (vid_next(cnt, cnt->imgs.image_virgin) == 0)
|
|
break;
|
|
SLEEP(2, 0);
|
|
}
|
|
|
|
if (i >= 5) {
|
|
memset(cnt->imgs.image_virgin, 0x80, cnt->imgs.size); /* initialize to grey */
|
|
draw_text(cnt->imgs.image_virgin, 10, 20, cnt->imgs.width,
|
|
"Error capturing first image", cnt->conf.text_double);
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Error capturing first image");
|
|
}
|
|
}
|
|
|
|
/* create a reference frame */
|
|
alg_update_reference_frame(cnt, RESET_REF_FRAME);
|
|
|
|
#if defined(HAVE_V4L2) && !defined(__FreeBSD__)
|
|
/* open video loopback devices if enabled */
|
|
if (cnt->conf.vidpipe) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Opening video loopback device for normal pictures");
|
|
|
|
/* vid_startpipe should get the output dimensions */
|
|
cnt->pipe = vlp_startpipe(cnt->conf.vidpipe, cnt->imgs.width, cnt->imgs.height);
|
|
|
|
if (cnt->pipe < 0) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Failed to open video loopback for normal pictures");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (cnt->conf.motionvidpipe) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Opening video loopback device for motion pictures");
|
|
|
|
/* vid_startpipe should get the output dimensions */
|
|
cnt->mpipe = vlp_startpipe(cnt->conf.motionvidpipe, cnt->imgs.width, cnt->imgs.height);
|
|
|
|
if (cnt->mpipe < 0) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Failed to open video loopback for motion pictures");
|
|
return -1;
|
|
}
|
|
}
|
|
#endif /* HAVE_V4L2 && !__FreeBSD__ */
|
|
|
|
#if defined(HAVE_MYSQL) || defined(HAVE_PGSQL) || defined(HAVE_SQLITE3)
|
|
if (cnt->conf.database_type) {
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: Database backend %s",
|
|
cnt->conf.database_type);
|
|
|
|
#ifdef HAVE_SQLITE3
|
|
/* if database_sqlite3 is NULL then we are using a non threaded version of
|
|
* sqlite3 and will need a seperate connection for each thread */
|
|
if (cnt->database_sqlite3) {
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: SQLite3 using shared handle");
|
|
} else if ((!strcmp(cnt->conf.database_type, "sqlite3")) && cnt->conf.database_dbname) {
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: SQLite3 Database filename %s",
|
|
cnt->conf.database_dbname);
|
|
if (sqlite3_open(cnt->conf.database_dbname, &cnt->database_sqlite3) != SQLITE_OK) {
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: Can't open database %s : %s",
|
|
cnt->conf.database_dbname, sqlite3_errmsg(cnt->database_sqlite3));
|
|
sqlite3_close(cnt->database_sqlite3);
|
|
exit(1);
|
|
}
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: database_busy_timeout %d msec",
|
|
cnt->conf.database_busy_timeout);
|
|
if (sqlite3_busy_timeout(cnt->database_sqlite3, cnt->conf.database_busy_timeout) != SQLITE_OK)
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: database_busy_timeout failed %s",
|
|
sqlite3_errmsg(cnt->database_sqlite3));
|
|
}
|
|
#endif /* HAVE_SQLITE3 */
|
|
|
|
#ifdef HAVE_MYSQL
|
|
if ((!strcmp(cnt->conf.database_type, "mysql")) && (cnt->conf.database_dbname)) {
|
|
// close database to be sure that we are not leaking
|
|
mysql_close(cnt->database);
|
|
|
|
cnt->database = mymalloc(sizeof(MYSQL));
|
|
mysql_init(cnt->database);
|
|
|
|
if (!mysql_real_connect(cnt->database, cnt->conf.database_host, cnt->conf.database_user,
|
|
cnt->conf.database_password, cnt->conf.database_dbname, 0, NULL, 0)) {
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: Cannot connect to MySQL database %s on host %s with user %s",
|
|
cnt->conf.database_dbname, cnt->conf.database_host,
|
|
cnt->conf.database_user);
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: MySQL error was %s", mysql_error(cnt->database));
|
|
return -2;
|
|
}
|
|
#if (defined(MYSQL_VERSION_ID)) && (MYSQL_VERSION_ID > 50012)
|
|
my_bool my_true = TRUE;
|
|
mysql_options(cnt->database, MYSQL_OPT_RECONNECT, &my_true);
|
|
#endif
|
|
}
|
|
#endif /* HAVE_MYSQL */
|
|
|
|
#ifdef HAVE_PGSQL
|
|
if ((!strcmp(cnt->conf.database_type, "postgresql")) && (cnt->conf.database_dbname)) {
|
|
char connstring[255];
|
|
|
|
/*
|
|
* Create the connection string.
|
|
* Quote the values so we can have null values (blank)
|
|
*/
|
|
snprintf(connstring, 255,
|
|
"dbname='%s' host='%s' user='%s' password='%s' port='%d'",
|
|
cnt->conf.database_dbname, /* dbname */
|
|
(cnt->conf.database_host ? cnt->conf.database_host : ""), /* host (may be blank) */
|
|
(cnt->conf.database_user ? cnt->conf.database_user : ""), /* user (may be blank) */
|
|
(cnt->conf.database_password ? cnt->conf.database_password : ""), /* password (may be blank) */
|
|
cnt->conf.database_port
|
|
);
|
|
|
|
cnt->database_pg = PQconnectdb(connstring);
|
|
if (PQstatus(cnt->database_pg) == CONNECTION_BAD) {
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: Connection to PostgreSQL database '%s' failed: %s",
|
|
cnt->conf.database_dbname, PQerrorMessage(cnt->database_pg));
|
|
return -2;
|
|
}
|
|
}
|
|
#endif /* HAVE_PGSQL */
|
|
|
|
|
|
/* Set the sql mask file according to the SQL config options*/
|
|
|
|
cnt->sql_mask = cnt->conf.sql_log_image * (FTYPE_IMAGE + FTYPE_IMAGE_MOTION) +
|
|
cnt->conf.sql_log_snapshot * FTYPE_IMAGE_SNAPSHOT +
|
|
cnt->conf.sql_log_movie * (FTYPE_MPEG + FTYPE_MPEG_MOTION) +
|
|
cnt->conf.sql_log_timelapse * FTYPE_MPEG_TIMELAPSE;
|
|
}
|
|
|
|
#endif /* defined(HAVE_MYSQL) || defined(HAVE_PGSQL) || defined(HAVE_SQLITE3) */
|
|
|
|
/* Load the mask file if any */
|
|
if (cnt->conf.mask_file) {
|
|
if ((picture = myfopen(cnt->conf.mask_file, "r"))) {
|
|
/*
|
|
* NOTE: The mask is expected to have the output dimensions. I.e., the mask
|
|
* applies to the already rotated image, not the capture image. Thus, use
|
|
* width and height from imgs.
|
|
*/
|
|
cnt->imgs.mask = get_pgm(picture, cnt->imgs.width, cnt->imgs.height);
|
|
myfclose(picture);
|
|
} else {
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Error opening mask file %s",
|
|
cnt->conf.mask_file);
|
|
/*
|
|
* Try to write an empty mask file to make it easier
|
|
* for the user to edit it
|
|
*/
|
|
put_fixed_mask(cnt, cnt->conf.mask_file);
|
|
}
|
|
|
|
if (!cnt->imgs.mask) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Failed to read mask image. Mask feature disabled.");
|
|
} else {
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: Maskfile \"%s\" loaded.",
|
|
cnt->conf.mask_file);
|
|
}
|
|
} else {
|
|
cnt->imgs.mask = NULL;
|
|
}
|
|
|
|
init_mask_privacy(cnt);
|
|
|
|
/* Always initialize smart_mask - someone could turn it on later... */
|
|
memset(cnt->imgs.smartmask, 0, cnt->imgs.motionsize);
|
|
memset(cnt->imgs.smartmask_final, 255, cnt->imgs.motionsize);
|
|
memset(cnt->imgs.smartmask_buffer, 0, cnt->imgs.motionsize * sizeof(*cnt->imgs.smartmask_buffer));
|
|
|
|
/* Set noise level */
|
|
cnt->noise = cnt->conf.noise;
|
|
|
|
/* Set threshold value */
|
|
cnt->threshold = cnt->conf.max_changes;
|
|
|
|
/* Initialize stream server if stream port is specified to not 0 */
|
|
if (cnt->conf.stream_port) {
|
|
if (stream_init(cnt) == -1) {
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Problem enabling motion-stream server in port %d",
|
|
cnt->conf.stream_port);
|
|
cnt->finish = 1;
|
|
} else {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Started motion-stream server on port %d (auth %s)",
|
|
cnt->conf.stream_port, cnt->conf.stream_auth_method ? "Enabled":"Disabled");
|
|
}
|
|
}
|
|
|
|
/* Prevent first few frames from triggering motion... */
|
|
cnt->moved = 8;
|
|
/* 2 sec startup delay so FPS is calculated correct */
|
|
cnt->startup_frames = cnt->conf.frame_limit * 2;
|
|
|
|
/* Initialize the double sized characters if needed. */
|
|
if (cnt->conf.text_double)
|
|
cnt->text_size_factor = 2;
|
|
else
|
|
cnt->text_size_factor = 1;
|
|
|
|
|
|
/* Work out expected frame rate based on config setting */
|
|
if (cnt->conf.frame_limit < 2)
|
|
cnt->conf.frame_limit = 2;
|
|
|
|
cnt->required_frame_time = 1000000L / cnt->conf.frame_limit;
|
|
|
|
cnt->frame_delay = cnt->required_frame_time;
|
|
|
|
/*
|
|
* Reserve enough space for a 10 second timing history buffer. Note that,
|
|
* if there is any problem on the allocation, mymalloc does not return.
|
|
*/
|
|
cnt->rolling_average_data = NULL;
|
|
cnt->rolling_average_limit = 10 * cnt->conf.frame_limit;
|
|
cnt->rolling_average_data = mymalloc(sizeof(cnt->rolling_average_data) * cnt->rolling_average_limit);
|
|
|
|
/* Preset history buffer with expected frame rate */
|
|
for (indx = 0; indx < cnt->rolling_average_limit; indx++)
|
|
cnt->rolling_average_data[indx] = cnt->required_frame_time;
|
|
|
|
|
|
if (cnt->track.type)
|
|
cnt->moved = track_center(cnt, cnt->video_dev, 0, 0, 0);
|
|
|
|
setup_signals_BSD(cnt);
|
|
|
|
/* Initialize area detection */
|
|
cnt->area_minx[0] = cnt->area_minx[3] = cnt->area_minx[6] = 0;
|
|
cnt->area_miny[0] = cnt->area_miny[1] = cnt->area_miny[2] = 0;
|
|
|
|
cnt->area_minx[1] = cnt->area_minx[4] = cnt->area_minx[7] = cnt->imgs.width / 3;
|
|
cnt->area_maxx[0] = cnt->area_maxx[3] = cnt->area_maxx[6] = cnt->imgs.width / 3;
|
|
|
|
cnt->area_minx[2] = cnt->area_minx[5] = cnt->area_minx[8] = cnt->imgs.width / 3 * 2;
|
|
cnt->area_maxx[1] = cnt->area_maxx[4] = cnt->area_maxx[7] = cnt->imgs.width / 3 * 2;
|
|
|
|
cnt->area_miny[3] = cnt->area_miny[4] = cnt->area_miny[5] = cnt->imgs.height / 3;
|
|
cnt->area_maxy[0] = cnt->area_maxy[1] = cnt->area_maxy[2] = cnt->imgs.height / 3;
|
|
|
|
cnt->area_miny[6] = cnt->area_miny[7] = cnt->area_miny[8] = cnt->imgs.height / 3 * 2;
|
|
cnt->area_maxy[3] = cnt->area_maxy[4] = cnt->area_maxy[5] = cnt->imgs.height / 3 * 2;
|
|
|
|
cnt->area_maxx[2] = cnt->area_maxx[5] = cnt->area_maxx[8] = cnt->imgs.width;
|
|
cnt->area_maxy[6] = cnt->area_maxy[7] = cnt->area_maxy[8] = cnt->imgs.height;
|
|
|
|
cnt->areadetect_eventnbr = 0;
|
|
|
|
cnt->timenow = 0;
|
|
cnt->timebefore = 0;
|
|
cnt->rate_limit = 0;
|
|
cnt->lastframetime = 0;
|
|
cnt->minimum_frame_time_downcounter = cnt->conf.minimum_frame_time;
|
|
cnt->get_image = 1;
|
|
|
|
cnt->olddiffs = 0;
|
|
cnt->smartmask_ratio = 0;
|
|
cnt->smartmask_count = 20;
|
|
|
|
cnt->previous_diffs = 0;
|
|
cnt->previous_location_x = 0;
|
|
cnt->previous_location_y = 0;
|
|
|
|
cnt->time_last_frame = 1;
|
|
cnt->time_current_frame = 0;
|
|
|
|
cnt->smartmask_lastrate = 0;
|
|
|
|
cnt->passflag = 0; //only purpose to flag first frame
|
|
cnt->rolling_frame = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* motion_cleanup
|
|
*
|
|
* This routine is called from motion_loop when thread ends to
|
|
* cleanup all memory etc. that motion_init did.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt Pointer to the motion context structure
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void motion_cleanup(struct context *cnt)
|
|
{
|
|
/* Stop stream */
|
|
event(cnt, EVENT_STOP, NULL, NULL, NULL, NULL);
|
|
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, NULL);
|
|
event(cnt, EVENT_ENDMOTION, NULL, NULL, NULL, NULL);
|
|
|
|
if (cnt->video_dev >= 0) {
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: Calling vid_close() from motion_cleanup");
|
|
vid_close(cnt);
|
|
}
|
|
|
|
free(cnt->imgs.out);
|
|
cnt->imgs.out = NULL;
|
|
|
|
free(cnt->imgs.ref);
|
|
cnt->imgs.ref = NULL;
|
|
|
|
free(cnt->imgs.ref_dyn);
|
|
cnt->imgs.ref_dyn = NULL;
|
|
|
|
free(cnt->imgs.image_virgin);
|
|
cnt->imgs.image_virgin = NULL;
|
|
|
|
free(cnt->imgs.labels);
|
|
cnt->imgs.labels = NULL;
|
|
|
|
free(cnt->imgs.labelsize);
|
|
cnt->imgs.labelsize = NULL;
|
|
|
|
free(cnt->imgs.smartmask);
|
|
cnt->imgs.smartmask = NULL;
|
|
|
|
free(cnt->imgs.smartmask_final);
|
|
cnt->imgs.smartmask_final = NULL;
|
|
|
|
free(cnt->imgs.smartmask_buffer);
|
|
cnt->imgs.smartmask_buffer = NULL;
|
|
|
|
if (cnt->imgs.mask) free(cnt->imgs.mask);
|
|
cnt->imgs.mask = NULL;
|
|
|
|
if (cnt->imgs.mask_privacy) free(cnt->imgs.mask_privacy);
|
|
cnt->imgs.mask_privacy = NULL;
|
|
|
|
free(cnt->imgs.common_buffer);
|
|
cnt->imgs.common_buffer = NULL;
|
|
|
|
free(cnt->imgs.preview_image.image);
|
|
cnt->imgs.preview_image.image = NULL;
|
|
|
|
image_ring_destroy(cnt); /* Cleanup the precapture ring buffer */
|
|
|
|
rotate_deinit(cnt); /* cleanup image rotation data */
|
|
|
|
if (cnt->pipe != -1) {
|
|
close(cnt->pipe);
|
|
cnt->pipe = -1;
|
|
}
|
|
|
|
if (cnt->mpipe != -1) {
|
|
close(cnt->mpipe);
|
|
cnt->mpipe = -1;
|
|
}
|
|
|
|
/* Cleanup the current time structure */
|
|
free(cnt->currenttime_tm);
|
|
cnt->currenttime_tm = NULL;
|
|
|
|
/* Cleanup the event time structure */
|
|
free(cnt->eventtime_tm);
|
|
cnt->eventtime_tm = NULL;
|
|
|
|
if (cnt->conf.database_type) {
|
|
#ifdef HAVE_MYSQL
|
|
if ( (!strcmp(cnt->conf.database_type, "mysql")) && (cnt->conf.database_dbname)) {
|
|
mysql_close(cnt->database);
|
|
}
|
|
#endif /* HAVE_MYSQL */
|
|
|
|
#ifdef HAVE_PGSQL
|
|
if ((!strcmp(cnt->conf.database_type, "postgresql")) && (cnt->conf.database_dbname)) {
|
|
PQfinish(cnt->database_pg);
|
|
}
|
|
#endif /* HAVE_PGSQL */
|
|
|
|
#ifdef HAVE_SQLITE3
|
|
/* Close the SQLite database */
|
|
if ((!strcmp(cnt->conf.database_type, "sqlite3")) && (cnt->conf.database_dbname)) {
|
|
sqlite3_close(cnt->database_sqlite3);
|
|
}
|
|
#endif /* HAVE_SQLITE3 */
|
|
}
|
|
}
|
|
|
|
static void mlp_mask_privacy(struct context *cnt){
|
|
|
|
int indxloc;
|
|
|
|
if (cnt->imgs.mask_privacy != NULL){
|
|
for (indxloc = 0; indxloc < (cnt->imgs.height * cnt->imgs.width); indxloc++) {
|
|
if (cnt->imgs.mask_privacy[indxloc] == 0x00)
|
|
cnt->current_image->image[indxloc] = 0x00;
|
|
}
|
|
for (indxloc = (cnt->imgs.height * cnt->imgs.width); indxloc < cnt->imgs.size; indxloc++) {
|
|
if (cnt->imgs.mask_privacy[indxloc] == 0x00)
|
|
cnt->current_image->image[indxloc] = 0x80;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
static void mlp_areadetect(struct context *cnt){
|
|
int i, j, z = 0;
|
|
/*
|
|
* Simple hack to recognize motion in a specific area
|
|
* Do we need a new coversion specifier as well??
|
|
*/
|
|
if ((cnt->conf.area_detect) &&
|
|
(cnt->event_nr != cnt->areadetect_eventnbr) &&
|
|
(cnt->current_image->flags & IMAGE_TRIGGER)) {
|
|
j = strlen(cnt->conf.area_detect);
|
|
for (i = 0; i < j; i++) {
|
|
z = cnt->conf.area_detect[i] - 49; /* characters are stored as ascii 48-57 (0-9) */
|
|
if ((z >= 0) && (z < 9)) {
|
|
if (cnt->current_image->location.x > cnt->area_minx[z] &&
|
|
cnt->current_image->location.x < cnt->area_maxx[z] &&
|
|
cnt->current_image->location.y > cnt->area_miny[z] &&
|
|
cnt->current_image->location.y < cnt->area_maxy[z]) {
|
|
event(cnt, EVENT_AREA_DETECTED, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
cnt->areadetect_eventnbr = cnt->event_nr; /* Fire script only once per event */
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: Motion in area %d detected.", z + 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
static void mlp_prepare(struct context *cnt){
|
|
|
|
int frame_buffer_size;
|
|
struct timeval tv1;
|
|
|
|
/***** MOTION LOOP - PREPARE FOR NEW FRAME SECTION *****/
|
|
cnt->watchdog = WATCHDOG_TMO;
|
|
|
|
/* Get current time and preserver last time for frame interval calc. */
|
|
|
|
/* This may be better at the end of the loop or moving the part in
|
|
* the end doing elapsed time calc in here
|
|
*/
|
|
cnt->timebefore = cnt->timenow;
|
|
gettimeofday(&tv1, NULL);
|
|
cnt->timenow = tv1.tv_usec + 1000000L * tv1.tv_sec;
|
|
|
|
/*
|
|
* Calculate detection rate limit. Above 5fps we limit the detection
|
|
* rate to 3fps to reduce load at higher framerates.
|
|
*/
|
|
cnt->process_thisframe = 0;
|
|
cnt->rate_limit++;
|
|
if (cnt->rate_limit >= (cnt->lastrate / 3)) {
|
|
cnt->rate_limit = 0;
|
|
cnt->process_thisframe = 1;
|
|
}
|
|
|
|
/*
|
|
* Since we don't have sanity checks done when options are set,
|
|
* this sanity check must go in the main loop :(, before pre_captures
|
|
* are attempted.
|
|
*/
|
|
if (cnt->conf.minimum_motion_frames < 1)
|
|
cnt->conf.minimum_motion_frames = 1;
|
|
|
|
if (cnt->conf.pre_capture < 0)
|
|
cnt->conf.pre_capture = 0;
|
|
|
|
/*
|
|
* Check if our buffer is still the right size
|
|
* If pre_capture or minimum_motion_frames has been changed
|
|
* via the http remote control we need to re-size the ring buffer
|
|
*/
|
|
frame_buffer_size = cnt->conf.pre_capture + cnt->conf.minimum_motion_frames;
|
|
|
|
if (cnt->imgs.image_ring_size != frame_buffer_size)
|
|
image_ring_resize(cnt, frame_buffer_size);
|
|
|
|
/* Get time for current frame */
|
|
cnt->currenttime = time(NULL);
|
|
|
|
/*
|
|
* localtime returns static data and is not threadsafe
|
|
* so we use localtime_r which is reentrant and threadsafe
|
|
*/
|
|
localtime_r(&cnt->currenttime, cnt->currenttime_tm);
|
|
|
|
/*
|
|
* If we have started on a new second we reset the shots variable
|
|
* lastrate is updated to be the number of the last frame. last rate
|
|
* is used as the ffmpeg framerate when motion is detected.
|
|
*/
|
|
if (cnt->lastframetime != cnt->currenttime) {
|
|
cnt->lastrate = cnt->shots + 1;
|
|
cnt->shots = -1;
|
|
cnt->lastframetime = cnt->currenttime;
|
|
|
|
if (cnt->conf.minimum_frame_time) {
|
|
cnt->minimum_frame_time_downcounter--;
|
|
if (cnt->minimum_frame_time_downcounter == 0)
|
|
cnt->get_image = 1;
|
|
} else {
|
|
cnt->get_image = 1;
|
|
}
|
|
}
|
|
|
|
|
|
/* Increase the shots variable for each frame captured within this second */
|
|
cnt->shots++;
|
|
|
|
if (cnt->startup_frames > 0)
|
|
cnt->startup_frames--;
|
|
|
|
|
|
}
|
|
|
|
static void mlp_resetimages(struct context *cnt){
|
|
|
|
struct image_data *old_image;
|
|
|
|
if (cnt->conf.minimum_frame_time) {
|
|
cnt->minimum_frame_time_downcounter = cnt->conf.minimum_frame_time;
|
|
cnt->get_image = 0;
|
|
}
|
|
|
|
/* ring_buffer_in is pointing to current pos, update before put in a new image */
|
|
if (++cnt->imgs.image_ring_in >= cnt->imgs.image_ring_size)
|
|
cnt->imgs.image_ring_in = 0;
|
|
|
|
/* Check if we have filled the ring buffer, throw away last image */
|
|
if (cnt->imgs.image_ring_in == cnt->imgs.image_ring_out) {
|
|
if (++cnt->imgs.image_ring_out >= cnt->imgs.image_ring_size)
|
|
cnt->imgs.image_ring_out = 0;
|
|
}
|
|
|
|
/* cnt->current_image points to position in ring where to store image, diffs etc. */
|
|
old_image = cnt->current_image;
|
|
cnt->current_image = &cnt->imgs.image_ring[cnt->imgs.image_ring_in];
|
|
|
|
/* Init/clear current_image */
|
|
if (cnt->process_thisframe) {
|
|
/* set diffs to 0 now, will be written after we calculated diffs in new image */
|
|
cnt->current_image->diffs = 0;
|
|
|
|
/* Set flags to 0 */
|
|
cnt->current_image->flags = 0;
|
|
cnt->current_image->cent_dist = 0;
|
|
|
|
/* Clear location data */
|
|
memset(&cnt->current_image->location, 0, sizeof(cnt->current_image->location));
|
|
cnt->current_image->total_labels = 0;
|
|
} else if (cnt->current_image && old_image) {
|
|
/* not processing this frame: save some important values for next image */
|
|
cnt->current_image->diffs = old_image->diffs;
|
|
cnt->current_image->timestamp_tv = old_image->timestamp_tv;
|
|
cnt->current_image->shot = old_image->shot;
|
|
cnt->current_image->cent_dist = old_image->cent_dist;
|
|
cnt->current_image->flags = old_image->flags & (~IMAGE_SAVED);
|
|
cnt->current_image->location = old_image->location;
|
|
cnt->current_image->total_labels = old_image->total_labels;
|
|
}
|
|
|
|
/* Store time with pre_captured image */
|
|
gettimeofday(&cnt->current_image->timestamp_tv, NULL);
|
|
|
|
/* Store shot number with pre_captured image */
|
|
cnt->current_image->shot = cnt->shots;
|
|
|
|
}
|
|
|
|
static int mlp_retry(struct context *cnt){
|
|
|
|
/***** MOTION LOOP - RETRY INITIALIZING SECTION *****/
|
|
/*
|
|
* If a camera is not available we keep on retrying every 10 seconds
|
|
* until it shows up.
|
|
*/
|
|
if (cnt->video_dev < 0 &&
|
|
cnt->currenttime % 10 == 0 && cnt->shots == 0) {
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO,
|
|
"%s: Retrying until successful connection with camera");
|
|
cnt->video_dev = vid_start(cnt);
|
|
/*
|
|
* If the netcam has different dimensions than in the config file
|
|
* we need to restart Motion to re-allocate all the buffers
|
|
*/
|
|
if (cnt->imgs.width != cnt->conf.width || cnt->imgs.height != cnt->conf.height) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Camera has finally become available\n"
|
|
"Camera image has different width and height"
|
|
"from what is in the config file. You should fix that\n"
|
|
"Restarting Motion thread to reinitialize all "
|
|
"image buffers to new picture dimensions");
|
|
cnt->conf.width = cnt->imgs.width;
|
|
cnt->conf.height = cnt->imgs.height;
|
|
/*
|
|
* Break out of main loop terminating thread
|
|
* watchdog will start us again
|
|
*/
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int mlp_capture(struct context *cnt){
|
|
|
|
const char *tmpin;
|
|
char tmpout[80];
|
|
int vid_return_code = 0; /* Return code used when calling vid_next */
|
|
struct timeval tv1;
|
|
|
|
/***** MOTION LOOP - IMAGE CAPTURE SECTION *****/
|
|
/*
|
|
* Fetch next frame from camera
|
|
* If vid_next returns 0 all is well and we got a new picture
|
|
* Any non zero value is an error.
|
|
* 0 = OK, valid picture
|
|
* <0 = fatal error - leave the thread by breaking out of the main loop
|
|
* >0 = non fatal error - copy last image or show grey image with message
|
|
*/
|
|
if (cnt->video_dev >= 0)
|
|
vid_return_code = vid_next(cnt, cnt->current_image->image);
|
|
else
|
|
vid_return_code = 1; /* Non fatal error */
|
|
|
|
// VALID PICTURE
|
|
if (vid_return_code == 0) {
|
|
cnt->lost_connection = 0;
|
|
cnt->connectionlosttime = 0;
|
|
|
|
/* If all is well reset missing_frame_counter */
|
|
if (cnt->missing_frame_counter >= MISSING_FRAMES_TIMEOUT * cnt->conf.frame_limit) {
|
|
/* If we previously logged starting a grey image, now log video re-start */
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Video signal re-acquired");
|
|
// event for re-acquired video signal can be called here
|
|
event(cnt, EVENT_CAMERA_FOUND, NULL, NULL, NULL, NULL);
|
|
}
|
|
cnt->missing_frame_counter = 0;
|
|
|
|
/*
|
|
* Save the newly captured still virgin image to a buffer
|
|
* which we will not alter with text and location graphics
|
|
*/
|
|
memcpy(cnt->imgs.image_virgin, cnt->current_image->image, cnt->imgs.size);
|
|
|
|
mlp_mask_privacy(cnt);
|
|
|
|
/*
|
|
* If the camera is a netcam we let the camera decide the pace.
|
|
* Otherwise we will keep on adding duplicate frames.
|
|
* By resetting the timer the framerate becomes maximum the rate
|
|
* of the Netcam.
|
|
*/
|
|
if (cnt->conf.netcam_url) {
|
|
gettimeofday(&tv1, NULL);
|
|
cnt->timenow = tv1.tv_usec + 1000000L * tv1.tv_sec;
|
|
}
|
|
// FATAL ERROR - leave the thread by breaking out of the main loop
|
|
} else if (vid_return_code < 0) {
|
|
/* Fatal error - Close video device */
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Video device fatal error - Closing video device");
|
|
vid_close(cnt);
|
|
/*
|
|
* Use virgin image, if we are not able to open it again next loop
|
|
* a gray image with message is applied
|
|
* flag lost_connection
|
|
*/
|
|
memcpy(cnt->current_image->image, cnt->imgs.image_virgin, cnt->imgs.size);
|
|
cnt->lost_connection = 1;
|
|
/* NO FATAL ERROR -
|
|
* copy last image or show grey image with message
|
|
* flag on lost_connection if :
|
|
* vid_return_code == NETCAM_RESTART_ERROR
|
|
* cnt->video_dev < 0
|
|
* cnt->missing_frame_counter > (MISSING_FRAMES_TIMEOUT * cnt->conf.frame_limit)
|
|
*/
|
|
} else {
|
|
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: vid_return_code %d",vid_return_code);
|
|
/*
|
|
* Netcams that change dimensions while Motion is running will
|
|
* require that Motion restarts to reinitialize all the many
|
|
* buffers inside Motion. It will be a mess to try and recover any
|
|
* other way
|
|
*/
|
|
if (vid_return_code == NETCAM_RESTART_ERROR) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Restarting Motion thread to reinitialize all "
|
|
"image buffers");
|
|
/*
|
|
* Break out of main loop terminating thread
|
|
* watchdog will start us again
|
|
* Set lost_connection flag on
|
|
*/
|
|
cnt->lost_connection = 1;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* First missed frame - store timestamp
|
|
* Don't reset time when thread restarts
|
|
*/
|
|
if (cnt->connectionlosttime == 0)
|
|
cnt->connectionlosttime = cnt->currenttime;
|
|
|
|
/*
|
|
* Increase missing_frame_counter
|
|
* The first MISSING_FRAMES_TIMEOUT seconds we copy previous virgin image
|
|
* After MISSING_FRAMES_TIMEOUT seconds we put a grey error image in the buffer
|
|
* If we still have not yet received the initial image from a camera
|
|
* we go straight for the grey error image.
|
|
*/
|
|
++cnt->missing_frame_counter;
|
|
|
|
if (cnt->video_dev >= 0 &&
|
|
cnt->missing_frame_counter < (MISSING_FRAMES_TIMEOUT * cnt->conf.frame_limit)) {
|
|
memcpy(cnt->current_image->image, cnt->imgs.image_virgin, cnt->imgs.size);
|
|
} else {
|
|
cnt->lost_connection = 1;
|
|
|
|
if (cnt->video_dev >= 0)
|
|
tmpin = "CONNECTION TO CAMERA LOST\\nSINCE %Y-%m-%d %T";
|
|
else
|
|
tmpin = "UNABLE TO OPEN VIDEO DEVICE\\nSINCE %Y-%m-%d %T";
|
|
|
|
tv1.tv_sec=cnt->connectionlosttime;
|
|
tv1.tv_usec = 0;
|
|
memset(cnt->current_image->image, 0x80, cnt->imgs.size);
|
|
mystrftime(cnt, tmpout, sizeof(tmpout), tmpin, &tv1, NULL, 0);
|
|
draw_text(cnt->current_image->image, 10, 20 * cnt->text_size_factor, cnt->imgs.width,
|
|
tmpout, cnt->conf.text_double);
|
|
|
|
/* Write error message only once */
|
|
if (cnt->missing_frame_counter == MISSING_FRAMES_TIMEOUT * cnt->conf.frame_limit) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Video signal lost - Adding grey image");
|
|
// Event for lost video signal can be called from here
|
|
event(cnt, EVENT_CAMERA_LOST, NULL, NULL, NULL, &tv1);
|
|
}
|
|
|
|
/*
|
|
* If we don't get a valid frame for a long time, try to close/reopen device
|
|
* Only try this when a device is open
|
|
*/
|
|
if ((cnt->video_dev > 0) &&
|
|
(cnt->missing_frame_counter == (MISSING_FRAMES_TIMEOUT * 4) * cnt->conf.frame_limit)) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Video signal still lost - "
|
|
"Trying to close video device");
|
|
vid_close(cnt);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
|
|
}
|
|
|
|
static void mlp_detection(struct context *cnt){
|
|
|
|
|
|
/***** MOTION LOOP - MOTION DETECTION SECTION *****/
|
|
/*
|
|
* The actual motion detection takes place in the following
|
|
* diffs is the number of pixels detected as changed
|
|
* Make a differences picture in image_out
|
|
*
|
|
* alg_diff_standard is the slower full feature motion detection algorithm
|
|
* alg_diff first calls a fast detection algorithm which only looks at a
|
|
* fraction of the pixels. If this detects possible motion alg_diff_standard
|
|
* is called.
|
|
*/
|
|
if (cnt->process_thisframe) {
|
|
if (cnt->threshold && !cnt->pause) {
|
|
/*
|
|
* If we've already detected motion and we want to see if there's
|
|
* still motion, don't bother trying the fast one first. IF there's
|
|
* motion, the alg_diff will trigger alg_diff_standard
|
|
* anyway
|
|
*/
|
|
if (cnt->detecting_motion || cnt->conf.setup_mode)
|
|
cnt->current_image->diffs = alg_diff_standard(cnt, cnt->imgs.image_virgin);
|
|
else
|
|
cnt->current_image->diffs = alg_diff(cnt, cnt->imgs.image_virgin);
|
|
|
|
/* Lightswitch feature - has light intensity changed?
|
|
* This can happen due to change of light conditions or due to a sudden change of the camera
|
|
* sensitivity. If alg_lightswitch detects lightswitch we suspend motion detection the next
|
|
* 5 frames to allow the camera to settle.
|
|
* Don't check if we have lost connection, we detect "Lost signal" frame as lightswitch
|
|
*/
|
|
if (cnt->conf.lightswitch > 1 && !cnt->lost_connection) {
|
|
if (alg_lightswitch(cnt, cnt->current_image->diffs)) {
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: Lightswitch detected");
|
|
|
|
if (cnt->moved < 5)
|
|
cnt->moved = 5;
|
|
|
|
cnt->current_image->diffs = 0;
|
|
alg_update_reference_frame(cnt, RESET_REF_FRAME);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Switchfilter feature tries to detect a change in the video signal
|
|
* from one camera to the next. This is normally used in the Round
|
|
* Robin feature. The algorithm is not very safe.
|
|
* The algorithm takes a little time so we only call it when needed
|
|
* ie. when feature is enabled and diffs>threshold.
|
|
* We do not suspend motion detection like we did for lightswitch
|
|
* because with Round Robin this is controlled by roundrobin_skip.
|
|
*/
|
|
if (cnt->conf.switchfilter && cnt->current_image->diffs > cnt->threshold) {
|
|
cnt->current_image->diffs = alg_switchfilter(cnt, cnt->current_image->diffs,
|
|
cnt->current_image->image);
|
|
|
|
if (cnt->current_image->diffs <= cnt->threshold) {
|
|
cnt->current_image->diffs = 0;
|
|
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: Switchfilter detected");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Despeckle feature
|
|
* First we run (as given by the despeckle_filter option iterations
|
|
* of erode and dilate algorithms.
|
|
* Finally we run the labelling feature.
|
|
* All this is done in the alg_despeckle code.
|
|
*/
|
|
cnt->current_image->total_labels = 0;
|
|
cnt->imgs.largest_label = 0;
|
|
cnt->olddiffs = 0;
|
|
|
|
if (cnt->conf.despeckle_filter && cnt->current_image->diffs > 0) {
|
|
cnt->olddiffs = cnt->current_image->diffs;
|
|
cnt->current_image->diffs = alg_despeckle(cnt, cnt->olddiffs);
|
|
} else if (cnt->imgs.labelsize_max) {
|
|
cnt->imgs.labelsize_max = 0; /* Disable labeling if enabled */
|
|
}
|
|
|
|
} else if (!cnt->conf.setup_mode) {
|
|
cnt->current_image->diffs = 0;
|
|
}
|
|
}
|
|
|
|
//TODO: This section needs investigation for purpose, cause and effect
|
|
/* Manipulate smart_mask sensitivity (only every smartmask_ratio seconds) */
|
|
if ((cnt->smartmask_speed && (cnt->event_nr != cnt->prev_event)) &&
|
|
(!--cnt->smartmask_count)) {
|
|
alg_tune_smartmask(cnt);
|
|
cnt->smartmask_count = cnt->smartmask_ratio;
|
|
}
|
|
|
|
/*
|
|
* cnt->moved is set by the tracking code when camera has been asked to move.
|
|
* When camera is moving we do not want motion to detect motion or we will
|
|
* get our camera chasing itself like crazy and we will get motion detected
|
|
* which is not really motion. So we pretend there is no motion by setting
|
|
* cnt->diffs = 0.
|
|
* We also pretend to have a moving camera when we start Motion and when light
|
|
* switch has been detected to allow camera to settle.
|
|
*/
|
|
if (cnt->moved) {
|
|
cnt->moved--;
|
|
cnt->current_image->diffs = 0;
|
|
}
|
|
|
|
}
|
|
|
|
static void mlp_tuning(struct context *cnt){
|
|
|
|
/***** MOTION LOOP - TUNING SECTION *****/
|
|
|
|
/*
|
|
* If noise tuning was selected, do it now. but only when
|
|
* no frames have been recorded and only once per second
|
|
*/
|
|
if ((cnt->conf.noise_tune && cnt->shots == 0) &&
|
|
(!cnt->detecting_motion && (cnt->current_image->diffs <= cnt->threshold)))
|
|
alg_noise_tune(cnt, cnt->imgs.image_virgin);
|
|
|
|
|
|
/*
|
|
* If we are not noise tuning lets make sure that remote controlled
|
|
* changes of noise_level are used.
|
|
*/
|
|
if (cnt->process_thisframe) {
|
|
if (!cnt->conf.noise_tune)
|
|
cnt->noise = cnt->conf.noise;
|
|
|
|
/*
|
|
* threshold tuning if enabled
|
|
* if we are not threshold tuning lets make sure that remote controlled
|
|
* changes of threshold are used.
|
|
*/
|
|
if (cnt->conf.threshold_tune)
|
|
alg_threshold_tune(cnt, cnt->current_image->diffs, cnt->detecting_motion);
|
|
else
|
|
cnt->threshold = cnt->conf.max_changes;
|
|
|
|
/*
|
|
* If motion is detected (cnt->current_image->diffs > cnt->threshold) and before we add text to the pictures
|
|
* we find the center and size coordinates of the motion to be used for text overlays and later
|
|
* for adding the locate rectangle
|
|
*/
|
|
if (cnt->current_image->diffs > cnt->threshold)
|
|
alg_locate_center_size(&cnt->imgs, cnt->imgs.width, cnt->imgs.height, &cnt->current_image->location);
|
|
|
|
/*
|
|
* Update reference frame.
|
|
* micro-lighswitch: trying to auto-detect lightswitch events.
|
|
* frontdoor illumination. Updates are rate-limited to 3 per second at
|
|
* framerates above 5fps to save CPU resources and to keep sensitivity
|
|
* at a constant level.
|
|
*/
|
|
|
|
if ((cnt->current_image->diffs > cnt->threshold) && (cnt->conf.lightswitch == 1) &&
|
|
(cnt->lightswitch_framecounter < (cnt->lastrate * 2)) && /* two seconds window only */
|
|
/* number of changed pixels almost the same in two consecutive frames and */
|
|
((abs(cnt->previous_diffs - cnt->current_image->diffs)) < (cnt->previous_diffs / 15)) &&
|
|
/* center of motion in about the same place ? */
|
|
((abs(cnt->current_image->location.x - cnt->previous_location_x)) <= (cnt->imgs.width / 150)) &&
|
|
((abs(cnt->current_image->location.y - cnt->previous_location_y)) <= (cnt->imgs.height / 150))) {
|
|
alg_update_reference_frame(cnt, RESET_REF_FRAME);
|
|
cnt->current_image->diffs = 0;
|
|
cnt->lightswitch_framecounter = 0;
|
|
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: micro-lightswitch!");
|
|
} else {
|
|
alg_update_reference_frame(cnt, UPDATE_REF_FRAME);
|
|
}
|
|
cnt->previous_diffs = cnt->current_image->diffs;
|
|
cnt->previous_location_x = cnt->current_image->location.x;
|
|
cnt->previous_location_y = cnt->current_image->location.y;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
static void mlp_overlay(struct context *cnt){
|
|
|
|
char tmp[PATH_MAX];
|
|
/***** MOTION LOOP - TEXT AND GRAPHICS OVERLAY SECTION *****/
|
|
/*
|
|
* Some overlays on top of the motion image
|
|
* Note that these now modifies the cnt->imgs.out so this buffer
|
|
* can no longer be used for motion detection features until next
|
|
* picture frame is captured.
|
|
*/
|
|
|
|
/* Smartmask overlay */
|
|
if (cnt->smartmask_speed && (cnt->conf.motion_img || cnt->conf.ffmpeg_output_debug ||
|
|
cnt->conf.setup_mode))
|
|
overlay_smartmask(cnt, cnt->imgs.out);
|
|
|
|
/* Largest labels overlay */
|
|
if (cnt->imgs.largest_label && (cnt->conf.motion_img || cnt->conf.ffmpeg_output_debug ||
|
|
cnt->conf.setup_mode))
|
|
overlay_largest_label(cnt, cnt->imgs.out);
|
|
|
|
/* Fixed mask overlay */
|
|
if (cnt->imgs.mask && (cnt->conf.motion_img || cnt->conf.ffmpeg_output_debug ||
|
|
cnt->conf.setup_mode))
|
|
overlay_fixed_mask(cnt, cnt->imgs.out);
|
|
|
|
/* Initialize the double sized characters if needed. */
|
|
if (cnt->conf.text_double && cnt->text_size_factor == 1) {
|
|
cnt->text_size_factor = 2;
|
|
/* If text_double is set to off, then reset the scaling text_size_factor. */
|
|
} else if (!cnt->conf.text_double && cnt->text_size_factor == 2) {
|
|
cnt->text_size_factor = 1;
|
|
}
|
|
|
|
/* Add changed pixels in upper right corner of the pictures */
|
|
if (cnt->conf.text_changes) {
|
|
if (!cnt->pause)
|
|
sprintf(tmp, "%d", cnt->current_image->diffs);
|
|
else
|
|
sprintf(tmp, "-");
|
|
|
|
draw_text(cnt->current_image->image, cnt->imgs.width - 10, 10,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
}
|
|
|
|
/*
|
|
* Add changed pixels to motion-images (for stream) in setup_mode
|
|
* and always overlay smartmask (not only when motion is detected)
|
|
*/
|
|
if (cnt->conf.setup_mode) {
|
|
sprintf(tmp, "D:%5d L:%3d N:%3d", cnt->current_image->diffs,
|
|
cnt->current_image->total_labels, cnt->noise);
|
|
draw_text(cnt->imgs.out, cnt->imgs.width - 10, cnt->imgs.height - 30 * cnt->text_size_factor,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
sprintf(tmp, "THREAD %d SETUP", cnt->threadnr);
|
|
draw_text(cnt->imgs.out, cnt->imgs.width - 10, cnt->imgs.height - 10 * cnt->text_size_factor,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
}
|
|
|
|
/* Add text in lower left corner of the pictures */
|
|
if (cnt->conf.text_left) {
|
|
mystrftime(cnt, tmp, sizeof(tmp), cnt->conf.text_left,
|
|
&cnt->current_image->timestamp_tv, NULL, 0);
|
|
draw_text(cnt->current_image->image, 10, cnt->imgs.height - 10 * cnt->text_size_factor,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
}
|
|
|
|
/* Add text in lower right corner of the pictures */
|
|
if (cnt->conf.text_right) {
|
|
mystrftime(cnt, tmp, sizeof(tmp), cnt->conf.text_right,
|
|
&cnt->current_image->timestamp_tv, NULL, 0);
|
|
draw_text(cnt->current_image->image, cnt->imgs.width - 10,
|
|
cnt->imgs.height - 10 * cnt->text_size_factor,
|
|
cnt->imgs.width, tmp, cnt->conf.text_double);
|
|
}
|
|
|
|
}
|
|
|
|
static void mlp_actions(struct context *cnt){
|
|
|
|
int indx;
|
|
|
|
/***** MOTION LOOP - ACTIONS AND EVENT CONTROL SECTION *****/
|
|
|
|
if (cnt->current_image->diffs > cnt->threshold) {
|
|
/* flag this image, it have motion */
|
|
cnt->current_image->flags |= IMAGE_MOTION;
|
|
cnt->lightswitch_framecounter++; /* micro lightswitch */
|
|
} else {
|
|
cnt->lightswitch_framecounter = 0;
|
|
}
|
|
|
|
/*
|
|
* If motion has been detected we take action and start saving
|
|
* pictures and movies etc by calling motion_detected().
|
|
* Is emulate_motion enabled we always call motion_detected()
|
|
* If post_capture is enabled we also take care of this in the this
|
|
* code section.
|
|
*/
|
|
if (cnt->conf.emulate_motion && (cnt->startup_frames == 0)) {
|
|
cnt->detecting_motion = 1;
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: Emulating motion");
|
|
if (cnt->conf.post_capture > 0) {
|
|
/* Setup the postcap counter */
|
|
cnt->postcap = cnt->conf.post_capture;
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: (Em) Init post capture %d",
|
|
cnt->postcap);
|
|
}
|
|
|
|
cnt->current_image->flags |= (IMAGE_TRIGGER | IMAGE_SAVE);
|
|
motion_detected(cnt, cnt->video_dev, cnt->current_image);
|
|
} else if ((cnt->current_image->flags & IMAGE_MOTION) && (cnt->startup_frames == 0)) {
|
|
/*
|
|
* Did we detect motion (like the cat just walked in :) )?
|
|
* If so, ensure the motion is sustained if minimum_motion_frames
|
|
*/
|
|
|
|
/* Count how many frames with motion there is in the last minimum_motion_frames in precap buffer */
|
|
int frame_count = 0;
|
|
int pos = cnt->imgs.image_ring_in;
|
|
|
|
for (indx = 0; indx < cnt->conf.minimum_motion_frames; indx++) {
|
|
if (cnt->imgs.image_ring[pos].flags & IMAGE_MOTION)
|
|
frame_count++;
|
|
|
|
if (pos == 0)
|
|
pos = cnt->imgs.image_ring_size-1;
|
|
else
|
|
pos--;
|
|
}
|
|
|
|
if (frame_count >= cnt->conf.minimum_motion_frames) {
|
|
|
|
cnt->current_image->flags |= (IMAGE_TRIGGER | IMAGE_SAVE);
|
|
/* If we were previously detecting motion, started a movie, then got
|
|
* no motion then we reset the start movie time so that we do not
|
|
* get a pause in the movie.
|
|
*/
|
|
if ( (cnt->detecting_motion == 0) && (cnt->ffmpeg_output != NULL) ) {
|
|
cnt->ffmpeg_output->start_time.tv_sec=cnt->current_image->timestamp_tv.tv_sec;
|
|
cnt->ffmpeg_output->start_time.tv_usec=cnt->current_image->timestamp_tv.tv_usec;
|
|
}
|
|
|
|
cnt->detecting_motion = 1;
|
|
|
|
/* Setup the postcap counter */
|
|
cnt->postcap = cnt->conf.post_capture;
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: Setup post capture %d",
|
|
cnt->postcap);
|
|
|
|
/* Mark all images in image_ring to be saved */
|
|
for (indx = 0; indx < cnt->imgs.image_ring_size; indx++)
|
|
cnt->imgs.image_ring[indx].flags |= IMAGE_SAVE;
|
|
|
|
} else if (cnt->postcap > 0) {
|
|
/* we have motion in this frame, but not enought frames for trigger. Check postcap */
|
|
cnt->current_image->flags |= (IMAGE_POSTCAP | IMAGE_SAVE);
|
|
cnt->postcap--;
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: post capture %d",
|
|
cnt->postcap);
|
|
} else {
|
|
cnt->current_image->flags |= IMAGE_PRECAP;
|
|
}
|
|
|
|
/* Always call motion_detected when we have a motion image */
|
|
motion_detected(cnt, cnt->video_dev, cnt->current_image);
|
|
} else if (cnt->postcap > 0) {
|
|
/* No motion, doing postcap */
|
|
cnt->current_image->flags |= (IMAGE_POSTCAP | IMAGE_SAVE);
|
|
cnt->postcap--;
|
|
MOTION_LOG(DBG, TYPE_ALL, NO_ERRNO, "%s: post capture %d",
|
|
cnt->postcap);
|
|
} else {
|
|
/* Done with postcap, so just have the image in the precap buffer */
|
|
cnt->current_image->flags |= IMAGE_PRECAP;
|
|
/* gapless movie feature */
|
|
if ((cnt->conf.event_gap == 0) && (cnt->detecting_motion == 1))
|
|
cnt->makemovie = 1;
|
|
cnt->detecting_motion = 0;
|
|
}
|
|
|
|
/* Update last frame saved time, so we can end event after gap time */
|
|
if (cnt->current_image->flags & IMAGE_SAVE)
|
|
cnt->lasttime = cnt->current_image->timestamp_tv.tv_sec;
|
|
|
|
|
|
mlp_areadetect(cnt);
|
|
|
|
/*
|
|
* Is the movie too long? Then make movies
|
|
* First test for max_movie_time
|
|
*/
|
|
if ((cnt->conf.max_movie_time && cnt->event_nr == cnt->prev_event) &&
|
|
(cnt->currenttime - cnt->eventtime >= cnt->conf.max_movie_time))
|
|
cnt->makemovie = 1;
|
|
|
|
/*
|
|
* Now test for quiet longer than 'gap' OR make movie as decided in
|
|
* previous statement.
|
|
*/
|
|
if (((cnt->currenttime - cnt->lasttime >= cnt->conf.event_gap) && cnt->conf.event_gap > 0) ||
|
|
cnt->makemovie) {
|
|
if (cnt->event_nr == cnt->prev_event || cnt->makemovie) {
|
|
|
|
/* Flush image buffer */
|
|
process_image_ring(cnt, IMAGE_BUFFER_FLUSH);
|
|
|
|
/* Save preview_shot here at the end of event */
|
|
if (cnt->imgs.preview_image.diffs) {
|
|
preview_save(cnt);
|
|
cnt->imgs.preview_image.diffs = 0;
|
|
}
|
|
|
|
event(cnt, EVENT_ENDMOTION, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
|
|
/*
|
|
* If tracking is enabled we center our camera so it does not
|
|
* point to a place where it will miss the next action
|
|
*/
|
|
if (cnt->track.type)
|
|
cnt->moved = track_center(cnt, cnt->video_dev, 0, 0, 0);
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: End of event %d",
|
|
cnt->event_nr);
|
|
|
|
cnt->makemovie = 0;
|
|
/* Reset post capture */
|
|
cnt->postcap = 0;
|
|
|
|
/* Finally we increase the event number */
|
|
cnt->event_nr++;
|
|
cnt->lightswitch_framecounter = 0;
|
|
|
|
/*
|
|
* And we unset the text_event_string to avoid that buffered
|
|
* images get a timestamp from previous event.
|
|
*/
|
|
cnt->text_event_string[0] = '\0';
|
|
}
|
|
}
|
|
|
|
/* Save/send to movie some images */
|
|
process_image_ring(cnt, 2);
|
|
|
|
|
|
}
|
|
|
|
static void mlp_setupmode(struct context *cnt){
|
|
/***** MOTION LOOP - SETUP MODE CONSOLE OUTPUT SECTION *****/
|
|
|
|
/* If CAMERA_VERBOSE enabled output some numbers to console */
|
|
if (cnt->conf.setup_mode) {
|
|
char msg[1024] = "\0";
|
|
char part[100];
|
|
|
|
if (cnt->conf.despeckle_filter) {
|
|
snprintf(part, 99, "Raw changes: %5d - changes after '%s': %5d",
|
|
cnt->olddiffs, cnt->conf.despeckle_filter, cnt->current_image->diffs);
|
|
strcat(msg, part);
|
|
if (strchr(cnt->conf.despeckle_filter, 'l')) {
|
|
sprintf(part, " - labels: %3d", cnt->current_image->total_labels);
|
|
strcat(msg, part);
|
|
}
|
|
} else {
|
|
sprintf(part, "Changes: %5d", cnt->current_image->diffs);
|
|
strcat(msg, part);
|
|
}
|
|
|
|
if (cnt->conf.noise_tune) {
|
|
sprintf(part, " - noise level: %2d", cnt->noise);
|
|
strcat(msg, part);
|
|
}
|
|
|
|
if (cnt->conf.threshold_tune) {
|
|
sprintf(part, " - threshold: %d", cnt->threshold);
|
|
strcat(msg, part);
|
|
}
|
|
|
|
MOTION_LOG(INF, TYPE_ALL, NO_ERRNO, "%s: %s", msg);
|
|
}
|
|
|
|
}
|
|
|
|
static void mlp_snapshot(struct context *cnt){
|
|
/***** MOTION LOOP - SNAPSHOT FEATURE SECTION *****/
|
|
/*
|
|
* Did we get triggered to make a snapshot from control http? Then shoot a snap
|
|
* If snapshot_interval is not zero and time since epoch MOD snapshot_interval = 0 then snap
|
|
* We actually allow the time to run over the interval in case we have a delay
|
|
* from slow camera.
|
|
* Note: Negative value means SIGALRM snaps are enabled
|
|
* httpd-control snaps are always enabled.
|
|
*/
|
|
|
|
/* time_current_frame is used both for snapshot and timelapse features */
|
|
cnt->time_current_frame = cnt->currenttime;
|
|
|
|
if ((cnt->conf.snapshot_interval > 0 && cnt->shots == 0 &&
|
|
cnt->time_current_frame % cnt->conf.snapshot_interval <= cnt->time_last_frame % cnt->conf.snapshot_interval) ||
|
|
cnt->snapshot) {
|
|
event(cnt, EVENT_IMAGE_SNAPSHOT, cnt->current_image->image, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
cnt->snapshot = 0;
|
|
}
|
|
|
|
}
|
|
|
|
static void mlp_timelapse(struct context *cnt){
|
|
struct tm timestamp_tm;
|
|
|
|
/***** MOTION LOOP - TIMELAPSE FEATURE SECTION *****/
|
|
|
|
if (cnt->conf.timelapse) {
|
|
localtime_r(&cnt->current_image->timestamp_tv.tv_sec, ×tamp_tm);
|
|
|
|
/*
|
|
* Check to see if we should start a new timelapse file. We start one when
|
|
* we are on the first shot, and and the seconds are zero. We must use the seconds
|
|
* to prevent the timelapse file from getting reset multiple times during the minute.
|
|
*/
|
|
if (timestamp_tm.tm_min == 0 &&
|
|
(cnt->time_current_frame % 60 < cnt->time_last_frame % 60) &&
|
|
cnt->shots == 0) {
|
|
|
|
if (strcasecmp(cnt->conf.timelapse_mode, "manual") == 0) {
|
|
;/* No action */
|
|
|
|
/* If we are daily, raise timelapseend event at midnight */
|
|
} else if (strcasecmp(cnt->conf.timelapse_mode, "daily") == 0) {
|
|
if (timestamp_tm.tm_hour == 0)
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
|
|
/* handle the hourly case */
|
|
} else if (strcasecmp(cnt->conf.timelapse_mode, "hourly") == 0) {
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
|
|
/* If we are weekly-sunday, raise timelapseend event at midnight on sunday */
|
|
} else if (strcasecmp(cnt->conf.timelapse_mode, "weekly-sunday") == 0) {
|
|
if (timestamp_tm.tm_wday == 0 &&
|
|
timestamp_tm.tm_hour == 0)
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
/* If we are weekly-monday, raise timelapseend event at midnight on monday */
|
|
} else if (strcasecmp(cnt->conf.timelapse_mode, "weekly-monday") == 0) {
|
|
if (timestamp_tm.tm_wday == 1 &&
|
|
timestamp_tm.tm_hour == 0)
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
/* If we are monthly, raise timelapseend event at midnight on first day of month */
|
|
} else if (strcasecmp(cnt->conf.timelapse_mode, "monthly") == 0) {
|
|
if (timestamp_tm.tm_mday == 1 &&
|
|
timestamp_tm.tm_hour == 0)
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
/* If invalid we report in syslog once and continue in manual mode */
|
|
} else {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Invalid timelapse_mode argument '%s'",
|
|
cnt->conf.timelapse_mode);
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO, "%:s Defaulting to manual timelapse mode");
|
|
conf_cmdparse(&cnt, (char *)"ffmpeg_timelapse_mode",(char *)"manual");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If ffmpeg timelapse is enabled and time since epoch MOD ffmpeg_timelaps = 0
|
|
* add a timelapse frame to the timelapse movie.
|
|
*/
|
|
if (cnt->shots == 0 && cnt->time_current_frame % cnt->conf.timelapse <=
|
|
cnt->time_last_frame % cnt->conf.timelapse)
|
|
event(cnt, EVENT_TIMELAPSE, cnt->current_image->image, NULL, NULL,
|
|
&cnt->current_image->timestamp_tv);
|
|
} else if (cnt->ffmpeg_timelapse) {
|
|
/*
|
|
* If timelapse movie is in progress but conf.timelapse is zero then close timelapse file
|
|
* This is an important feature that allows manual roll-over of timelapse file using the http
|
|
* remote control via a cron job.
|
|
*/
|
|
event(cnt, EVENT_TIMELAPSEEND, NULL, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
}
|
|
|
|
cnt->time_last_frame = cnt->time_current_frame;
|
|
|
|
|
|
}
|
|
|
|
static void mlp_loopback(struct context *cnt){
|
|
/*
|
|
* Feed last image and motion image to video device pipes and the stream clients
|
|
* In setup mode we send the special setup mode image to both stream and vloopback pipe
|
|
* In normal mode we feed the latest image to vloopback device and we send
|
|
* the image to the stream. We always send the first image in a second to the stream.
|
|
* Other image are sent only when the config option stream_motion is off
|
|
* The result is that with stream_motion on the stream stream is normally at the minimal
|
|
* 1 frame per second but the minute motion is detected the motion_detected() function
|
|
* sends all detected pictures to the stream except the 1st per second which is already sent.
|
|
*/
|
|
if (cnt->conf.setup_mode) {
|
|
event(cnt, EVENT_IMAGE, cnt->imgs.out, NULL, &cnt->pipe, &cnt->current_image->timestamp_tv);
|
|
event(cnt, EVENT_STREAM, cnt->imgs.out, NULL, NULL, &cnt->current_image->timestamp_tv);
|
|
} else {
|
|
event(cnt, EVENT_IMAGE, cnt->current_image->image, NULL,
|
|
&cnt->pipe, &cnt->current_image->timestamp_tv);
|
|
|
|
if (!cnt->conf.stream_motion || cnt->shots == 1)
|
|
event(cnt, EVENT_STREAM, cnt->current_image->image, NULL, NULL,
|
|
&cnt->current_image->timestamp_tv);
|
|
}
|
|
|
|
event(cnt, EVENT_IMAGEM, cnt->imgs.out, NULL, &cnt->mpipe, &cnt->current_image->timestamp_tv);
|
|
|
|
}
|
|
|
|
static void mlp_parmsupdate(struct context *cnt){
|
|
/***** MOTION LOOP - ONCE PER SECOND PARAMETER UPDATE SECTION *****/
|
|
|
|
/* Check for some config parameter changes but only every second */
|
|
if (cnt->shots == 0) {
|
|
if (strcasecmp(cnt->conf.output_pictures, "on") == 0)
|
|
cnt->new_img = NEWIMG_ON;
|
|
else if (strcasecmp(cnt->conf.output_pictures, "first") == 0)
|
|
cnt->new_img = NEWIMG_FIRST;
|
|
else if (strcasecmp(cnt->conf.output_pictures, "best") == 0)
|
|
cnt->new_img = NEWIMG_BEST;
|
|
else if (strcasecmp(cnt->conf.output_pictures, "center") == 0)
|
|
cnt->new_img = NEWIMG_CENTER;
|
|
else
|
|
cnt->new_img = NEWIMG_OFF;
|
|
|
|
if (strcasecmp(cnt->conf.locate_motion_mode, "on") == 0)
|
|
cnt->locate_motion_mode = LOCATE_ON;
|
|
else if (strcasecmp(cnt->conf.locate_motion_mode, "preview") == 0)
|
|
cnt->locate_motion_mode = LOCATE_PREVIEW;
|
|
else
|
|
cnt->locate_motion_mode = LOCATE_OFF;
|
|
|
|
if (strcasecmp(cnt->conf.locate_motion_style, "box") == 0)
|
|
cnt->locate_motion_style = LOCATE_BOX;
|
|
else if (strcasecmp(cnt->conf.locate_motion_style, "redbox") == 0)
|
|
cnt->locate_motion_style = LOCATE_REDBOX;
|
|
else if (strcasecmp(cnt->conf.locate_motion_style, "cross") == 0)
|
|
cnt->locate_motion_style = LOCATE_CROSS;
|
|
else if (strcasecmp(cnt->conf.locate_motion_style, "redcross") == 0)
|
|
cnt->locate_motion_style = LOCATE_REDCROSS;
|
|
else
|
|
cnt->locate_motion_style = LOCATE_BOX;
|
|
|
|
/* Sanity check for smart_mask_speed, silly value disables smart mask */
|
|
if (cnt->conf.smart_mask_speed < 0 || cnt->conf.smart_mask_speed > 10)
|
|
cnt->conf.smart_mask_speed = 0;
|
|
|
|
/* Has someone changed smart_mask_speed or framerate? */
|
|
if (cnt->conf.smart_mask_speed != cnt->smartmask_speed ||
|
|
cnt->smartmask_lastrate != cnt->lastrate) {
|
|
if (cnt->conf.smart_mask_speed == 0) {
|
|
memset(cnt->imgs.smartmask, 0, cnt->imgs.motionsize);
|
|
memset(cnt->imgs.smartmask_final, 255, cnt->imgs.motionsize);
|
|
}
|
|
|
|
cnt->smartmask_lastrate = cnt->lastrate;
|
|
cnt->smartmask_speed = cnt->conf.smart_mask_speed;
|
|
/*
|
|
* Decay delay - based on smart_mask_speed (framerate independent)
|
|
* This is always 5*smartmask_speed seconds
|
|
*/
|
|
cnt->smartmask_ratio = 5 * cnt->lastrate * (11 - cnt->smartmask_speed);
|
|
}
|
|
|
|
#if defined(HAVE_MYSQL) || defined(HAVE_PGSQL) || defined(HAVE_SQLITE3)
|
|
|
|
/*
|
|
* Set the sql mask file according to the SQL config options
|
|
* We update it for every frame in case the config was updated
|
|
* via remote control.
|
|
*/
|
|
cnt->sql_mask = cnt->conf.sql_log_image * (FTYPE_IMAGE + FTYPE_IMAGE_MOTION) +
|
|
cnt->conf.sql_log_snapshot * FTYPE_IMAGE_SNAPSHOT +
|
|
cnt->conf.sql_log_movie * (FTYPE_MPEG + FTYPE_MPEG_MOTION) +
|
|
cnt->conf.sql_log_timelapse * FTYPE_MPEG_TIMELAPSE;
|
|
#endif /* defined(HAVE_MYSQL) || defined(HAVE_PGSQL) || defined(HAVE_SQLITE3) */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
static void mlp_frametiming(struct context *cnt){
|
|
|
|
int indx;
|
|
struct timeval tv2;
|
|
unsigned long int elapsedtime; //TODO: Need to evaluate logic for needing this.
|
|
long int delay_time_nsec;
|
|
|
|
/***** MOTION LOOP - FRAMERATE TIMING AND SLEEPING SECTION *****/
|
|
/*
|
|
* Work out expected frame rate based on config setting which may
|
|
* have changed from http-control
|
|
*/
|
|
if (cnt->conf.frame_limit)
|
|
cnt->required_frame_time = 1000000L / cnt->conf.frame_limit;
|
|
else
|
|
cnt->required_frame_time = 0;
|
|
|
|
/* Get latest time to calculate time taken to process video data */
|
|
gettimeofday(&tv2, NULL);
|
|
elapsedtime = (tv2.tv_usec + 1000000L * tv2.tv_sec) - cnt->timenow;
|
|
|
|
/*
|
|
* Update history buffer but ignore first pass as timebefore
|
|
* variable will be inaccurate
|
|
*/
|
|
if (cnt->passflag)
|
|
cnt->rolling_average_data[cnt->rolling_frame] = cnt->timenow - cnt->timebefore;
|
|
else
|
|
cnt->passflag = 1;
|
|
|
|
cnt->rolling_frame++;
|
|
if (cnt->rolling_frame >= cnt->rolling_average_limit)
|
|
cnt->rolling_frame = 0;
|
|
|
|
/* Calculate 10 second average and use deviation in delay calculation */
|
|
cnt->rolling_average = 0L;
|
|
|
|
for (indx = 0; indx < cnt->rolling_average_limit; indx++)
|
|
cnt->rolling_average += cnt->rolling_average_data[indx];
|
|
|
|
cnt->rolling_average /= cnt->rolling_average_limit;
|
|
cnt->frame_delay = cnt->required_frame_time - elapsedtime - (cnt->rolling_average - cnt->required_frame_time);
|
|
|
|
if (cnt->frame_delay > 0) {
|
|
/* Apply delay to meet frame time */
|
|
if (cnt->frame_delay > cnt->required_frame_time)
|
|
cnt->frame_delay = cnt->required_frame_time;
|
|
|
|
/* Delay time in nanoseconds for SLEEP */
|
|
delay_time_nsec = cnt->frame_delay * 1000;
|
|
|
|
if (delay_time_nsec > 999999999)
|
|
delay_time_nsec = 999999999;
|
|
|
|
/* SLEEP as defined in motion.h A safe sleep using nanosleep */
|
|
SLEEP(0, delay_time_nsec);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* motion_loop
|
|
*
|
|
* Thread function for the motion handling threads.
|
|
*
|
|
*/
|
|
static void *motion_loop(void *arg)
|
|
{
|
|
struct context *cnt = arg;
|
|
|
|
if (motion_init(cnt) < 0) goto err;
|
|
|
|
while (!cnt->finish || cnt->makemovie) {
|
|
mlp_prepare(cnt);
|
|
if (cnt->get_image) {
|
|
mlp_resetimages(cnt);
|
|
if (mlp_retry(cnt) == 1) break;
|
|
if (mlp_capture(cnt) == 1) break;
|
|
mlp_detection(cnt);
|
|
mlp_tuning(cnt);
|
|
mlp_overlay(cnt);
|
|
mlp_actions(cnt);
|
|
mlp_setupmode(cnt);
|
|
}
|
|
mlp_snapshot(cnt);
|
|
mlp_timelapse(cnt);
|
|
mlp_loopback(cnt);
|
|
mlp_parmsupdate(cnt);
|
|
mlp_frametiming(cnt);
|
|
}
|
|
|
|
err:
|
|
free(cnt->rolling_average_data);
|
|
|
|
cnt->lost_connection = 1;
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Thread exiting");
|
|
|
|
motion_cleanup(cnt);
|
|
|
|
pthread_mutex_lock(&global_lock);
|
|
threads_running--;
|
|
pthread_mutex_unlock(&global_lock);
|
|
|
|
if (!cnt->restart)
|
|
cnt->watchdog = WATCHDOG_OFF;
|
|
|
|
cnt->running = 0;
|
|
cnt->finish = 0;
|
|
|
|
pthread_exit(NULL);
|
|
}
|
|
|
|
/**
|
|
* become_daemon
|
|
*
|
|
* Turns Motion into a daemon through forking. The parent process (i.e. the
|
|
* one initially calling this function) will exit inside this function, while
|
|
* control will be returned to the child process. Standard input/output are
|
|
* released properly, and the current directory is set to / in order to not
|
|
* lock up any file system.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - current thread's context struct
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void become_daemon(void)
|
|
{
|
|
int i;
|
|
FILE *pidf = NULL;
|
|
struct sigaction sig_ign_action;
|
|
|
|
/* Setup sig_ign_action */
|
|
#ifdef SA_RESTART
|
|
sig_ign_action.sa_flags = SA_RESTART;
|
|
#else
|
|
sig_ign_action.sa_flags = 0;
|
|
#endif
|
|
sig_ign_action.sa_handler = SIG_IGN;
|
|
sigemptyset(&sig_ign_action.sa_mask);
|
|
|
|
/* fork */
|
|
if (fork()) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion going to daemon mode");
|
|
exit(0);
|
|
}
|
|
|
|
/*
|
|
* Create the pid file if defined, if failed exit
|
|
* If we fail we report it. If we succeed we postpone the log entry till
|
|
* later when we have closed stdout. Otherwise Motion hangs in the terminal waiting
|
|
* for an enter.
|
|
*/
|
|
if (cnt_list[0]->conf.pid_file) {
|
|
pidf = myfopen(cnt_list[0]->conf.pid_file, "w+");
|
|
|
|
if (pidf) {
|
|
(void)fprintf(pidf, "%d\n", getpid());
|
|
myfclose(pidf);
|
|
} else {
|
|
MOTION_LOG(EMG, TYPE_ALL, SHOW_ERRNO, "%s: Exit motion, cannot create process"
|
|
" id file (pid file) %s", cnt_list[0]->conf.pid_file);
|
|
if (ptr_logfile)
|
|
myfclose(ptr_logfile);
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Changing dir to root enables people to unmount a disk
|
|
* without having to stop Motion
|
|
*/
|
|
if (chdir("/"))
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Could not change directory");
|
|
|
|
|
|
#if (defined(BSD) && !defined(__APPLE__))
|
|
setpgrp(0, getpid());
|
|
#else
|
|
setpgrp();
|
|
#endif
|
|
|
|
|
|
if ((i = open("/dev/tty", O_RDWR)) >= 0) {
|
|
ioctl(i, TIOCNOTTY, NULL);
|
|
close(i);
|
|
}
|
|
|
|
setsid();
|
|
i = open("/dev/null", O_RDONLY);
|
|
|
|
if (i != -1) {
|
|
dup2(i, STDIN_FILENO);
|
|
close(i);
|
|
}
|
|
|
|
i = open("/dev/null", O_WRONLY);
|
|
|
|
if (i != -1) {
|
|
dup2(i, STDOUT_FILENO);
|
|
dup2(i, STDERR_FILENO);
|
|
close(i);
|
|
}
|
|
|
|
/* Now it is safe to add the PID creation to the logs */
|
|
if (pidf)
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Created process id file %s. Process ID is %d",
|
|
cnt_list[0]->conf.pid_file, getpid());
|
|
|
|
sigaction(SIGTTOU, &sig_ign_action, NULL);
|
|
sigaction(SIGTTIN, &sig_ign_action, NULL);
|
|
sigaction(SIGTSTP, &sig_ign_action, NULL);
|
|
}
|
|
|
|
/**
|
|
* cntlist_create
|
|
*
|
|
* Sets up the 'cnt_list' variable by allocating room for (and actually
|
|
* allocating) one context struct. Also loads the configuration from
|
|
* the config file(s).
|
|
*
|
|
* Parameters:
|
|
* argc - size of argv
|
|
* argv - command-line options, passed initially from 'main'
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void cntlist_create(int argc, char *argv[])
|
|
{
|
|
/*
|
|
* cnt_list is an array of pointers to the context structures cnt for each thread.
|
|
* First we reserve room for a pointer to thread 0's context structure
|
|
* and a NULL pointer which indicates that end of the array of pointers to
|
|
* thread context structures.
|
|
*/
|
|
cnt_list = mymalloc(sizeof(struct context *) * 2);
|
|
|
|
/* Now we reserve room for thread 0's context structure and let cnt_list[0] point to it */
|
|
cnt_list[0] = mymalloc(sizeof(struct context));
|
|
|
|
/* Populate context structure with start/default values */
|
|
context_init(cnt_list[0]);
|
|
|
|
/* cnt_list[1] pointing to zero indicates no more thread context structures - they get added later */
|
|
cnt_list[1] = NULL;
|
|
|
|
/*
|
|
* Command line arguments are being pointed to from cnt_list[0] and we call conf_load which loads
|
|
* the config options from motion.conf, thread config files and the command line.
|
|
*/
|
|
cnt_list[0]->conf.argv = argv;
|
|
cnt_list[0]->conf.argc = argc;
|
|
cnt_list = conf_load(cnt_list);
|
|
}
|
|
|
|
|
|
/**
|
|
* motion_shutdown
|
|
*
|
|
* Responsible for performing cleanup when Motion is shut down or restarted,
|
|
* including freeing memory for all the context structs as well as for the
|
|
* context struct list itself.
|
|
*
|
|
* Parameters: none
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void motion_shutdown(void)
|
|
{
|
|
int i = -1;
|
|
|
|
motion_remove_pid();
|
|
|
|
while (cnt_list[++i])
|
|
context_destroy(cnt_list[i]);
|
|
|
|
free(cnt_list);
|
|
cnt_list = NULL;
|
|
|
|
vid_mutex_destroy();
|
|
|
|
}
|
|
|
|
/**
|
|
* motion_startup
|
|
*
|
|
* Responsible for initializing stuff when Motion starts up or is restarted,
|
|
* including daemon initialization and creating the context struct list.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* daemonize - non-zero to do daemon init (if the config parameters says so),
|
|
* or 0 to skip it
|
|
* argc - size of argv
|
|
* argv - command-line options, passed initially from 'main'
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void motion_startup(int daemonize, int argc, char *argv[])
|
|
{
|
|
/* Initialize our global mutex */
|
|
pthread_mutex_init(&global_lock, NULL);
|
|
|
|
/*
|
|
* Create the list of context structures and load the
|
|
* configuration.
|
|
*/
|
|
cntlist_create(argc, argv);
|
|
|
|
if ((cnt_list[0]->conf.log_level > ALL) ||
|
|
(cnt_list[0]->conf.log_level == 0)) {
|
|
cnt_list[0]->conf.log_level = LEVEL_DEFAULT;
|
|
cnt_list[0]->log_level = cnt_list[0]->conf.log_level;
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Using default log level (%s) (%d)",
|
|
get_log_level_str(cnt_list[0]->log_level), SHOW_LEVEL_VALUE(cnt_list[0]->log_level));
|
|
} else {
|
|
cnt_list[0]->log_level = cnt_list[0]->conf.log_level - 1; // Let's make syslog compatible
|
|
}
|
|
|
|
|
|
//set_log_level(cnt_list[0]->log_level);
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion "VERSION" Started");
|
|
|
|
if ((cnt_list[0]->conf.log_file) && (strncmp(cnt_list[0]->conf.log_file, "syslog", 6))) {
|
|
set_log_mode(LOGMODE_FILE);
|
|
ptr_logfile = set_logfile(cnt_list[0]->conf.log_file);
|
|
|
|
if (ptr_logfile) {
|
|
set_log_mode(LOGMODE_SYSLOG);
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Logging to file (%s)",
|
|
cnt_list[0]->conf.log_file);
|
|
set_log_mode(LOGMODE_FILE);
|
|
} else {
|
|
MOTION_LOG(EMG, TYPE_ALL, SHOW_ERRNO, "%s: Exit motion, cannot create log file %s",
|
|
cnt_list[0]->conf.log_file);
|
|
exit(0);
|
|
}
|
|
} else {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Logging to syslog");
|
|
}
|
|
|
|
if ((cnt_list[0]->conf.log_type_str == NULL) ||
|
|
!(cnt_list[0]->log_type = get_log_type(cnt_list[0]->conf.log_type_str))) {
|
|
cnt_list[0]->log_type = TYPE_DEFAULT;
|
|
cnt_list[0]->conf.log_type_str = mystrcpy(cnt_list[0]->conf.log_type_str, "ALL");
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Using default log type (%s)",
|
|
get_log_type_str(cnt_list[0]->log_type));
|
|
}
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Using log type (%s) log level (%s)",
|
|
get_log_type_str(cnt_list[0]->log_type), get_log_level_str(cnt_list[0]->log_level));
|
|
|
|
set_log_level(cnt_list[0]->log_level);
|
|
set_log_type(cnt_list[0]->log_type);
|
|
|
|
initialize_chars();
|
|
|
|
if (daemonize) {
|
|
/*
|
|
* If daemon mode is requested, and we're not going into setup mode,
|
|
* become daemon.
|
|
*/
|
|
if (cnt_list[0]->daemon && cnt_list[0]->conf.setup_mode == 0) {
|
|
become_daemon();
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion running as daemon process");
|
|
}
|
|
}
|
|
|
|
vid_mutex_init();
|
|
}
|
|
|
|
/**
|
|
* start_motion_thread
|
|
*
|
|
* Called from main when start a motion thread
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - Thread context pointer
|
|
* thread_attr - pointer to thread attributes
|
|
*
|
|
* Returns: nothing
|
|
*/
|
|
static void start_motion_thread(struct context *cnt, pthread_attr_t *thread_attr)
|
|
{
|
|
int i;
|
|
|
|
/*
|
|
* Check the stream port number for conflicts.
|
|
* First we check for conflict with the control port.
|
|
* Second we check for that two threads does not use the same port number
|
|
* for the stream. If a duplicate port is found the stream feature gets disabled (port = 0)
|
|
* for this thread and a warning is written to console and syslog.
|
|
*/
|
|
|
|
if (cnt->conf.stream_port != 0) {
|
|
/* Compare against the control port. */
|
|
if (cnt_list[0]->conf.webcontrol_port == cnt->conf.stream_port) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO,
|
|
"%s: Stream port number %d for thread %d conflicts with the control port",
|
|
cnt->conf.stream_port, cnt->threadnr);
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO, "%s: Stream feature for thread %d is disabled.",
|
|
cnt->threadnr);
|
|
cnt->conf.stream_port = 0;
|
|
}
|
|
|
|
/* Compare against stream ports of other threads. */
|
|
for (i = 1; cnt_list[i]; i++) {
|
|
if (cnt_list[i] == cnt)
|
|
continue;
|
|
|
|
if (cnt_list[i]->conf.stream_port == cnt->conf.stream_port) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO,
|
|
"%s: Stream port number %d for thread %d conflicts with thread %d",
|
|
cnt->conf.stream_port, cnt->threadnr, cnt_list[i]->threadnr);
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO,
|
|
"%s: Stream feature for thread %d is disabled.",
|
|
cnt->threadnr);
|
|
cnt->conf.stream_port = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Update how many threads we have running. This is done within a
|
|
* mutex lock to prevent multiple simultaneous updates to
|
|
* 'threads_running'.
|
|
*/
|
|
pthread_mutex_lock(&global_lock);
|
|
threads_running++;
|
|
pthread_mutex_unlock(&global_lock);
|
|
|
|
/* Set a flag that we want this thread running */
|
|
cnt->restart = 1;
|
|
|
|
/* Give the thread WATCHDOG_TMO to start */
|
|
cnt->watchdog = WATCHDOG_TMO;
|
|
|
|
/* Flag it as running outside of the thread, otherwise if the main loop
|
|
* checked if it is was running before the thread set it to 1, it would
|
|
* start another thread for this device. */
|
|
cnt->running = 1;
|
|
|
|
/*
|
|
* Create the actual thread. Use 'motion_loop' as the thread
|
|
* function.
|
|
*/
|
|
if (pthread_create(&cnt->thread_id, thread_attr, &motion_loop, cnt)) {
|
|
/* thread create failed, undo running state */
|
|
cnt->running = 0;
|
|
pthread_mutex_lock(&global_lock);
|
|
threads_running--;
|
|
pthread_mutex_unlock(&global_lock);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* main
|
|
*
|
|
* Main entry point of Motion. Launches all the motion threads and contains
|
|
* the logic for starting up, restarting and cleaning up everything.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* argc - size of argv
|
|
* argv - command-line options
|
|
*
|
|
* Returns: Motion exit status = 0 always
|
|
*/
|
|
int main (int argc, char **argv)
|
|
{
|
|
int i;
|
|
pthread_attr_t thread_attr;
|
|
pthread_t thread_id;
|
|
|
|
/*
|
|
* Setup signals and do some initialization. 1 in the call to
|
|
* 'motion_startup' means that Motion will become a daemon if so has been
|
|
* requested, and argc and argc are necessary for reading the command
|
|
* line options.
|
|
*/
|
|
struct sigaction sig_handler_action;
|
|
struct sigaction sigchild_action;
|
|
setup_signals(&sig_handler_action, &sigchild_action);
|
|
|
|
motion_startup(1, argc, argv);
|
|
|
|
ffmpeg_init();
|
|
|
|
#ifdef HAVE_MYSQL
|
|
if (mysql_library_init(0, NULL, NULL)) {
|
|
fprintf(stderr, "could not initialize MySQL library\n");
|
|
exit(1);
|
|
}
|
|
#endif /* HAVE_MYSQL */
|
|
#ifdef HAVE_SQLITE3
|
|
/* database_sqlite3 == NULL if not changed causes each thread to creat their own
|
|
* sqlite3 connection this will only happens when using a non-threaded sqlite version */
|
|
cnt_list[0]->database_sqlite3=NULL;
|
|
if (cnt_list[0]->conf.database_type && ((!strcmp(cnt_list[0]->conf.database_type, "sqlite3")) && cnt_list[0]->conf.database_dbname)) {
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: SQLite3 Database filename %s",
|
|
cnt_list[0]->conf.database_dbname);
|
|
|
|
int thread_safe = sqlite3_threadsafe();
|
|
if (thread_safe > 0) {
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: SQLite3 is threadsafe");
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: SQLite3 serialized %s",
|
|
(sqlite3_config(SQLITE_CONFIG_SERIALIZED)?"FAILED":"SUCCESS"));
|
|
if (sqlite3_open( cnt_list[0]->conf.database_dbname, &cnt_list[0]->database_sqlite3) != SQLITE_OK) {
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: Can't open database %s : %s",
|
|
cnt_list[0]->conf.database_dbname, sqlite3_errmsg( cnt_list[0]->database_sqlite3));
|
|
sqlite3_close( cnt_list[0]->database_sqlite3);
|
|
exit(1);
|
|
}
|
|
MOTION_LOG(NTC, TYPE_DB, NO_ERRNO, "%s: database_busy_timeout %d msec",
|
|
cnt_list[0]->conf.database_busy_timeout);
|
|
if (sqlite3_busy_timeout( cnt_list[0]->database_sqlite3, cnt_list[0]->conf.database_busy_timeout) != SQLITE_OK)
|
|
MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: database_busy_timeout failed %s",
|
|
sqlite3_errmsg( cnt_list[0]->database_sqlite3));
|
|
}
|
|
|
|
}
|
|
#endif /* HAVE_SQLITE3 */
|
|
|
|
/*
|
|
* In setup mode, Motion is very communicative towards the user, which
|
|
* allows the user to experiment with the config parameters in order to
|
|
* optimize motion detection and stuff.
|
|
*/
|
|
if (cnt_list[0]->conf.setup_mode)
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion running in setup mode.");
|
|
|
|
/*
|
|
* Create and a thread attribute for the threads we spawn later on.
|
|
* PTHREAD_CREATE_DETACHED means to create threads detached, i.e.
|
|
* their termination cannot be synchronized through 'pthread_join'.
|
|
*/
|
|
pthread_attr_init(&thread_attr);
|
|
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
|
|
|
|
/* Create the TLS key for thread number. */
|
|
pthread_key_create(&tls_key_threadnr, NULL);
|
|
|
|
do {
|
|
if (restart) {
|
|
/*
|
|
* Handle the restart situation. Currently the approach is to
|
|
* cleanup everything, and then initialize everything again
|
|
* (including re-reading the config file(s)).
|
|
*/
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO, "%s: Restarting motion.");
|
|
motion_shutdown();
|
|
restart = 0; /* only one reset for now */
|
|
|
|
SLEEP(5, 0); // maybe some cameras needs less time
|
|
|
|
motion_startup(0, argc, argv); /* 0 = skip daemon init */
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO, "%s: Motion restarted");
|
|
}
|
|
|
|
/*
|
|
* Start the motion threads. First 'cnt_list' item is global if 'thread'
|
|
* option is used, so start at 1 then and 0 otherwise.
|
|
*/
|
|
for (i = cnt_list[1] != NULL ? 1 : 0; cnt_list[i]; i++) {
|
|
/* If i is 0 it means no thread files and we then set the thread number to 1 */
|
|
cnt_list[i]->threadnr = i ? i : 1;
|
|
/* camera_id is not defined in the config, then the camera id needs to be generated.
|
|
* the load order will generate a # that will become the camera_id
|
|
*/
|
|
cnt_list[i]->conf.camera_id = cnt_list[i]->conf.camera_id ? cnt_list[i]->conf.camera_id: i;
|
|
|
|
if (strcmp(cnt_list[i]->conf_filename, ""))
|
|
{
|
|
cnt_list[i]->conf_filename[sizeof(cnt_list[i]->conf_filename) - 1] = '\0';
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Camera %d is from %s",
|
|
cnt_list[i]->conf.camera_id, cnt_list[i]->conf_filename);
|
|
}
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Camera %d is device: %s input %d",
|
|
cnt_list[i]->conf.camera_id, cnt_list[i]->conf.netcam_url ?
|
|
cnt_list[i]->conf.netcam_url : cnt_list[i]->conf.video_device,
|
|
cnt_list[i]->conf.netcam_url ? -1 : cnt_list[i]->conf.input);
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Stream port %d",
|
|
cnt_list[i]->conf.stream_port);
|
|
#ifdef HAVE_SQLITE
|
|
/* this is done to share the seralized handle
|
|
* and supress creation of new handles in the threads */
|
|
cnt_list[i]->database_sqlite3=cnt_list[0]->database_sqlite3;
|
|
#endif
|
|
start_motion_thread(cnt_list[i], &thread_attr);
|
|
}
|
|
|
|
/*
|
|
* Create a thread for the control interface if requested. Create it
|
|
* detached and with 'motion_web_control' as the thread function.
|
|
*/
|
|
if (cnt_list[0]->conf.webcontrol_port) {
|
|
pthread_mutex_lock(&global_lock);
|
|
threads_running++;
|
|
/* set outside the loop to avoid thread set vs main thread check */
|
|
cnt_list[0]->webcontrol_running = 1;
|
|
pthread_mutex_unlock(&global_lock);
|
|
if (pthread_create(&thread_id, &thread_attr, &motion_web_control,
|
|
cnt_list)) {
|
|
/* thread create failed, undo running state */
|
|
pthread_mutex_lock(&global_lock);
|
|
threads_running--;
|
|
cnt_list[0]->webcontrol_running = 0;
|
|
pthread_mutex_unlock(&global_lock);
|
|
}
|
|
}
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Waiting for threads to finish, pid: %d",
|
|
getpid());
|
|
|
|
/*
|
|
* Crude way of waiting for all threads to finish - check the thread
|
|
* counter (because we cannot do join on the detached threads).
|
|
*/
|
|
while (1) {
|
|
SLEEP(1, 0);
|
|
|
|
/*
|
|
* Calculate how many threads are running or wants to run
|
|
* if zero and we want to finish, break out
|
|
*/
|
|
int motion_threads_running = 0;
|
|
|
|
for (i = (cnt_list[1] != NULL ? 1 : 0); cnt_list[i]; i++) {
|
|
if (cnt_list[i]->running || cnt_list[i]->restart)
|
|
motion_threads_running++;
|
|
}
|
|
if (cnt_list[0]->conf.webcontrol_port &&
|
|
cnt_list[0]->webcontrol_running)
|
|
motion_threads_running++;
|
|
|
|
if (((motion_threads_running == 0) && finish) ||
|
|
((motion_threads_running == 0) && (threads_running == 0))) {
|
|
MOTION_LOG(ALL, TYPE_ALL, NO_ERRNO, "%s: DEBUG-1 threads_running %d motion_threads_running %d "
|
|
", finish %d", threads_running, motion_threads_running, finish);
|
|
break;
|
|
}
|
|
|
|
for (i = (cnt_list[1] != NULL ? 1 : 0); cnt_list[i]; i++) {
|
|
/* Check if threads wants to be restarted */
|
|
if ((!cnt_list[i]->running) && (cnt_list[i]->restart)) {
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion thread %d restart",
|
|
cnt_list[i]->threadnr);
|
|
start_motion_thread(cnt_list[i], &thread_attr);
|
|
}
|
|
|
|
if (cnt_list[i]->watchdog > WATCHDOG_OFF) {
|
|
if (cnt_list[i]->watchdog == WATCHDOG_KILL) {
|
|
/* if 0 then it finally did clean up (and will restart without any further action here)
|
|
* kill(, 0) == ESRCH means the thread is no longer running
|
|
* if it is no longer running with running set, then cleanup here so it can restart
|
|
*/
|
|
if(cnt_list[i]->running && pthread_kill(cnt_list[i]->thread_id, 0) == ESRCH) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: cleaning Thread %d", cnt_list[i]->threadnr);
|
|
pthread_mutex_lock(&global_lock);
|
|
threads_running--;
|
|
pthread_mutex_unlock(&global_lock);
|
|
motion_cleanup(cnt_list[i]);
|
|
cnt_list[i]->running = 0;
|
|
cnt_list[i]->finish = 0;
|
|
} else {
|
|
/* keep sending signals so it doesn't get stuck in a blocking call */
|
|
pthread_kill(cnt_list[i]->thread_id, SIGVTALRM);
|
|
}
|
|
} else {
|
|
cnt_list[i]->watchdog--;
|
|
|
|
|
|
if (cnt_list[i]->watchdog == 0) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Thread %d - Watchdog timeout, trying to do "
|
|
"a graceful restart", cnt_list[i]->threadnr);
|
|
cnt_list[i]->finish = 1;
|
|
}
|
|
|
|
if (cnt_list[i]->watchdog == WATCHDOG_KILL) {
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO, "%s: Thread %d - Watchdog timeout, did NOT restart graceful,"
|
|
"killing it!", cnt_list[i]->threadnr);
|
|
/* The problem is pthread_cancel might just wake up the thread so it runs to completion
|
|
* or it might not. In either case don't rip the carpet out under it by
|
|
* doing motion_cleanup until it no longer is running.
|
|
*/
|
|
pthread_cancel(cnt_list[i]->thread_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MOTION_LOG(ALL, TYPE_ALL, NO_ERRNO, "%s: DEBUG-2 threads_running %d motion_threads_running %d finish %d",
|
|
threads_running, motion_threads_running, finish);
|
|
}
|
|
/* Reset end main loop flag */
|
|
finish = 0;
|
|
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Threads finished");
|
|
|
|
/* Rest for a while if we're supposed to restart. */
|
|
if (restart)
|
|
SLEEP(2, 0);
|
|
|
|
} while (restart); /* loop if we're supposed to restart */
|
|
|
|
ffmpeg_finalise();
|
|
|
|
// Be sure that http control exits fine
|
|
cnt_list[0]->webcontrol_finish = 1;
|
|
SLEEP(1, 0);
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: Motion terminating");
|
|
|
|
/* Perform final cleanup. */
|
|
pthread_key_delete(tls_key_threadnr);
|
|
pthread_attr_destroy(&thread_attr);
|
|
pthread_mutex_destroy(&global_lock);
|
|
motion_shutdown();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* mymalloc
|
|
*
|
|
* Allocates some memory and checks if that succeeded or not. If it failed,
|
|
* do some errorlogging and bail out.
|
|
*
|
|
* NOTE: Kenneth Lavrsen changed printing of size_t types so instead of using
|
|
* conversion specifier %zd I changed it to %llu and casted the size_t
|
|
* variable to unsigned long long. The reason for this nonsense is that older
|
|
* versions of gcc like 2.95 uses %Zd and does not understand %zd. So to avoid
|
|
* this mess I used a more generic way. Long long should have enough bits for
|
|
* 64-bit machines with large memory areas.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* nbytes - no. of bytes to allocate
|
|
*
|
|
* Returns: a pointer to the allocated memory
|
|
*/
|
|
void * mymalloc(size_t nbytes)
|
|
{
|
|
void *dummy = calloc(nbytes, 1);
|
|
|
|
if (!dummy) {
|
|
MOTION_LOG(EMG, TYPE_ALL, SHOW_ERRNO, "%s: Could not allocate %llu bytes of memory!",
|
|
(unsigned long long)nbytes);
|
|
motion_remove_pid();
|
|
exit(1);
|
|
}
|
|
|
|
return dummy;
|
|
}
|
|
|
|
/**
|
|
* myrealloc
|
|
*
|
|
* Re-allocate (i.e., resize) some memory and check if that succeeded or not.
|
|
* If it failed, do some errorlogging and bail out. If the new memory size
|
|
* is 0, the memory is freed.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* ptr - pointer to the memory to resize/reallocate
|
|
* size - new memory size
|
|
* desc - name of the calling function
|
|
*
|
|
* Returns: a pointer to the reallocated memory, or NULL if the memory was
|
|
* freed
|
|
*/
|
|
void *myrealloc(void *ptr, size_t size, const char *desc)
|
|
{
|
|
void *dummy = NULL;
|
|
|
|
if (size == 0) {
|
|
free(ptr);
|
|
MOTION_LOG(WRN, TYPE_ALL, NO_ERRNO,
|
|
"%s: Warning! Function %s tries to resize memoryblock at %p to 0 bytes!",
|
|
desc, ptr);
|
|
} else {
|
|
dummy = realloc(ptr, size);
|
|
if (!dummy) {
|
|
MOTION_LOG(EMG, TYPE_ALL, NO_ERRNO,
|
|
"%s: Could not resize memory-block at offset %p to %llu bytes (function %s)!",
|
|
ptr, (unsigned long long)size, desc);
|
|
motion_remove_pid();
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
return dummy;
|
|
}
|
|
|
|
|
|
/**
|
|
* create_path
|
|
*
|
|
* This function creates a whole path, like mkdir -p. Example paths:
|
|
* this/is/an/example/
|
|
* /this/is/an/example/
|
|
* Warning: a path *must* end with a slash!
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - current thread's context structure (for logging)
|
|
* path - the path to create
|
|
*
|
|
* Returns: 0 on success, -1 on failure
|
|
*/
|
|
int create_path(const char *path)
|
|
{
|
|
char *start;
|
|
mode_t mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
|
|
|
|
if (path[0] == '/')
|
|
start = strchr(path + 1, '/');
|
|
else
|
|
start = strchr(path, '/');
|
|
|
|
while (start) {
|
|
char *buffer = mystrdup(path);
|
|
buffer[start-path] = 0x00;
|
|
|
|
if (mkdir(buffer, mode) == -1 && errno != EEXIST) {
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Problem creating directory %s",
|
|
buffer);
|
|
free(buffer);
|
|
return -1;
|
|
}
|
|
|
|
start = strchr(start + 1, '/');
|
|
|
|
if (!start)
|
|
MOTION_LOG(NTC, TYPE_ALL, NO_ERRNO, "%s: creating directory %s", buffer);
|
|
|
|
free(buffer);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* myfopen
|
|
*
|
|
* This function opens a file, if that failed because of an ENOENT error
|
|
* (which is: path does not exist), the path is created and then things are
|
|
* tried again. This is faster then trying to create that path over and over
|
|
* again. If someone removes the path after it was created, myfopen will
|
|
* recreate the path automatically.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* path - path to the file to open
|
|
* mode - open mode
|
|
*
|
|
* Returns: the file stream object
|
|
*/
|
|
FILE * myfopen(const char *path, const char *mode)
|
|
{
|
|
/* first, just try to open the file */
|
|
FILE *dummy = fopen(path, mode);
|
|
if (dummy) return dummy;
|
|
|
|
/* could not open file... */
|
|
/* path did not exist? */
|
|
if (errno == ENOENT) {
|
|
|
|
/* create path for file... */
|
|
if (create_path(path) == -1)
|
|
return NULL;
|
|
|
|
/* and retry opening the file */
|
|
dummy = fopen(path, mode);
|
|
}
|
|
if (!dummy) {
|
|
/*
|
|
* Two possibilities
|
|
* 1: there was an other error while trying to open the file for the
|
|
* first time
|
|
* 2: could still not open the file after the path was created
|
|
*/
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Error opening file %s with mode %s",
|
|
path, mode);
|
|
return NULL;
|
|
}
|
|
|
|
return dummy;
|
|
}
|
|
|
|
/**
|
|
* myfclose
|
|
*
|
|
* Motion-specific variant of fclose()
|
|
*
|
|
* Returns: fclose() return value
|
|
*/
|
|
int myfclose(FILE* fh)
|
|
{
|
|
int rval = fclose(fh);
|
|
|
|
if (rval != 0)
|
|
MOTION_LOG(ERR, TYPE_ALL, SHOW_ERRNO, "%s: Error closing file");
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* mystrftime_long
|
|
*
|
|
* Motion-specific long form of format specifiers.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - current thread's context structure.
|
|
* width - width associated with the format specifier.
|
|
* word - beginning of the format specifier's word.
|
|
* l - length of the format specifier's word.
|
|
* out - output buffer where to store the result. Size: PATH_MAX.
|
|
*
|
|
* This is called if a format specifier with the format below was found:
|
|
*
|
|
* % { word }
|
|
*
|
|
* As a special edge case, an incomplete format at the end of the string
|
|
* is processed as well:
|
|
*
|
|
* % { word \0
|
|
*
|
|
* Any valid format specified width is supported, e.g. "%12{host}".
|
|
*
|
|
* The following specifier keywords are currently supported:
|
|
*
|
|
* host Replaced with the name of the local machine (see gethostname(2)).
|
|
* fps Equivalent to %fps.
|
|
*/
|
|
static void mystrftime_long (const struct context *cnt,
|
|
int width, const char *word, int l, char *out)
|
|
{
|
|
#define SPECIFIERWORD(k) ((strlen(k)==l) && (!strncmp (k, word, l)))
|
|
|
|
if (SPECIFIERWORD("host")) {
|
|
char host[PATH_MAX];
|
|
gethostname (host, PATH_MAX);
|
|
host[PATH_MAX-1] = 0; // see man page for gethostname.
|
|
snprintf (out, PATH_MAX, "%*s", width, host);
|
|
return;
|
|
}
|
|
if (SPECIFIERWORD("fps")) {
|
|
sprintf(out, "%*d", width, cnt->movie_fps);
|
|
return;
|
|
}
|
|
// Not a valid modifier keyword. Log the error and ignore.
|
|
MOTION_LOG(ERR, TYPE_ALL, NO_ERRNO,
|
|
"%s: invalid format specifier keyword %*.*s", l, l, word);
|
|
|
|
// Do not let the output buffer empty, or else where to restart the
|
|
// interpretation of the user string will become dependent to far too
|
|
// many conditions. Maybe change loop to "if (*pos_userformat == '%') {
|
|
// ...} __else__ ..."?
|
|
out[0] = '~'; out[1] = 0;
|
|
}
|
|
|
|
/**
|
|
* mystrftime
|
|
*
|
|
* Motion-specific variant of strftime(3) that supports additional format
|
|
* specifiers in the format string.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* cnt - current thread's context structure
|
|
* s - destination string
|
|
* max - max number of bytes to write
|
|
* userformat - format string
|
|
* tm - time information
|
|
* filename - string containing full path of filename
|
|
* set this to NULL if not relevant
|
|
* sqltype - Filetype as used in SQL feature, set to 0 if not relevant
|
|
*
|
|
* Returns: number of bytes written to the string s
|
|
*/
|
|
size_t mystrftime(const struct context *cnt, char *s, size_t max, const char *userformat,
|
|
const struct timeval *tv1, const char *filename, int sqltype)
|
|
{
|
|
char formatstring[PATH_MAX] = "";
|
|
char tempstring[PATH_MAX] = "";
|
|
char *format, *tempstr;
|
|
const char *pos_userformat;
|
|
int width;
|
|
struct tm timestamp_tm;
|
|
|
|
localtime_r(&tv1->tv_sec, ×tamp_tm);
|
|
|
|
format = formatstring;
|
|
|
|
/* if mystrftime is called with userformat = NULL we return a zero length string */
|
|
if (userformat == NULL) {
|
|
*s = '\0';
|
|
return 0;
|
|
}
|
|
|
|
for (pos_userformat = userformat; *pos_userformat; ++pos_userformat) {
|
|
|
|
if (*pos_userformat == '%') {
|
|
/*
|
|
* Reset 'tempstr' to point to the beginning of 'tempstring',
|
|
* otherwise we will eat up tempstring if there are many
|
|
* format specifiers.
|
|
*/
|
|
tempstr = tempstring;
|
|
tempstr[0] = '\0';
|
|
width = 0;
|
|
while ('0' <= pos_userformat[1] && pos_userformat[1] <= '9') {
|
|
width *= 10;
|
|
width += pos_userformat[1] - '0';
|
|
++pos_userformat;
|
|
}
|
|
|
|
switch (*++pos_userformat) {
|
|
case '\0': // end of string
|
|
--pos_userformat;
|
|
break;
|
|
|
|
case 'v': // event
|
|
sprintf(tempstr, "%0*d", width ? width : 2, cnt->event_nr);
|
|
break;
|
|
|
|
case 'q': // shots
|
|
sprintf(tempstr, "%0*d", width ? width : 2,
|
|
cnt->current_image->shot);
|
|
break;
|
|
|
|
case 'D': // diffs
|
|
sprintf(tempstr, "%*d", width, cnt->current_image->diffs);
|
|
break;
|
|
|
|
case 'N': // noise
|
|
sprintf(tempstr, "%*d", width, cnt->noise);
|
|
break;
|
|
|
|
case 'i': // motion width
|
|
sprintf(tempstr, "%*d", width,
|
|
cnt->current_image->location.width);
|
|
break;
|
|
|
|
case 'J': // motion height
|
|
sprintf(tempstr, "%*d", width,
|
|
cnt->current_image->location.height);
|
|
break;
|
|
|
|
case 'K': // motion center x
|
|
sprintf(tempstr, "%*d", width, cnt->current_image->location.x);
|
|
break;
|
|
|
|
case 'L': // motion center y
|
|
sprintf(tempstr, "%*d", width, cnt->current_image->location.y);
|
|
break;
|
|
|
|
case 'o': // threshold
|
|
sprintf(tempstr, "%*d", width, cnt->threshold);
|
|
break;
|
|
|
|
case 'Q': // number of labels
|
|
sprintf(tempstr, "%*d", width,
|
|
cnt->current_image->total_labels);
|
|
break;
|
|
|
|
case 't': // camera id
|
|
sprintf(tempstr, "%*d", width, cnt->conf.camera_id);
|
|
break;
|
|
|
|
case 'C': // text_event
|
|
if (cnt->text_event_string[0])
|
|
snprintf(tempstr, PATH_MAX, "%*s", width,
|
|
cnt->text_event_string);
|
|
else
|
|
++pos_userformat;
|
|
break;
|
|
|
|
case 'w': // picture width
|
|
sprintf(tempstr, "%*d", width, cnt->imgs.width);
|
|
break;
|
|
|
|
case 'h': // picture height
|
|
sprintf(tempstr, "%*d", width, cnt->imgs.height);
|
|
break;
|
|
|
|
case 'f': // filename -- or %fps
|
|
if ((*(pos_userformat+1) == 'p') && (*(pos_userformat+2) == 's')) {
|
|
sprintf(tempstr, "%*d", width, cnt->movie_fps);
|
|
pos_userformat += 2;
|
|
break;
|
|
}
|
|
|
|
if (filename)
|
|
snprintf(tempstr, PATH_MAX, "%*s", width, filename);
|
|
else
|
|
++pos_userformat;
|
|
break;
|
|
|
|
case 'n': // sqltype
|
|
if (sqltype)
|
|
sprintf(tempstr, "%*d", width, sqltype);
|
|
else
|
|
++pos_userformat;
|
|
break;
|
|
|
|
case '{': // long format specifier word.
|
|
{
|
|
const char *word = ++pos_userformat;
|
|
while ((*pos_userformat != '}') && (*pos_userformat != 0))
|
|
++pos_userformat;
|
|
mystrftime_long (cnt, width, word, (int)(pos_userformat-word), tempstr);
|
|
if (*pos_userformat == '\0') --pos_userformat;
|
|
}
|
|
break;
|
|
|
|
case '$': // thread name
|
|
if (cnt->conf.camera_name && cnt->conf.camera_name[0])
|
|
snprintf(tempstr, PATH_MAX, "%s", cnt->conf.camera_name);
|
|
else
|
|
++pos_userformat;
|
|
break;
|
|
|
|
default: // Any other code is copied with the %-sign
|
|
*format++ = '%';
|
|
*format++ = *pos_userformat;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* If a format specifier was found and used, copy the result from
|
|
* 'tempstr' to 'format'.
|
|
*/
|
|
if (tempstr[0]) {
|
|
while ((*format = *tempstr++) != '\0')
|
|
++format;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* For any other character than % we just simply copy the character */
|
|
*format++ = *pos_userformat;
|
|
}
|
|
|
|
*format = '\0';
|
|
format = formatstring;
|
|
|
|
return strftime(s, max, format, ×tamp_tm);
|
|
}
|
|
|