Merge h264
This commit is contained in:
commit
c9a1a9020d
|
@ -401,6 +401,59 @@ else(MYSQLCLIENT_LIBRARIES)
|
|||
"ZoneMinder requires mysqlclient but it was not found on your system")
|
||||
endif(MYSQLCLIENT_LIBRARIES)
|
||||
|
||||
# x264 (using find_library and find_path)
|
||||
find_library(X264_LIBRARIES x264)
|
||||
if(X264_LIBRARIES)
|
||||
set(HAVE_LIBX264 1)
|
||||
list(APPEND ZM_BIN_LIBS "${X264_LIBRARIES}")
|
||||
find_path(X264_INCLUDE_DIR x264.h)
|
||||
if(X264_INCLUDE_DIR)
|
||||
include_directories("${X264_INCLUDE_DIR}")
|
||||
set(CMAKE_REQUIRED_INCLUDES "${X264_INCLUDE_DIR}")
|
||||
endif(X264_INCLUDE_DIR)
|
||||
mark_as_advanced(FORCE X264_LIBRARIES X264_INCLUDE_DIR)
|
||||
check_include_files("stdint.h;x264.h" HAVE_X264_H)
|
||||
set(optlibsfound "${optlibsfound} x264")
|
||||
else(X264_LIBRARIES)
|
||||
set(optlibsnotfound "${optlibsnotfound} x264")
|
||||
endif(X264_LIBRARIES)
|
||||
|
||||
# mp4v2 (using find_library and find_path)
|
||||
find_library(MP4V2_LIBRARIES mp4v2)
|
||||
if(MP4V2_LIBRARIES)
|
||||
set(HAVE_LIBMP4V2 1)
|
||||
list(APPEND ZM_BIN_LIBS "${MP4V2_LIBRARIES}")
|
||||
|
||||
# mp4v2/mp4v2.h
|
||||
find_path(MP4V2_INCLUDE_DIR mp4v2/mp4v2.h)
|
||||
if(MP4V2_INCLUDE_DIR)
|
||||
include_directories("${MP4V2_INCLUDE_DIR}")
|
||||
set(CMAKE_REQUIRED_INCLUDES "${MP4V2_INCLUDE_DIR}")
|
||||
endif(MP4V2_INCLUDE_DIR)
|
||||
check_include_file("mp4v2/mp4v2.h" HAVE_MP4V2_MP4V2_H)
|
||||
|
||||
# mp4v2.h
|
||||
find_path(MP4V2_INCLUDE_DIR mp4v2.h)
|
||||
if(MP4V2_INCLUDE_DIR)
|
||||
include_directories("${MP4V2_INCLUDE_DIR}")
|
||||
set(CMAKE_REQUIRED_INCLUDES "${MP4V2_INCLUDE_DIR}")
|
||||
endif(MP4V2_INCLUDE_DIR)
|
||||
check_include_file("mp4v2.h" HAVE_MP4V2_H)
|
||||
|
||||
# mp4.h
|
||||
find_path(MP4V2_INCLUDE_DIR mp4.h)
|
||||
if(MP4V2_INCLUDE_DIR)
|
||||
include_directories("${MP4V2_INCLUDE_DIR}")
|
||||
set(CMAKE_REQUIRED_INCLUDES "${MP4V2_INCLUDE_DIR}")
|
||||
endif(MP4V2_INCLUDE_DIR)
|
||||
check_include_file("mp4.h" HAVE_MP4_H)
|
||||
|
||||
mark_as_advanced(FORCE MP4V2_LIBRARIES MP4V2_INCLUDE_DIR)
|
||||
set(optlibsfound "${optlibsfound} mp4v2")
|
||||
else(MP4V2_LIBRARIES)
|
||||
set(optlibsnotfound "${optlibsnotfound} mp4v2")
|
||||
endif(MP4V2_LIBRARIES)
|
||||
|
||||
set(PATH_FFMPEG "")
|
||||
set(OPT_FFMPEG "no")
|
||||
# Do not check for ffmpeg if ZM_NO_FFMPEG is on
|
||||
|
|
18
README.md
18
README.md
|
@ -1,7 +1,19 @@
|
|||
ZoneMinder
|
||||
==========
|
||||
ZoneMinder H264 Patch
|
||||
|
||||
[![Build Status](https://travis-ci.org/ZoneMinder/ZoneMinder.png)](https://travis-ci.org/ZoneMinder/ZoneMinder) [![Bountysource](https://api.bountysource.com/badge/team?team_id=204&style=bounties_received)](https://www.bountysource.com/teams/zoneminder/issues?utm_source=ZoneMinder&utm_medium=shield&utm_campaign=bounties_received)
|
||||
[![Build Status](https://travis-ci.org/ZoneMinder/ZoneMinder.png?branch=feature-h264-videostorage)](https://travis-ci.org/ZoneMinder/ZoneMinder) [![Bountysource](https://api.bountysource.com/badge/team?team_id=204&style=bounties_received)](https://www.bountysource.com/teams/zoneminder/issues?utm_source=ZoneMinder&utm_medium=shield&utm_campaign=bounties_received)
|
||||
|
||||
##Feature-h264-videostorage Branch Details
|
||||
This branch supports direct recording of h264 cameras into MP4 format uisng the h264 Passthrough option. As well it provides h264 encoding for local or mpeg cameras. If you encounter any issues, please open an issue on GitHub and attach it to the h264 milestone. But do remember this is bleeding edge so it will have problems.
|
||||
Thanks to @chriswiggins and @mastertheknife for their work, @SteveGilvarry is now maintaining this branch and welcomes any assistance.
|
||||
|
||||
**The following SQL changes are required, these will be merged to zmupdate once we are ready to merge this branch to master.**
|
||||
```
|
||||
ALTER TABLE `Monitors` ADD `SaveJPEGs` TINYINT NOT NULL DEFAULT '3' AFTER `Deinterlacing` ,
|
||||
ADD `VideoWriter` TINYINT NOT NULL DEFAULT '0' AFTER `SaveJPEGs` ,
|
||||
ADD `EncoderParameters` TEXT NOT NULL AFTER `VideoWriter` ;
|
||||
|
||||
ALTER TABLE `Events` ADD `DefaultVideo` VARCHAR( 64 ) NOT NULL AFTER `AlarmFrames` ;
|
||||
```
|
||||
|
||||
All documentation for ZoneMinder is now online at https://zoneminder.readthedocs.org
|
||||
|
||||
|
|
|
@ -364,6 +364,7 @@ fi
|
|||
AC_CHECK_LIB(pcre,pcre_compile,,AC_MSG_WARN(libpcre.a may be required for remote/network camera support))
|
||||
AC_CHECK_LIB(z,zlibVersion)
|
||||
AC_CHECK_LIB(x264,x264_predict_16x16_init)
|
||||
AC_CHECK_LIB(mp4v2,MP4AddH264VideoTrack)
|
||||
AC_CHECK_LIB(avutil,av_malloc,,AC_MSG_WARN(libavutil.a may be required for MPEG streaming))
|
||||
# Don't bother to warn about this one
|
||||
AC_CHECK_LIB(avcore,av_image_copy,,)
|
||||
|
@ -425,6 +426,8 @@ AC_CHECK_HEADERS(sys/ipc.h,,,)
|
|||
AC_CHECK_HEADERS(sys/shm.h,,,)
|
||||
fi
|
||||
AC_CHECK_HEADERS(zlib.h,,,)
|
||||
AC_CHECK_HEADERS(x264.h,,,)
|
||||
AC_CHECK_HEADERS([mp4v2/mp4v2.h mp4v2.h mp4.h],,,)
|
||||
AC_CHECK_HEADERS(vlc/vlc.h,,,)
|
||||
AC_CHECK_HEADERS(curl/curl.h,,,)
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@ zoneminder (1.28.1+1-trusty-SNAPSHOT2015071501) trusty; urgency=medium
|
|||
* version upgraded to .100
|
||||
|
||||
-- Isaac Connor <iconnor@connortechnology.com> Wed, 15 Jul 2015 11:56:28 -0400
|
||||
zoneminder (1.28.1+1-trusty-SNAPSHOT2015030201ubuntu1) UNRELEASED; urgency=medium
|
||||
|
||||
* add h264
|
||||
|
||||
-- Isaac Connor <iconnor@connortechnology.com> Fri, 27 Mar 2015 10:08:27 -0400
|
||||
|
||||
zoneminder (1.28.1+1-trusty-SNAPSHOT2015030201) trusty; urgency=medium
|
||||
|
||||
|
|
|
@ -258,5 +258,5 @@ if ( $concat_name ) {
|
|||
}
|
||||
#unlink $input_file_list;
|
||||
#print( STDOUT $event->{MonitorId}.'/'.$event->{Id}.'/'.$video_file."\n" );
|
||||
#print( STDOUT $video_file."\n" );
|
||||
#print( STDOUT $event_path . '/' . $video_file ."\n" );
|
||||
exit( 0 );
|
||||
|
|
|
@ -4,7 +4,51 @@
|
|||
configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY)
|
||||
|
||||
# Group together all the source files that are used by all the binaries (zmc, zma, zmu, zms etc)
|
||||
set(ZM_BIN_SRC_FILES zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_camera.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_ffmpeg.cpp zm_mpeg.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_zone.cpp zm_storage.cpp)
|
||||
set(ZM_BIN_SRC_FILES \
|
||||
zm_box.cpp\
|
||||
zm_buffer.cpp\
|
||||
zm_camera.cpp\
|
||||
zm_comms.cpp\
|
||||
zm_config.cpp\
|
||||
zm_coord.cpp\
|
||||
zm_curl_camera.cpp\
|
||||
zm.cpp\
|
||||
zm_db.cpp\
|
||||
zm_logger.cpp\
|
||||
zm_event.cpp\
|
||||
zm_exception.cpp\
|
||||
zm_file_camera.cpp\
|
||||
zm_ffmpeg_camera.cpp\
|
||||
zm_image.cpp\
|
||||
zm_jpeg.cpp\
|
||||
zm_libvlc_camera.cpp\
|
||||
zm_local_camera.cpp\
|
||||
zm_monitor.cpp\
|
||||
zm_ffmpeg.cpp\
|
||||
zm_mpeg.cpp\
|
||||
zm_poly.cpp\
|
||||
zm_regexp.cpp\
|
||||
zm_remote_camera.cpp\
|
||||
zm_remote_camera_http.cpp\
|
||||
zm_remote_camera_rtsp.cpp\
|
||||
zm_rtp.cpp\
|
||||
zm_rtp_ctrl.cpp\
|
||||
zm_rtp_data.cpp\
|
||||
zm_rtp_source.cpp\
|
||||
zm_rtsp.cpp\
|
||||
zm_rtsp_auth.cpp\
|
||||
zm_sdp.cpp\
|
||||
zm_signal.cpp\
|
||||
zm_stream.cpp\
|
||||
zm_thread.cpp\
|
||||
zm_time.cpp\
|
||||
zm_timer.cpp\
|
||||
zm_user.cpp\
|
||||
zm_utils.cpp\
|
||||
zm_video.cpp\
|
||||
zm_videostore.cpp\
|
||||
zm_zone.cpp\
|
||||
zm_storage.cpp)
|
||||
|
||||
# A fix for cmake recompiling the source files for every target.
|
||||
add_library(zm STATIC ${ZM_BIN_SRC_FILES})
|
||||
|
|
|
@ -61,6 +61,8 @@ zm_SOURCES = \
|
|||
zm_timer.cpp \
|
||||
zm_user.cpp \
|
||||
zm_utils.cpp \
|
||||
zm_video.cpp \
|
||||
zm_videostore.cpp \
|
||||
zm_zone.cpp
|
||||
|
||||
zmc_SOURCES = zmc.cpp $(zm_SOURCES)
|
||||
|
@ -118,6 +120,8 @@ noinst_HEADERS = \
|
|||
zm_timer.h \
|
||||
zm_user.h \
|
||||
zm_utils.h \
|
||||
zm_video.h \
|
||||
zm_videostore.h \
|
||||
zm_zone.h
|
||||
|
||||
EXTRA_DIST = \
|
||||
|
|
|
@ -74,10 +74,13 @@ public:
|
|||
|
||||
bool CanCapture() const { return( capture ); }
|
||||
|
||||
bool SupportsNativeVideo() const { return( (type == FFMPEG_SRC )||(type == REMOTE_SRC)); }
|
||||
|
||||
virtual int PrimeCapture() { return( 0 ); }
|
||||
virtual int PreCapture()=0;
|
||||
virtual int Capture( Image &image )=0;
|
||||
virtual int PostCapture()=0;
|
||||
virtual int CaptureAndRecord( Image &image, bool recording, char* event_directory)=0;
|
||||
};
|
||||
|
||||
#endif // ZM_CAMERA_H
|
||||
|
|
|
@ -311,6 +311,13 @@ int cURLCamera::PostCapture()
|
|||
return( 0 );
|
||||
}
|
||||
|
||||
int cURLCamera::CaptureAndRecord( Image &image, bool recording, char* event_directory )
|
||||
{
|
||||
// Nothing to do here
|
||||
return( 0 );
|
||||
}
|
||||
|
||||
|
||||
size_t cURLCamera::data_callback(void *buffer, size_t size, size_t nmemb, void *userdata)
|
||||
{
|
||||
lock();
|
||||
|
|
|
@ -79,6 +79,8 @@ public:
|
|||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int PostCapture();
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory);
|
||||
|
||||
|
||||
size_t data_callback(void *buffer, size_t size, size_t nmemb, void *userdata);
|
||||
size_t header_callback(void *buffer, size_t size, size_t nmemb, void *userdata);
|
||||
|
|
|
@ -54,15 +54,18 @@ bool Event::initialised = false;
|
|||
char Event::capture_file_format[PATH_MAX];
|
||||
char Event::analyse_file_format[PATH_MAX];
|
||||
char Event::general_file_format[PATH_MAX];
|
||||
char Event::video_file_format[PATH_MAX];
|
||||
|
||||
int Event::pre_alarm_count = 0;
|
||||
Event::PreAlarmData Event::pre_alarm_data[MAX_PRE_ALARM_FRAMES] = { { 0 } };
|
||||
|
||||
Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap ) :
|
||||
Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap, bool p_videoEvent ) :
|
||||
monitor( p_monitor ),
|
||||
start_time( p_start_time ),
|
||||
cause( p_cause ),
|
||||
noteSetMap( p_noteSetMap )
|
||||
noteSetMap( p_noteSetMap ),
|
||||
videoEvent( p_videoEvent ),
|
||||
videowriter( NULL )
|
||||
{
|
||||
if ( !initialised )
|
||||
Initialise();
|
||||
|
@ -82,7 +85,7 @@ Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string
|
|||
static char sql[ZM_SQL_MED_BUFSIZ];
|
||||
|
||||
struct tm *stime = localtime( &start_time.tv_sec );
|
||||
snprintf( sql, sizeof(sql), "insert into Events ( MonitorId, StorageId, Name, StartTime, Width, Height, Cause, Notes ) values ( %d, %d, 'New Event', from_unixtime( %ld ), %d, %d, '%s', '%s' )", monitor->Id(), storage->Id(), start_time.tv_sec, monitor->Width(), monitor->Height(), cause.c_str(), notes.c_str() );
|
||||
snprintf( sql, sizeof(sql), "insert into Events ( MonitorId, StorageId, Name, StartTime, Width, Height, Cause, Notes, Videoed ) values ( %d, %d, 'New Event', from_unixtime( %ld ), %d, %d, '%s', '%s' )", monitor->Id(), storage->Id(), start_time.tv_sec, monitor->Width(), monitor->Height(), cause.c_str(), notes.c_str(), videoEvent );
|
||||
if ( mysql_query( &dbconn, sql ) )
|
||||
{
|
||||
Error( "Can't insert event: %s", mysql_error( &dbconn ) );
|
||||
|
@ -172,6 +175,49 @@ Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string
|
|||
Fatal( "Can't fopen %s: %s", id_file, strerror(errno));
|
||||
}
|
||||
last_db_frame = 0;
|
||||
|
||||
video_name[0] = 0;
|
||||
|
||||
/* Save as video */
|
||||
|
||||
if ( monitor->GetOptVideoWriter() != 0 ) {
|
||||
int nRet;
|
||||
snprintf( video_name, sizeof(video_name), "%d-%s", id, "video.mp4" );
|
||||
snprintf( video_file, sizeof(video_file), video_file_format, path, video_name );
|
||||
snprintf( timecodes_name, sizeof(timecodes_name), "%d-%s", id, "video.timecodes" );
|
||||
snprintf( timecodes_file, sizeof(timecodes_file), video_file_format, path, timecodes_name );
|
||||
|
||||
|
||||
/* X264 MP4 video writer */
|
||||
if(monitor->GetOptVideoWriter() == 1) {
|
||||
#if ZM_HAVE_VIDEOWRITER_X264MP4
|
||||
videowriter = new X264MP4Writer(video_file, monitor->Width(), monitor->Height(), monitor->Colours(), monitor->SubpixelOrder(), monitor->GetOptEncoderParams());
|
||||
#else
|
||||
videowriter = NULL;
|
||||
Error("ZoneMinder was not compiled with the X264 MP4 video writer, check dependencies (x264 and mp4v2)");
|
||||
#endif
|
||||
}
|
||||
|
||||
if(videowriter != NULL) {
|
||||
/* Open the video stream */
|
||||
nRet = videowriter->Open();
|
||||
if(nRet != 0) {
|
||||
Error("Failed opening video stream");
|
||||
delete videowriter;
|
||||
videowriter = NULL;
|
||||
}
|
||||
|
||||
/* Create timecodes file */
|
||||
timecodes_fd = fopen(timecodes_file, "wb");
|
||||
if(timecodes_fd == NULL) {
|
||||
Error("Failed creating timecodes file");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* No video object */
|
||||
videowriter = NULL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Event::~Event()
|
||||
|
@ -191,12 +237,28 @@ Event::~Event()
|
|||
}
|
||||
}
|
||||
|
||||
/* Close the video file */
|
||||
if ( videowriter != NULL ) {
|
||||
int nRet;
|
||||
|
||||
nRet = videowriter->Close();
|
||||
if(nRet != 0) {
|
||||
Error("Failed closing video stream");
|
||||
}
|
||||
delete videowriter;
|
||||
videowriter = NULL;
|
||||
|
||||
/* Close the timecodes file */
|
||||
fclose(timecodes_fd);
|
||||
timecodes_fd = NULL;
|
||||
}
|
||||
|
||||
static char sql[ZM_SQL_MED_BUFSIZ];
|
||||
|
||||
struct DeltaTimeval delta_time;
|
||||
DELTA_TIMEVAL( delta_time, end_time, start_time, DT_PREC_2 );
|
||||
|
||||
snprintf( sql, sizeof(sql), "update Events set Name='%s%d', EndTime = from_unixtime( %ld ), Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d where Id = %d", monitor->EventPrefix(), id, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, id );
|
||||
snprintf( sql, sizeof(sql), "update Events set Name='%s%d', EndTime = from_unixtime( %ld ), Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d, DefaultVideo = '%s' where Id = %d", monitor->EventPrefix(), id, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, video_name, id );
|
||||
if ( mysql_query( &dbconn, sql ) )
|
||||
{
|
||||
Error( "Can't update event: %s", mysql_error( &dbconn ) );
|
||||
|
@ -375,6 +437,40 @@ bool Event::WriteFrameImage( Image *image, struct timeval timestamp, const char
|
|||
return( true );
|
||||
}
|
||||
|
||||
bool Event::WriteFrameVideo( const Image *image, const struct timeval timestamp, VideoWriter* videow )
|
||||
{
|
||||
const Image* frameimg = image;
|
||||
Image ts_image;
|
||||
|
||||
/* Checking for invalid parameters */
|
||||
if ( videow == NULL ) {
|
||||
Error("NULL Video object");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* If the image does not contain a timestamp, add the timestamp */
|
||||
if (!config.timestamp_on_capture) {
|
||||
ts_image = *image;
|
||||
monitor->TimestampImage( &ts_image, ×tamp );
|
||||
frameimg = &ts_image;
|
||||
}
|
||||
|
||||
/* Calculate delta time */
|
||||
struct DeltaTimeval delta_time3;
|
||||
DELTA_TIMEVAL( delta_time3, timestamp, start_time, DT_PREC_3 );
|
||||
unsigned int timeMS = (delta_time3.sec * delta_time3.prec) + delta_time3.fsec;
|
||||
|
||||
/* Encode and write the frame */
|
||||
if(videowriter->Encode(frameimg, timeMS) != 0) {
|
||||
Error("Failed encoding video frame");
|
||||
}
|
||||
|
||||
/* Add the frame to the timecodes file */
|
||||
fprintf(timecodes_fd, "%u\n", timeMS);
|
||||
|
||||
return( true );
|
||||
}
|
||||
|
||||
void Event::updateNotes( const StringSetMap &newNoteSetMap )
|
||||
{
|
||||
bool update = false;
|
||||
|
@ -519,9 +615,21 @@ void Event::AddFramesInternal( int n_frames, int start_frame, Image **images, st
|
|||
|
||||
static char event_file[PATH_MAX];
|
||||
snprintf( event_file, sizeof(event_file), capture_file_format, path, frames );
|
||||
|
||||
Debug( 1, "Writing pre-capture frame %d", frames );
|
||||
WriteFrameImage( images[i], *(timestamps[i]), event_file );
|
||||
if ( monitor->GetOptSaveJPEGs() & 4) {
|
||||
//If this is the first frame, we should add a thumbnail to the event directory
|
||||
if(frames == 10){
|
||||
char snapshot_file[PATH_MAX];
|
||||
snprintf( snapshot_file, sizeof(snapshot_file), "%s/snapshot.jpg", path );
|
||||
WriteFrameImage( images[i], *(timestamps[i]), snapshot_file );
|
||||
}
|
||||
}
|
||||
if ( monitor->GetOptSaveJPEGs() & 1) {
|
||||
Debug( 1, "Writing pre-capture frame %d", frames );
|
||||
WriteFrameImage( images[i], *(timestamps[i]), event_file );
|
||||
}
|
||||
if ( videowriter != NULL ) {
|
||||
WriteFrameVideo( images[i], *(timestamps[i]), videowriter );
|
||||
}
|
||||
|
||||
struct DeltaTimeval delta_time;
|
||||
DELTA_TIMEVAL( delta_time, *(timestamps[i]), start_time, DT_PREC_2 );
|
||||
|
@ -561,9 +669,22 @@ void Event::AddFrame( Image *image, struct timeval timestamp, int score, Image *
|
|||
|
||||
static char event_file[PATH_MAX];
|
||||
snprintf( event_file, sizeof(event_file), capture_file_format, path, frames );
|
||||
|
||||
Debug( 1, "Writing capture frame %d", frames );
|
||||
WriteFrameImage( image, timestamp, event_file );
|
||||
|
||||
if ( monitor->GetOptSaveJPEGs() & 4) {
|
||||
//If this is the first frame, we should add a thumbnail to the event directory
|
||||
if(frames == 10){
|
||||
char snapshot_file[PATH_MAX];
|
||||
snprintf( snapshot_file, sizeof(snapshot_file), "%s/snapshot.jpg", path );
|
||||
WriteFrameImage( image, timestamp, snapshot_file );
|
||||
}
|
||||
}
|
||||
if( monitor->GetOptSaveJPEGs() & 1) {
|
||||
Debug( 1, "Writing capture frame %d", frames );
|
||||
WriteFrameImage( image, timestamp, event_file );
|
||||
}
|
||||
if ( videowriter != NULL ) {
|
||||
WriteFrameVideo( image, timestamp, videowriter );
|
||||
}
|
||||
|
||||
struct DeltaTimeval delta_time;
|
||||
DELTA_TIMEVAL( delta_time, timestamp, start_time, DT_PREC_2 );
|
||||
|
@ -614,7 +735,9 @@ void Event::AddFrame( Image *image, struct timeval timestamp, int score, Image *
|
|||
snprintf( event_file, sizeof(event_file), analyse_file_format, path, frames );
|
||||
|
||||
Debug( 1, "Writing analysis frame %d", frames );
|
||||
WriteFrameImage( alarm_image, timestamp, event_file, true );
|
||||
if ( monitor->GetOptSaveJPEGs() & 2) {
|
||||
WriteFrameImage( alarm_image, timestamp, event_file, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
#include "zm.h"
|
||||
#include "zm_image.h"
|
||||
#include "zm_stream.h"
|
||||
#include "zm_video.h"
|
||||
|
||||
class Zone;
|
||||
class Monitor;
|
||||
|
@ -55,6 +56,7 @@ protected:
|
|||
static char capture_file_format[PATH_MAX];
|
||||
static char analyse_file_format[PATH_MAX];
|
||||
static char general_file_format[PATH_MAX];
|
||||
static char video_file_format[PATH_MAX];
|
||||
|
||||
protected:
|
||||
static int sd;
|
||||
|
@ -84,11 +86,18 @@ protected:
|
|||
struct timeval end_time;
|
||||
std::string cause;
|
||||
StringSetMap noteSetMap;
|
||||
bool videoEvent;
|
||||
int frames;
|
||||
int alarm_frames;
|
||||
unsigned int tot_score;
|
||||
unsigned int max_score;
|
||||
char path[PATH_MAX];
|
||||
VideoWriter* videowriter;
|
||||
FILE* timecodes_fd;
|
||||
char video_name[PATH_MAX];
|
||||
char video_file[PATH_MAX];
|
||||
char timecodes_name[PATH_MAX];
|
||||
char timecodes_file[PATH_MAX];
|
||||
|
||||
protected:
|
||||
int last_db_frame;
|
||||
|
@ -102,6 +111,7 @@ protected:
|
|||
snprintf( capture_file_format, sizeof(capture_file_format), "%%s/%%0%dd-capture.jpg", config.event_image_digits );
|
||||
snprintf( analyse_file_format, sizeof(analyse_file_format), "%%s/%%0%dd-analyse.jpg", config.event_image_digits );
|
||||
snprintf( general_file_format, sizeof(general_file_format), "%%s/%%0%dd-%%s", config.event_image_digits );
|
||||
snprintf( video_file_format, sizeof(video_file_format), "%%s/%%s");
|
||||
|
||||
initialised = true;
|
||||
}
|
||||
|
@ -113,7 +123,7 @@ public:
|
|||
static bool ValidateFrameSocket( int );
|
||||
|
||||
public:
|
||||
Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap );
|
||||
Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap, bool p_videoEvent=false );
|
||||
~Event();
|
||||
|
||||
int Id() const { return( id ); }
|
||||
|
@ -127,6 +137,7 @@ public:
|
|||
|
||||
bool SendFrameImage( const Image *image, bool alarm_frame=false );
|
||||
bool WriteFrameImage( Image *image, struct timeval timestamp, const char *event_file, bool alarm_frame=false );
|
||||
bool WriteFrameVideo( const Image *image, const struct timeval timestamp, VideoWriter* videow );
|
||||
|
||||
void updateNotes( const StringSetMap &stringSetMap );
|
||||
|
||||
|
@ -148,6 +159,10 @@ public:
|
|||
return( Event::getSubPath( localtime( time ) ) );
|
||||
}
|
||||
|
||||
char* getEventFile(void){
|
||||
return video_file;
|
||||
}
|
||||
|
||||
public:
|
||||
static int PreAlarmCount()
|
||||
{
|
||||
|
|
|
@ -23,6 +23,16 @@
|
|||
|
||||
#if HAVE_LIBAVCODEC || HAVE_LIBAVUTIL || HAVE_LIBSWSCALE
|
||||
|
||||
void FFMPEGInit() {
|
||||
static bool bInit = false;
|
||||
|
||||
if(!bInit) {
|
||||
av_register_all();
|
||||
av_log_set_level(AV_LOG_DEBUG);
|
||||
bInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_LIBAVUTIL
|
||||
enum _AVPIXELFORMAT GetFFMPEGPixelFormat(unsigned int p_colours, unsigned p_subpixelorder) {
|
||||
enum _AVPIXELFORMAT pf;
|
||||
|
@ -140,10 +150,7 @@ int SWScale::Convert(const uint8_t* in_buffer, const size_t in_buffer_size, uint
|
|||
Error("NULL Input or output buffer");
|
||||
return -1;
|
||||
}
|
||||
if(in_pf == 0 || out_pf == 0) {
|
||||
Error("Invalid input or output pixel formats");
|
||||
return -2;
|
||||
}
|
||||
|
||||
if(!width || !height) {
|
||||
Error("Invalid width or height");
|
||||
return -3;
|
||||
|
@ -172,7 +179,7 @@ int SWScale::Convert(const uint8_t* in_buffer, const size_t in_buffer_size, uint
|
|||
}
|
||||
|
||||
/* Get the context */
|
||||
swscale_ctx = sws_getCachedContext( NULL, width, height, in_pf, width, height, out_pf, 0, NULL, NULL, NULL );
|
||||
swscale_ctx = sws_getCachedContext( swscale_ctx, width, height, in_pf, width, height, out_pf, SWS_FAST_BILINEAR, NULL, NULL, NULL );
|
||||
if(swscale_ctx == NULL) {
|
||||
Error("Failed getting swscale context");
|
||||
return -6;
|
||||
|
@ -233,3 +240,81 @@ int SWScale::ConvertDefaults(const uint8_t* in_buffer, const size_t in_buffer_si
|
|||
#endif // HAVE_LIBSWSCALE && HAVE_LIBAVUTIL
|
||||
|
||||
#endif // HAVE_LIBAVCODEC || HAVE_LIBAVUTIL || HAVE_LIBSWSCALE
|
||||
|
||||
#if HAVE_LIBAVUTIL
|
||||
int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb, int duration, int64_t *last, AVRational out_tb){
|
||||
int64_t a, b, this_thing;
|
||||
|
||||
av_assert0(in_ts != AV_NOPTS_VALUE);
|
||||
av_assert0(duration >= 0);
|
||||
|
||||
if (*last == AV_NOPTS_VALUE || !duration || in_tb.num*(int64_t)out_tb.den <= out_tb.num*(int64_t)in_tb.den) {
|
||||
simple_round:
|
||||
*last = av_rescale_q(in_ts, in_tb, fs_tb) + duration;
|
||||
return av_rescale_q(in_ts, in_tb, out_tb);
|
||||
}
|
||||
|
||||
a = av_rescale_q_rnd(2*in_ts-1, in_tb, fs_tb, AV_ROUND_DOWN) >>1;
|
||||
b = (av_rescale_q_rnd(2*in_ts+1, in_tb, fs_tb, AV_ROUND_UP )+1)>>1;
|
||||
if (*last < 2*a - b || *last > 2*b - a)
|
||||
goto simple_round;
|
||||
|
||||
this_thing = av_clip64(*last, a, b);
|
||||
*last = this_thing + duration;
|
||||
|
||||
return av_rescale_q(this_thing, fs_tb, out_tb);
|
||||
}
|
||||
#endif
|
||||
|
||||
int hacked_up_context2_for_older_ffmpeg(AVFormatContext **avctx, AVOutputFormat *oformat, const char *format, const char *filename) {
|
||||
AVFormatContext *s = avformat_alloc_context();
|
||||
int ret = 0;
|
||||
|
||||
*avctx = NULL;
|
||||
if (!s) {
|
||||
av_log(s, AV_LOG_ERROR, "Out of memory\n");
|
||||
ret = AVERROR(ENOMEM);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!oformat) {
|
||||
if (format) {
|
||||
oformat = av_guess_format(format, NULL, NULL);
|
||||
if (!oformat) {
|
||||
av_log(s, AV_LOG_ERROR, "Requested output format '%s' is not a suitable output format\n", format);
|
||||
ret = AVERROR(EINVAL);
|
||||
}
|
||||
} else {
|
||||
oformat = av_guess_format(NULL, filename, NULL);
|
||||
if (!oformat) {
|
||||
ret = AVERROR(EINVAL);
|
||||
av_log(s, AV_LOG_ERROR, "Unable to find a suitable output format for '%s'\n", filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
avformat_free_context(s);
|
||||
return ret;
|
||||
} else {
|
||||
s->oformat = oformat;
|
||||
if (s->oformat->priv_data_size > 0) {
|
||||
s->priv_data = av_mallocz(s->oformat->priv_data_size);
|
||||
if (s->priv_data) {
|
||||
if (s->oformat->priv_class) {
|
||||
*(const AVClass**)s->priv_data= s->oformat->priv_class;
|
||||
av_opt_set_defaults(s->priv_data);
|
||||
}
|
||||
} else {
|
||||
av_log(s, AV_LOG_ERROR, "Out of memory\n");
|
||||
ret = AVERROR(ENOMEM);
|
||||
return ret;
|
||||
}
|
||||
s->priv_data = NULL;
|
||||
}
|
||||
|
||||
if (filename) strncpy(s->filename, filename, sizeof(s->filename));
|
||||
*avctx = s;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ extern "C" {
|
|||
|
||||
// AVUTIL
|
||||
#if HAVE_LIBAVUTIL_AVUTIL_H
|
||||
#include "libavutil/avassert.h"
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/base64.h>
|
||||
#include <libavutil/mathematics.h>
|
||||
|
@ -194,6 +195,9 @@ extern "C" {
|
|||
#endif
|
||||
#endif
|
||||
|
||||
/* A single function to initialize ffmpeg, to avoid multiple initializations */
|
||||
void FFMPEGInit();
|
||||
|
||||
#if HAVE_LIBAVUTIL
|
||||
enum _AVPIXELFORMAT GetFFMPEGPixelFormat(unsigned int p_colours, unsigned p_subpixelorder);
|
||||
#endif // HAVE_LIBAVUTIL
|
||||
|
@ -275,4 +279,34 @@ protected:
|
|||
|
||||
#endif // ( HAVE_LIBAVUTIL_AVUTIL_H || HAVE_LIBAVCODEC_AVCODEC_H || HAVE_LIBAVFORMAT_AVFORMAT_H || HAVE_LIBAVDEVICE_AVDEVICE_H )
|
||||
|
||||
#ifndef avformat_alloc_output_context2
|
||||
int hacked_up_context2_for_older_ffmpeg(AVFormatContext **avctx, AVOutputFormat *oformat, const char *format, const char *filename);
|
||||
#define avformat_alloc_output_context2(x,y,z,a) hacked_up_context2_for_older_ffmpeg(x,y,z,a)
|
||||
#endif
|
||||
|
||||
#ifndef av_rescale_delta
|
||||
/**
|
||||
* Rescale a timestamp while preserving known durations.
|
||||
*/
|
||||
int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb, int duration, int64_t *last, AVRational out_tb);
|
||||
#endif
|
||||
|
||||
#ifndef av_clip64
|
||||
/**
|
||||
* Clip a signed 64bit integer value into the amin-amax range.
|
||||
* @param a value to clip
|
||||
* @param amin minimum value of the clip range
|
||||
* @param amax maximum value of the clip range
|
||||
* @return clipped value
|
||||
*/
|
||||
static av_always_inline av_const int64_t av_clip64_c(int64_t a, int64_t amin, int64_t amax)
|
||||
{
|
||||
if (a < amin) return amin;
|
||||
else if (a > amax) return amax;
|
||||
else return a;
|
||||
}
|
||||
|
||||
#define av_clip64 av_clip64_c
|
||||
#endif
|
||||
|
||||
#endif // ZM_FFMPEG_H
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
|
||||
#include "zm_ffmpeg_camera.h"
|
||||
|
||||
extern "C"{
|
||||
#include "libavutil/time.h"
|
||||
}
|
||||
#ifndef AV_ERROR_MAX_STRING_SIZE
|
||||
#define AV_ERROR_MAX_STRING_SIZE 64
|
||||
#endif
|
||||
|
@ -46,15 +49,18 @@ FfmpegCamera::FfmpegCamera( int p_id, const std::string &p_path, const std::stri
|
|||
|
||||
mFormatContext = NULL;
|
||||
mVideoStreamId = -1;
|
||||
mAudioStreamId = -1;
|
||||
mCodecContext = NULL;
|
||||
mCodec = NULL;
|
||||
mRawFrame = NULL;
|
||||
mFrame = NULL;
|
||||
frameCount = 0;
|
||||
startTime=0;
|
||||
mIsOpening = false;
|
||||
mCanCapture = false;
|
||||
mOpenStart = 0;
|
||||
mReopenThread = 0;
|
||||
wasRecording = false;
|
||||
|
||||
#if HAVE_LIBSWSCALE
|
||||
mConvertContext = NULL;
|
||||
|
@ -101,6 +107,8 @@ void FfmpegCamera::Terminate()
|
|||
|
||||
int FfmpegCamera::PrimeCapture()
|
||||
{
|
||||
mVideoStreamId = -1;
|
||||
mAudioStreamId = -1;
|
||||
Info( "Priming capture from %s", mPath.c_str() );
|
||||
|
||||
if (OpenFfmpeg() != 0){
|
||||
|
@ -284,6 +292,12 @@ int FfmpegCamera::OpenFfmpeg() {
|
|||
mIsOpening = false;
|
||||
Debug ( 1, "Opened input" );
|
||||
|
||||
Info( "Stream open %s", mPath.c_str() );
|
||||
startTime=av_gettime();//FIXME here or after find_Stream_info
|
||||
|
||||
//FIXME can speed up initial analysis but need sensible parameters...
|
||||
//mFormatContext->probesize = 32;
|
||||
//mFormatContext->max_analyze_duration = 32;
|
||||
// Locate stream info from avformat_open_input
|
||||
#if !LIBAVFORMAT_VERSION_CHECK(53, 6, 0, 6, 0)
|
||||
Debug ( 1, "Calling av_find_stream_info" );
|
||||
|
@ -305,9 +319,20 @@ int FfmpegCamera::OpenFfmpeg() {
|
|||
#else
|
||||
if ( mFormatContext->streams[i]->codec->codec_type == CODEC_TYPE_VIDEO )
|
||||
#endif
|
||||
{
|
||||
mVideoStreamId = i;
|
||||
break;
|
||||
}
|
||||
if(mAudioStreamId == -1) //FIXME best way to copy all other streams?
|
||||
{
|
||||
mVideoStreamId = i;
|
||||
break;
|
||||
#if (LIBAVCODEC_VERSION_CHECK(52, 64, 0, 64, 0) || LIBAVUTIL_VERSION_CHECK(50, 14, 0, 14, 0))
|
||||
if ( mFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO )
|
||||
#else
|
||||
if ( mFormatContext->streams[i]->codec->codec_type == CODEC_TYPE_AUDIO )
|
||||
#endif
|
||||
{
|
||||
mAudioStreamId = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( mVideoStreamId == -1 )
|
||||
|
@ -470,4 +495,146 @@ void *FfmpegCamera::ReopenFfmpegThreadCallback(void *ctx){
|
|||
}
|
||||
}
|
||||
|
||||
//Function to handle capture and store
|
||||
int FfmpegCamera::CaptureAndRecord( Image &image, bool recording, char* event_file )
|
||||
{
|
||||
if (!mCanCapture){
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If the reopen thread has a value, but mCanCapture != 0, then we have just reopened the connection to the ffmpeg device, and we can clean up the thread.
|
||||
if (mReopenThread != 0) {
|
||||
void *retval = 0;
|
||||
int ret;
|
||||
|
||||
ret = pthread_join(mReopenThread, &retval);
|
||||
if (ret != 0){
|
||||
Error("Could not join reopen thread.");
|
||||
}
|
||||
|
||||
Info( "Successfully reopened stream." );
|
||||
mReopenThread = 0;
|
||||
}
|
||||
|
||||
AVPacket packet;
|
||||
uint8_t* directbuffer;
|
||||
|
||||
/* Request a writeable buffer of the target image */
|
||||
directbuffer = image.WriteBuffer(width, height, colours, subpixelorder);
|
||||
if(directbuffer == NULL) {
|
||||
Error("Failed requesting writeable buffer for the captured image.");
|
||||
return (-1);
|
||||
}
|
||||
|
||||
int frameComplete = false;
|
||||
while ( !frameComplete )
|
||||
{
|
||||
int avResult = av_read_frame( mFormatContext, &packet );
|
||||
if ( avResult < 0 )
|
||||
{
|
||||
char errbuf[AV_ERROR_MAX_STRING_SIZE];
|
||||
av_strerror(avResult, errbuf, AV_ERROR_MAX_STRING_SIZE);
|
||||
if (
|
||||
// Check if EOF.
|
||||
(avResult == AVERROR_EOF || (mFormatContext->pb && mFormatContext->pb->eof_reached)) ||
|
||||
// Check for Connection failure.
|
||||
(avResult == -110)
|
||||
)
|
||||
{
|
||||
Info( "av_read_frame returned \"%s\". Reopening stream.", errbuf);
|
||||
ReopenFfmpeg();
|
||||
}
|
||||
|
||||
Error( "Unable to read packet from stream %d: error %d \"%s\".", packet.stream_index, avResult, errbuf );
|
||||
return( -1 );
|
||||
}
|
||||
Debug( 5, "Got packet from stream %d", packet.stream_index );
|
||||
if ( packet.stream_index == mVideoStreamId )
|
||||
{
|
||||
#if LIBAVCODEC_VERSION_CHECK(52, 23, 0, 23, 0)
|
||||
if ( avcodec_decode_video2( mCodecContext, mRawFrame, &frameComplete, &packet ) < 0 )
|
||||
#else
|
||||
if ( avcodec_decode_video( mCodecContext, mRawFrame, &frameComplete, packet.data, packet.size ) < 0 )
|
||||
#endif
|
||||
Fatal( "Unable to decode frame at frame %d", frameCount );
|
||||
|
||||
Debug( 4, "Decoded video packet at frame %d", frameCount );
|
||||
|
||||
if ( frameComplete )
|
||||
{
|
||||
Debug( 3, "Got frame %d", frameCount );
|
||||
|
||||
avpicture_fill( (AVPicture *)mFrame, directbuffer, imagePixFormat, width, height);
|
||||
|
||||
//Keep the last keyframe so we can establish immediate video
|
||||
/*if(packet.flags & AV_PKT_FLAG_KEY)
|
||||
av_copy_packet(&lastKeyframePkt, &packet);*/
|
||||
//TODO I think we need to store the key frame location for seeking as part of the event
|
||||
|
||||
//Video recording
|
||||
if(recording && !wasRecording){
|
||||
//Instantiate the video storage module
|
||||
|
||||
videoStore = new VideoStore((const char *)event_file, "mp4", mFormatContext->streams[mVideoStreamId],mAudioStreamId==-1?NULL:mFormatContext->streams[mAudioStreamId],startTime);
|
||||
wasRecording = true;
|
||||
strcpy(oldDirectory, event_file);
|
||||
|
||||
}else if(!recording && wasRecording && videoStore){
|
||||
Info("Deleting videoStore instance");
|
||||
delete videoStore;
|
||||
videoStore = NULL;
|
||||
}
|
||||
|
||||
//The directory we are recording to is no longer tied to the current event. Need to re-init the videostore with the correct directory and start recording again
|
||||
if(recording && wasRecording && (strcmp(oldDirectory, event_file)!=0) && (packet.flags & AV_PKT_FLAG_KEY) ){ //don't open new videostore until we're on a key frame..would this require an offset adjustment for the event as a result?...if we store our key frame location with the event will that be enough?
|
||||
Info("Re-starting video storage module");
|
||||
if(videoStore){
|
||||
delete videoStore;
|
||||
videoStore = NULL;
|
||||
}
|
||||
|
||||
videoStore = new VideoStore((const char *)event_file, "mp4", mFormatContext->streams[mVideoStreamId],mAudioStreamId==-1?NULL:mFormatContext->streams[mAudioStreamId],startTime);
|
||||
strcpy(oldDirectory, event_file);
|
||||
}
|
||||
|
||||
if(videoStore && recording){
|
||||
//Write the packet to our video store
|
||||
int ret = videoStore->writeVideoFramePacket(&packet, mFormatContext->streams[mVideoStreamId]);//, &lastKeyframePkt);
|
||||
if(ret<0){//Less than zero and we skipped a frame
|
||||
av_free_packet( &packet );
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_LIBSWSCALE
|
||||
if(mConvertContext == NULL) {
|
||||
mConvertContext = sws_getContext( mCodecContext->width, mCodecContext->height, mCodecContext->pix_fmt, width, height, imagePixFormat, SWS_BICUBIC, NULL, NULL, NULL );
|
||||
if(mConvertContext == NULL)
|
||||
Fatal( "Unable to create conversion context for %s", mPath.c_str() );
|
||||
}
|
||||
|
||||
if ( sws_scale( mConvertContext, mRawFrame->data, mRawFrame->linesize, 0, mCodecContext->height, mFrame->data, mFrame->linesize ) < 0 )
|
||||
Fatal( "Unable to convert raw format %u to target format %u at frame %d", mCodecContext->pix_fmt, imagePixFormat, frameCount );
|
||||
#else // HAVE_LIBSWSCALE
|
||||
Fatal( "You must compile ffmpeg with the --enable-swscale option to use ffmpeg cameras" );
|
||||
#endif // HAVE_LIBSWSCALE
|
||||
|
||||
frameCount++;
|
||||
}
|
||||
}else if(packet.stream_index == mAudioStreamId){//FIXME best way to copy all other streams
|
||||
if(videoStore && recording){
|
||||
//Write the packet to our video store
|
||||
int ret = videoStore->writeAudioFramePacket(&packet, mFormatContext->streams[packet.stream_index]); //FIXME no relevance of last key frame
|
||||
if(ret<0){//Less than zero and we skipped a frame
|
||||
av_free_packet( &packet );
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
av_free_packet( &packet );
|
||||
}
|
||||
return (frameCount);
|
||||
}
|
||||
|
||||
|
||||
#endif // HAVE_LIBAVFORMAT
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include "zm_buffer.h"
|
||||
//#include "zm_utils.h"
|
||||
#include "zm_ffmpeg.h"
|
||||
#include "zm_videostore.h"
|
||||
|
||||
//
|
||||
// Class representing 'ffmpeg' cameras, i.e. those which are
|
||||
|
@ -42,6 +43,7 @@ protected:
|
|||
#if HAVE_LIBAVFORMAT
|
||||
AVFormatContext *mFormatContext;
|
||||
int mVideoStreamId;
|
||||
int mAudioStreamId;
|
||||
AVCodecContext *mCodecContext;
|
||||
AVCodec *mCodec;
|
||||
AVFrame *mRawFrame;
|
||||
|
@ -58,11 +60,18 @@ protected:
|
|||
int mOpenStart;
|
||||
pthread_t mReopenThread;
|
||||
#endif // HAVE_LIBAVFORMAT
|
||||
|
||||
bool wasRecording;
|
||||
VideoStore *videoStore;
|
||||
char oldDirectory[4096];
|
||||
//AVPacket lastKeyframePkt;
|
||||
|
||||
#if HAVE_LIBSWSCALE
|
||||
struct SwsContext *mConvertContext;
|
||||
#endif
|
||||
|
||||
int64_t startTime;
|
||||
|
||||
public:
|
||||
FfmpegCamera( int p_id, const std::string &path, const std::string &p_method, const std::string &p_options, int p_width, int p_height, int p_colours, int p_brightness, int p_contrast, int p_hue, int p_colour, bool p_capture );
|
||||
~FfmpegCamera();
|
||||
|
@ -77,6 +86,7 @@ public:
|
|||
int PrimeCapture();
|
||||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory );
|
||||
int PostCapture();
|
||||
};
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ public:
|
|||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int PostCapture();
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory ) {return(0);};
|
||||
};
|
||||
|
||||
#endif // ZM_FILE_CAMERA_H
|
||||
|
|
|
@ -211,6 +211,20 @@ int LibvlcCamera::Capture( Image &image )
|
|||
return (0);
|
||||
}
|
||||
|
||||
// Should not return -1 as cancels capture. Always wait for image if available.
|
||||
int LibvlcCamera::CaptureAndRecord( Image &image, bool recording, char* event_directory )
|
||||
{
|
||||
while(!mLibvlcData.newImage.getValueImmediate())
|
||||
mLibvlcData.newImage.getUpdatedValue(1);
|
||||
|
||||
mLibvlcData.mutex.lock();
|
||||
image.Assign(width, height, colours, subpixelorder, mLibvlcData.buffer, width * height * mBpp);
|
||||
mLibvlcData.newImage.setValueImmediate(false);
|
||||
mLibvlcData.mutex.unlock();
|
||||
|
||||
return (0);
|
||||
}
|
||||
|
||||
int LibvlcCamera::PostCapture()
|
||||
{
|
||||
return(0);
|
||||
|
|
|
@ -70,6 +70,7 @@ public:
|
|||
int PrimeCapture();
|
||||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory );
|
||||
int PostCapture();
|
||||
};
|
||||
|
||||
|
|
|
@ -138,6 +138,7 @@ public:
|
|||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int PostCapture();
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory ) {return(0);};
|
||||
|
||||
static bool GetCurrentSettings( const char *device, char *output, int version, bool verbose );
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
#include "zm_mpeg.h"
|
||||
#include "zm_signal.h"
|
||||
#include "zm_monitor.h"
|
||||
#include "zm_video.h"
|
||||
#if ZM_HAS_V4L
|
||||
#include "zm_local_camera.h"
|
||||
#endif // ZM_HAS_V4L
|
||||
|
@ -277,6 +278,9 @@ Monitor::Monitor(
|
|||
Camera *p_camera,
|
||||
int p_orientation,
|
||||
unsigned int p_deinterlacing,
|
||||
int p_savejpegs,
|
||||
int p_videowriter,
|
||||
std::string p_encoderparams,
|
||||
const char *p_event_prefix,
|
||||
const char *p_label_format,
|
||||
const Coord &p_label_coord,
|
||||
|
@ -312,6 +316,9 @@ Monitor::Monitor(
|
|||
height( (p_orientation==ROTATE_90||p_orientation==ROTATE_270)?p_camera->Width():p_camera->Height() ),
|
||||
orientation( (Orientation)p_orientation ),
|
||||
deinterlacing( p_deinterlacing ),
|
||||
savejpegspref( p_savejpegs ),
|
||||
videowriterpref( p_videowriter ),
|
||||
encoderparams( p_encoderparams ),
|
||||
label_coord( p_label_coord ),
|
||||
label_size( p_label_size ),
|
||||
image_buffer_count( p_image_buffer_count ),
|
||||
|
@ -367,6 +374,9 @@ Monitor::Monitor(
|
|||
}
|
||||
}
|
||||
|
||||
/* Parse encoder parameters */
|
||||
ParseEncoderParameters(encoderparams.c_str(), &encoderparamsvec);
|
||||
|
||||
fps = 0.0;
|
||||
event_count = 0;
|
||||
image_count = 0;
|
||||
|
@ -393,6 +403,7 @@ Monitor::Monitor(
|
|||
|
||||
mem_size = sizeof(SharedData)
|
||||
+ sizeof(TriggerData)
|
||||
+ sizeof(VideoStoreData) //Information to pass back to the capture process
|
||||
+ (image_buffer_count*sizeof(struct timeval))
|
||||
+ (image_buffer_count*camera->ImageSize())
|
||||
+ 64; /* Padding used to permit aligning the images buffer to 16 byte boundary */
|
||||
|
@ -428,6 +439,10 @@ Monitor::Monitor(
|
|||
trigger_data->trigger_text[0] = 0;
|
||||
trigger_data->trigger_showtext[0] = 0;
|
||||
shared_data->valid = true;
|
||||
video_store_data->recording = false;
|
||||
snprintf(video_store_data->event_file, sizeof(video_store_data->event_file), "nothing");
|
||||
video_store_data->size = sizeof(VideoStoreData);
|
||||
//video_store_data->frameNumber = 0;
|
||||
} else if ( purpose == ANALYSIS ) {
|
||||
this->connect();
|
||||
if ( ! mem_ptr ) exit(-1);
|
||||
|
@ -571,7 +586,8 @@ bool Monitor::connect() {
|
|||
#endif // ZM_MEM_MAPPED
|
||||
shared_data = (SharedData *)mem_ptr;
|
||||
trigger_data = (TriggerData *)((char *)shared_data + sizeof(SharedData));
|
||||
struct timeval *shared_timestamps = (struct timeval *)((char *)trigger_data + sizeof(TriggerData));
|
||||
video_store_data = (VideoStoreData *)((char *)trigger_data + sizeof(TriggerData));
|
||||
struct timeval *shared_timestamps = (struct timeval *)((char *)video_store_data + sizeof(VideoStoreData));
|
||||
unsigned char *shared_images = (unsigned char *)((char *)shared_timestamps + (image_buffer_count*sizeof(struct timeval)));
|
||||
|
||||
if(((unsigned long)shared_images % 16) != 0) {
|
||||
|
@ -621,10 +637,10 @@ Monitor::~Monitor()
|
|||
delete[] images;
|
||||
images = 0;
|
||||
}
|
||||
if ( privacy_bitmask ) {
|
||||
delete[] privacy_bitmask;
|
||||
privacy_bitmask = NULL;
|
||||
}
|
||||
if ( privacy_bitmask ) {
|
||||
delete[] privacy_bitmask;
|
||||
privacy_bitmask = NULL;
|
||||
}
|
||||
if ( mem_ptr ) {
|
||||
if ( event )
|
||||
Info( "%s: %03d - Closing event %d, shutting down", name, image_count, event->Id() );
|
||||
|
@ -1412,6 +1428,10 @@ bool Monitor::Analyse()
|
|||
{
|
||||
bool signal = shared_data->signal;
|
||||
bool signal_change = (signal != last_signal);
|
||||
|
||||
//Set video recording flag for event start constructor and easy reference in code
|
||||
bool videoRecording = ((GetOptVideoWriter() == 2) && camera->SupportsNativeVideo());
|
||||
|
||||
if ( trigger_data->trigger_state != TRIGGER_OFF )
|
||||
{
|
||||
unsigned int score = 0;
|
||||
|
@ -1523,10 +1543,15 @@ bool Monitor::Analyse()
|
|||
if ( noteSet.size() > 0 )
|
||||
noteSetMap[LINKED_CAUSE] = noteSet;
|
||||
}
|
||||
|
||||
//TODO: What happens is the event closes and sets recording to false then recording to true again so quickly that our capture daemon never picks it up. Maybe need a refresh flag?
|
||||
if ( (!signal_change && signal) && (function == RECORD || function == MOCORD) )
|
||||
{
|
||||
if ( event )
|
||||
{
|
||||
//TODO: We shouldn't have to do this every time. Not sure why it clears itself if this isn't here??
|
||||
snprintf(video_store_data->event_file, sizeof(video_store_data->event_file), "%s", event->getEventFile());
|
||||
|
||||
int section_mod = timestamp->tv_sec%section_length;
|
||||
if ( section_mod < last_section_mod )
|
||||
{
|
||||
|
@ -1552,9 +1577,12 @@ bool Monitor::Analyse()
|
|||
{
|
||||
|
||||
// Create event
|
||||
event = new Event( this, *timestamp, "Continuous", noteSetMap );
|
||||
event = new Event( this, *timestamp, "Continuous", noteSetMap, videoRecording );
|
||||
shared_data->last_event = event->Id();
|
||||
|
||||
//set up video store data
|
||||
snprintf(video_store_data->event_file, sizeof(video_store_data->event_file), "%s", event->getEventFile());
|
||||
video_store_data->recording = true;
|
||||
|
||||
Info( "%s: %03d - Opening new event %d, section start", name, image_count, event->Id() );
|
||||
|
||||
/* To prevent cancelling out an existing alert\prealarm\alarm state */
|
||||
|
@ -1673,6 +1701,9 @@ bool Monitor::Analyse()
|
|||
event = new Event( this, *(image_buffer[pre_index].timestamp), cause, noteSetMap );
|
||||
}
|
||||
shared_data->last_event = event->Id();
|
||||
//set up video store data
|
||||
snprintf(video_store_data->event_file, sizeof(video_store_data->event_file), "%s", event->getEventFile());
|
||||
video_store_data->recording = true;
|
||||
|
||||
Info( "%s: %03d - Opening new event %d, alarm start", name, image_count, event->Id() );
|
||||
|
||||
|
@ -1819,6 +1850,11 @@ bool Monitor::Analyse()
|
|||
}
|
||||
else if ( state == TAPE )
|
||||
{
|
||||
//Video Storage: activate only for supported cameras. Event::AddFrame knows whether or not we are recording video and saves frames accordingly
|
||||
if((GetOptVideoWriter() == 2) && camera->SupportsNativeVideo())
|
||||
{
|
||||
video_store_data->recording = true;
|
||||
}
|
||||
if ( !(image_count%(frame_skip+1)) )
|
||||
{
|
||||
if ( config.bulk_frame_interval > 1 )
|
||||
|
@ -2074,7 +2110,7 @@ void Monitor::ReloadLinkedMonitors( const char *p_linked_monitors )
|
|||
#if ZM_HAS_V4L
|
||||
int Monitor::LoadLocalMonitors( const char *device, Monitor **&monitors, Purpose purpose )
|
||||
{
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, Method, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, SignalCheckColour, Exif from Monitors where Function != 'None' and Type = 'Local'";
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, Method, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, SaveJPEGs, VideoWriter, EncoderParameters, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, SignalCheckColour, Exif from Monitors where Function != 'None' and Type = 'Local'";
|
||||
;
|
||||
if ( device[0] ) {
|
||||
sql += " AND Device='";
|
||||
|
@ -2136,6 +2172,11 @@ Debug( 1, "Got %d for v4l_captures_per_frame", v4l_captures_per_frame );
|
|||
int palette = atoi(dbrow[col]); col++;
|
||||
Orientation orientation = (Orientation)atoi(dbrow[col]); col++;
|
||||
unsigned int deinterlacing = atoi(dbrow[col]); col++;
|
||||
|
||||
int savejpegs = atoi(dbrow[col]); col++;
|
||||
int videowriter = atoi(dbrow[col]); col++;
|
||||
std::string encoderparams = dbrow[col]; col++;
|
||||
|
||||
int brightness = atoi(dbrow[col]); col++;
|
||||
int contrast = atoi(dbrow[col]); col++;
|
||||
int hue = atoi(dbrow[col]); col++;
|
||||
|
@ -2210,6 +2251,9 @@ Debug( 1, "Got %d for v4l_captures_per_frame", v4l_captures_per_frame );
|
|||
camera,
|
||||
orientation,
|
||||
deinterlacing,
|
||||
savejpegs,
|
||||
videowriter,
|
||||
encoderparams,
|
||||
event_prefix,
|
||||
label_format,
|
||||
Coord( label_x, label_y ),
|
||||
|
@ -2257,7 +2301,7 @@ Debug( 1, "Got %d for v4l_captures_per_frame", v4l_captures_per_frame );
|
|||
|
||||
int Monitor::LoadRemoteMonitors( const char *protocol, const char *host, const char *port, const char *path, Monitor **&monitors, Purpose purpose )
|
||||
{
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Protocol, Method, Host, Port, Path, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif from Monitors where Function != 'None' and Type = 'Remote'";
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Protocol, Method, Host, Port, Path, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, SaveJPEGs, VideoWriter, EncoderParameters, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif from Monitors where Function != 'None' and Type = 'Remote'";
|
||||
if ( staticConfig.SERVER_ID ) {
|
||||
sql += stringtf( " AND ServerId=%d", staticConfig.SERVER_ID );
|
||||
}
|
||||
|
@ -2301,6 +2345,10 @@ int Monitor::LoadRemoteMonitors( const char *protocol, const char *host, const c
|
|||
Orientation orientation = (Orientation)atoi(dbrow[col]); col++;
|
||||
unsigned int deinterlacing = atoi(dbrow[col]); col++;
|
||||
bool rtsp_describe = (*dbrow[col] != '0'); col++;
|
||||
int savejpegs = atoi(dbrow[col]); col++;
|
||||
int videowriter = atoi(dbrow[col]); col++;
|
||||
std::string encoderparams = dbrow[col]; col++;
|
||||
|
||||
int brightness = atoi(dbrow[col]); col++;
|
||||
int contrast = atoi(dbrow[col]); col++;
|
||||
int hue = atoi(dbrow[col]); col++;
|
||||
|
@ -2391,6 +2439,9 @@ int Monitor::LoadRemoteMonitors( const char *protocol, const char *host, const c
|
|||
camera,
|
||||
orientation,
|
||||
deinterlacing,
|
||||
savejpegs,
|
||||
videowriter,
|
||||
encoderparams,
|
||||
event_prefix.c_str(),
|
||||
label_format.c_str(),
|
||||
Coord( label_x, label_y ),
|
||||
|
@ -2438,7 +2489,7 @@ int Monitor::LoadRemoteMonitors( const char *protocol, const char *host, const c
|
|||
|
||||
int Monitor::LoadFileMonitors( const char *file, Monitor **&monitors, Purpose purpose )
|
||||
{
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Path, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif from Monitors where Function != 'None' and Type = 'File'";
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Path, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, SaveJPEGs, VideoWriter, EncoderParameters, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif from Monitors where Function != 'None' and Type = 'File'";
|
||||
if ( file[0] ) {
|
||||
sql += " AND Path='";
|
||||
sql += file;
|
||||
|
@ -2478,6 +2529,11 @@ int Monitor::LoadFileMonitors( const char *file, Monitor **&monitors, Purpose pu
|
|||
/* int palette = atoi(dbrow[col]); */ col++;
|
||||
Orientation orientation = (Orientation)atoi(dbrow[col]); col++;
|
||||
unsigned int deinterlacing = atoi(dbrow[col]); col++;
|
||||
|
||||
int savejpegs = atoi(dbrow[col]); col++;
|
||||
int videowriter = atoi(dbrow[col]); col++;
|
||||
std::string encoderparams = dbrow[col]; col++;
|
||||
|
||||
int brightness = atoi(dbrow[col]); col++;
|
||||
int contrast = atoi(dbrow[col]); col++;
|
||||
int hue = atoi(dbrow[col]); col++;
|
||||
|
@ -2536,6 +2592,9 @@ int Monitor::LoadFileMonitors( const char *file, Monitor **&monitors, Purpose pu
|
|||
camera,
|
||||
orientation,
|
||||
deinterlacing,
|
||||
savejpegs,
|
||||
videowriter,
|
||||
encoderparams,
|
||||
event_prefix,
|
||||
label_format,
|
||||
Coord( label_x, label_y ),
|
||||
|
@ -2583,7 +2642,7 @@ int Monitor::LoadFileMonitors( const char *file, Monitor **&monitors, Purpose pu
|
|||
#if HAVE_LIBAVFORMAT
|
||||
int Monitor::LoadFfmpegMonitors( const char *file, Monitor **&monitors, Purpose purpose )
|
||||
{
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Path, Method, Options, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif from Monitors where Function != 'None' and Type = 'Ffmpeg'";
|
||||
std::string sql = "select Id, Name, ServerId, StorageId, Function+0, Enabled, LinkedMonitors, Path, Method, Options, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, SaveJPEGs, VideoWriter, EncoderParameters, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, Exif from Monitors where Function != 'None' and Type = 'Ffmpeg'";
|
||||
if ( file[0] ) {
|
||||
sql += " AND Path = '";
|
||||
sql += file;
|
||||
|
@ -2625,6 +2684,11 @@ int Monitor::LoadFfmpegMonitors( const char *file, Monitor **&monitors, Purpose
|
|||
/* int palette = atoi(dbrow[col]); */ col++;
|
||||
Orientation orientation = (Orientation)atoi(dbrow[col]); col++;
|
||||
unsigned int deinterlacing = atoi(dbrow[col]); col++;
|
||||
|
||||
int savejpegs = atoi(dbrow[col]); col++;
|
||||
int videowriter = atoi(dbrow[col]); col++;
|
||||
std::string encoderparams = dbrow[col]; col++;
|
||||
|
||||
int brightness = atoi(dbrow[col]); col++;
|
||||
int contrast = atoi(dbrow[col]); col++;
|
||||
int hue = atoi(dbrow[col]); col++;
|
||||
|
@ -2685,6 +2749,9 @@ int Monitor::LoadFfmpegMonitors( const char *file, Monitor **&monitors, Purpose
|
|||
camera,
|
||||
orientation,
|
||||
deinterlacing,
|
||||
savejpegs,
|
||||
videowriter,
|
||||
encoderparams,
|
||||
event_prefix,
|
||||
label_format,
|
||||
Coord( label_x, label_y ),
|
||||
|
@ -2732,7 +2799,7 @@ int Monitor::LoadFfmpegMonitors( const char *file, Monitor **&monitors, Purpose
|
|||
|
||||
Monitor *Monitor::Load( unsigned int p_id, bool load_zones, Purpose purpose )
|
||||
{
|
||||
std::string sql = stringtf( "select Id, Name, ServerId, StorageId, Type, Function+0, Enabled, LinkedMonitors, Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, Protocol, Method, Host, Port, Path, Options, User, Pass, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, SignalCheckColour, Exif from Monitors where Id = %d", p_id );
|
||||
std::string sql = stringtf( "select Id, Name, ServerId, StorageId, Type, Function+0, Enabled, LinkedMonitors, Device, Channel, Format, V4LMultiBuffer, V4LCapturesPerFrame, Protocol, Method, Host, Port, Path, Options, User, Pass, Width, Height, Colours, Palette, Orientation+0, Deinterlacing, RTSPDescribe, SaveJPEGs, VideoWriter, EncoderParameters, Brightness, Contrast, Hue, Colour, EventPrefix, LabelFormat, LabelX, LabelY, LabelSize, ImageBufferCount, WarmupCount, PreEventCount, PostEventCount, StreamReplayBuffer, AlarmFrameCount, SectionLength, FrameSkip, MotionFrameSkip, AnalysisFPS, AnalysisUpdateDelay, MaxFPS, AlarmMaxFPS, FPSReportInterval, RefBlendPerc, AlarmRefBlendPerc, TrackMotion, SignalCheckColour, Exif from Monitors where Id = %d", p_id );
|
||||
|
||||
MYSQL_ROW dbrow = zmDbFetchOne( sql.c_str() );
|
||||
if ( ! dbrow ) {
|
||||
|
@ -2790,6 +2857,10 @@ Debug( 1, "Got %d for v4l_captures_per_frame", v4l_captures_per_frame );
|
|||
Orientation orientation = (Orientation)atoi(dbrow[col]); col++;
|
||||
unsigned int deinterlacing = atoi(dbrow[col]); col++;
|
||||
bool rtsp_describe = (*dbrow[col] != '0'); col++;
|
||||
int savejpegs = atoi(dbrow[col]); col++;
|
||||
int videowriter = atoi(dbrow[col]); col++;
|
||||
std::string encoderparams = dbrow[col]; col++;
|
||||
|
||||
int brightness = atoi(dbrow[col]); col++;
|
||||
int contrast = atoi(dbrow[col]); col++;
|
||||
int hue = atoi(dbrow[col]); col++;
|
||||
|
@ -3001,6 +3072,9 @@ Debug( 1, "Got %d for v4l_captures_per_frame", v4l_captures_per_frame );
|
|||
camera,
|
||||
orientation,
|
||||
deinterlacing,
|
||||
savejpegs,
|
||||
videowriter,
|
||||
encoderparams,
|
||||
event_prefix.c_str(),
|
||||
label_format.c_str(),
|
||||
Coord( label_x, label_y ),
|
||||
|
@ -3054,19 +3128,36 @@ int Monitor::Capture()
|
|||
/* Copy the next image into the shared memory */
|
||||
capture_image->CopyBuffer(*(next_buffer.image));
|
||||
}
|
||||
|
||||
/* Capture a new next image */
|
||||
|
||||
//Check if FFMPEG camera
|
||||
if((GetOptVideoWriter() == 2) && camera->SupportsNativeVideo()){
|
||||
captureResult = camera->CaptureAndRecord(*(next_buffer.image), video_store_data->recording, video_store_data->event_file);
|
||||
}else{
|
||||
captureResult = camera->Capture(*(next_buffer.image));
|
||||
}
|
||||
|
||||
/* Capture a new next image */
|
||||
captureResult = camera->Capture(*(next_buffer.image));
|
||||
if ( FirstCapture ) {
|
||||
FirstCapture = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( FirstCapture ) {
|
||||
FirstCapture = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Capture directly into image buffer, avoiding the need to memcpy() */
|
||||
captureResult = camera->Capture(*capture_image);
|
||||
}
|
||||
} else {
|
||||
//Check if FFMPEG camera
|
||||
if((GetOptVideoWriter() == 2) && camera->SupportsNativeVideo()){
|
||||
//Warning("ZMC: Recording: %d", video_store_data->recording);
|
||||
captureResult = camera->CaptureAndRecord(*capture_image, video_store_data->recording, video_store_data->event_file);
|
||||
}else{
|
||||
/* Capture directly into image buffer, avoiding the need to memcpy() */
|
||||
captureResult = camera->Capture(*capture_image);
|
||||
}
|
||||
}
|
||||
|
||||
if((GetOptVideoWriter() == 2) && captureResult > 0){
|
||||
//video_store_data->frameNumber = captureResult;
|
||||
captureResult = 0;
|
||||
}
|
||||
|
||||
if ( captureResult != 0 )
|
||||
{
|
||||
|
@ -3236,6 +3327,7 @@ void Monitor::TimestampImage( Image *ts_image, const struct timeval *ts_time ) c
|
|||
|
||||
bool Monitor::closeEvent()
|
||||
{
|
||||
video_store_data->recording = false;
|
||||
if ( event )
|
||||
{
|
||||
if ( function == RECORD || function == MOCORD )
|
||||
|
|
|
@ -154,6 +154,20 @@ protected:
|
|||
void* padding;
|
||||
};
|
||||
|
||||
//TODO: Technically we can't exclude this struct when people don't have avformat as the Memory.pm module doesn't know about avformat
|
||||
#if 1
|
||||
//sizeOf(VideoStoreData) expected to be 4104 bytes on 32bit and 64bit
|
||||
typedef struct
|
||||
{
|
||||
uint32_t size;
|
||||
char event_file[4096];
|
||||
uint32_t recording; //bool arch dependent so use uint32 instead
|
||||
//uint32_t frameNumber;
|
||||
|
||||
} VideoStoreData;
|
||||
|
||||
#endif // HAVE_LIBAVFORMAT
|
||||
|
||||
class MonitorLink
|
||||
{
|
||||
protected:
|
||||
|
@ -174,6 +188,7 @@ protected:
|
|||
|
||||
volatile SharedData *shared_data;
|
||||
volatile TriggerData *trigger_data;
|
||||
volatile VideoStoreData *video_store_data;
|
||||
|
||||
int last_state;
|
||||
int last_event;
|
||||
|
@ -223,6 +238,12 @@ protected:
|
|||
unsigned int v4l_captures_per_frame;
|
||||
Orientation orientation; // Whether the image has to be rotated at all
|
||||
unsigned int deinterlacing;
|
||||
|
||||
int savejpegspref;
|
||||
int videowriterpref;
|
||||
std::string encoderparams;
|
||||
std::vector<EncoderParameter_t> encoderparamsvec;
|
||||
|
||||
int brightness; // The statically saved brightness of the camera
|
||||
int contrast; // The statically saved contrast of the camera
|
||||
int hue; // The statically saved hue of the camera
|
||||
|
@ -288,6 +309,7 @@ protected:
|
|||
|
||||
SharedData *shared_data;
|
||||
TriggerData *trigger_data;
|
||||
VideoStoreData *video_store_data;
|
||||
|
||||
Snapshot *image_buffer;
|
||||
Snapshot next_buffer; /* Used by four field deinterlacing */
|
||||
|
@ -311,7 +333,47 @@ protected:
|
|||
public:
|
||||
// OurCheckAlarms seems to be unused. Check it on zm_monitor.cpp for more info.
|
||||
//bool OurCheckAlarms( Zone *zone, const Image *pImage );
|
||||
Monitor( int p_id, const char *p_name, unsigned int p_server_id, unsigned int p_storage_id, int p_function, bool p_enabled, const char *p_linked_monitors, Camera *p_camera, int p_orientation, unsigned int p_deinterlacing, const char *p_event_prefix, const char *p_label_format, const Coord &p_label_coord, int label_size, int p_image_buffer_count, int p_warmup_count, int p_pre_event_count, int p_post_event_count, int p_stream_replay_buffer, int p_alarm_frame_count, int p_section_length, int p_frame_skip, int p_motion_frame_skip, double p_analysis_fps, unsigned int p_analysis_update_delay, int p_capture_delay, int p_alarm_capture_delay, int p_fps_report_interval, int p_ref_blend_perc, int p_alarm_ref_blend_perc, bool p_track_motion, Rgb p_signal_check_colour, bool p_embed_exif, Purpose p_purpose, int p_n_zones=0, Zone *p_zones[]=0 );
|
||||
Monitor(
|
||||
int p_id,
|
||||
const char *p_name,
|
||||
unsigned int p_server_id,
|
||||
unsigned int p_storage_id,
|
||||
int p_function,
|
||||
bool p_enabled,
|
||||
const char *p_linked_monitors,
|
||||
Camera *p_camera,
|
||||
int p_orientation,
|
||||
unsigned int p_deinterlacing,
|
||||
int p_savejpegs,
|
||||
int p_videowriter,
|
||||
std::string p_encoderparams,
|
||||
const char *p_event_prefix,
|
||||
const char *p_label_format,
|
||||
const Coord &p_label_coord,
|
||||
int label_size,
|
||||
int p_image_buffer_count,
|
||||
int p_warmup_count,
|
||||
int p_pre_event_count,
|
||||
int p_post_event_count,
|
||||
int p_stream_replay_buffer,
|
||||
int p_alarm_frame_count,
|
||||
int p_section_length,
|
||||
int p_frame_skip,
|
||||
int p_motion_frame_skip,
|
||||
double p_analysis_fps,
|
||||
unsigned int p_analysis_update_delay,
|
||||
int p_capture_delay,
|
||||
int p_alarm_capture_delay,
|
||||
int p_fps_report_interval,
|
||||
int p_ref_blend_perc,
|
||||
int p_alarm_ref_blend_perc,
|
||||
bool p_track_motion,
|
||||
Rgb p_signal_check_colour,
|
||||
bool p_embed_exif,
|
||||
Purpose p_purpose,
|
||||
int p_n_zones=0,
|
||||
Zone *p_zones[]=0
|
||||
);
|
||||
~Monitor();
|
||||
|
||||
void AddZones( int p_n_zones, Zone *p_zones[] );
|
||||
|
@ -373,7 +435,10 @@ public:
|
|||
unsigned int Height() const { return( height ); }
|
||||
unsigned int Colours() const { return( camera->Colours() ); }
|
||||
unsigned int SubpixelOrder() const { return( camera->SubpixelOrder() ); }
|
||||
|
||||
|
||||
int GetOptSaveJPEGs() const { return( savejpegspref ); }
|
||||
int GetOptVideoWriter() const { return( videowriterpref ); }
|
||||
const std::vector<EncoderParameter_t>* GetOptEncoderParams() const { return( &encoderparamsvec ); }
|
||||
|
||||
State GetState() const;
|
||||
int GetImage( int index=-1, int scale=100 );
|
||||
|
|
|
@ -74,6 +74,7 @@ public:
|
|||
virtual int PreCapture() = 0;
|
||||
virtual int Capture( Image &image ) = 0;
|
||||
virtual int PostCapture() = 0;
|
||||
virtual int CaptureAndRecord( Image &image, bool recording, char* event_directory )=0;
|
||||
};
|
||||
|
||||
#endif // ZM_REMOTE_CAMERA_H
|
||||
|
|
|
@ -58,6 +58,7 @@ public:
|
|||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int PostCapture();
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory ) {return(0);};
|
||||
};
|
||||
|
||||
#endif // ZM_REMOTE_CAMERA_HTTP_H
|
||||
|
|
|
@ -52,11 +52,14 @@ RemoteCameraRtsp::RemoteCameraRtsp( int p_id, const std::string &p_method, const
|
|||
|
||||
mFormatContext = NULL;
|
||||
mVideoStreamId = -1;
|
||||
mAudioStreamId = -1;
|
||||
mCodecContext = NULL;
|
||||
mCodec = NULL;
|
||||
mRawFrame = NULL;
|
||||
mFrame = NULL;
|
||||
frameCount = 0;
|
||||
wasRecording = false;
|
||||
startTime=0;
|
||||
|
||||
#if HAVE_LIBSWSCALE
|
||||
mConvertContext = NULL;
|
||||
|
@ -364,6 +367,175 @@ int RemoteCameraRtsp::Capture( Image &image )
|
|||
return (0) ;
|
||||
}
|
||||
|
||||
//Function to handle capture and store
|
||||
int RemoteCameraRtsp::CaptureAndRecord( Image &image, bool recording, char* event_file )
|
||||
{
|
||||
AVPacket packet;
|
||||
uint8_t* directbuffer;
|
||||
int frameComplete = false;
|
||||
|
||||
/* Request a writeable buffer of the target image */
|
||||
directbuffer = image.WriteBuffer(width, height, colours, subpixelorder);
|
||||
if(directbuffer == NULL) {
|
||||
Error("Failed requesting writeable buffer for the captured image.");
|
||||
return (-1);
|
||||
}
|
||||
|
||||
while ( true )
|
||||
{
|
||||
buffer.clear();
|
||||
if ( !rtspThread->isRunning() )
|
||||
return (-1);
|
||||
|
||||
if ( rtspThread->getFrame( buffer ) )
|
||||
{
|
||||
Debug( 3, "Read frame %d bytes", buffer.size() );
|
||||
Debug( 4, "Address %p", buffer.head() );
|
||||
Hexdump( 4, buffer.head(), 16 );
|
||||
|
||||
if ( !buffer.size() )
|
||||
return( -1 );
|
||||
|
||||
if(mCodecContext->codec_id == AV_CODEC_ID_H264)
|
||||
{
|
||||
// SPS and PPS frames should be saved and appended to IDR frames
|
||||
int nalType = (buffer.head()[3] & 0x1f);
|
||||
|
||||
// SPS
|
||||
if(nalType == 7)
|
||||
{
|
||||
lastSps = buffer;
|
||||
continue;
|
||||
}
|
||||
// PPS
|
||||
else if(nalType == 8)
|
||||
{
|
||||
lastPps = buffer;
|
||||
continue;
|
||||
}
|
||||
// IDR
|
||||
else if(nalType == 5)
|
||||
{
|
||||
buffer += lastSps;
|
||||
buffer += lastPps;
|
||||
}
|
||||
}
|
||||
|
||||
av_init_packet( &packet );
|
||||
|
||||
if ( packet.stream_index == mVideoStreamId )
|
||||
{
|
||||
|
||||
while ( !frameComplete && buffer.size() > 0 )
|
||||
{
|
||||
packet.data = buffer.head();
|
||||
packet.size = buffer.size();
|
||||
#if LIBAVCODEC_VERSION_CHECK(52, 23, 0, 23, 0)
|
||||
int len = avcodec_decode_video2( mCodecContext, mRawFrame, &frameComplete, &packet );
|
||||
#else
|
||||
int len = avcodec_decode_video( mCodecContext, mRawFrame, &frameComplete, packet.data, packet.size );
|
||||
#endif
|
||||
if ( len < 0 )
|
||||
{
|
||||
Error( "Error while decoding frame %d", frameCount );
|
||||
Hexdump( Logger::ERROR, buffer.head(), buffer.size()>256?256:buffer.size() );
|
||||
buffer.clear();
|
||||
continue;
|
||||
}
|
||||
Debug( 2, "Frame: %d - %d/%d", frameCount, len, buffer.size() );
|
||||
//if ( buffer.size() < 400 )
|
||||
//Hexdump( 0, buffer.head(), buffer.size() );
|
||||
|
||||
buffer -= len;
|
||||
|
||||
}
|
||||
if ( frameComplete ) {
|
||||
|
||||
Debug( 3, "Got frame %d", frameCount );
|
||||
|
||||
avpicture_fill( (AVPicture *)mFrame, directbuffer, imagePixFormat, width, height);
|
||||
|
||||
//Video recording
|
||||
if(recording && !wasRecording){
|
||||
//Instantiate the video storage module
|
||||
|
||||
videoStore = new VideoStore((const char *)event_file, "mp4", mFormatContext->streams[mVideoStreamId],mAudioStreamId==-1?NULL:mFormatContext->streams[mAudioStreamId],startTime);
|
||||
wasRecording = true;
|
||||
strcpy(oldDirectory, event_file);
|
||||
|
||||
}else if(!recording && wasRecording && videoStore){
|
||||
Info("Deleting videoStore instance");
|
||||
delete videoStore;
|
||||
videoStore = NULL;
|
||||
}
|
||||
|
||||
//The directory we are recording to is no longer tied to the current event. Need to re-init the videostore with the correct directory and start recording again
|
||||
if(recording && wasRecording && (strcmp(oldDirectory, event_file)!=0) && (packet.flags & AV_PKT_FLAG_KEY) ){ //don't open new videostore until we're on a key frame..would this require an offset adjustment for the event as a result?...if we store our key frame location with the event will that be enough?
|
||||
Info("Re-starting video storage module");
|
||||
if(videoStore){
|
||||
delete videoStore;
|
||||
videoStore = NULL;
|
||||
}
|
||||
|
||||
videoStore = new VideoStore((const char *)event_file, "mp4", mFormatContext->streams[mVideoStreamId],mAudioStreamId==-1?NULL:mFormatContext->streams[mAudioStreamId],startTime);
|
||||
strcpy(oldDirectory, event_file);
|
||||
}
|
||||
|
||||
if(videoStore && recording){
|
||||
//Write the packet to our video store
|
||||
int ret = videoStore->writeVideoFramePacket(&packet, mFormatContext->streams[mVideoStreamId]);//, &lastKeyframePkt);
|
||||
if(ret<0){//Less than zero and we skipped a frame
|
||||
av_free_packet( &packet );
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_LIBSWSCALE
|
||||
if(mConvertContext == NULL) {
|
||||
mConvertContext = sws_getContext( mCodecContext->width, mCodecContext->height, mCodecContext->pix_fmt, width, height, imagePixFormat, SWS_BICUBIC, NULL, NULL, NULL );
|
||||
|
||||
if(mConvertContext == NULL)
|
||||
Fatal( "Unable to create conversion context");
|
||||
}
|
||||
|
||||
if ( sws_scale( mConvertContext, mRawFrame->data, mRawFrame->linesize, 0, mCodecContext->height, mFrame->data, mFrame->linesize ) < 0 )
|
||||
Fatal( "Unable to convert raw format %u to target format %u at frame %d", mCodecContext->pix_fmt, imagePixFormat, frameCount );
|
||||
#else // HAVE_LIBSWSCALE
|
||||
Fatal( "You must compile ffmpeg with the --enable-swscale option to use RTSP cameras" );
|
||||
#endif // HAVE_LIBSWSCALE
|
||||
|
||||
frameCount++;
|
||||
|
||||
} /* frame complete */
|
||||
}
|
||||
else if(packet.stream_index == mAudioStreamId)
|
||||
{
|
||||
if(videoStore && recording)
|
||||
{
|
||||
//Write the packet to our video store
|
||||
int ret = videoStore->writeAudioFramePacket(&packet, mFormatContext->streams[packet.stream_index]); //FIXME no relevance of last key frame
|
||||
if(ret<0) //Less than zero and we skipped a frame
|
||||
{
|
||||
av_free_packet( &packet );
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if LIBAVCODEC_VERSION_CHECK(57, 8, 0, 12, 100)
|
||||
av_packet_unref( &packet);
|
||||
#else
|
||||
av_free_packet( &packet );
|
||||
#endif
|
||||
} /* getFrame() */
|
||||
|
||||
if(frameComplete)
|
||||
return (0);
|
||||
|
||||
}
|
||||
return (0) ;
|
||||
}
|
||||
|
||||
int RemoteCameraRtsp::PostCapture()
|
||||
{
|
||||
return( 0 );
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include "zm_utils.h"
|
||||
#include "zm_rtsp.h"
|
||||
#include "zm_ffmpeg.h"
|
||||
#include "zm_videostore.h"
|
||||
|
||||
//
|
||||
// Class representing 'rtsp' cameras, i.e. those which are
|
||||
|
@ -55,12 +56,17 @@ protected:
|
|||
#if HAVE_LIBAVFORMAT
|
||||
AVFormatContext *mFormatContext;
|
||||
int mVideoStreamId;
|
||||
int mAudioStreamId;
|
||||
AVCodecContext *mCodecContext;
|
||||
AVCodec *mCodec;
|
||||
AVFrame *mRawFrame;
|
||||
AVFrame *mFrame;
|
||||
_AVPIXELFORMAT imagePixFormat;
|
||||
#endif // HAVE_LIBAVFORMAT
|
||||
bool wasRecording;
|
||||
VideoStore *videoStore;
|
||||
char oldDirectory[4096];
|
||||
int64_t startTime;
|
||||
|
||||
#if HAVE_LIBSWSCALE
|
||||
struct SwsContext *mConvertContext;
|
||||
|
@ -79,7 +85,7 @@ public:
|
|||
int PreCapture();
|
||||
int Capture( Image &image );
|
||||
int PostCapture();
|
||||
|
||||
int CaptureAndRecord( Image &image, bool recording, char* event_directory );
|
||||
};
|
||||
|
||||
#endif // ZM_REMOTE_CAMERA_RTSP_H
|
||||
|
|
|
@ -0,0 +1,518 @@
|
|||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
//
|
||||
#include "zm.h"
|
||||
#include "zm_video.h"
|
||||
#include "zm_image.h"
|
||||
#include "zm_utils.h"
|
||||
#include "zm_rgb.h"
|
||||
#include <sstream>
|
||||
|
||||
VideoWriter::VideoWriter(const char* p_container, const char* p_codec, const char* p_path, const unsigned int p_width, const unsigned int p_height, const unsigned int p_colours, const unsigned int p_subpixelorder) :
|
||||
container(p_container), codec(p_codec), path(p_path), width(p_width), height(p_height), colours(p_colours), subpixelorder(p_subpixelorder), frame_count(0) {
|
||||
Debug(7,"Video object created");
|
||||
|
||||
/* Parameter checking */
|
||||
if(path.empty()) {
|
||||
Error("Invalid file path");
|
||||
}
|
||||
if(!width || !height) {
|
||||
Error("Invalid width or height");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
VideoWriter::~VideoWriter() {
|
||||
Debug(7,"Video object destroyed");
|
||||
|
||||
}
|
||||
|
||||
int VideoWriter::Reset(const char* new_path) {
|
||||
/* Common variables reset */
|
||||
|
||||
/* If there is a new path, use it */
|
||||
if(new_path != NULL) {
|
||||
path = new_path;
|
||||
}
|
||||
|
||||
/* Reset frame counter */
|
||||
frame_count = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
#if ZM_HAVE_VIDEOWRITER_X264MP4
|
||||
X264MP4Writer::X264MP4Writer(const char* p_path, const unsigned int p_width, const unsigned int p_height, const unsigned int p_colours, const unsigned int p_subpixelorder, const std::vector<EncoderParameter_t>* p_user_params) : VideoWriter("mp4", "h264", p_path, p_width, p_height, p_colours, p_subpixelorder), bOpen(false), bGotH264AVCInfo(false), bFirstFrame(true) {
|
||||
|
||||
/* Initialize ffmpeg if it hasn't been initialized yet */
|
||||
FFMPEGInit();
|
||||
|
||||
/* Initialize swscale */
|
||||
zm_pf = GetFFMPEGPixelFormat(colours,subpixelorder);
|
||||
if(zm_pf == 0) {
|
||||
Error("Unable to match ffmpeg pixelformat");
|
||||
}
|
||||
codec_pf = PIX_FMT_YUV420P;
|
||||
|
||||
swscaleobj.SetDefaults(zm_pf, codec_pf, width, height);
|
||||
|
||||
/* Calculate the image sizes. We will need this for parameter checking */
|
||||
zm_imgsize = colours * width * height;
|
||||
codec_imgsize = avpicture_get_size( codec_pf, width, height);
|
||||
if(!codec_imgsize) {
|
||||
Error("Failed calculating codec pixel format image size");
|
||||
}
|
||||
|
||||
/* If supplied with user parameters to the encoder, copy them */
|
||||
if(p_user_params != NULL) {
|
||||
user_params = *p_user_params;
|
||||
}
|
||||
|
||||
/* Setup x264 parameters */
|
||||
if(x264config() < 0) {
|
||||
Error("Failed setting x264 parameters");
|
||||
}
|
||||
|
||||
/* Allocate x264 input picture */
|
||||
x264_picture_alloc(&x264picin, X264_CSP_I420, x264params.i_width, x264params.i_height);
|
||||
|
||||
}
|
||||
|
||||
X264MP4Writer::~X264MP4Writer() {
|
||||
|
||||
/* Free x264 input picture */
|
||||
x264_picture_clean(&x264picin);
|
||||
|
||||
if(bOpen)
|
||||
Close();
|
||||
|
||||
}
|
||||
|
||||
int X264MP4Writer::Open() {
|
||||
|
||||
/* Open the encoder */
|
||||
x264enc = x264_encoder_open(&x264params);
|
||||
if(x264enc == NULL) {
|
||||
Error("Failed opening x264 encoder");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Debug(4,"x264 maximum delayed frames: %d",x264_encoder_maximum_delayed_frames(x264enc));
|
||||
|
||||
x264_nal_t* nals;
|
||||
int i_nals;
|
||||
if(!x264_encoder_headers(x264enc,&nals,&i_nals)) {
|
||||
Error("Failed getting encoder headers");
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* Search SPS NAL for AVC information */
|
||||
for(int i=0;i<i_nals;i++) {
|
||||
if(nals[i].i_type == NAL_SPS) {
|
||||
x264_profleindication = nals[i].p_payload[5];
|
||||
x264_profilecompat = nals[i].p_payload[6];
|
||||
x264_levelindication = nals[i].p_payload[7];
|
||||
bGotH264AVCInfo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!bGotH264AVCInfo) {
|
||||
Warning("Missing AVC information");
|
||||
}
|
||||
|
||||
/* Create the file */
|
||||
mp4h = MP4Create((path + ".incomplete").c_str());
|
||||
if(mp4h == MP4_INVALID_FILE_HANDLE) {
|
||||
Error("Failed creating mp4 file: %s",path.c_str());
|
||||
return -10;
|
||||
}
|
||||
|
||||
/* Set the global timescale */
|
||||
if(!MP4SetTimeScale(mp4h, 1000)) {
|
||||
Error("Failed setting timescale");
|
||||
return -11;
|
||||
}
|
||||
|
||||
/* Set the global video profile */
|
||||
/* I am a bit confused about this one.
|
||||
I couldn't find what the value should be
|
||||
Some use 0x15 while others use 0x7f. */
|
||||
MP4SetVideoProfileLevel(mp4h, 0x7f);
|
||||
|
||||
/* Add H264 video track */
|
||||
mp4vtid = MP4AddH264VideoTrack(mp4h,1000,MP4_INVALID_DURATION,width,height,x264_profleindication,x264_profilecompat,x264_levelindication,3);
|
||||
if(mp4vtid == MP4_INVALID_TRACK_ID) {
|
||||
Error("Failed adding H264 video track");
|
||||
return -12;
|
||||
}
|
||||
|
||||
bOpen = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int X264MP4Writer::Close() {
|
||||
|
||||
/* Flush all pending frames */
|
||||
for(int i = (x264_encoder_delayed_frames(x264enc) + 1); i > 0; i-- ) {
|
||||
x264encodeloop(true);
|
||||
}
|
||||
|
||||
/* Close the encoder */
|
||||
x264_encoder_close(x264enc);
|
||||
|
||||
/* Close MP4 handle */
|
||||
MP4Close(mp4h);
|
||||
|
||||
/* Required for proper HTTP streaming */
|
||||
MP4Optimize((path + ".incomplete").c_str(), path.c_str());
|
||||
|
||||
/* Delete the temporary file */
|
||||
unlink((path + ".incomplete").c_str());
|
||||
|
||||
bOpen = false;
|
||||
|
||||
Debug(7, "Video closed. Total frames: %d", frame_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int X264MP4Writer::Reset(const char* new_path) {
|
||||
|
||||
/* Close the encoder and file */
|
||||
if(bOpen)
|
||||
Close();
|
||||
|
||||
/* Reset common variables */
|
||||
VideoWriter::Reset(new_path);
|
||||
|
||||
/* Reset local variables */
|
||||
bFirstFrame = true;
|
||||
bGotH264AVCInfo = false;
|
||||
prevnals.clear();
|
||||
prevpayload.clear();
|
||||
|
||||
/* Reset x264 parameters */
|
||||
x264config();
|
||||
|
||||
/* Open the encoder */
|
||||
Open();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int X264MP4Writer::Encode(const uint8_t* data, const size_t data_size, const unsigned int frame_time) {
|
||||
|
||||
/* Parameter checking */
|
||||
if(data == NULL) {
|
||||
Error("NULL buffer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(data_size != zm_imgsize) {
|
||||
Error("The data buffer size does not match the expected size. Expected: %d Current: %d", zm_imgsize, data_size);
|
||||
return -2;
|
||||
}
|
||||
|
||||
if(!bOpen) {
|
||||
Warning("The encoder was not initialized, initializing now");
|
||||
Open();
|
||||
}
|
||||
|
||||
/* Convert the image into the x264 input picture */
|
||||
if(swscaleobj.ConvertDefaults(data, data_size, x264picin.img.plane[0], codec_imgsize) < 0) {
|
||||
Error("Image conversion failed");
|
||||
return -3;
|
||||
}
|
||||
|
||||
/* Set PTS */
|
||||
x264picin.i_pts = frame_time;
|
||||
|
||||
/* Do the encoding */
|
||||
x264encodeloop();
|
||||
|
||||
/* Increment frame counter */
|
||||
frame_count++;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int X264MP4Writer::Encode(const Image* img, const unsigned int frame_time) {
|
||||
|
||||
if(img->Width() != width) {
|
||||
Error("Source image width differs. Source: %d Output: %d",img->Width(), width);
|
||||
return -12;
|
||||
}
|
||||
|
||||
if(img->Height() != height) {
|
||||
Error("Source image height differs. Source: %d Output: %d",img->Height(), height);
|
||||
return -13;
|
||||
}
|
||||
|
||||
return Encode(img->Buffer(),img->Size(),frame_time);
|
||||
}
|
||||
|
||||
int X264MP4Writer::x264config() {
|
||||
/* Sets up the encoder configuration */
|
||||
|
||||
int x264ret;
|
||||
|
||||
/* Defaults */
|
||||
const char* preset = "veryfast";
|
||||
const char* tune = "stillimage";
|
||||
const char* profile = "main";
|
||||
|
||||
/* Search the user parameters for preset, tune and profile */
|
||||
for(unsigned int i=0; i < user_params.size(); i++) {
|
||||
if(strcmp(user_params[i].pname, "preset") == 0) {
|
||||
/* Got preset */
|
||||
preset = user_params[i].pvalue;
|
||||
} else if(strcmp(user_params[i].pname, "tune") == 0) {
|
||||
/* Got tune */
|
||||
tune = user_params[i].pvalue;
|
||||
} else if(strcmp(user_params[i].pname, "profile") == 0) {
|
||||
/* Got profile */
|
||||
profile = user_params[i].pvalue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set the defaults and preset and tune */
|
||||
x264ret = x264_param_default_preset(&x264params, preset, tune);
|
||||
if(x264ret != 0) {
|
||||
Error("Failed setting x264 preset %s and tune %s : %d",preset,tune,x264ret);
|
||||
}
|
||||
|
||||
/* Set the profile */
|
||||
x264ret = x264_param_apply_profile(&x264params, profile);
|
||||
if(x264ret != 0) {
|
||||
Error("Failed setting x264 profile %s : %d",profile,x264ret);
|
||||
}
|
||||
|
||||
/* Input format */
|
||||
x264params.i_width = width;
|
||||
x264params.i_height = height;
|
||||
x264params.i_csp = X264_CSP_I420;
|
||||
|
||||
/* Quality control */
|
||||
x264params.rc.i_rc_method = X264_RC_CRF;
|
||||
x264params.rc.f_rf_constant = 23.0;
|
||||
|
||||
/* Enable b-frames */
|
||||
x264params.i_bframe = 16;
|
||||
x264params.i_bframe_adaptive = 1;
|
||||
|
||||
/* Timebase */
|
||||
x264params.i_timebase_num = 1;
|
||||
x264params.i_timebase_den = 1000;
|
||||
|
||||
/* Enable variable frame rate */
|
||||
x264params.b_vfr_input = 1;
|
||||
|
||||
/* Disable annex-b (start codes) */
|
||||
x264params.b_annexb = 0;
|
||||
|
||||
/* TODO: Setup error handler */
|
||||
//x264params.i_log_level = X264_LOG_DEBUG;
|
||||
|
||||
/* Process user parameters (excluding preset, tune and profile) */
|
||||
for(unsigned int i=0; i < user_params.size(); i++) {
|
||||
/* Skip preset, tune and profile */
|
||||
if( (strcmp(user_params[i].pname, "preset") == 0) || (strcmp(user_params[i].pname, "tune") == 0) || (strcmp(user_params[i].pname, "profile") == 0) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Pass the name and value to x264 */
|
||||
x264ret = x264_param_parse(&x264params, user_params[i].pname, user_params[i].pvalue);
|
||||
|
||||
/* Error checking */
|
||||
if(x264ret != 0) {
|
||||
if(x264ret == X264_PARAM_BAD_NAME) {
|
||||
Error("Failed processing x264 user parameter %s=%s : Bad name", user_params[i].pname, user_params[i].pvalue);
|
||||
} else if(x264ret == X264_PARAM_BAD_VALUE) {
|
||||
Error("Failed processing x264 user parameter %s=%s : Bad value", user_params[i].pname, user_params[i].pvalue);
|
||||
} else {
|
||||
Error("Failed processing x264 user parameter %s=%s : Unknown error (%d)", user_params[i].pname, user_params[i].pvalue, x264ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void X264MP4Writer::x264encodeloop(bool bFlush) {
|
||||
|
||||
x264_nal_t* nals;
|
||||
int i_nals;
|
||||
int frame_size;
|
||||
|
||||
if(bFlush) {
|
||||
frame_size = x264_encoder_encode(x264enc, &nals, &i_nals, NULL, &x264picout);
|
||||
} else {
|
||||
frame_size = x264_encoder_encode(x264enc, &nals, &i_nals, &x264picin, &x264picout);
|
||||
}
|
||||
|
||||
if (frame_size > 0 || bFlush) {
|
||||
Debug(8, "x264 Frame: %d PTS: %d DTS: %d Size: %d\n",frame_count, x264picout.i_pts, x264picout.i_dts, frame_size);
|
||||
|
||||
/* Handle the previous frame */
|
||||
if(!bFirstFrame) {
|
||||
|
||||
buffer.clear();
|
||||
|
||||
/* Process the NALs for the previous frame */
|
||||
for(unsigned int i=0; i < prevnals.size(); i++) {
|
||||
Debug(9,"Processing NAL: Type %d Size %d",prevnals[i].i_type,prevnals[i].i_payload);
|
||||
|
||||
switch(prevnals[i].i_type) {
|
||||
case NAL_PPS:
|
||||
/* PPS NAL */
|
||||
MP4AddH264PictureParameterSet(mp4h, mp4vtid, prevnals[i].p_payload+4, prevnals[i].i_payload-4);
|
||||
break;
|
||||
case NAL_SPS:
|
||||
/* SPS NAL */
|
||||
MP4AddH264SequenceParameterSet(mp4h, mp4vtid, prevnals[i].p_payload+4, prevnals[i].i_payload-4);
|
||||
break;
|
||||
default:
|
||||
/* Anything else, hopefully frames, so copy it into the sample */
|
||||
buffer.append(prevnals[i].p_payload, prevnals[i].i_payload);
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculate frame duration and offset */
|
||||
int duration = x264picout.i_dts - prevDTS;
|
||||
int offset = prevPTS - prevDTS;
|
||||
|
||||
/* Write the sample */
|
||||
if(!buffer.empty()) {
|
||||
if(!MP4WriteSample(mp4h, mp4vtid, buffer.extract(buffer.size()), buffer.size(), duration, offset, prevKeyframe)) {
|
||||
Error("Failed writing sample");
|
||||
}
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
prevnals.clear();
|
||||
prevpayload.clear();
|
||||
|
||||
}
|
||||
|
||||
/* Got a frame. Copy this new frame into the previous frame */
|
||||
if(frame_size > 0) {
|
||||
/* Copy the NALs and the payloads */
|
||||
for(int i=0;i<i_nals;i++) {
|
||||
|
||||
prevnals.push_back(nals[i]);
|
||||
prevpayload.append(nals[i].p_payload, nals[i].i_payload);
|
||||
}
|
||||
|
||||
/* Update the payload pointers */
|
||||
/* This is done in a separate loop because the previous loop might reallocate memory when appending,
|
||||
making the pointers invalid */
|
||||
unsigned int payload_offset = 0;
|
||||
for(unsigned int i=0;i<prevnals.size();i++) {
|
||||
prevnals[i].p_payload = prevpayload.head() + payload_offset;
|
||||
payload_offset += nals[i].i_payload;
|
||||
}
|
||||
|
||||
/* We need this for the next frame */
|
||||
prevPTS = x264picout.i_pts;
|
||||
prevDTS = x264picout.i_dts;
|
||||
prevKeyframe = x264picout.b_keyframe;
|
||||
|
||||
bFirstFrame = false;
|
||||
}
|
||||
|
||||
} else if(frame_size == 0) {
|
||||
Debug(7,"x264 encode returned zero. Delayed frames: %d",x264_encoder_delayed_frames(x264enc));
|
||||
} else {
|
||||
Error("x264 encode failed: %d",frame_size);
|
||||
}
|
||||
}
|
||||
#endif // ZM_VIDEOWRITER_X264MP4
|
||||
|
||||
int ParseEncoderParameters(const char* str, std::vector<EncoderParameter_t>* vec) {
|
||||
if(vec == NULL) {
|
||||
Error("NULL Encoder parameters vector pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(str == NULL) {
|
||||
Error("NULL Encoder parameters string");
|
||||
return -2;
|
||||
}
|
||||
|
||||
vec->clear();
|
||||
|
||||
if(str[0] == 0) {
|
||||
/* Empty */
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::stringstream ss(str);
|
||||
size_t valueoffset;
|
||||
size_t valuelen;
|
||||
unsigned int lineno = 0;
|
||||
EncoderParameter_t param;
|
||||
|
||||
while(std::getline(ss, line) ) {
|
||||
lineno++;
|
||||
|
||||
/* Remove CR if exists */
|
||||
if(line.length() >= 1 && line[line.length()-1] == '\r') {
|
||||
line.erase(line.length()-1);
|
||||
}
|
||||
|
||||
/* Skip comments and empty lines */
|
||||
if(line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
valueoffset = line.find('=');
|
||||
if(valueoffset == std::string::npos || valueoffset+1 >= line.length() || valueoffset == 0) {
|
||||
Warning("Failed parsing encoder parameters line %d: Invalid pair", lineno);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if(valueoffset > (sizeof(param.pname)-1) ) {
|
||||
Warning("Failed parsing encoder parameters line %d: Name too long", lineno);
|
||||
continue;
|
||||
}
|
||||
|
||||
valuelen = line.length() - (valueoffset+1);
|
||||
|
||||
if( valuelen > (sizeof(param.pvalue)-1) ) {
|
||||
Warning("Failed parsing encoder parameters line %d: Value too long", lineno);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Copy and NULL terminate */
|
||||
line.copy(param.pname, valueoffset, 0);
|
||||
line.copy(param.pvalue, valuelen, valueoffset+1);
|
||||
param.pname[valueoffset] = 0;
|
||||
param.pvalue[valuelen] = 0;
|
||||
|
||||
/* Push to the vector */
|
||||
vec->push_back(param);
|
||||
|
||||
Debug(7, "Parsed encoder parameter: %s = %s", param.pname, param.pvalue);
|
||||
}
|
||||
|
||||
Debug(7, "Parsed %d lines", lineno);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
//
|
||||
|
||||
#ifndef ZM_VIDEO_H
|
||||
#define ZM_VIDEO_H
|
||||
|
||||
#include "zm.h"
|
||||
#include "zm_rgb.h"
|
||||
#include "zm_utils.h"
|
||||
#include "zm_ffmpeg.h"
|
||||
#include "zm_buffer.h"
|
||||
|
||||
/*
|
||||
#define HAVE_LIBX264 1
|
||||
#define HAVE_LIBMP4V2 1
|
||||
#define HAVE_X264_H 1
|
||||
#define HAVE_MP4_H 1
|
||||
*/
|
||||
|
||||
#if HAVE_MP4V2_MP4V2_H
|
||||
#include <mp4v2/mp4v2.h>
|
||||
#endif
|
||||
#if HAVE_MP4V2_H
|
||||
#include <mp4v2.h>
|
||||
#endif
|
||||
#if HAVE_MP4_H
|
||||
#include <mp4.h>
|
||||
#endif
|
||||
|
||||
#if HAVE_X264_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#include <x264.h>
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Structure for user parameters to the encoder */
|
||||
struct EncoderParameter_t {
|
||||
char pname[48];
|
||||
char pvalue[48];
|
||||
|
||||
};
|
||||
int ParseEncoderParameters(const char* str, std::vector<EncoderParameter_t>* vec);
|
||||
|
||||
/* VideoWriter is a generic interface that ZM uses to save events as videos */
|
||||
/* It is relatively simple and the functions are pure virtual, so they must be implemented by the deriving class */
|
||||
|
||||
class VideoWriter {
|
||||
|
||||
protected:
|
||||
std::string container;
|
||||
std::string codec;
|
||||
std::string path;
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
unsigned int colours;
|
||||
unsigned int subpixelorder;
|
||||
|
||||
unsigned int frame_count;
|
||||
|
||||
public:
|
||||
VideoWriter(const char* p_container, const char* p_codec, const char* p_path, const unsigned int p_width, const unsigned int p_height, const unsigned int p_colours, const unsigned int p_subpixelorder);
|
||||
virtual ~VideoWriter();
|
||||
virtual int Encode(const uint8_t* data, const size_t data_size, const unsigned int frame_time) = 0;
|
||||
virtual int Encode(const Image* img, const unsigned int frame_time) = 0;
|
||||
virtual int Open() = 0;
|
||||
virtual int Close() = 0;
|
||||
virtual int Reset(const char* new_path = NULL);
|
||||
|
||||
const char* GetContainer() const {
|
||||
return container.c_str();
|
||||
}
|
||||
const char* GetCodec() const {
|
||||
return codec.c_str();
|
||||
}
|
||||
const char* GetPath() const {
|
||||
return path.c_str();
|
||||
}
|
||||
unsigned int GetWidth() const {
|
||||
return width;
|
||||
}
|
||||
unsigned int GetHeight() const {
|
||||
return height;
|
||||
}
|
||||
unsigned int GetColours() const {
|
||||
return colours;
|
||||
}
|
||||
unsigned int GetSubpixelorder () const {
|
||||
return subpixelorder;
|
||||
}
|
||||
unsigned int GetFrameCount() const {
|
||||
return frame_count;
|
||||
}
|
||||
};
|
||||
|
||||
#if HAVE_LIBX264 && HAVE_LIBMP4V2 && HAVE_LIBAVUTIL && HAVE_LIBSWSCALE
|
||||
#define ZM_HAVE_VIDEOWRITER_X264MP4 1
|
||||
class X264MP4Writer : public VideoWriter {
|
||||
|
||||
protected:
|
||||
|
||||
bool bOpen;
|
||||
bool bGotH264AVCInfo;
|
||||
bool bFirstFrame;
|
||||
|
||||
/* SWScale */
|
||||
SWScale swscaleobj;
|
||||
enum PixelFormat zm_pf;
|
||||
enum PixelFormat codec_pf;
|
||||
size_t codec_imgsize;
|
||||
size_t zm_imgsize;
|
||||
|
||||
/* User parameters */
|
||||
std::vector<EncoderParameter_t> user_params;
|
||||
|
||||
/* AVC Information */
|
||||
uint8_t x264_profleindication;
|
||||
uint8_t x264_profilecompat;
|
||||
uint8_t x264_levelindication;
|
||||
|
||||
/* NALs */
|
||||
Buffer buffer;
|
||||
|
||||
/* Previous frame */
|
||||
int prevPTS;
|
||||
int prevDTS;
|
||||
bool prevKeyframe;
|
||||
Buffer prevpayload;
|
||||
std::vector<x264_nal_t> prevnals;
|
||||
|
||||
/* Internal functions */
|
||||
int x264config();
|
||||
void x264encodeloop(bool bFlush = false);
|
||||
|
||||
/* x264 objects */
|
||||
x264_t* x264enc;
|
||||
x264_param_t x264params;
|
||||
x264_picture_t x264picin;
|
||||
x264_picture_t x264picout;
|
||||
|
||||
/* MP4v2 objects */
|
||||
MP4FileHandle mp4h;
|
||||
MP4TrackId mp4vtid;
|
||||
|
||||
|
||||
public:
|
||||
X264MP4Writer(const char* p_path, const unsigned int p_width, const unsigned int p_height, const unsigned int p_colours, const unsigned int p_subpixelorder, const std::vector<EncoderParameter_t>* p_user_params = NULL);
|
||||
~X264MP4Writer();
|
||||
int Encode(const uint8_t* data, const size_t data_size, const unsigned int frame_time);
|
||||
int Encode(const Image* img, const unsigned int frame_time);
|
||||
int Open();
|
||||
int Close();
|
||||
int Reset(const char* new_path = NULL);
|
||||
|
||||
};
|
||||
#endif // HAVE_LIBX264 && HAVE_LIBMP4V2 && HAVE_LIBAVUTIL && HAVE_LIBSWSCALE
|
||||
|
||||
#endif // ZM_VIDEO_H
|
|
@ -0,0 +1,298 @@
|
|||
//
|
||||
// ZoneMinder Video Storage Implementation
|
||||
// Written by Chris Wiggins
|
||||
// http://chriswiggins.co.nz
|
||||
// Modification by Steve Gilvarry
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
//
|
||||
|
||||
#define __STDC_FORMAT_MACROS 1
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "zm.h"
|
||||
#include "zm_videostore.h"
|
||||
|
||||
extern "C"{
|
||||
#include "libavutil/time.h"
|
||||
}
|
||||
|
||||
VideoStore::VideoStore(const char *filename_in, const char *format_in,
|
||||
AVStream *input_st, AVStream *inpaud_st,
|
||||
int64_t nStartTime) {
|
||||
|
||||
AVDictionary *pmetadata = NULL;
|
||||
int dsr;
|
||||
|
||||
//store inputs in variables local to class
|
||||
filename = filename_in;
|
||||
format = format_in;
|
||||
|
||||
keyframeMessage = false;
|
||||
keyframeSkipNumber = 0;
|
||||
|
||||
|
||||
Info("Opening video storage stream %s\n", filename);
|
||||
|
||||
//Init everything we need
|
||||
int ret;
|
||||
av_register_all();
|
||||
|
||||
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
|
||||
|
||||
//Couldn't deduce format from filename, trying from format name
|
||||
if (!oc) {
|
||||
avformat_alloc_output_context2(&oc, NULL, format, filename);
|
||||
if (!oc) {
|
||||
Fatal("Could not create video storage stream %s as no output context"
|
||||
" could not be assigned based on filename or format %s",
|
||||
filename, format);
|
||||
}
|
||||
}
|
||||
|
||||
dsr = av_dict_set(&pmetadata, "title", "Zoneminder Security Recording", 0);
|
||||
if (dsr < 0) Warning("%s:%d: title set failed", __FILE__, __LINE__ );
|
||||
|
||||
oc->metadata = pmetadata;
|
||||
|
||||
fmt = oc->oformat;
|
||||
|
||||
video_st = avformat_new_stream(oc, input_st->codec->codec);
|
||||
if (!video_st) {
|
||||
Fatal("Unable to create video out stream\n");
|
||||
}
|
||||
|
||||
ret = avcodec_copy_context(video_st->codec, input_st->codec);
|
||||
if (ret < 0) {
|
||||
Fatal("Unable to copy input video context to output video context "
|
||||
"%s\n", av_make_error_string(ret).c_str());
|
||||
}
|
||||
|
||||
video_st->codec->codec_tag = 0;
|
||||
if (oc->oformat->flags & AVFMT_GLOBALHEADER) {
|
||||
video_st->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
||||
}
|
||||
|
||||
if (inpaud_st) {
|
||||
audio_st = avformat_new_stream(oc, inpaud_st->codec->codec);
|
||||
if (!audio_st) {
|
||||
Fatal("Unable to create audio out stream\n");
|
||||
}
|
||||
ret=avcodec_copy_context(audio_st->codec, inpaud_st->codec);
|
||||
if (ret < 0) {
|
||||
Fatal("Unable to copy audio context %s\n", av_make_error_string(ret).c_str());
|
||||
}
|
||||
audio_st->codec->codec_tag = 0;
|
||||
if (oc->oformat->flags & AVFMT_GLOBALHEADER) {
|
||||
audio_st->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
||||
}
|
||||
} else {
|
||||
audio_st = NULL;
|
||||
}
|
||||
|
||||
/* open the output file, if needed */
|
||||
if (!(fmt->flags & AVFMT_NOFILE)) {
|
||||
ret = avio_open2(&oc->pb, filename, AVIO_FLAG_WRITE,NULL,NULL);
|
||||
if (ret < 0) {
|
||||
Fatal("Could not open output file '%s': %s\n", filename,
|
||||
av_make_error_string(ret).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Write the stream header, if any. */
|
||||
ret = avformat_write_header(oc, NULL);
|
||||
if (ret < 0) {
|
||||
Fatal("Error occurred when writing output file header: %s\n",
|
||||
av_make_error_string(ret).c_str());
|
||||
}
|
||||
|
||||
prevDts = 0;
|
||||
startPts = 0;
|
||||
startDts = 0;
|
||||
filter_in_rescale_delta_last = AV_NOPTS_VALUE;
|
||||
|
||||
startTime=av_gettime()-nStartTime;//oc->start_time;
|
||||
Info("VideoStore startTime=%d\n",startTime);
|
||||
}
|
||||
|
||||
|
||||
VideoStore::~VideoStore(){
|
||||
/* Write the trailer before close */
|
||||
av_write_trailer(oc);
|
||||
|
||||
avcodec_close(video_st->codec);
|
||||
if (audio_st) {
|
||||
avcodec_close(audio_st->codec);
|
||||
}
|
||||
|
||||
if (!(fmt->flags & AVFMT_NOFILE)) {
|
||||
/* Close the output file. */
|
||||
avio_close(oc->pb);
|
||||
}
|
||||
|
||||
/* free the stream */
|
||||
avformat_free_context(oc);
|
||||
}
|
||||
|
||||
|
||||
void VideoStore::dumpPacket( AVPacket *pkt ){
|
||||
char b[10240];
|
||||
|
||||
snprintf(b, sizeof(b), " pts: %" PRId64 ", dts: %" PRId64 ", data: %p, size: %d, sindex: %d, dflags: %04x, s-pos: %" PRId64 ", c-duration: %" PRId64 "\n"
|
||||
, pkt->pts
|
||||
, pkt->dts
|
||||
, pkt->data
|
||||
, pkt->size
|
||||
, pkt->stream_index
|
||||
, pkt->flags
|
||||
, pkt->pos
|
||||
, pkt->convergence_duration
|
||||
);
|
||||
Info("%s:%d:DEBUG: %s", __FILE__, __LINE__, b);
|
||||
}
|
||||
|
||||
int VideoStore::writeVideoFramePacket(AVPacket *ipkt, AVStream *input_st){//, AVPacket *lastKeyframePkt){
|
||||
|
||||
int64_t ost_tb_start_time = av_rescale_q(startTime, AV_TIME_BASE_Q, video_st->time_base);
|
||||
|
||||
AVPacket opkt, safepkt;
|
||||
AVPicture pict;
|
||||
|
||||
av_init_packet(&opkt);
|
||||
|
||||
//Scale the PTS of the outgoing packet to be the correct time base
|
||||
if (ipkt->pts != AV_NOPTS_VALUE) {
|
||||
opkt.pts = av_rescale_q(ipkt->pts-startPts, input_st->time_base, video_st->time_base) - ost_tb_start_time;
|
||||
}else {
|
||||
opkt.pts = AV_NOPTS_VALUE;
|
||||
}
|
||||
|
||||
//Scale the DTS of the outgoing packet to be the correct time base
|
||||
if(ipkt->dts == AV_NOPTS_VALUE) {
|
||||
opkt.dts = av_rescale_q(input_st->cur_dts-startDts, AV_TIME_BASE_Q, video_st->time_base);
|
||||
} else {
|
||||
opkt.dts = av_rescale_q(ipkt->dts-startDts, input_st->time_base, video_st->time_base);
|
||||
}
|
||||
|
||||
opkt.dts -= ost_tb_start_time;
|
||||
|
||||
opkt.duration = av_rescale_q(ipkt->duration, input_st->time_base, video_st->time_base);
|
||||
opkt.flags = ipkt->flags;
|
||||
opkt.pos=-1;
|
||||
|
||||
opkt.data = ipkt->data;
|
||||
opkt.size = ipkt->size;
|
||||
opkt.stream_index = ipkt->stream_index;
|
||||
/*opkt.flags |= AV_PKT_FLAG_KEY;*/
|
||||
|
||||
if (video_st->codec->codec_type == AVMEDIA_TYPE_VIDEO && (fmt->flags & AVFMT_RAWPICTURE)) {
|
||||
/* store AVPicture in AVPacket, as expected by the output format */
|
||||
avpicture_fill(&pict, opkt.data, video_st->codec->pix_fmt, video_st->codec->width, video_st->codec->height);
|
||||
opkt.data = (uint8_t *)&pict;
|
||||
opkt.size = sizeof(AVPicture);
|
||||
opkt.flags |= AV_PKT_FLAG_KEY;
|
||||
}
|
||||
|
||||
memcpy(&safepkt, &opkt, sizeof(AVPacket));
|
||||
|
||||
if ((opkt.data == NULL)||(opkt.size < 1)) {
|
||||
Warning("%s:%d: Mangled AVPacket: discarding frame", __FILE__, __LINE__ );
|
||||
dumpPacket(&opkt);
|
||||
|
||||
} else if ((prevDts > 0) && (prevDts >= opkt.dts)) {
|
||||
Warning("%s:%d: DTS out of order: %lld \u226E %lld; discarding frame", __FILE__, __LINE__, prevDts, opkt.dts);
|
||||
prevDts = opkt.dts;
|
||||
dumpPacket(&opkt);
|
||||
|
||||
} else {
|
||||
int ret;
|
||||
|
||||
prevDts = opkt.dts; // Unsure if av_interleaved_write_frame() clobbers opkt.dts when out of order, so storing in advance
|
||||
ret = av_interleaved_write_frame(oc, &opkt);
|
||||
if(ret<0){
|
||||
// There's nothing we can really do if the frame is rejected, just drop it and get on with the next
|
||||
Warning("%s:%d: Writing frame [av_interleaved_write_frame()] failed: %s(%d) ", __FILE__, __LINE__, av_make_error_string(ret).c_str(), (ret));
|
||||
dumpPacket(&safepkt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
av_free_packet(&opkt);
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
int VideoStore::writeAudioFramePacket(AVPacket *ipkt, AVStream *input_st){
|
||||
|
||||
if(!audio_st)
|
||||
return -1;//FIXME -ve return codes do not free packet in ffmpeg_camera at the moment
|
||||
/*if(!keyframeMessage)
|
||||
return -1;*/
|
||||
|
||||
int64_t ost_tb_start_time = av_rescale_q(startTime, AV_TIME_BASE_Q, video_st->time_base);
|
||||
|
||||
AVPacket opkt;
|
||||
|
||||
av_init_packet(&opkt);
|
||||
|
||||
|
||||
//Scale the PTS of the outgoing packet to be the correct time base
|
||||
if (ipkt->pts != AV_NOPTS_VALUE)
|
||||
opkt.pts = av_rescale_q(ipkt->pts-startPts, input_st->time_base, audio_st->time_base) - ost_tb_start_time;
|
||||
else
|
||||
opkt.pts = AV_NOPTS_VALUE;
|
||||
|
||||
//Scale the DTS of the outgoing packet to be the correct time base
|
||||
if(ipkt->dts == AV_NOPTS_VALUE)
|
||||
opkt.dts = av_rescale_q(input_st->cur_dts-startDts, AV_TIME_BASE_Q, audio_st->time_base);
|
||||
else
|
||||
opkt.dts = av_rescale_q(ipkt->dts-startDts, input_st->time_base, audio_st->time_base);
|
||||
opkt.dts -= ost_tb_start_time;
|
||||
|
||||
if (audio_st->codec->codec_type == AVMEDIA_TYPE_AUDIO && ipkt->dts != AV_NOPTS_VALUE) {
|
||||
int duration = av_get_audio_frame_duration(input_st->codec, ipkt->size);
|
||||
if(!duration)
|
||||
duration = input_st->codec->frame_size;
|
||||
|
||||
//FIXME where to get filter_in_rescale_delta_last
|
||||
//FIXME av_rescale_delta doesn't exist in ubuntu vivid libavtools
|
||||
opkt.dts = opkt.pts = av_rescale_delta(input_st->time_base, ipkt->dts,
|
||||
(AVRational){1, input_st->codec->sample_rate}, duration, &filter_in_rescale_delta_last,
|
||||
audio_st->time_base) - ost_tb_start_time;
|
||||
}
|
||||
|
||||
opkt.duration = av_rescale_q(ipkt->duration, input_st->time_base, audio_st->time_base);
|
||||
opkt.pos=-1;
|
||||
opkt.flags = ipkt->flags;
|
||||
|
||||
opkt.data = ipkt->data;
|
||||
opkt.size = ipkt->size;
|
||||
opkt.stream_index = ipkt->stream_index;
|
||||
/*opkt.flags |= AV_PKT_FLAG_KEY;*/
|
||||
|
||||
int ret;
|
||||
ret = av_interleaved_write_frame(oc, &opkt);
|
||||
if(ret!=0){
|
||||
Fatal("Error encoding audio frame packet: %s\n", av_make_error_string(ret).c_str());
|
||||
}
|
||||
|
||||
av_free_packet(&opkt);
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
#ifndef ZM_VIDEOSTORE_H
|
||||
#define ZM_VIDEOSTORE_H
|
||||
|
||||
#include "zm_ffmpeg.h"
|
||||
|
||||
#if HAVE_LIBAVCODEC
|
||||
|
||||
class VideoStore {
|
||||
private:
|
||||
|
||||
AVOutputFormat *fmt;
|
||||
AVFormatContext *oc;
|
||||
AVStream *video_st;
|
||||
AVStream *audio_st;
|
||||
|
||||
const char *filename;
|
||||
const char *format;
|
||||
|
||||
bool keyframeMessage;
|
||||
int keyframeSkipNumber;
|
||||
|
||||
int64_t startTime;
|
||||
int64_t startPts;
|
||||
int64_t startDts;
|
||||
int64_t prevDts;
|
||||
int64_t filter_in_rescale_delta_last;
|
||||
|
||||
public:
|
||||
VideoStore(const char *filename_in, const char *format_in, AVStream *input_st, AVStream *inpaud_st, int64_t nStartTime);
|
||||
~VideoStore();
|
||||
|
||||
int writeVideoFramePacket(AVPacket *pkt, AVStream *input_st);//, AVPacket *lastKeyframePkt);
|
||||
int writeAudioFramePacket(AVPacket *pkt, AVStream *input_st);
|
||||
void dumpPacket( AVPacket *pkt );
|
||||
};
|
||||
|
||||
/*
|
||||
class VideoEvent {
|
||||
public:
|
||||
VideoEvent(unsigned int eid);
|
||||
~VideoEvent();
|
||||
|
||||
int createEventImage(unsigned int fid, char *&pBuff);
|
||||
|
||||
private:
|
||||
unsigned int m_eid;
|
||||
};*/
|
||||
|
||||
#endif //havelibav
|
||||
#endif //zm_videostore_h
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
; This file is for unifying the coding style for different editors and IDEs.
|
||||
; More information at http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
|
@ -0,0 +1,22 @@
|
|||
# User specific & automatically generated files #
|
||||
#################################################
|
||||
/app/Config/database.php
|
||||
/app/tmp
|
||||
/lib/Cake/Console/Templates/skel/tmp/
|
||||
/plugins
|
||||
/vendors
|
||||
/build
|
||||
/dist
|
||||
/tags
|
||||
/app/webroot/events
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
Icon?
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
|
@ -0,0 +1,5 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine on
|
||||
RewriteRule ^$ app/webroot/ [L]
|
||||
RewriteRule (.*) app/webroot/$1 [L]
|
||||
</IfModule>
|
|
@ -0,0 +1,116 @@
|
|||
language: php
|
||||
|
||||
php:
|
||||
- 5.2
|
||||
- 5.3
|
||||
- 5.4
|
||||
|
||||
env:
|
||||
- DB=mysql
|
||||
- DB=pgsql
|
||||
- DB=sqlite
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.4
|
||||
env:
|
||||
- PHPCS=1
|
||||
|
||||
before_script:
|
||||
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE cakephp_test;'; fi"
|
||||
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE cakephp_test2;'; fi"
|
||||
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE cakephp_test3;'; fi"
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi"
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'CREATE SCHEMA test2;' -U postgres -d cakephp_test; fi"
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'CREATE SCHEMA test3;' -U postgres -d cakephp_test; fi"
|
||||
- chmod -R 777 ./app/tmp
|
||||
- sudo apt-get install lighttpd
|
||||
- pear channel-discover pear.cakephp.org
|
||||
- pear install --alldeps cakephp/CakePHP_CodeSniffer
|
||||
- phpenv rehash
|
||||
- set +H
|
||||
- echo "<?php
|
||||
class DATABASE_CONFIG {
|
||||
private \$identities = array(
|
||||
'mysql' => array(
|
||||
'datasource' => 'Database/Mysql',
|
||||
'host' => '0.0.0.0',
|
||||
'login' => 'travis'
|
||||
),
|
||||
'pgsql' => array(
|
||||
'datasource' => 'Database/Postgres',
|
||||
'host' => '127.0.0.1',
|
||||
'login' => 'postgres',
|
||||
'database' => 'cakephp_test',
|
||||
'schema' => array(
|
||||
'default' => 'public',
|
||||
'test' => 'public',
|
||||
'test2' => 'test2',
|
||||
'test_database_three' => 'test3'
|
||||
)
|
||||
),
|
||||
'sqlite' => array(
|
||||
'datasource' => 'Database/Sqlite',
|
||||
'database' => array(
|
||||
'default' => ':memory:',
|
||||
'test' => ':memory:',
|
||||
'test2' => '/tmp/cakephp_test2.db',
|
||||
'test_database_three' => '/tmp/cakephp_test3.db'
|
||||
),
|
||||
)
|
||||
);
|
||||
public \$default = array(
|
||||
'persistent' => false,
|
||||
'host' => '',
|
||||
'login' => '',
|
||||
'password' => '',
|
||||
'database' => 'cakephp_test',
|
||||
'prefix' => ''
|
||||
);
|
||||
public \$test = array(
|
||||
'persistent' => false,
|
||||
'host' => '',
|
||||
'login' => '',
|
||||
'password' => '',
|
||||
'database' => 'cakephp_test',
|
||||
'prefix' => ''
|
||||
);
|
||||
public \$test2 = array(
|
||||
'persistent' => false,
|
||||
'host' => '',
|
||||
'login' => '',
|
||||
'password' => '',
|
||||
'database' => 'cakephp_test2',
|
||||
'prefix' => ''
|
||||
);
|
||||
public \$test_database_three = array(
|
||||
'persistent' => false,
|
||||
'host' => '',
|
||||
'login' => '',
|
||||
'password' => '',
|
||||
'database' => 'cakephp_test3',
|
||||
'prefix' => ''
|
||||
);
|
||||
public function __construct() {
|
||||
\$db = 'mysql';
|
||||
if (!empty(\$_SERVER['DB'])) {
|
||||
\$db = \$_SERVER['DB'];
|
||||
}
|
||||
foreach (array('default', 'test', 'test2', 'test_database_three') as \$source) {
|
||||
\$config = array_merge(\$this->{\$source}, \$this->identities[\$db]);
|
||||
if (is_array(\$config['database'])) {
|
||||
\$config['database'] = \$config['database'][\$source];
|
||||
}
|
||||
if (!empty(\$config['schema']) && is_array(\$config['schema'])) {
|
||||
\$config['schema'] = \$config['schema'][\$source];
|
||||
}
|
||||
\$this->{\$source} = \$config;
|
||||
}
|
||||
}
|
||||
}" > app/Config/database.php
|
||||
|
||||
script:
|
||||
- sh -c "if [ '$PHPCS' != '1' ]; then ./lib/Cake/Console/cake test core AllTests --stderr; else phpcs -p --extensions=php --standard=CakePHP ./lib/Cake; fi"
|
||||
|
||||
notifications:
|
||||
email: false
|
|
@ -117,6 +117,7 @@ $statusData = array(
|
|||
"Height" => true,
|
||||
"Length" => true,
|
||||
"Frames" => true,
|
||||
"DefaultVideo" => true,
|
||||
"AlarmFrames" => true,
|
||||
"TotScore" => true,
|
||||
"AvgScore" => true,
|
||||
|
@ -385,31 +386,33 @@ function getNearEvents()
|
|||
else
|
||||
$midSql = '';
|
||||
|
||||
$sql = "select E.Id as Id from Events as E inner join Monitors as M on E.MonitorId = M.Id where ".dbEscape($sortColumn)." ".($sortOrder=='asc'?'<=':'>=')." '".$event[$_REQUEST['sort_field']]."'".$_REQUEST['filter']['sql'].$midSql." order by $sortColumn ".($sortOrder=='asc'?'desc':'asc');
|
||||
$sql = "select E.* as Id from Events as E inner join Monitors as M on E.MonitorId = M.Id where ".dbEscape($sortColumn)." ".($sortOrder=='asc'?'<=':'>=')." '".$event[$_REQUEST['sort_field']]."'".$_REQUEST['filter']['sql'].$midSql." order by $sortColumn ".($sortOrder=='asc'?'desc':'asc');
|
||||
$result = dbQuery( $sql );
|
||||
while ( $id = dbFetchNext( $result, 'Id' ) )
|
||||
{
|
||||
if ( $id == $eventId )
|
||||
{
|
||||
$prevId = dbFetchNext( $result, 'Id' );
|
||||
$prevEvent = dbFetchNext( $result );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "select E.Id as Id from Events as E inner join Monitors as M on E.MonitorId = M.Id where $sortColumn ".($sortOrder=='asc'?'>=':'<=')." '".$event[$_REQUEST['sort_field']]."'".$_REQUEST['filter']['sql'].$midSql." order by $sortColumn $sortOrder";
|
||||
$sql = "select E.* as Id from Events as E inner join Monitors as M on E.MonitorId = M.Id where $sortColumn ".($sortOrder=='asc'?'>=':'<=')." '".$event[$_REQUEST['sort_field']]."'".$_REQUEST['filter']['sql'].$midSql." order by $sortColumn $sortOrder";
|
||||
$result = dbQuery( $sql );
|
||||
while ( $id = dbFetchNext( $result, 'Id' ) )
|
||||
{
|
||||
if ( $id == $eventId )
|
||||
{
|
||||
$nextId = dbFetchNext( $result, 'Id' );
|
||||
$nextEvent = dbFetchNext( $result );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$result = array( 'EventId'=>$eventId );
|
||||
$result['PrevEventId'] = empty($prevId)?0:$prevId;
|
||||
$result['NextEventId'] = empty($nextId)?0:$nextId;
|
||||
$result['PrevEventId'] = empty($prevEvent)?0:$prevEvent['Id'];
|
||||
$result['NextEventId'] = empty($nextEvent)?0:$nextEvent['Id'];
|
||||
$result['PrevEventDefVideoPath'] = empty($prevEvent)?0:(getEventDefaultVideoPath($prevEvent));
|
||||
$result['NextEventDefVideoPath'] = empty($nextEvent)?0:(getEventDefaultVideoPath($nextEvent));
|
||||
return( $result );
|
||||
}
|
||||
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::
|
||||
:: Bake is a shell script for running CakePHP bake script
|
||||
::
|
||||
:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
::
|
||||
:: Licensed under The MIT License
|
||||
:: Redistributions of files must retain the above copyright notice.
|
||||
::
|
||||
:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
:: @link http://cakephp.org CakePHP(tm) Project
|
||||
:: @package app.Console
|
||||
:: @since CakePHP(tm) v 2.0
|
||||
:: @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
||||
:: In order for this script to work as intended, the cake\console\ folder must be in your PATH
|
||||
|
||||
@echo.
|
||||
@echo off
|
||||
|
||||
SET app=%0
|
||||
SET lib=%~dp0
|
||||
|
||||
php -q "%lib%cake.php" -working "%CD% " %*
|
||||
|
||||
echo.
|
||||
|
||||
exit /B %ERRORLEVEL%
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::
|
||||
:: Bake is a shell script for running CakePHP bake script
|
||||
::
|
||||
:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
::
|
||||
:: Licensed under The MIT License
|
||||
:: Redistributions of files must retain the above copyright notice.
|
||||
::
|
||||
:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
:: @link http://cakephp.org CakePHP(tm) Project
|
||||
:: @package app.Console
|
||||
:: @since CakePHP(tm) v 2.0
|
||||
:: @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
||||
:: In order for this script to work as intended, the cake\console\ folder must be in your PATH
|
||||
|
||||
@echo.
|
||||
@echo off
|
||||
|
||||
SET app=%0
|
||||
SET lib=%~dp0
|
||||
|
||||
php -q "%lib%cake.php" -working "%CD% " %*
|
||||
|
||||
echo.
|
||||
|
||||
exit /B %ERRORLEVEL%
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::
|
||||
:: Bake is a shell script for running CakePHP bake script
|
||||
::
|
||||
:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
::
|
||||
:: Licensed under The MIT License
|
||||
:: Redistributions of files must retain the above copyright notice.
|
||||
::
|
||||
:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
:: @link http://cakephp.org CakePHP(tm) Project
|
||||
:: @package app.Console
|
||||
:: @since CakePHP(tm) v 2.0
|
||||
::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
||||
:: In order for this script to work as intended, the cake\console\ folder must be in your PATH
|
||||
|
||||
@echo.
|
||||
@echo off
|
||||
|
||||
SET app=%0
|
||||
SET lib=%~dp0
|
||||
|
||||
php -q "%lib%cake.php" -working "%CD% " %*
|
||||
|
||||
echo.
|
||||
|
||||
exit /B %ERRORLEVEL%
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::
|
||||
:: Bake is a shell script for running CakePHP bake script
|
||||
::
|
||||
:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
::
|
||||
:: Licensed under The MIT License
|
||||
:: Redistributions of files must retain the above copyright notice.
|
||||
::
|
||||
:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
:: @link http://cakephp.org CakePHP(tm) Project
|
||||
:: @package app.Console
|
||||
:: @since CakePHP(tm) v 2.0
|
||||
::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
||||
:: In order for this script to work as intended, the cake\console\ folder must be in your PATH
|
||||
|
||||
@echo.
|
||||
@echo off
|
||||
|
||||
SET app=%0
|
||||
SET lib=%~dp0
|
||||
|
||||
php -q "%lib%cake.php" -working "%CD% " %*
|
||||
|
||||
echo.
|
||||
|
||||
exit /B %ERRORLEVEL%
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::
|
||||
:: Bake is a shell script for running CakePHP bake script
|
||||
::
|
||||
:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
::
|
||||
:: Licensed under The MIT License
|
||||
:: Redistributions of files must retain the above copyright notice.
|
||||
::
|
||||
:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
:: @link http://cakephp.org CakePHP(tm) Project
|
||||
:: @package Cake.Console
|
||||
:: @since CakePHP(tm) v 1.2.0.5012
|
||||
:: @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
||||
@echo off
|
||||
|
||||
SET app=%0
|
||||
SET lib=%~dp0
|
||||
|
||||
php -q "%lib%cake.php" -working "%CD% " %*
|
||||
|
||||
echo.
|
||||
|
||||
exit /B %ERRORLEVEL%
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::
|
||||
:: Bake is a shell script for running CakePHP bake script
|
||||
::
|
||||
:: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
:: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
::
|
||||
:: Licensed under The MIT License
|
||||
:: Redistributions of files must retain the above copyright notice.
|
||||
::
|
||||
:: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
:: @link http://cakephp.org CakePHP(tm) Project
|
||||
:: @package Cake.Console
|
||||
:: @since CakePHP(tm) v 1.2.0.5012
|
||||
:: @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
||||
@echo off
|
||||
|
||||
SET app=%0
|
||||
SET lib=%~dp0
|
||||
|
||||
php -q "%lib%cake.php" -working "%CD% " %*
|
||||
|
||||
echo.
|
||||
|
||||
exit /B %ERRORLEVEL%
|
||||
|
|
|
@ -524,6 +524,9 @@ function getEventPath( $event )
|
|||
return( $eventPath );
|
||||
}
|
||||
|
||||
function getEventDefaultVideoPath( $event ) {
|
||||
return ZM_DIR_EVENTS . "/" . getEventPath($event) . "/" . $event['DefaultVideo'];
|
||||
}
|
||||
|
||||
function deletePath( $path )
|
||||
{
|
||||
|
@ -1119,7 +1122,11 @@ function getImageSrc( $event, $frame, $scale=SCALE_BASE, $captureOnly=false, $ov
|
|||
$frame = array( 'FrameId'=>$frame, 'Type'=>'' );
|
||||
|
||||
//echo "S:$scale, CO:$captureOnly<br>";
|
||||
$captImage = sprintf( "%0".ZM_EVENT_IMAGE_DIGITS."d-capture.jpg", $frame['FrameId'] );
|
||||
$currEvent = dbFetchOne( 'SELECT M.SaveJPEGs FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = '.$event['Id'] );
|
||||
if ( $currEvent['SaveJPEGs'] == "4" )
|
||||
$captImage = "snapshot.jpg";
|
||||
else
|
||||
$captImage = sprintf( "%0".ZM_EVENT_IMAGE_DIGITS."d-capture.jpg", $frame['FrameId'] );
|
||||
$captPath = $eventPath.'/'.$captImage;
|
||||
$thumbCaptPath = ZM_DIR_IMAGES.'/'.$event['Id'].'-'.$captImage;
|
||||
//echo "CI:$captImage, CP:$captPath, TCP:$thumbCaptPath<br>";
|
||||
|
|
|
@ -5,4 +5,5 @@ webdir = @WEB_PREFIX@/js
|
|||
dist_web_DATA = \
|
||||
logger.js \
|
||||
overlay.js \
|
||||
mootools.ext.js
|
||||
mootools.ext.js \
|
||||
videojs.zoomrotate.js
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
console.log('zoomrotate: Start');
|
||||
|
||||
(function(){
|
||||
var defaults, extend;
|
||||
console.log('zoomrotate: Init defaults');
|
||||
defaults = {
|
||||
zoom: 1,
|
||||
rotate: 0
|
||||
};
|
||||
console.log('zoomrotate: Init Extend');
|
||||
extend = function() {
|
||||
var args, target, i, object, property;
|
||||
args = Array.prototype.slice.call(arguments);
|
||||
target = args.shift() || {};
|
||||
for (i in args) {
|
||||
object = args[i];
|
||||
for (property in object) {
|
||||
if (object.hasOwnProperty(property)) {
|
||||
if (typeof object[property] === 'object') {
|
||||
target[property] = extend(target[property], object[property]);
|
||||
} else {
|
||||
target[property] = object[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* register the zoomrotate plugin
|
||||
*/
|
||||
videojs.plugin('zoomrotate', function(options){
|
||||
console.log('zoomrotate: Register init');
|
||||
var settings, player, video, poster;
|
||||
settings = extend(defaults, options);
|
||||
|
||||
/* Grab the necessary DOM elements */
|
||||
player = this.el();
|
||||
video = this.el().getElementsByTagName('video')[0];
|
||||
poster = this.el().getElementsByTagName('div')[1]; // div vjs-poster
|
||||
|
||||
console.log('zoomrotate: '+video.style);
|
||||
console.log('zoomrotate: '+poster.style);
|
||||
console.log('zoomrotate: '+options.rotate);
|
||||
console.log('zoomrotate: '+options.zoom);
|
||||
|
||||
/* Array of possible browser specific settings for transformation */
|
||||
var properties = ['transform', 'WebkitTransform', 'MozTransform',
|
||||
'msTransform', 'OTransform'],
|
||||
prop = properties[0];
|
||||
|
||||
/* Iterators */
|
||||
var i,j;
|
||||
|
||||
/* Find out which CSS transform the browser supports */
|
||||
for(i=0,j=properties.length;i<j;i++){
|
||||
if(typeof player.style[properties[i]] !== 'undefined'){
|
||||
prop = properties[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Let's do it */
|
||||
player.style.overflow = 'hidden';
|
||||
video.style[prop]='scale('+options.zoom+') rotate('+options.rotate+'deg)';
|
||||
poster.style[prop]='scale('+options.zoom+') rotate('+options.rotate+'deg)';
|
||||
console.log('zoomrotate: Register end');
|
||||
});
|
||||
})();
|
||||
|
||||
console.log('zoomrotate: End');
|
|
@ -544,6 +544,7 @@ $SLANG = array(
|
|||
'OpNe' => 'not equal to',
|
||||
'OpNotIn' => 'not in set',
|
||||
'OpNotMatches' => 'does not match',
|
||||
'OptionalEncoderParam' => 'Optional Encoder Parameters',
|
||||
'OptionHelp' => 'Option Help',
|
||||
'OptionRestartWarning' => 'These changes may not come into effect fully\nwhile the system is running. When you have\nfinished making your changes please ensure that\nyou restart ZoneMinder.',
|
||||
'Options' => 'Options',
|
||||
|
@ -618,6 +619,7 @@ $SLANG = array(
|
|||
'RunState' => 'Run State',
|
||||
'SaveAs' => 'Save as',
|
||||
'SaveFilter' => 'Save Filter',
|
||||
'SaveJPEGs' => 'Save JPEGs',
|
||||
'Save' => 'Save',
|
||||
'Scale' => 'Scale',
|
||||
'Score' => 'Score',
|
||||
|
@ -726,6 +728,7 @@ $SLANG = array(
|
|||
'VideoGenParms' => 'Video Generation Parameters',
|
||||
'VideoGenSucceeded' => 'Video Generation Succeeded!',
|
||||
'VideoSize' => 'Video Size',
|
||||
'VideoWriter' => 'Video Writer',
|
||||
'Video' => 'Video',
|
||||
'ViewAll' => 'View All',
|
||||
'ViewEvent' => 'View Event',
|
||||
|
|
|
@ -57,6 +57,23 @@
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
#videoBar1 div {
|
||||
text-align: center;
|
||||
float: center;
|
||||
}
|
||||
|
||||
#videoBar1 #prevEvent {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#videoBar1 #dlEvent {
|
||||
float: center;
|
||||
}
|
||||
|
||||
#videoBar1 #nextEvent {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#imageFeed {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -236,3 +236,64 @@
|
|||
height: 10px;
|
||||
background-color: #444444;
|
||||
}
|
||||
#eventVideo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#video-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 5px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity .3s;
|
||||
-moz-transition: opacity .3s;
|
||||
-o-transition: opacity .3s;
|
||||
-ms-transition: opacity .3s;
|
||||
transition: opacity .3s;
|
||||
background-image: linear-gradient(bottom, rgb(3,113,168) 13%, rgb(0,136,204) 100%);
|
||||
background-image: -o-linear-gradient(bottom, rgb(3,113,168) 13%, rgb(0,136,204) 100%);
|
||||
background-image: -moz-linear-gradient(bottom, rgb(3,113,168) 13%, rgb(0,136,204) 100%);
|
||||
background-image: -webkit-linear-gradient(bottom, rgb(3,113,168) 13%, rgb(0,136,204) 100%);
|
||||
background-image: -ms-linear-gradient(bottom, rgb(3,113,168) 13%, rgb(0,136,204) 100%);
|
||||
|
||||
background-image: -webkit-gradient(
|
||||
linear,
|
||||
left bottom,
|
||||
left top,
|
||||
color-stop(0.13, rgb(3,113,168)),
|
||||
color-stop(1, rgb(0,136,204))
|
||||
);
|
||||
}
|
||||
|
||||
#eventVideo:hover #video-controls {
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
button {
|
||||
background: rgba(0,0,0,.5);
|
||||
border: 0;
|
||||
color: #EEE;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-o-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#seekbar {
|
||||
width: 360px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#volume-bar {
|
||||
width: 60px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -261,3 +261,38 @@ if ( focusWindow )
|
|||
}
|
||||
window.addEvent( 'domready', checkSize);
|
||||
|
||||
function convertLabelFormat(LabelFormat, monitorName){
|
||||
//convert label format from strftime to moment's format (modified from
|
||||
//https://raw.githubusercontent.com/benjaminoakes/moment-strftime/master/lib/moment-strftime.js
|
||||
//added %f and %N below (TODO: add %Q)
|
||||
var replacements = { a: 'ddd', A: 'dddd', b: 'MMM', B: 'MMMM', d: 'DD', e: 'D', F: 'YYYY-MM-DD', H: 'HH', I: 'hh', j: 'DDDD', k: 'H', l: 'h', m: 'MM', M: 'mm', p: 'A', S: 'ss', u: 'E', w: 'd', W: 'WW', y: 'YY', Y: 'YYYY', z: 'ZZ', Z: 'z', 'f': 'SS', 'N': "["+monitorName+"]", '%': '%' };
|
||||
var momentLabelFormat = Object.keys(replacements).reduce(function (momentFormat, key) {
|
||||
var value = replacements[key];
|
||||
return momentFormat.replace("%" + key, value);
|
||||
}, LabelFormat);
|
||||
return momentLabelFormat;
|
||||
}
|
||||
|
||||
function addVideoTimingTrack(video, LabelFormat, monitorName, duration, startTime){
|
||||
var labelFormat = convertLabelFormat(LabelFormat, monitorName);
|
||||
var webvttformat = 'HH:mm:ss.SSS', webvttdata="WEBVTT\n\n";
|
||||
|
||||
startTime = moment(startTime);
|
||||
|
||||
var seconds = moment({s:0}), endduration = moment({s:duration});
|
||||
while(seconds.isBefore(endduration)){
|
||||
webvttdata += seconds.format(webvttformat) + " --> ";
|
||||
seconds.add(1,'s');
|
||||
webvttdata += seconds.format(webvttformat) + "\n";
|
||||
webvttdata += startTime.format(labelFormat) + "\n\n";
|
||||
startTime.add(1, 's');
|
||||
}
|
||||
var track = document.createElement('track');
|
||||
track.kind = "captions";
|
||||
track.srclang = "en";
|
||||
track.label = "English";
|
||||
track['default'] = true;
|
||||
track.src = 'data:plain/text;charset=utf-8,'+encodeURIComponent(webvttdata);
|
||||
video.appendChild(track);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ if ( !canView( 'Events' ) )
|
|||
$eid = validInt( $_REQUEST['eid'] );
|
||||
$fid = !empty($_REQUEST['fid'])?validInt($_REQUEST['fid']):1;
|
||||
|
||||
$sql = 'SELECT E.*,M.Name AS MonitorName,M.Width,M.Height,M.DefaultRate,M.DefaultScale FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?';
|
||||
$sql = 'SELECT E.*,M.Name AS MonitorName,M.Width,M.Height,M.DefaultRate,M.DefaultScale,M.VideoWriter,M.SaveJPEGs,M.Orientation,M.LabelFormat FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?';
|
||||
$sql_values = array( $eid );
|
||||
|
||||
if ( $user['MonitorIds'] ) {
|
||||
|
@ -55,7 +55,7 @@ $replayModes = array(
|
|||
if ( isset( $_REQUEST['streamMode'] ) )
|
||||
$streamMode = validHtmlStr($_REQUEST['streamMode']);
|
||||
else
|
||||
$streamMode = canStream()?'stream':'stills';
|
||||
$streamMode = video;
|
||||
|
||||
if ( isset( $_REQUEST['replayMode'] ) )
|
||||
$replayMode = validHtmlStr($_REQUEST['replayMode']);
|
||||
|
@ -66,6 +66,15 @@ else {
|
|||
$replayMode = array_shift( $keys );
|
||||
}
|
||||
|
||||
// videojs zoomrotate only when direct recording
|
||||
$Zoom = 1;
|
||||
$Rotation = 0;
|
||||
if ( $event['VideoWriter'] == "2" ) {
|
||||
$Rotation = $event['Orientation'];
|
||||
if ( in_array($event['Orientation'],array("90","270")))
|
||||
$Zoom = $event['Height']/$event['Width'];
|
||||
}
|
||||
|
||||
parseSort();
|
||||
parseFilter( $_REQUEST['filter'] );
|
||||
$filterQuery = $_REQUEST['filter']['query'];
|
||||
|
@ -108,36 +117,55 @@ if ( canEdit( 'Events' ) )
|
|||
?>
|
||||
<div id="deleteEvent"><a href="#" onclick="deleteEvent()"><?php echo translate('Delete') ?></a></div>
|
||||
<div id="editEvent"><a href="#" onclick="editEvent()"><?php echo translate('Edit') ?></a></div>
|
||||
<div id="archiveEvent" class="hidden"><a href="#" onclick="archiveEvent()"><?php echo translate('Archive') ?></a></div>
|
||||
<div id="unarchiveEvent" class="hidden"><a href="#" onclick="unarchiveEvent()"><?php echo translate('Unarchive') ?></a></div>
|
||||
<?php
|
||||
}
|
||||
if ( canView( 'Events' ) )
|
||||
{
|
||||
?>
|
||||
<div id="exportEvent"><a href="#" onclick="exportEvent()"><?php echo translate('Export') ?></a></div>
|
||||
<div id="framesEvent"><a href="#" onclick="showEventFrames()"><?php echo translate('Frames') ?></a></div>
|
||||
<?php
|
||||
}
|
||||
if ( canEdit( 'Events' ) )
|
||||
if ( $event['SaveJPEGs'] & 3 )
|
||||
{
|
||||
?>
|
||||
<div id="archiveEvent" class="hidden"><a href="#" onclick="archiveEvent()"><?php echo translate('Archive') ?></a></div>
|
||||
<div id="unarchiveEvent" class="hidden"><a href="#" onclick="unarchiveEvent()"><?php echo translate('Unarchive') ?></a></div>
|
||||
<div id="stillsEvent"<?php if ( $streamMode == 'still' ) { ?> class="hidden"<?php } ?>><a href="#" onclick="showStills()"><?php echo translate('Stills') ?></a></div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<div id="framesEvent"><a href="#" onclick="showEventFrames()"><?php echo translate('Frames') ?></a></div>
|
||||
<div id="streamEvent"<?php if ( $streamMode == 'stream' ) { ?> class="hidden"<?php } ?>><a href="#" onclick="showStream()"><?php echo translate('Stream') ?></a></div>
|
||||
<div id="stillsEvent"<?php if ( $streamMode == 'still' ) { ?> class="hidden"<?php } ?>><a href="#" onclick="showStills()"><?php echo translate('Stills') ?></a></div>
|
||||
<div id="videoEvent"<?php if ( $streamMode == 'video' ) { ?> class="hidden"<?php } ?>><a href="#" onclick="showVideo()"><?php echo translate('Video') ?></a></div>
|
||||
<div id="exportEvent"><a href="#" onclick="exportEvent()"><?php echo translate('Export') ?></a></div>
|
||||
</div>
|
||||
<div id="eventVideo" class="">
|
||||
<?php
|
||||
if ( $event['DefaultVideo'] )
|
||||
{
|
||||
?>
|
||||
<div id="videoFeed">
|
||||
<video id="videoobj" class="video-js vjs-default-skin" width="<?php echo reScale( $event['Width'], $scale ) ?>" height="<?php echo reScale( $event['Height'], $scale ) ?>" data-setup='{ "controls": true, "playbackRates": [0.5, 1, 1.5, 2, 4, 8, 16, 32, 64, 128, 256], "autoplay": true, "preload": "auto", "plugins": { "zoomrotate": { "rotate": "<?php echo $Rotation ?>", "zoom": "<?php echo $Zoom ?>"}}}'>
|
||||
<source src="<?php echo getEventDefaultVideoPath($event) ?>" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
<!--script>includeVideoJs();</script-->
|
||||
<link href="//vjs.zencdn.net/4.11/video-js.css" rel="stylesheet">
|
||||
<script src="//vjs.zencdn.net/4.11/video.js"></script>
|
||||
<script src="./js/videojs.zoomrotate.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
|
||||
<script>
|
||||
var LabelFormat = "<?php echo validJsStr($event['LabelFormat'])?>";
|
||||
var monitorName = "<?php echo validJsStr($event['MonitorName'])?>";
|
||||
var duration = <?php echo $event['Length'] ?>, startTime = '<?php echo $event['StartTime'] ?>';
|
||||
|
||||
addVideoTimingTrack(document.getElementById('videoobj'), LabelFormat, monitorName, duration, startTime);
|
||||
</script>
|
||||
|
||||
<?php
|
||||
if ( ZM_OPT_FFMPEG )
|
||||
}
|
||||
else
|
||||
{
|
||||
?>
|
||||
<div id="videoEvent"><a href="#" onclick="videoEvent()"><?php echo translate('Video') ?></a></div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div id="eventStream">
|
||||
<div id="imageFeed">
|
||||
<div id="imageFeed">
|
||||
<?php
|
||||
if ( ZM_WEB_STREAM_METHOD == 'mpeg' && ZM_MPEG_LIVE_FORMAT )
|
||||
{
|
||||
|
@ -184,14 +212,22 @@ else
|
|||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
if ($event['SaveJPEGs'] & 3)
|
||||
{
|
||||
?>
|
||||
<div id="eventStills" class="hidden">
|
||||
<div id="eventThumbsPanel">
|
||||
<div id="eventThumbs">
|
||||
</div>
|
||||
</div>
|
||||
<div id="eventImagePanel" class="hidden">
|
||||
<div id="eventImagePanel">
|
||||
<div id="eventImageFrame">
|
||||
<img id="eventImage" src="graphics/transparent.gif" alt=""/>
|
||||
<div id="eventImageBar">
|
||||
|
@ -220,7 +256,10 @@ else
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -28,7 +28,7 @@ $eid = validInt($_REQUEST['eid']);
|
|||
if ( !empty($_REQUEST['fid']) )
|
||||
$fid = validInt($_REQUEST['fid']);
|
||||
|
||||
$sql = 'SELECT E.*,M.Name AS MonitorName,M.DefaultScale FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?';
|
||||
$sql = 'SELECT E.*,M.Name AS MonitorName,M.DefaultScale,M.VideoWriter FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?';
|
||||
$event = dbFetchOne( $sql, NULL, array($eid) );
|
||||
|
||||
if ( !empty($fid) ) {
|
||||
|
@ -75,7 +75,13 @@ xhtmlHeaders(__FILE__, translate('Frame')." - ".$event['Id']." - ".$frame['Frame
|
|||
<h2><?php echo translate('Frame') ?> <?php echo $event['Id']."-".$frame['FrameId']." (".$frame['Score'].")" ?></h2>
|
||||
</div>
|
||||
<div id="content">
|
||||
<p id="image"><?php if ( $imageData['hasAnalImage'] ) { ?><a href="?view=frame&eid=<?php echo $event['Id'] ?>&fid=<?php echo $frame['FrameId'] ?>&scale=<?php echo $scale ?>&show=<?php echo $imageData['isAnalImage']?"capt":"anal" ?>"><?php } ?><img src="<?php echo viewImagePath( $imagePath ) ?>" width="<?php echo reScale( $event['Width'], $event['DefaultScale'], $scale ) ?>" height="<?php echo reScale( $event['Height'], $event['DefaultScale'], $scale ) ?>" alt="<?php echo $frame['EventId']."-".$frame['FrameId'] ?>" class="<?php echo $imageData['imageClass'] ?>"/><?php if ( $imageData['hasAnalImage'] ) { ?></a><?php } ?></p>
|
||||
<p id="image">
|
||||
<?php if ( in_array($event['VideoWriter'],array("1","2")) ) { ?>
|
||||
<img src="?view=image-ffmpeg&eid=<?php echo $event['Id'] ?>&fid=<?php echo $frame['FrameId'] ?>&scale=<?php echo $event['DefaultScale'] ?>" class="<?php echo $imageData['imageClass'] ?>">
|
||||
<?php } else {
|
||||
if ( $imageData['hasAnalImage'] ) { ?><a href="?view=frame&eid=<?php echo $event['Id'] ?>&fid=<?php echo $frame['FrameId'] ?>&scale=<?php echo $scale ?>&show=<?php echo $imageData['isAnalImage']?"capt":"anal" ?>"><?php } ?><img src="<?php echo viewImagePath( $imagePath ) ?>" width="<?php echo reScale( $event['Width'], $event['DefaultScale'], $scale ) ?>" height="<?php echo reScale( $event['Height'], $event['DefaultScale'], $scale ) ?>" alt="<?php echo $frame['EventId']."-".$frame['FrameId'] ?>" class="<?php echo $imageData['imageClass'] ?>"/><?php if ( $imageData['hasAnalImage'] ) { ?></a><?php } ?>
|
||||
<?php } ?>
|
||||
</p>
|
||||
<p id="controls">
|
||||
<?php if ( $frame['FrameId'] > 1 ) { ?>
|
||||
<a id="firstLink" href="?view=frame&eid=<?php echo $event['Id'] ?>&fid=<?php echo $firstFid ?>&scale=<?php echo $scale ?>"><?php echo translate('First') ?></a>
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
//
|
||||
// ZoneMinder web frame view file, $Date$, $Revision$
|
||||
// Copyright (C) 2001-2008 Philip Coombes
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
//
|
||||
|
||||
if ( !canView( 'Events' ) )
|
||||
{
|
||||
$view = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
$eid = validInt($_REQUEST['eid']);
|
||||
if ( !empty($_REQUEST['fid']) )
|
||||
$fid = validInt($_REQUEST['fid']);
|
||||
|
||||
$sql = 'SELECT E.*,M.Name AS MonitorName,M.DefaultScale,M.VideoWriter,M.Orientation FROM Events AS E INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id = ?';
|
||||
$event = dbFetchOne( $sql, NULL, array($eid) );
|
||||
|
||||
if ( !empty($fid) ) {
|
||||
$sql = 'SELECT * FROM Frames WHERE EventId = ? AND FrameId = ?';
|
||||
if ( !($frame = dbFetchOne( $sql, NULL, array($eid, $fid) )) )
|
||||
$frame = array( 'FrameId'=>$fid, 'Type'=>'Normal', 'Score'=>0 );
|
||||
} else {
|
||||
$frame = dbFetchOne( 'SELECT * FROM Frames WHERE EventId = ? AND Score = ?', NULL, array( $eid, $event['MaxScore'] ) );
|
||||
}
|
||||
|
||||
$maxFid = $event['Frames'];
|
||||
|
||||
$firstFid = 1;
|
||||
$prevFid = $frame['FrameId']-1;
|
||||
$nextFid = $frame['FrameId']+1;
|
||||
$lastFid = $maxFid;
|
||||
|
||||
$alarmFrame = $frame['Type']=='Alarm';
|
||||
|
||||
if ( isset( $_REQUEST['scale'] ) )
|
||||
$scale = validInt($_REQUEST['scale']);
|
||||
else
|
||||
$scale = max( reScale( SCALE_BASE, $event['DefaultScale'], ZM_WEB_DEFAULT_SCALE ), SCALE_BASE );
|
||||
|
||||
$Transpose = '';
|
||||
if ( $event['VideoWriter'] == "2" ) {
|
||||
$Rotation = $event['Orientation'];
|
||||
// rotate right
|
||||
if ( in_array($event['Orientation'],array("90")))
|
||||
$Transpose = 'transpose=1,';
|
||||
// rotate 180 // upside down cam
|
||||
if ( in_array($event['Orientation'],array("180")))
|
||||
$Transpose = 'transpose=2,transpose=2,';
|
||||
// rotate left
|
||||
if ( in_array($event['Orientation'],array("270")))
|
||||
$Transpose = 'transpose=2,';
|
||||
}
|
||||
$focusWindow = true;
|
||||
$Scale = 100/$scale;
|
||||
$fid = $fid - 1;
|
||||
#$command = 'ffmpeg -v 0 -i '.getEventDefaultVideoPath($event).' -vf "select=gte(selected_n\,'.$fid.'),setpts=PTS-STARTPTS" '.$Transpose.',scale=iw/'.$Scale.':-1" -frames:v 1 -f mjpeg -';
|
||||
$command = 'ffmpeg -v 0 -i '.getEventDefaultVideoPath($event).' -vf "select=gte(n\\,'.$fid.'),setpts=PTS-STARTPTS,'.$Transpose.'scale=iw/'.$Scale.':-1" -f image2 -';
|
||||
header('Content-Type: image/jpeg');
|
||||
system($command);
|
||||
?>
|
|
@ -12,12 +12,15 @@ function changeScale()
|
|||
var newWidth = ( baseWidth * scale ) / SCALE_BASE;
|
||||
var newHeight = ( baseHeight * scale ) / SCALE_BASE;
|
||||
|
||||
streamScale( scale );
|
||||
|
||||
/*Stream could be an applet so can't use moo tools*/
|
||||
var streamImg = document.getElementById('evtStream');
|
||||
streamImg.style.width = newWidth + "px";
|
||||
streamImg.style.height = newHeight + "px";
|
||||
if(vid) {
|
||||
vid.width = newWidth;
|
||||
vid.height = newHeight;
|
||||
} else {
|
||||
streamScale( scale );
|
||||
var streamImg = document.getElementById('evtStream');
|
||||
streamImg.style.width = newWidth + "px";
|
||||
streamImg.style.height = newHeight + "px";
|
||||
}
|
||||
}
|
||||
|
||||
function changeReplayMode()
|
||||
|
@ -86,8 +89,7 @@ function streamPause( action )
|
|||
setButtonState( $('slowFwdBtn'), 'inactive' );
|
||||
setButtonState( $('slowRevBtn'), 'inactive' );
|
||||
setButtonState( $('fastRevBtn'), 'unavail' );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_PAUSE );
|
||||
streamReq.send( streamParms+"&command="+CMD_PAUSE );
|
||||
}
|
||||
|
||||
function streamPlay( action )
|
||||
|
@ -99,10 +101,7 @@ function streamPlay( action )
|
|||
setButtonState( $('slowFwdBtn'), 'unavail' );
|
||||
setButtonState( $('slowRevBtn'), 'unavail' );
|
||||
setButtonState( $('fastRevBtn'), 'inactive' );
|
||||
if ( action )
|
||||
{
|
||||
streamReq.send( streamParms+"&command="+CMD_PLAY );
|
||||
}
|
||||
streamReq.send( streamParms+"&command="+CMD_PLAY );
|
||||
}
|
||||
|
||||
function streamFastFwd( action )
|
||||
|
@ -113,8 +112,7 @@ function streamFastFwd( action )
|
|||
setButtonState( $('slowFwdBtn'), 'unavail' );
|
||||
setButtonState( $('slowRevBtn'), 'unavail' );
|
||||
setButtonState( $('fastRevBtn'), 'inactive' );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_FASTFWD );
|
||||
streamReq.send( streamParms+"&command="+CMD_FASTFWD );
|
||||
}
|
||||
|
||||
function streamSlowFwd( action )
|
||||
|
@ -125,9 +123,8 @@ function streamSlowFwd( action )
|
|||
setButtonState( $('slowFwdBtn'), 'active' );
|
||||
setButtonState( $('slowRevBtn'), 'inactive' );
|
||||
setButtonState( $('fastRevBtn'), 'unavail' );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_SLOWFWD );
|
||||
setButtonState( $('pauseBtn'), 'active' );
|
||||
streamReq.send( streamParms+"&command="+CMD_SLOWFWD );
|
||||
setButtonState( $('pauseBtn'), 'inactive' );
|
||||
setButtonState( $('slowFwdBtn'), 'inactive' );
|
||||
}
|
||||
|
||||
|
@ -139,9 +136,8 @@ function streamSlowRev( action )
|
|||
setButtonState( $('slowFwdBtn'), 'inactive' );
|
||||
setButtonState( $('slowRevBtn'), 'active' );
|
||||
setButtonState( $('fastRevBtn'), 'unavail' );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_SLOWREV );
|
||||
setButtonState( $('pauseBtn'), 'active' );
|
||||
streamReq.send( streamParms+"&command="+CMD_SLOWREV );
|
||||
setButtonState( $('pauseBtn'), 'inactive' );
|
||||
setButtonState( $('slowRevBtn'), 'inactive' );
|
||||
}
|
||||
|
||||
|
@ -153,24 +149,22 @@ function streamFastRev( action )
|
|||
setButtonState( $('slowFwdBtn'), 'unavail' );
|
||||
setButtonState( $('slowRevBtn'), 'unavail' );
|
||||
setButtonState( $('fastRevBtn'), 'inactive' );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_FASTREV );
|
||||
streamReq.send( streamParms+"&command="+CMD_FASTREV );
|
||||
}
|
||||
|
||||
function streamPrev( action )
|
||||
{
|
||||
streamPlay( false );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_PREV );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_PREV );
|
||||
}
|
||||
|
||||
function streamNext( action )
|
||||
{
|
||||
streamPlay( false );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_NEXT );
|
||||
if ( action )
|
||||
streamReq.send( streamParms+"&command="+CMD_NEXT );
|
||||
}
|
||||
|
||||
|
||||
function streamZoomIn( x, y )
|
||||
{
|
||||
streamReq.send( streamParms+"&command="+CMD_ZOOMIN+"&x="+x+"&y="+y );
|
||||
|
@ -259,6 +253,8 @@ function eventQuery( eventId )
|
|||
|
||||
var prevEventId = 0;
|
||||
var nextEventId = 0;
|
||||
var PrevEventDefVideoPath = "";
|
||||
var NextEventDefVideoPath = "";
|
||||
|
||||
function getNearEventsResponse( respObj, respText )
|
||||
{
|
||||
|
@ -266,6 +262,8 @@ function getNearEventsResponse( respObj, respText )
|
|||
return;
|
||||
prevEventId = respObj.nearevents.PrevEventId;
|
||||
nextEventId = respObj.nearevents.NextEventId;
|
||||
PrevEventDefVideoPath = respObj.nearevents.PrevEventDefVideoPath;
|
||||
NextEventDefVideoPath = respObj.nearevents.NextEventDefVideoPath;
|
||||
|
||||
$('prevEventBtn').disabled = !prevEventId;
|
||||
$('nextEventBtn').disabled = !nextEventId;
|
||||
|
@ -630,22 +628,28 @@ function showEventFrames()
|
|||
createPopup( '?view=frames&eid='+eventData.Id, 'zmFrames', 'frames' );
|
||||
}
|
||||
|
||||
function showStream()
|
||||
function showVideo()
|
||||
{
|
||||
$('eventStills').addClass( 'hidden' );
|
||||
$('eventStream').removeClass( 'hidden' );
|
||||
$('streamEvent').addClass( 'hidden' );
|
||||
$('eventVideo').removeClass( 'hidden' );
|
||||
|
||||
$('stillsEvent').removeClass( 'hidden' );
|
||||
|
||||
//$(window).removeEvent( 'resize', updateStillsSizes );
|
||||
$('videoEvent').addClass( 'hidden' );
|
||||
|
||||
streamMode = 'video';
|
||||
|
||||
}
|
||||
|
||||
function showStills()
|
||||
{
|
||||
$('eventStream').addClass( 'hidden' );
|
||||
$('eventStills').removeClass( 'hidden' );
|
||||
$('eventVideo').addClass( 'hidden' );
|
||||
|
||||
$('stillsEvent').addClass( 'hidden' );
|
||||
$('streamEvent').removeClass( 'hidden' );
|
||||
$('videoEvent').removeClass( 'hidden' );
|
||||
|
||||
streamMode = 'stills';
|
||||
|
||||
streamPause( true );
|
||||
if ( !scroll )
|
||||
{
|
||||
|
@ -667,10 +671,6 @@ function showFrameStats()
|
|||
createPopup( '?view=stats&eid='+eventData.Id+'&fid='+fid, 'zmStats', 'stats', eventData.Width, eventData.Height );
|
||||
}
|
||||
|
||||
function videoEvent()
|
||||
{
|
||||
createPopup( '?view=video&eid='+eventData.Id, 'zmVideo', 'video', eventData.Width, eventData.Height );
|
||||
}
|
||||
|
||||
function drawProgressBar()
|
||||
{
|
||||
|
@ -735,18 +735,138 @@ function handleClick( event )
|
|||
else
|
||||
streamZoomIn( x, y );
|
||||
}
|
||||
function setupListener()
|
||||
{
|
||||
|
||||
// Buttons
|
||||
var playButton = document.getElementById("play-pause");
|
||||
var muteButton = document.getElementById("mute");
|
||||
var fullScreenButton = document.getElementById("full-screen");
|
||||
|
||||
// Sliders
|
||||
var seekBar = document.getElementById("seekbar");
|
||||
var volumeBar = document.getElementById("volume-bar");
|
||||
|
||||
|
||||
// Event listener for the play/pause button
|
||||
playButton.addEventListener("click", function() {
|
||||
if (vid.paused == true) {
|
||||
// Play the video
|
||||
vid.play();
|
||||
|
||||
// Update the button text to 'Pause'
|
||||
playButton.innerHTML = "Pause";
|
||||
} else {
|
||||
// Pause the video
|
||||
vid.pause();
|
||||
|
||||
// Update the button text to 'Play'
|
||||
playButton.innerHTML = "Play";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Event listener for the mute button
|
||||
muteButton.addEventListener("click", function() {
|
||||
if (vid.muted == false) {
|
||||
// Mute the video
|
||||
vid.muted = true;
|
||||
|
||||
// Update the button text
|
||||
muteButton.innerHTML = "Unmute";
|
||||
} else {
|
||||
// Unmute the video
|
||||
vid.muted = false;
|
||||
|
||||
// Update the button text
|
||||
muteButton.innerHTML = "Mute";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Event listener for the full-screen button
|
||||
fullScreenButton.addEventListener("click", function() {
|
||||
if (vid.requestFullscreen) {
|
||||
vid.requestFullscreen();
|
||||
} else if (vid.mozRequestFullScreen) {
|
||||
vid.mozRequestFullScreen(); // Firefox
|
||||
} else if (vid.webkitRequestFullscreen) {
|
||||
vid.webkitRequestFullscreen(); // Chrome and Safari
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Event listener for the seek bar
|
||||
seekBar.addEventListener("change", function() {
|
||||
// Calculate the new time
|
||||
var time = vid.duration * (seekBar.value / 100);
|
||||
|
||||
// Update the video time
|
||||
vid.currentTime = time;
|
||||
});
|
||||
|
||||
|
||||
// Update the seek bar as the video plays
|
||||
vid.addEventListener("timeupdate", function() {
|
||||
// Calculate the slider value
|
||||
var value = (100 / vid.duration) * vid.currentTime;
|
||||
|
||||
// Update the slider value
|
||||
seekBar.value = value;
|
||||
});
|
||||
|
||||
// Pause the video when the seek handle is being dragged
|
||||
seekBar.addEventListener("mousedown", function() {
|
||||
vid.pause();
|
||||
});
|
||||
|
||||
// Play the video when the seek handle is dropped
|
||||
seekBar.addEventListener("mouseup", function() {
|
||||
vid.play();
|
||||
});
|
||||
|
||||
// Event listener for the volume bar
|
||||
volumeBar.addEventListener("change", function() {
|
||||
// Update the video volume
|
||||
vid.volume = volumeBar.value;
|
||||
});
|
||||
}
|
||||
|
||||
function initPage()
|
||||
{
|
||||
streamCmdTimer = streamQuery.delay( 250 );
|
||||
eventQuery.pass( eventData.Id ).delay( 500 );
|
||||
|
||||
if ( canStreamNative )
|
||||
//FIXME prevent blocking...not sure what is happening or best way to unblock
|
||||
vid=$('videoobj');
|
||||
if (vid)
|
||||
{
|
||||
var streamImg = $('imageFeed').getElement('img');
|
||||
if ( !streamImg )
|
||||
streamImg = $('imageFeed').getElement('object');
|
||||
$(streamImg).addEvent( 'click', function( event ) { handleClick( event ); } );
|
||||
/*setupListener();
|
||||
vid.removeAttribute("controls");
|
||||
/* window.videoobj.oncanplay=null;
|
||||
window.videoobj.currentTime=window.videoobj.currentTime-1;
|
||||
window.videoobj.currentTime=window.videoobj.currentTime+1;//may not be symetrical of course
|
||||
|
||||
vid.onstalled=function(){window.vid.currentTime=window.vid.currentTime-1;window.vid.currentTime=window.vid.currentTime+1;}
|
||||
vid.onwaiting=function(){window.vid.currentTime=window.vid.currentTime-1;window.vid.currentTime=window.vid.currentTime+1;}
|
||||
vid.onloadstart=function(){window.vid.currentTime=window.vid.currentTime-1;window.vid.currentTime=window.vid.currentTime+1;}
|
||||
vid.onplay=function(){window.vid.currentTime=window.vid.currentTime-1;window.vid.currentTime=window.vid.currentTime+1;}
|
||||
vid.onplaying=function(){window.vid.currentTime=window.vid.currentTime-1;window.vid.currentTime=window.vid.currentTime+1;}
|
||||
//window.vid.hide();//does not help
|
||||
var sources = window.videoobj.getElementsByTagName('source');
|
||||
sources[0].src=null;
|
||||
window.videoobj.load();
|
||||
streamPlay(); */
|
||||
}
|
||||
else
|
||||
{
|
||||
streamCmdTimer = streamQuery.delay( 250 );
|
||||
eventQuery.pass( eventData.Id ).delay( 500 );
|
||||
|
||||
if ( canStreamNative )
|
||||
{
|
||||
var streamImg = $('imageFeed').getElement('img');
|
||||
if ( !streamImg )
|
||||
streamImg = $('imageFeed').getElement('object');
|
||||
$(streamImg).addEvent( 'click', function( event ) { handleClick( event ); } );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
var events = new Object();
|
||||
var events = {};
|
||||
|
||||
function showEvent( eid, fid, width, height )
|
||||
{
|
||||
{
|
||||
var url = '?view=event&eid='+eid+'&fid='+fid;
|
||||
url += filterQuery;
|
||||
createPopup( url, 'zmEvent', 'event', width, height );
|
||||
var pop=createPopup( url, 'zmEvent', 'event', width, height );
|
||||
pop.vid=$('preview');
|
||||
|
||||
//video element is blocking video elements elsewhere in chrome possible interaction with mouseover event?
|
||||
//FIXME unless an exact cause can be determined should store all video controls and do something to the other controls when we want to load a new video seek etc or whatever may block
|
||||
/*var vid= $('preview');
|
||||
vid.oncanplay=null;
|
||||
// vid.currentTime=vid.currentTime-0.1;
|
||||
vid.pause();*/
|
||||
}
|
||||
|
||||
function createEventHtml( event, frame )
|
||||
|
@ -14,7 +22,7 @@ function createEventHtml( event, frame )
|
|||
if ( event.Archived > 0 )
|
||||
eventHtml.addClass( 'archived' );
|
||||
|
||||
new Element( 'p' ).inject( eventHtml ).set( 'text', monitorNames[event.MonitorId] );
|
||||
new Element( 'p' ).inject( eventHtml ).set( 'text', monitors[event.MonitorId].Name );
|
||||
new Element( 'p' ).inject( eventHtml ).set( 'text', event.Name+(frame?("("+frame.FrameId+")"):"") );
|
||||
new Element( 'p' ).inject( eventHtml ).set( 'text', event.StartTime+" - "+event.Length+"s" );
|
||||
new Element( 'p' ).inject( eventHtml ).set( 'text', event.Cause );
|
||||
|
@ -71,8 +79,8 @@ function frameDataResponse( respObj, respText )
|
|||
|
||||
event['frames'][frame.FrameId] = frame;
|
||||
event['frames'][frame.FrameId]['html'] = createEventHtml( event, frame );
|
||||
showEventDetail( event['frames'][frame.FrameId]['html'] );
|
||||
loadEventImage( frame.Image.imagePath, event.Id, frame.FrameId, event.Width, event.Height );
|
||||
|
||||
previewEvent(frame.EventId, frame.FrameId);
|
||||
}
|
||||
|
||||
var eventQuery = new Request.JSON( { url: thisUrl, method: 'get', timeout: AJAX_TIMEOUT, link: 'cancel', onSuccess: eventDataResponse } );
|
||||
|
@ -94,14 +102,18 @@ function requestFrameData( eventId, frameId )
|
|||
|
||||
function previewEvent( eventId, frameId )
|
||||
{
|
||||
|
||||
if ( events[eventId] )
|
||||
{
|
||||
if ( events[eventId]['frames'] )
|
||||
var event = events[eventId];
|
||||
if ( event['frames'] )
|
||||
{
|
||||
if ( events[eventId]['frames'][frameId] )
|
||||
if ( event['frames'][frameId] )
|
||||
{
|
||||
showEventDetail( events[eventId]['frames'][frameId]['html'] );
|
||||
loadEventImage( events[eventId].frames[frameId].Image.imagePath, eventId, frameId, events[eventId].Width, events[eventId].Height );
|
||||
showEventDetail( event['frames'][frameId]['html'] );
|
||||
var imagePath = event.frames[frameId].Image.imagePath;
|
||||
var videoName = event.DefaultVideo;
|
||||
loadEventImage( imagePath, eventId, frameId, event.Width, event.Height, event.Frames/event.Length, videoName, event.Length, event.StartTime, monitors[event.MonitorId]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -109,12 +121,46 @@ function previewEvent( eventId, frameId )
|
|||
requestFrameData( eventId, frameId );
|
||||
}
|
||||
|
||||
function loadEventImage( imagePath, eid, fid, width, height )
|
||||
function loadEventImage( imagePath, eid, fid, width, height, fps, videoName, duration, startTime, Monitor )
|
||||
{
|
||||
var vid= $('preview');
|
||||
var imageSrc = $('imageSrc');
|
||||
imageSrc.setProperty( 'src', imagePrefix + '&eid='+eid+'&fid='+fid );
|
||||
imageSrc.removeEvent( 'click' );
|
||||
imageSrc.addEvent( 'click', showEvent.pass( [ eid, fid, width, height ] ) );
|
||||
if(videoName)
|
||||
{
|
||||
vid.show();
|
||||
imageSrc.hide();
|
||||
var newsource=imagePrefix+imagePath.slice(0,imagePath.lastIndexOf('/'))+"/"+videoName;
|
||||
//console.log(newsource);
|
||||
//console.log(sources[0].src.slice(-newsource.length));
|
||||
if(newsource!=vid.currentSrc.slice(-newsource.length) || vid.readyState==0)
|
||||
{
|
||||
//console.log("loading new");
|
||||
//it is possible to set a long source list here will that be unworkable?
|
||||
var sources = vid.getElementsByTagName('source');
|
||||
sources[0].src=newsource;
|
||||
var tracks = vid.getElementsByTagName('track');
|
||||
if(tracks.length){
|
||||
tracks[0].parentNode.removeChild(tracks[0]);
|
||||
}
|
||||
vid.load();
|
||||
addVideoTimingTrack(vid, Monitor.LabelFormat, Monitor.Name, duration, startTime)
|
||||
vid.currentTime = fid/fps;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!vid.seeking)
|
||||
vid.currentTime=fid/fps;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
vid.hide();
|
||||
imageSrc.show();
|
||||
imageSrc.setProperty( 'src', imagePrefix+imagePath );
|
||||
imageSrc.removeEvent( 'click' );
|
||||
imageSrc.addEvent( 'click', showEvent.pass( [ eid, fid, width, height ] ) );
|
||||
}
|
||||
|
||||
var eventData = $('eventData');
|
||||
eventData.removeEvent( 'click' );
|
||||
eventData.addEvent( 'click', showEvent.pass( [ eid, fid, width, height ] ) );
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
var filterQuery = '<?php echo validJsStr($filterQuery) ?>';
|
||||
|
||||
var monitorNames = new Object();
|
||||
<?php
|
||||
$jsMonitors = array();
|
||||
$fields = array('Name', 'LabelFormat', 'SaveJPEGs', 'VideoWriter');
|
||||
foreach ( $monitors as $monitor )
|
||||
{
|
||||
if ( !empty($monitorIds[$monitor['Id']]) )
|
||||
{
|
||||
?>
|
||||
monitorNames[<?php echo $monitor['Id'] ?>] = '<?php echo validJsStr($monitor['Name']) ?>';
|
||||
<?php
|
||||
$jsMonitor = array();
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
$jsMonitor[$field] = $monitor[$field];
|
||||
}
|
||||
$jsMonitors[$monitor['Id']] = $jsMonitor;
|
||||
}
|
||||
}
|
||||
?>
|
||||
var monitors = <?php echo json_encode($jsMonitors) ?>;
|
||||
|
||||
var archivedString = "<?php echo translate('Archived') ?>";
|
||||
|
|
|
@ -30,6 +30,7 @@ if ( !canView( 'Monitors' ) )
|
|||
$tabs = array();
|
||||
$tabs["general"] = translate('General');
|
||||
$tabs["source"] = translate('Source');
|
||||
$tabs["storage"] = translate('Storage');
|
||||
$tabs["timestamp"] = translate('Timestamp');
|
||||
$tabs["buffers"] = translate('Buffers');
|
||||
if ( ZM_OPT_CONTROL && canView( 'Control' ) )
|
||||
|
@ -83,6 +84,9 @@ if ( ! empty($_REQUEST['mid']) ) {
|
|||
'Orientation' => "0",
|
||||
'Deinterlacing' => 0,
|
||||
'RTSPDescribe' => 0,
|
||||
'SaveJPEGs' => "3",
|
||||
'VideoWriter' => "0",
|
||||
'EncoderParameters' => "# Lines beginning with # are a comment \n# For changing quality, use the crf option\n# 1 is best, 51 is worst quality\n#crf=23\n",
|
||||
'LabelFormat' => '%N - %d/%m/%y %H:%M:%S',
|
||||
'LabelX' => 0,
|
||||
'LabelY' => 0,
|
||||
|
@ -443,6 +447,20 @@ $label_size = array(
|
|||
"Large" => 2
|
||||
);
|
||||
|
||||
$savejpegopts = array(
|
||||
"Disabled" => 0,
|
||||
"Frames only" => 1,
|
||||
"Analysis images only (if available)" => 2,
|
||||
"Frames + Analysis images (if available)" => 3,
|
||||
"Snapshot Only" => 4
|
||||
);
|
||||
|
||||
$videowriteropts = array(
|
||||
"Disabled" => 0,
|
||||
"X264 Encode" => 1,
|
||||
"H264 Camera Passthrough" => 2
|
||||
);
|
||||
|
||||
xhtmlHeaders(__FILE__, translate('Monitor')." - ".validHtmlStr($monitor['Name']) );
|
||||
?>
|
||||
<body>
|
||||
|
@ -571,6 +589,14 @@ if ( $tab != 'source' )
|
|||
<input type="hidden" name="newMonitor[Deinterlacing]" value="<?php echo validHtmlStr($newMonitor['Deinterlacing']) ?>"/>
|
||||
<?php
|
||||
}
|
||||
if ( $tab != 'storage' )
|
||||
{
|
||||
?>
|
||||
<input type="hidden" name="newMonitor[SaveJPEGs]" value="<?php echo validHtmlStr($newMonitor['SaveJPEGs']) ?>"/>
|
||||
<input type="hidden" name="newMonitor[VideoWriter]" value="<?php echo validHtmlStr($newMonitor['VideoWriter']) ?>"/>
|
||||
<input type="hidden" name="newMonitor[EncoderParameters]" value="<?php echo validHtmlStr($newMonitor['EncoderParameters']) ?>"/>
|
||||
<?php
|
||||
}
|
||||
if ( $tab != 'source' || ($newMonitor['Type'] != 'Remote' && $newMonitor['Protocol'] != 'RTSP'))
|
||||
{
|
||||
?>
|
||||
|
@ -878,6 +904,13 @@ switch ( $tab )
|
|||
<?php
|
||||
break;
|
||||
}
|
||||
case 'storage' :
|
||||
?>
|
||||
<tr><td><?php echo translate('SaveJPEGs') ?></td><td><select name="newMonitor[SaveJPEGs]"><?php foreach ( $savejpegopts as $name => $value ) { ?><option value="<?php echo $value ?>"<?php if ( $value == $newMonitor['SaveJPEGs'] ) { ?> selected="selected"<?php } ?>><?php echo $name ?></option><?php } ?></select></td></tr>
|
||||
<tr><td><?php echo translate('VideoWriter') ?></td><td><select name="newMonitor[VideoWriter]"><?php foreach ( $videowriteropts as $name => $value ) { ?><option value="<?php echo $value ?>"<?php if ( $value == $newMonitor['VideoWriter'] ) { ?> selected="selected"<?php } ?>><?php echo $name ?></option><?php } ?></select></td></tr>
|
||||
<tr><td><?php echo translate('OptionalEncoderParam') ?></td><td><textarea name="newMonitor[EncoderParameters]" rows="4" cols="36"><?php echo validHtmlStr($newMonitor['EncoderParameters']) ?></textarea></td></tr>
|
||||
<?php
|
||||
break;
|
||||
case 'timestamp' :
|
||||
{
|
||||
?>
|
||||
|
|
|
@ -811,7 +811,18 @@ xhtmlHeaders(__FILE__, translate('Timeline') );
|
|||
<div id="content" class="chartSize">
|
||||
<div id="topPanel" class="graphWidth">
|
||||
<div id="imagePanel">
|
||||
<div id="image" class="imageHeight"><img id="imageSrc" class="imageWidth" src="graphics/transparent.gif" alt="<?php echo translate('ViewEvent') ?>" title="<?php echo translate('ViewEvent') ?>"/></div>
|
||||
<div id="image" class="imageHeight">
|
||||
<img id="imageSrc" class="imageWidth" src="graphics/transparent.gif" alt="<?php echo translate('ViewEvent') ?>" title="<?php echo translate('ViewEvent') ?>"/>
|
||||
<?php
|
||||
//due to chrome bug, has to enable https://code.google.com/p/chromium/issues/detail?id=472300
|
||||
//crossorigin has to be added below to make caption work in chrome
|
||||
?>
|
||||
<video id="preview" width="100%" controls crossorigin="anonymous">
|
||||
<source src="<?php echo "/events/".getEventPath($event)."/event.mp4"; ?>" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="dataPanel">
|
||||
<div id="textPanel">
|
||||
|
@ -921,6 +932,10 @@ foreach( array_keys($monEventSlots) as $monitorId )
|
|||
<?php
|
||||
unset( $currEventSlots );
|
||||
$currEventSlots = &$monEventSlots[$monitorId];
|
||||
$monitorMouseover = $mouseover;
|
||||
if ($monitors[$monitorId]['SaveJPEGs'] == 2) {
|
||||
$monitorMouseover = false;
|
||||
}
|
||||
for ( $i = 0; $i < $chart['graph']['width']; $i++ )
|
||||
{
|
||||
if ( isset($currEventSlots[$i]) )
|
||||
|
@ -928,7 +943,7 @@ foreach( array_keys($monEventSlots) as $monitorId )
|
|||
unset( $slot );
|
||||
$slot = &$currEventSlots[$i];
|
||||
|
||||
if ( $mouseover )
|
||||
if ( $monitorMouseover )
|
||||
{
|
||||
$behaviours = array(
|
||||
'onclick="'.getSlotShowEventBehaviour( $slot ).'"',
|
||||
|
@ -968,5 +983,6 @@ foreach( array_keys($monEventSlots) as $monitorId )
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -42,6 +42,12 @@
|
|||
#cmakedefine HAVE_GNUTLS_GNUTLS_H 1
|
||||
#cmakedefine HAVE_LIBMYSQLCLIENT 1
|
||||
#cmakedefine HAVE_MYSQL_H 1
|
||||
#cmakedefine HAVE_LIBX264 1
|
||||
#cmakedefine HAVE_X264_H 1
|
||||
#cmakedefine HAVE_LIBMP4V2 1
|
||||
#cmakedefine HAVE_MP4V2_MP4V2_H 1
|
||||
#cmakedefine HAVE_MP4V2_H 1
|
||||
#cmakedefine HAVE_MP4_H 1
|
||||
#cmakedefine HAVE_LIBAVFORMAT 1
|
||||
#cmakedefine HAVE_LIBAVFORMAT_AVFORMAT_H 1
|
||||
#cmakedefine HAVE_LIBAVCODEC 1
|
||||
|
|
Loading…
Reference in New Issue