diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index da0d9b8c4..6c271292d 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -232,6 +232,7 @@ CREATE TABLE `Filters` ( `AutoExecute` tinyint(3) unsigned NOT NULL default '0', `AutoExecuteCmd` tinytext, `AutoDelete` tinyint(3) unsigned NOT NULL default '0', + `UpdateDiskSpace` tinyint(3) unsigned NOT NULL default '0', `Background` tinyint(1) unsigned NOT NULL default '0', `Concurrent` tinyint(1) unsigned NOT NULL default '0', PRIMARY KEY (`Id`), @@ -289,6 +290,7 @@ CREATE TABLE `Logs` ( ) ENGINE=@ZM_MYSQL_ENGINE@; CREATE INDEX `Logs_TimeKey_idx` ON `Logs` (`TimeKey`); +CREATE INDEX `Logs_Level_idx` ON `Logs` (`Level`); -- -- Table structure for table `Manufacturers` -- @@ -382,6 +384,8 @@ CREATE TABLE `Monitors` ( `Deinterlacing` int(10) unsigned NOT NULL default '0', `SaveJPEGs` TINYINT NOT NULL DEFAULT '3' , `VideoWriter` TINYINT NOT NULL DEFAULT '0', + `OutputCodec` enum('h264','mjpeg'), + `OutputContainer` enum('mp4','mkv'), `EncoderParameters` TEXT, `RecordAudio` TINYINT NOT NULL DEFAULT '0', `RTSPDescribe` tinyint(1) unsigned, diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index 545bfaaad..82deff6ff 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm @@ -125,7 +125,7 @@ sub Execute { push @results, $event; } $sth->finish(); - Debug("Loaded " . @results . " events for filter $_[0]{Name} using query ($sql)"); + Debug('Loaded ' . @results . " events for filter $_[0]{Name} using query ($sql)"); return @results; } @@ -147,38 +147,59 @@ sub Sql { foreach my $term ( @{$filter_expr->{terms}} ) { if ( exists($term->{cnj}) ) { - $self->{Sql} .= " ".$term->{cnj}." "; + $self->{Sql} .= ' '.$term->{cnj}." "; } if ( exists($term->{obr}) ) { - $self->{Sql} .= " ".str_repeat( "(", $term->{obr} )." "; + $self->{Sql} .= ' '.str_repeat( "(", $term->{obr} )." "; } my $value = $term->{val}; my @value_list; if ( $term->{attr} ) { if ( $term->{attr} =~ /^Monitor/ ) { my ( $temp_attr_name ) = $term->{attr} =~ /^Monitor(.+)$/; - $self->{Sql} .= "M.".$temp_attr_name; + $self->{Sql} .= 'M.'.$temp_attr_name; } elsif ( $term->{attr} =~ /^Server/ ) { - $self->{Sql} .= "M.".$term->{attr}; + $self->{Sql} .= 'M.'.$term->{attr}; + +# StartTime options } elsif ( $term->{attr} eq 'DateTime' ) { - $self->{Sql} .= "E.StartTime"; + $self->{Sql} .= 'E.StartTime'; + } elsif ( $term->{attr} eq 'StartDateTime' ) { + $self->{Sql} .= 'E.StartTime'; } elsif ( $term->{attr} eq 'Date' ) { - $self->{Sql} .= "to_days( E.StartTime )"; + $self->{Sql} .= 'to_days( E.StartTime )'; + } elsif ( $term->{attr} eq 'StartDate' ) { + $self->{Sql} .= 'to_days( E.StartTime )'; } elsif ( $term->{attr} eq 'Time' ) { $self->{Sql} .= "extract( hour_second from E.StartTime )"; } elsif ( $term->{attr} eq 'Weekday' ) { $self->{Sql} .= "weekday( E.StartTime )"; + +# EndTIme options + } elsif ( $term->{attr} eq 'EndDateTime' ) { + $self->{Sql} .= 'E.EndTime'; + } elsif ( $term->{attr} eq 'EndDate' ) { + $self->{Sql} .= 'to_days( E.EndTime )'; + } elsif ( $term->{attr} eq 'EndTime' ) { + $self->{Sql} .= "extract( hour_second from E.EndTime )"; + } elsif ( $term->{attr} eq 'EndWeekday' ) { + $self->{Sql} .= "weekday( E.EndTime )"; + +# + } elsif ( $term->{attr} eq 'DiskSpace' ) { + $self->{Sql} .= 'E.DiskSpace'; + $self->{HasDiskPercent} = !undef; } elsif ( $term->{attr} eq 'DiskPercent' ) { - $self->{Sql} .= "zmDiskPercent"; + $self->{Sql} .= 'zmDiskPercent'; $self->{HasDiskPercent} = !undef; } elsif ( $term->{attr} eq 'DiskBlocks' ) { - $self->{Sql} .= "zmDiskBlocks"; + $self->{Sql} .= 'zmDiskBlocks'; $self->{HasDiskBlocks} = !undef; } elsif ( $term->{attr} eq 'SystemLoad' ) { - $self->{Sql} .= "zmSystemLoad"; + $self->{Sql} .= 'zmSystemLoad'; $self->{HasSystemLoad} = !undef; } else { - $self->{Sql} .= "E.".$term->{attr}; + $self->{Sql} .= 'E.'.$term->{attr}; } ( my $stripped_value = $value ) =~ s/^["\']+?(.+)["\']+?$/$1/; @@ -243,11 +264,11 @@ sub Sql { } elsif ( $term->{op} eq '!~' ) { $self->{Sql} .= " not in (".join( ",", @value_list ).")"; } else { - $self->{Sql} .= " ".$term->{op}." $value"; + $self->{Sql} .= ' '.$term->{op}." $value"; } } # end if has an operator if ( exists($term->{cbr}) ) { - $self->{Sql} .= " ".str_repeat( ")", $term->{cbr} )." "; + $self->{Sql} .= ' '.str_repeat( ")", $term->{cbr} )." "; } } # end foreach term } # end if terms @@ -284,7 +305,7 @@ sub Sql { push @auto_terms, "E.Executed = 0"; } if ( @auto_terms ) { - $sql .= " and ( ".join( " or ", @auto_terms )." )"; + $sql .= " and ( ".join( ' or ', @auto_terms )." )"; } if ( !$filter_expr->{sort_field} ) { $filter_expr->{sort_field} = 'StartTime'; @@ -292,30 +313,34 @@ sub Sql { } my $sort_column = ''; if ( $filter_expr->{sort_field} eq 'Id' ) { - $sort_column = "E.Id"; + $sort_column = 'E.Id'; } elsif ( $filter_expr->{sort_field} eq 'MonitorName' ) { - $sort_column = "M.Name"; + $sort_column = 'M.Name'; } elsif ( $filter_expr->{sort_field} eq 'Name' ) { - $sort_column = "E.Name"; + $sort_column = 'E.Name'; } elsif ( $filter_expr->{sort_field} eq 'StartTime' ) { - $sort_column = "E.StartTime"; + $sort_column = 'E.StartTime'; + } elsif ( $filter_expr->{sort_field} eq 'EndTime' ) { + $sort_column = 'E.EndTime'; } elsif ( $filter_expr->{sort_field} eq 'Secs' ) { - $sort_column = "E.Length"; + $sort_column = 'E.Length'; } elsif ( $filter_expr->{sort_field} eq 'Frames' ) { - $sort_column = "E.Frames"; + $sort_column = 'E.Frames'; } elsif ( $filter_expr->{sort_field} eq 'AlarmFrames' ) { - $sort_column = "E.AlarmFrames"; + $sort_column = 'E.AlarmFrames'; } elsif ( $filter_expr->{sort_field} eq 'TotScore' ) { - $sort_column = "E.TotScore"; + $sort_column = 'E.TotScore'; } elsif ( $filter_expr->{sort_field} eq 'AvgScore' ) { - $sort_column = "E.AvgScore"; + $sort_column = 'E.AvgScore'; } elsif ( $filter_expr->{sort_field} eq 'MaxScore' ) { - $sort_column = "E.MaxScore"; + $sort_column = 'E.MaxScore'; + } elsif ( $filter_expr->{sort_field} eq 'DiskSpace' ) { + $sort_column = 'E.DiskSpace'; } else { - $sort_column = "E.StartTime"; + $sort_column = 'E.StartTime'; } - my $sort_order = $filter_expr->{sort_asc}?"asc":"desc"; - $sql .= " order by ".$sort_column." ".$sort_order; + my $sort_order = $filter_expr->{sort_asc}?'asc':'desc'; + $sql .= ' order by '.$sort_column." ".$sort_order; if ( $filter_expr->{limit} ) { $sql .= " limit 0,".$filter_expr->{limit}; } @@ -325,7 +350,7 @@ sub Sql { } # end sub Sql sub getDiskPercent { - my $command = "df " . ($_[0] ? $_[0] : '.'); + my $command = 'df ' . ($_[0] ? $_[0] : '.'); my $df = qx( $command ); my $space = -1; if ( $df =~ /\s(\d+)%/ms ) { @@ -335,7 +360,7 @@ sub getDiskPercent { } sub getDiskBlocks { - my $command = "df ."; + my $command = 'df .'; my $df = qx( $command ); my $space = -1; if ( $df =~ /\s(\d+)\s+\d+\s+\d+%/ms ) { @@ -345,7 +370,7 @@ sub getDiskBlocks { } sub getLoad { - my $command = "uptime ."; + my $command = 'uptime .'; my $uptime = qx( $command ); my $load = -1; if ( $uptime =~ /load average:\s+([\d.]+)/ms ) { diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm index 031a2b7b6..d0d6b234b 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm @@ -678,6 +678,8 @@ sub Dump { fetch()->logPrint( DEBUG, Data::Dumper->Dump( [ $var ], [ $label ] ) ); } +sub debug { fetch()->logPrint( DEBUG, @_ ); } + sub Debug( @ ) { fetch()->logPrint( DEBUG, @_ ); } @@ -685,14 +687,24 @@ sub Debug( @ ) { sub Info( @ ) { fetch()->logPrint( INFO, @_ ); } +sub info { + fetch()->logPrint( INFO, @_ ); +} + sub Warning( @ ) { fetch()->logPrint( WARNING, @_ ); } +sub warn { + fetch()->logPrint( WARNING, @_ ); +} sub Error( @ ) { fetch()->logPrint( ERROR, @_ ); } +sub error { + fetch()->logPrint( ERROR, @_ ); +} sub Fatal( @ ) { fetch()->logPrint( FATAL, @_ ); diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm index 63193bccf..a2d9fa021 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm @@ -42,7 +42,13 @@ use ZoneMinder::Config qw(:all); use ZoneMinder::Logger qw(:all); use ZoneMinder::Database qw(:all); -use vars qw/ $AUTOLOAD /; +use vars qw/ $AUTOLOAD $log $dbh/; + +*log = \$ZoneMinder::Logger::logger; +*dbh = \$ZoneMinder::Database::dbh; + +my $debug = 1; +use constant DEBUG_ALL=>0; sub new { my ( $parent, $id, $data ) = @_; @@ -110,7 +116,269 @@ sub AUTOLOAD { return $_[0]{$name}; } +sub save { + my ( $self, $data, $force_insert ) = @_; + my $type = ref $self; + if ( ! $type ) { + my ( $caller, undef, $line ) = caller; + $log->error("No type in Object::save. self:$self from $caller:$line"); + } + my $local_dbh = eval '$'.$type.'::dbh'; + $local_dbh = $ZoneMinder::Database::dbh if ! $local_dbh; + $self->set( $data ? $data : {} ); + if ( $debug or DEBUG_ALL ) { + if ( $data ) { + foreach my $k ( keys %$data ) { + $log->debug("Object::save after set $k => $$data{$k} $$self{$k}"); + } + } else { + $log->debug("No data after set"); + } + } +#$debug = 0; + + my $table = eval '$'.$type.'::table'; + my $fields = eval '\%'.$type.'::fields'; + my $debug = eval '$'.$type.'::debug'; + #$debug = DEBUG_ALL if ! $debug; + + my %sql; + foreach my $k ( keys %$fields ) { + $sql{$$fields{$k}} = $$self{$k} if defined $$fields{$k}; + } # end foreach + if ( ! $force_insert ) { + $sql{$$fields{updated_on}} = 'NOW()' if exists $$fields{updated_on}; + } # end if + my $serial = eval '$'.$type.'::serial'; + my @identified_by = eval '@'.$type.'::identified_by'; + + my $ac = sql::start_transaction( $local_dbh ); + if ( ! $serial ) { + my $insert = $force_insert; + my %serial = eval '%'.$type.'::serial'; + if ( ! %serial ) { +$log->debug("No serial") if $debug; + # No serial columns defined, which means that we will do saving by delete/insert instead of insert/update + if ( @identified_by ) { + my $where = join(' AND ', map { $$fields{$_}.'=?' } @identified_by ); + if ( $debug ) { + $log->debug("DELETE FROM $table WHERE $where"); + } # end if + + if ( ! ( ( $_ = $local_dbh->prepare("DELETE FROM $table WHERE $where") ) and $_->execute( @$self{@identified_by} ) ) ) { + $where =~ s/\?/\%s/g; + $log->error("Error deleting: DELETE FROM $table WHERE " . sprintf($where, map { defined $_ ? $_ : 'undef' } ( @$self{@identified_by}) ).'):' . $local_dbh->errstr); + $local_dbh->rollback(); + sql::end_transaction( $local_dbh, $ac ); + return $local_dbh->errstr; + } elsif ( $debug ) { + $log->debug("SQL succesful DELETE FROM $table WHERE $where"); + } # end if + } # end if + $insert = 1; + } else { + foreach my $id ( @identified_by ) { + if ( ! $serial{$id} ) { + my ( $caller, undef, $line ) = caller; + $log->error("$id nor in serial for $type from $caller:$line") if $debug; + next; + } + if ( ! $$self{$id} ) { + ($$self{$id}) = ($sql{$$fields{$id}}) = $local_dbh->selectrow_array( q{SELECT nextval('} . $serial{$id} . q{')} ); + $log->debug("SQL statement execution SELECT nextval('$serial{$id}') returned $$self{$id}") if $debug or DEBUG_ALL; + $insert = 1; + } # end if + } # end foreach + } # end if ! %serial + + if ( $insert ) { + my @keys = keys %sql; + my $command = "INSERT INTO $table (" . join(',', @keys ) . ') VALUES (' . join(',', map { '?' } @sql{@keys} ) . ')'; + if ( ! ( ( $_ = $local_dbh->prepare($command) ) and $_->execute( @sql{@keys} ) ) ) { + my $error = $local_dbh->errstr; + $command =~ s/\?/\%s/g; + $log->error('SQL statement execution failed: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys}) ).'):' . $local_dbh->errstr); + $local_dbh->rollback(); + sql::end_transaction( $local_dbh, $ac ); + return $error; + } # end if + if ( $debug or DEBUG_ALL ) { + $command =~ s/\?/\%s/g; + $log->debug('SQL statement execution: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys} ) ).'):' ); + } # end if + } else { + my @keys = keys %sql; + my $command = "UPDATE $table SET " . join(',', map { $_ . ' = ?' } @keys ) . ' WHERE ' . join(' AND ', map { $_ . ' = ?' } @$fields{@identified_by} ); + if ( ! ( $_ = $local_dbh->prepare($command) and $_->execute( @sql{@keys,@$fields{@identified_by}} ) ) ) { + my $error = $local_dbh->errstr; + $command =~ s/\?/\%s/g; + $log->error('SQL failed: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys, @$fields{@identified_by}}) ).'):' . $local_dbh->errstr); + $local_dbh->rollback(); + sql::end_transaction( $local_dbh, $ac ); + return $error; + } # end if + if ( $debug or DEBUG_ALL ) { + $command =~ s/\?/\%s/g; + $log->debug('SQL DEBUG: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys,@$fields{@identified_by}} ) ).'):' ); + } # end if + } # end if + } else { # not identified_by + @identified_by = ('id') if ! @identified_by; + my $need_serial = ! ( @identified_by == map { $$self{$_} ? $_ : () } @identified_by ); + + if ( $force_insert or $need_serial ) { + + if ( $need_serial ) { + if ( $serial ) { + @$self{@identified_by} = @sql{@$fields{@identified_by}} = $local_dbh->selectrow_array( q{SELECT nextval('} . $serial . q{')} ); + if ( $local_dbh->errstr() ) { + $log->error("Error getting next id. " . $local_dbh->errstr() ); + $log->error("SQL statement execution SELECT nextval('$serial') returned ".join(',',@$self{@identified_by})); + } elsif ( $debug or DEBUG_ALL ) { + $log->debug("SQL statement execution SELECT nextval('$serial') returned ".join(',',@$self{@identified_by})); + } # end if + } # end if + } # end if + my @keys = keys %sql; + my $command = "INSERT INTO $table (" . join(',', @keys ) . ') VALUES (' . join(',', map { '?' } @sql{@keys} ) . ')'; + if ( ! ( $_ = $local_dbh->prepare($command) and $_->execute( @sql{@keys} ) ) ) { + $command =~ s/\?/\%s/g; + my $error = $local_dbh->errstr; + $log->error('SQL failed: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys}) ).'):' . $error); + $local_dbh->rollback(); + sql::end_transaction( $local_dbh, $ac ); + return $error; + } # end if + if ( $debug or DEBUG_ALL ) { + $command =~ s/\?/\%s/g; + $log->debug('SQL DEBUG: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys} ) ).'):' ); + } # end if + } else { + delete $sql{created_on}; + my @keys = keys %sql; + @keys = sets::exclude( [ @$fields{@identified_by} ], \@keys ); + my $command = "UPDATE $table SET " . join(',', map { $_ . ' = ?' } @keys ) . ' WHERE ' . join(' AND ', map { $$fields{$_} .'= ?' } @identified_by ); + if ( ! ( $_ = $local_dbh->prepare($command) and $_->execute( @sql{@keys}, @sql{@$fields{@identified_by}} ) ) ) { + my $error = $local_dbh->errstr; + $command =~ s/\?/\%s/g; + $log->error('SQL failed: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys}, @sql{@$fields{@identified_by}} ) ).'):' . $error) if $log; + $local_dbh->rollback(); + sql::end_transaction( $local_dbh, $ac ); + return $error; + } # end if + if ( $debug or DEBUG_ALL ) { + $command =~ s/\?/\%s/g; + $log->debug('SQL DEBUG: ('.sprintf($command, map { defined $_ ? ( ref $_ eq 'ARRAY' ? join(',',@{$_}) : $_ ) : 'undef' } ( @sql{@keys}, @$self{@identified_by} ) ).'):' ); + } # end if + } # end if + } # end if + sql::end_transaction( $local_dbh, $ac ); + $self->load(); + #if ( $$fields{id} ) { + #if ( ! $ZoneMinder::Object::cache{$type}{$$self{id}} ) { + #$ZoneMinder::Object::cache{$type}{$$self{id}} = $self; + #} # end if + #delete $ZoneMinder::Object::cache{$config{db_name}}{$type}{$$self{id}}; + #} # end if +#$log->debug("after delete"); + #eval 'if ( %'.$type.'::find_cache ) { %'.$type.'::find_cache = (); }'; +#$log->debug("after clear cache"); + return ''; +} # end sub save + +sub set { + my ( $self, $params ) = @_; + my @set_fields = (); + + my $type = ref $self; + my %fields = eval ('%'.$type.'::fields'); + if ( ! %fields ) { + $log->warn('ZoneMinder::Object::set called on an object with no fields'); + } # end if + my %defaults = eval('%'.$type.'::defaults'); + if ( ref $params ne 'HASH' ) { + my ( $caller, undef, $line ) = caller; + $openprint::log->error("$type -> set called with non-hash params from $caller $line"); + } + + foreach my $field ( keys %fields ) { +$log->debug("field: $field, param: ".$$params{$field}) if $debug; + if ( exists $$params{$field} ) { +$openprint::log->debug("field: $field, $$self{$field} =? param: ".$$params{$field}) if $debug; + if ( ( ! defined $$self{$field} ) or ($$self{$field} ne $params->{$field}) ) { +# Only make changes to fields that have changed + if ( defined $fields{$field} ) { + $$self{$field} = $$params{$field} if defined $fields{$field}; + push @set_fields, $fields{$field}, $$params{$field}; #mark for sql updating + } # end if +$openprint::log->debug("Running $field with $$params{$field}") if $debug; + if ( my $func = $self->can( $field ) ) { + $func->( $self, $$params{$field} ); + } # end if + } # end if + } # end if + + if ( defined $fields{$field} ) { + if ( $$self{$field} ) { + $$self{$field} = transform( $type, $field, $$self{$field} ); + } # end if $$self{field} + } + } # end foreach field + + foreach my $field ( keys %defaults ) { + + if ( ( ! exists $$self{$field} ) or (!defined $$self{$field}) or ( $$self{$field} eq '' ) ) { + $log->debug("Setting default ($field) ($$self{$field}) ($defaults{$field}) ") if $debug; + if ( defined $defaults{$field} ) { + $log->debug("Default $field is defined: $defaults{$field}") if $debug; + if ( $defaults{$field} eq 'NOW()' ) { + $$self{$field} = 'NOW()'; + } else { + $$self{$field} = eval($defaults{$field}); + $log->error( "Eval error of object default $field default ($defaults{$field}) Reason: " . $@ ) if $@; + } # end if + } else { + $$self{$field} = $defaults{$field}; + } # end if +#$$self{$field} = ( defined $defaults{$field} ) ? eval($defaults{$field}) : $defaults{$field}; + $log->debug("Setting default for ($field) using ($defaults{$field}) to ($$self{$field}) ") if $debug; + } # end if + } # end foreach default + return @set_fields; +} # end sub set + +sub transform { + my $type = ref $_[0]; + $type = $_[0] if ! $type; + my $fields = eval '\%'.$type.'::fields'; + my $value = $_[2]; + + if ( defined $$fields{$_[1]} ) { + my @transforms = eval('@{$'.$type.'::transforms{$_[1]}}'); + $openprint::log->debug("Transforms for $_[1] before $_[2]: @transforms") if $debug; + if ( @transforms ) { + foreach my $transform ( @transforms ) { + if ( $transform =~ /^s\// or $transform =~ /^tr\// ) { + eval '$value =~ ' . $transform; + } elsif ( $transform =~ /^<(\d+)/ ) { + if ( $value > $1 ) { + $value = undef; + } # end if + } else { + $openprint::log->debug("evalling $value ".$transform . " Now value is $value" ); + eval '$value '.$transform; + $openprint::log->error("Eval error $@") if $@; + } + $openprint::log->debug("After $transform: $value") if $debug; + } # end foreach + } # end if + } else { + $openprint::log->error("Object::transform ($_[1]) not in fields for $type"); + } # end if + return $value; + +} # end sub transform 1; __END__ diff --git a/scripts/zmfilter.pl.in b/scripts/zmfilter.pl.in index 0560fb0c0..fca5b3d7a 100644 --- a/scripts/zmfilter.pl.in +++ b/scripts/zmfilter.pl.in @@ -214,6 +214,7 @@ sub getFilters { or AutoMessage = 1 or AutoExecute = 1 or AutoDelete = 1 + or UpdateDiskSpace = 1 ) ORDER BY Name'; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Unable toprepare '$sql': ".$dbh->errstr() ); diff --git a/src/zm_event.cpp b/src/zm_event.cpp index 87d0e27d2..bac9226b5 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -168,7 +168,6 @@ Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string snprintf( video_name, sizeof(video_name), "%d-%s", id, "video.mp4" ); snprintf( video_file, sizeof(video_file), staticConfig.video_file_format, path, video_name ); Debug(1,"Writing video file to %s", video_file ); - #if 0 /* X264 MP4 video writer */ if ( monitor->GetOptVideoWriter() == Monitor::X264ENCODE ) { @@ -200,8 +199,8 @@ Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string } else { /* No video object */ videowriter = NULL; - } #endif + } } // Event::Event( Monitor *p_monitor, struct timeval p_start_time, const std::string &p_cause, const StringSetMap &p_noteSetMap, bool p_videoEvent ) @@ -239,7 +238,7 @@ Event::~Event() { Error( "Can't update event: %s", mysql_error( &dbconn ) ); exit( mysql_errno( &dbconn ) ); } -} +} // ~Event void Event::createNotes( std::string ¬es ) { notes.clear(); diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index 8bb393475..fa32745b4 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -113,22 +113,6 @@ FfmpegCamera::FfmpegCamera( int p_id, const std::string &p_path, const std::stri mOpenStart = 0; mReopenThread = 0; -#if HAVE_LIBSWSCALE - mConvertContext = NULL; -#endif - /* Has to be located inside the constructor so other components such as zma will receive correct colours and subpixel order */ - if ( colours == ZM_COLOUR_RGB32 ) { - subpixelorder = ZM_SUBPIX_ORDER_RGBA; - imagePixFormat = AV_PIX_FMT_RGBA; - } else if ( colours == ZM_COLOUR_RGB24 ) { - subpixelorder = ZM_SUBPIX_ORDER_RGB; - imagePixFormat = AV_PIX_FMT_RGB24; - } else if ( colours == ZM_COLOUR_GRAY8 ) { - subpixelorder = ZM_SUBPIX_ORDER_NONE; - imagePixFormat = AV_PIX_FMT_GRAY8; - } else { - Panic("Unexpected colours: %d",colours); - } } // end FFmpegCamera::FFmpegCamera FfmpegCamera::~FfmpegCamera() { @@ -218,94 +202,6 @@ ZMPacket * FfmpegCamera::Capture( Image &image ) { } Debug( 5, "Got packet from stream %d dts (%d) pts(%d)", packet.stream_index, packet.pts, packet.dts ); -#if 0 - // What about audio stream? Maybe someday we could do sound detection... - if ( ( packet.stream_index == mVideoStreamId ) && ( keyframe || have_video_keyframe ) ) { -#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) - ret = avcodec_send_packet( mVideoCodecContext, &packet ); - if ( ret < 0 ) { - av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); - Error( "Unable to send packet at frame %d: %s, continuing", frameCount, errbuf ); - zm_av_packet_unref( &packet ); - continue; - } - -#if HAVE_AVUTIL_HWCONTEXT_H - if ( hwaccel ) { - ret = avcodec_receive_frame( mVideoCodecContext, hwFrame ); - if ( ret < 0 ) { - av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); - Error( "Unable to send packet at frame %d: %s, continuing", frameCount, errbuf ); - zm_av_packet_unref( &packet ); - continue; - } - ret = av_hwframe_transfer_data(mRawFrame, hwFrame, 0); - if (ret < 0) { - av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); - Error( "Unable to transfer frame at frame %d: %s, continuing", frameCount, errbuf ); - zm_av_packet_unref( &packet ); - continue; - } - } else { -#endif - ret = avcodec_receive_frame( mVideoCodecContext, mRawFrame ); - if ( ret < 0 ) { - av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); - Error( "Unable to send packet at frame %d: %s, continuing", frameCount, errbuf ); - zm_av_packet_unref( &packet ); - continue; - } - -#if HAVE_AVUTIL_HWCONTEXT_H - } -#endif - - frameComplete = 1; -# else - ret = zm_avcodec_decode_video( mVideoCodecContext, mRawFrame, &frameComplete, &packet ); - if ( ret < 0 ) { - av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); - Error( "Unable to decode frame at frame %d: %s, continuing", frameCount, errbuf ); - zm_av_packet_unref( &packet ); - continue; - } -#endif - - Debug( 4, "Decoded video packet at frame %d", frameCount ); - - if ( frameComplete ) { - Debug( 4, "Got frame %d", frameCount ); - - 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); - } - -#if LIBAVUTIL_VERSION_CHECK(54, 6, 0, 6, 0) - av_image_fill_arrays(mFrame->data, mFrame->linesize, - directbuffer, imagePixFormat, width, height, 1); -#else - avpicture_fill( (AVPicture *)mFrame, directbuffer, - imagePixFormat, width, height); -#endif - -#if HAVE_LIBSWSCALE - if ( sws_scale(mConvertContext, mRawFrame->data, mRawFrame->linesize, 0, mVideoCodecContext->height, mFrame->data, mFrame->linesize) < 0 ) - Fatal("Unable to convert raw format %u to target format %u at frame %d", mVideoCodecContext->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++; - } // end if frameComplete - } else { - Debug( 4, "Different stream_index %d", packet.stream_index ); - } // end if packet.stream_index == mVideoStreamId -#endif zm_packet = new ZMPacket( &packet ); zm_av_packet_unref( &packet ); return zm_packet; @@ -517,6 +413,18 @@ int FfmpegCamera::OpenFfmpeg() { } } + if ( mVideoCodecContext->codec_id != AV_CODEC_ID_H264 ) { +#ifdef AV_CODEC_ID_H265 + if ( mVideoCodecContext->codec_id == AV_CODEC_ID_H265 ) { + Debug( 1, "Input stream appears to be h265. The stored event file may not be viewable in browser." ); + } else { +#endif + Warning( "Input stream is not h264. The stored event file may not be viewable in browser." ); +#ifdef AV_CODEC_ID_H265 + } +#endif + } + if (mVideoCodecContext->hwaccel != NULL) { Debug(1, "HWACCEL in use"); } else { @@ -551,51 +459,11 @@ int FfmpegCamera::OpenFfmpeg() { // Allocate space for the native video frame mRawFrame = zm_av_frame_alloc(); - // Allocate space for the converted video frame - mFrame = zm_av_frame_alloc(); - - if ( mRawFrame == NULL || mFrame == NULL ) + if ( mRawFrame == NULL ) Fatal( "Unable to allocate frame for %s", mPath.c_str() ); Debug ( 1, "Allocated frames" ); -#if LIBAVUTIL_VERSION_CHECK(54, 6, 0, 6, 0) - int pSize = av_image_get_buffer_size( imagePixFormat, width, height,1 ); -#else - int pSize = avpicture_get_size( imagePixFormat, width, height ); -#endif - - if ( (unsigned int)pSize != imagesize ) { - Fatal("Image size mismatch. Required: %d Available: %d",pSize,imagesize); - } - - Debug ( 1, "Validated imagesize" ); - -#if HAVE_LIBSWSCALE - Debug ( 1, "Calling sws_isSupportedInput" ); - if ( !sws_isSupportedInput(mVideoCodecContext->pix_fmt) ) { - Fatal("swscale does not support the codec format: %c%c%c%c", (mVideoCodecContext->pix_fmt)&0xff, ((mVideoCodecContext->pix_fmt >> 8)&0xff), ((mVideoCodecContext->pix_fmt >> 16)&0xff), ((mVideoCodecContext->pix_fmt >> 24)&0xff)); - } - - if ( !sws_isSupportedOutput(imagePixFormat) ) { - Fatal("swscale does not support the target format: %c%c%c%c",(imagePixFormat)&0xff,((imagePixFormat>>8)&0xff),((imagePixFormat>>16)&0xff),((imagePixFormat>>24)&0xff)); - } - - mConvertContext = sws_getContext(mVideoCodecContext->width, - mVideoCodecContext->height, - mVideoCodecContext->pix_fmt, - width, height, - imagePixFormat, SWS_BICUBIC, NULL, - NULL, NULL); - if ( mConvertContext == NULL ) - Fatal( "Unable to create conversion context for %s", mPath.c_str() ); -#else // HAVE_LIBSWSCALE - Fatal( "You must compile ffmpeg with the --enable-swscale option to use ffmpeg cameras" ); -#endif // HAVE_LIBSWSCALE - - if ( (unsigned int)mVideoCodecContext->width != width || (unsigned int)mVideoCodecContext->height != height ) { - Warning( "Monitor dimensions are %dx%d but camera is sending %dx%d", width, height, mVideoCodecContext->width, mVideoCodecContext->height ); - } mCanCapture = true; @@ -636,12 +504,6 @@ int FfmpegCamera::CloseFfmpeg() { mRawFrame = NULL; } -#if HAVE_LIBSWSCALE - if ( mConvertContext ) { - sws_freeContext( mConvertContext ); - mConvertContext = NULL; - } -#endif if ( mVideoCodecContext ) { avcodec_close(mVideoCodecContext); @@ -664,7 +526,7 @@ int FfmpegCamera::CloseFfmpeg() { } return 0; -} +} // end int FfmpegCamera::CloseFfmpeg() int FfmpegCamera::FfmpegInterruptCallback(void *ctx) { Debug(3,"FfmpegInteruptCallback"); @@ -678,9 +540,9 @@ int FfmpegCamera::FfmpegInterruptCallback(void *ctx) { } return 0; -} +} // end int FfmpegCamera::FfmpegInterruptCallback(void *ctx) -void *FfmpegCamera::ReopenFfmpegThreadCallback(void *ctx){ +void *FfmpegCamera::ReopenFfmpegThreadCallback(void *ctx) { Debug(3,"FfmpegReopenThreadtCallback"); if ( ctx == NULL ) return NULL; @@ -702,6 +564,47 @@ void *FfmpegCamera::ReopenFfmpegThreadCallback(void *ctx){ return NULL; } } -} +} // end void *FfmpegCamera::ReopenFfmpegThreadCallback(void *ctx) +/* + Responsible for populating the decoded frame member of the zm_packet. +*/ +Image * FfmpegCamera::decode( Image *i = NULL, _AVPIXELFORMAT imagePixFormat, struct SwsContext *mConvertContext ) { + + if ( ! frame ) { + Error("Can't get image without frame.. maybe need to decode first"); + return NULL; + } + + if ( ! image ) { + if ( ! i ) { + Error("Need a pre-allocated image buffer"); + return NULL; + } + image = i; + } + /* Request a writeable buffer of the target image */ + uint8_t* directbuffer = image->WriteBuffer(); + //uint8_t* directbuffer = image.WriteBuffer(width, height, colours, subpixelorder); + if ( directbuffer == NULL ) { + Error("Failed requesting writeable buffer for the captured image."); + image = NULL; + return NULL; + } + + // mFrame is an intermediate. + AVFrame *mFrame = zm_av_frame_alloc(); +#if LIBAVUTIL_VERSION_CHECK(54, 6, 0, 6, 0) + av_image_fill_arrays(mFrame->data, mFrame->linesize, directbuffer, imagePixFormat, frame->width, frame->height, 1); +#else + avpicture_fill( (AVPicture *)mFrame, directbuffer, imagePixFormat, frame->width, frame->height); +#endif + if (sws_scale(mConvertContext, frame->data, frame->linesize, + 0, mVideoCodecContext->height, mFrame->data, mFrame->linesize) < 0) { + Fatal("Unable to convert raw format %u to target format %u", + mVideoCodecContext->pix_fmt, imagePixFormat); + } + av_frame_free( &mFrame ); + return image; +} #endif // HAVE_LIBAVFORMAT diff --git a/src/zm_ffmpeg_camera.h b/src/zm_ffmpeg_camera.h index ac63735ab..9e645012e 100644 --- a/src/zm_ffmpeg_camera.h +++ b/src/zm_ffmpeg_camera.h @@ -40,6 +40,7 @@ class FfmpegCamera : public Camera { std::string mPath; std::string mMethod; std::string mOptions; + std::string encoder_options; int frameCount; @@ -51,7 +52,6 @@ class FfmpegCamera : public Camera { AVCodec *mAudioCodec; AVFrame *mRawFrame; AVFrame *mFrame; - _AVPIXELFORMAT imagePixFormat; bool hwaccel; #if HAVE_AVUTIL_HWCONTEXT_H @@ -82,9 +82,6 @@ class FfmpegCamera : public Camera { #endif // HAVE_LIBAVFORMAT -#if HAVE_LIBSWSCALE - struct SwsContext *mConvertContext; -#endif int64_t startTime; diff --git a/src/zm_ffmpeg_output.cpp b/src/zm_ffmpeg_output.cpp new file mode 100644 index 000000000..423595d19 --- /dev/null +++ b/src/zm_ffmpeg_output.cpp @@ -0,0 +1,180 @@ + +#include "zm_ffmpeg_input.h" +#include "zm_logger.h" +#include "zm_ffmpeg.h" + +FFmpeg_Output::FFmpeg_Output() { + input_format_context = NULL; + video_stream_id = -1; + audio_stream_id = -1; + av_register_all(); + avcodec_register_all(); + +} +FFmpeg_Output::~FFmpeg_Output() { +} + +int FFmpeg_Output::Open( const char *filepath ) { + + int error; + + /** Open the input file to read from it. */ + if ( (error = avformat_open_input( &input_format_context, filepath, NULL, NULL)) < 0 ) { + + Error("Could not open input file '%s' (error '%s')\n", + filepath, av_make_error_string(error).c_str() ); + input_format_context = NULL; + return error; + } + + /** Get information on the input file (number of streams etc.). */ + if ( (error = avformat_find_stream_info(input_format_context, NULL)) < 0 ) { + Error( "Could not open find stream info (error '%s')\n", + av_make_error_string(error).c_str() ); + avformat_close_input(&input_format_context); + return error; + } + + for ( unsigned int i = 0; i < input_format_context->nb_streams; i += 1 ) { + if ( is_video_stream( input_format_context->streams[i] ) ) { + zm_dump_stream_format(input_format_context, i, 0, 0); + if ( video_stream_id == -1 ) { + video_stream_id = i; + // if we break, then we won't find the audio stream + } else { + Warning( "Have another video stream." ); + } + } else if ( is_audio_stream( input_format_context->streams[i] ) ) { + if ( audio_stream_id == -1 ) { + audio_stream_id = i; + } else { + Warning( "Have another audio stream." ); + } + } + + streams[i].frame_count = 0; +#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + streams[i].context = avcodec_alloc_context3( NULL ); + avcodec_parameters_to_context( streams[i].context, input_format_context->streams[i]->codecpar ); +#else + streams[i].context = input_format_context->streams[i]->codec; +#endif + + if ( !(streams[i].codec = avcodec_find_decoder(streams[i].context->codec_id)) ) { + Error( "Could not find input codec\n"); + avformat_close_input(&input_format_context); + return AVERROR_EXIT; + } else { + Debug(1, "Using codec (%s) for stream %d", streams[i].codec->name, i ); + } + + if ((error = avcodec_open2( streams[i].context, streams[i].codec, NULL)) < 0) { + Error( "Could not open input codec (error '%s')\n", + av_make_error_string(error).c_str() ); +#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + avcodec_free_context( &streams[i].context ); +#endif + avformat_close_input(&input_format_context); + return error; + } + } // end foreach stream + + if ( video_stream_id == -1 ) + Error( "Unable to locate video stream in %s", filepath ); + if ( audio_stream_id == -1 ) + Debug( 3, "Unable to locate audio stream in %s", filepath ); + + return 0; +} // end int FFmpeg_Output::Open( const char * filepath ) + +AVFrame *FFmpeg_Output::get_frame( int stream_id ) { + Debug(1, "Getting frame from stream %d", stream_id ); + + int frameComplete = false; + AVPacket packet; + av_init_packet( &packet ); + AVFrame *frame = zm_av_frame_alloc(); + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + + while ( !frameComplete ) { + int ret = av_read_frame( input_format_context, &packet ); + if ( ret < 0 ) { + av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE); + if ( + // Check if EOF. + (ret == AVERROR_EOF || (input_format_context->pb && input_format_context->pb->eof_reached)) || + // Check for Connection failure. + (ret == -110) + ) { + Info( "av_read_frame returned %s.", errbuf ); + return NULL; + } + Error( "Unable to read packet from stream %d: error %d \"%s\".", packet.stream_index, ret, errbuf ); + return NULL; + } + + if ( (stream_id < 0 ) || ( packet.stream_index == stream_id ) ) { + Debug(1,"Packet is for our stream (%d)", packet.stream_index ); + + AVCodecContext *context = streams[packet.stream_index].context; + +#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + ret = avcodec_send_packet( context, &packet ); + if ( ret < 0 ) { + av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); + Error( "Unable to send packet at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf ); + zm_av_packet_unref( &packet ); + continue; + } else { + Debug(1, "Success getting a packet"); + } + +#if HAVE_AVUTIL_HWCONTEXT_H + if ( hwaccel ) { + ret = avcodec_receive_frame( context, hwFrame ); + if ( ret < 0 ) { + av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); + Error( "Unable to receive frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf ); + zm_av_packet_unref( &packet ); + continue; + } + ret = av_hwframe_transfer_data(frame, hwFrame, 0); + if (ret < 0) { + av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); + Error( "Unable to transfer frame at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf ); + zm_av_packet_unref( &packet ); + continue; + } + } else { +#endif + Debug(1,"Getting a frame?"); + ret = avcodec_receive_frame( context, frame ); + if ( ret < 0 ) { + av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); + Error( "Unable to send packet at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf ); + zm_av_packet_unref( &packet ); + continue; + } + +#if HAVE_AVUTIL_HWCONTEXT_H + } +#endif + + frameComplete = 1; +# else + ret = zm_avcodec_decode_video( streams[packet.stream_index].context, frame, &frameComplete, &packet ); + if ( ret < 0 ) { + av_strerror( ret, errbuf, AV_ERROR_MAX_STRING_SIZE ); + Error( "Unable to decode frame at frame %d: %s, continuing", streams[packet.stream_index].frame_count, errbuf ); + zm_av_packet_unref( &packet ); + continue; + } +#endif + } // end if it's the right stream + + zm_av_packet_unref( &packet ); + + } // end while ! frameComplete + return frame; + +} // end AVFrame *FFmpeg_Output::get_frame diff --git a/src/zm_ffmpeg_output.h b/src/zm_ffmpeg_output.h new file mode 100644 index 000000000..76afad0d7 --- /dev/null +++ b/src/zm_ffmpeg_output.h @@ -0,0 +1,46 @@ +#ifndef ZM_FFMPEG_INPUT_H +#define ZM_FFMPEG_INPUT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "libavformat/avformat.h" +#include "libavformat/avio.h" +#include "libavcodec/avcodec.h" + +#ifdef __cplusplus +} +#endif + +class FFmpeg_Output { + + public: + FFmpeg_Output(); + ~FFmpeg_Output(); + + int Open( const char *filename ); + int Close(); + AVFrame *put_frame( int stream_id=-1 ); + AVFrame *put_packet( int stream_id=-1 ); + int get_video_stream_id() { + return video_stream_id; + } + int get_audio_stream_id() { + return audio_stream_id; + } + + private: + typedef struct { + AVCodecContext *context; + AVCodec *codec; + int frame_count; + } stream; + + stream streams[2]; + int video_stream_id; + int audio_stream_id; + AVFormatContext *input_format_context; +}; + +#endif diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 0ce1d0f73..1c1e4c767 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -241,6 +241,7 @@ Monitor::Monitor( int p_orientation, unsigned int p_deinterlacing, int p_savejpegs, + int p_colours, VideoWriter p_videowriter, std::string p_encoderparams, bool p_record_audio, @@ -280,6 +281,7 @@ Monitor::Monitor( orientation( (Orientation)p_orientation ), deinterlacing( p_deinterlacing ), savejpegspref( p_savejpegs ), + colours( p_colours ), videowriter( p_videowriter ), encoderparams( p_encoderparams ), record_audio( p_record_audio ), @@ -336,6 +338,49 @@ Monitor::Monitor( /* Parse encoder parameters */ ParseEncoderParameters(encoderparams.c_str(), &encoderparamsvec); +#if HAVE_LIBSWSCALE + mConvertContext = NULL; +#endif + /* Has to be located inside the constructor so other components such as zma will receive correct colours and subpixel order */ + if ( colours == ZM_COLOUR_RGB32 ) { + subpixelorder = ZM_SUBPIX_ORDER_RGBA; + imagePixFormat = AV_PIX_FMT_RGBA; + } else if ( colours == ZM_COLOUR_RGB24 ) { + subpixelorder = ZM_SUBPIX_ORDER_RGB; + imagePixFormat = AV_PIX_FMT_RGB24; + } else if ( colours == ZM_COLOUR_GRAY8 ) { + subpixelorder = ZM_SUBPIX_ORDER_NONE; + imagePixFormat = AV_PIX_FMT_GRAY8; + } else { + Panic("Unexpected colours: %d",colours); + } +#if HAVE_LIBSWSCALE +//FIXME, need to be able to query the camera input for what it is going to be getting, which needs to be called after the camera is open. + //Debug ( 1, "Calling sws_isSupportedInput" ); + //if ( !sws_isSupportedInput(mVideoCodecContext->pix_fmt) ) { + //Fatal("swscale does not support the codec format: %c%c%c%c", (mVideoCodecContext->pix_fmt)&0xff, ((mVideoCodecContext->pix_fmt >> 8)&0xff), ((mVideoCodecContext->pix_fmt >> 16)&0xff), ((mVideoCodecContext->pix_fmt >> 24)&0xff)); + //} + + if ( !sws_isSupportedOutput(imagePixFormat) ) { + Fatal("swscale does not support the target format: %c%c%c%c",(imagePixFormat)&0xff,((imagePixFormat>>8)&0xff),((imagePixFormat>>16)&0xff),((imagePixFormat>>24)&0xff)); + } + + // We don't know this yet, need to open the camera first. + //mConvertContext = sws_getContext(mVideoCodecContext->width, + //mVideoCodecContext->height, + //mVideoCodecContext->pix_fmt, + //width, height, + //imagePixFormat, SWS_BICUBIC, NULL, + //NULL, NULL); + //if ( mConvertContext == NULL ) + //Fatal( "Unable to create conversion context for %s", mPath.c_str() ); +#else // HAVE_LIBSWSCALE + //Fatal( "You must compile ffmpeg with the --enable-swscale option to use ffmpeg cameras" ); +#endif // HAVE_LIBSWSCALE + + //if ( (unsigned int)mVideoCodecContext->width != width || (unsigned int)mVideoCodecContext->height != height ) { + //Warning( "Monitor dimensions are %dx%d but camera is sending %dx%d", width, height, mVideoCodecContext->width, mVideoCodecContext->height ); + //} fps = 0.0; event_count = 0; @@ -568,6 +613,12 @@ Monitor::~Monitor() { delete videoStore; videoStore = NULL; } +#if HAVE_LIBSWSCALE + if ( mConvertContext ) { + sws_freeContext( mConvertContext ); + mConvertContext = NULL; + } +#endif if ( timestamps ) { delete[] timestamps; timestamps = 0; @@ -1126,7 +1177,7 @@ bool Monitor::CheckSignal( const Image *image ) { return true; } - } else if(colours == ZM_COLOUR_RGB32) { + } else if ( colours == ZM_COLOUR_RGB32 ) { if ( usedsubpixorder == ZM_SUBPIX_ORDER_ARGB || usedsubpixorder == ZM_SUBPIX_ORDER_ABGR) { if ( ARGB_ABGR_ZEROALPHA(*(((const Rgb*)buffer)+index)) != ARGB_ABGR_ZEROALPHA(colour_val) ) return true; @@ -1359,12 +1410,12 @@ Debug(3,"before DetectMotion"); 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()); - Debug( 3, "Detected new event at (%d.%d)", timestamp->tv_sec,timestamp->tv_usec ); + //Debug( 3, "Detected new event at (%d.%d)", timestamp->tv_sec,timestamp->tv_usec ); if ( section_length ) { // TODO: Wouldn't this be clearer if we just did something like if now - event->start > section_length ? int section_mod = timestamp->tv_sec % section_length; - Debug( 3, "Section length (%d) Last Section Mod(%d), new section mod(%d)", section_length, last_section_mod, section_mod ); + Debug( 4, "Section length (%d) Last Section Mod(%d), new section mod(%d)", section_length, last_section_mod, section_mod ); if ( section_mod < last_section_mod ) { //if ( state == IDLE || state == TAPE || event_close_mode == CLOSE_TIME ) { //if ( state == TAPE ) { @@ -1976,6 +2027,7 @@ int Monitor::LoadLocalMonitors( const char *device, Monitor **&monitors, Purpose orientation, deinterlacing, savejpegs, + colours, videowriter, encoderparams, record_audio, @@ -2160,6 +2212,7 @@ int Monitor::LoadRemoteMonitors( const char *protocol, const char *host, const c orientation, deinterlacing, savejpegs, + colours, videowriter, encoderparams, record_audio, @@ -2309,6 +2362,7 @@ int Monitor::LoadFileMonitors( const char *file, Monitor **&monitors, Purpose pu orientation, deinterlacing, savejpegs, + colours, videowriter, encoderparams, record_audio, @@ -2468,6 +2522,7 @@ int Monitor::LoadFfmpegMonitors( const char *file, Monitor **&monitors, Purpose orientation, deinterlacing, savejpegs, + colours, videowriter, encoderparams, record_audio, @@ -2795,6 +2850,7 @@ Monitor *Monitor::Load( unsigned int p_id, bool load_zones, Purpose purpose ) { orientation, deinterlacing, savejpegs, + colours, videowriter, encoderparams, record_audio, diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 59eaa62df..c6eca8bd6 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -240,9 +240,15 @@ protected: bool videoRecording; int savejpegspref; + int colours; VideoWriter videowriter; std::string encoderparams; std::vector encoderparamsvec; + _AVPIXELFORMAT imagePixFormat; + unsigned int subpixelorder; +#if HAVE_LIBSWSCALE + struct SwsContext *mConvertContext; +#endif bool record_audio; // Whether to store the audio that we receive int brightness; // The statically saved brightness of the camera @@ -348,6 +354,7 @@ public: int p_orientation, unsigned int p_deinterlacing, int p_savejpegs, + int p_colours, VideoWriter p_videowriter, std::string p_encoderparams, bool p_record_audio, @@ -434,6 +441,7 @@ public: int GetOptSaveJPEGs() const { return( savejpegspref ); } VideoWriter GetOptVideoWriter() const { return( videowriter ); } const std::vector* GetOptEncoderParams() const { return( &encoderparamsvec ); } + const std::string &GetEncoderOptions() const { return( encoderparams ); } uint32_t GetLastEventId() const { return shared_data->last_event_id; } uint32_t GetVideoWriterEventId() const { return video_store_data->current_event; } void SetVideoWriterEventId( uint32_t p_event_id ) { video_store_data->current_event = p_event_id; } diff --git a/src/zm_packet.cpp b/src/zm_packet.cpp index 34a78bd26..6b1fe0999 100644 --- a/src/zm_packet.cpp +++ b/src/zm_packet.cpp @@ -123,41 +123,4 @@ int ZMPacket::decode( AVCodecContext *ctx ) { return 1; } // end ZMPacket::decode -Image * ZMPacket::get_image( Image *i = NULL ) { - - if ( ! frame ) { - Error("Can't get image without frame.. maybe need to decode first"); - return NULL; - } - - if ( ! image ) { - if ( ! i ) { - Error("Need a pre-allocated image buffer"); - return NULL; - } - image = i; - } - /* Request a writeable buffer of the target image */ - uint8_t* directbuffer = image->WriteBuffer(); - //uint8_t* directbuffer = image.WriteBuffer(width, height, colours, subpixelorder); - if ( directbuffer == NULL ) { - Error("Failed requesting writeable buffer for the captured image."); - image = NULL; - return NULL; - } - - AVFrame *mFrame = zm_av_frame_alloc(); -#if LIBAVUTIL_VERSION_CHECK(54, 6, 0, 6, 0) - av_image_fill_arrays(mFrame->data, mFrame->linesize, directbuffer, imagePixFormat, frame->width, frame->height, 1); -#else - avpicture_fill( (AVPicture *)mFrame, directbuffer, imagePixFormat, frame->width, frame->height); -#endif - if (sws_scale(mConvertContext, frame->data, frame->linesize, - 0, mVideoCodecContext->height, mFrame->data, mFrame->linesize) < 0) { - Fatal("Unable to convert raw format %u to target format %u", - mVideoCodecContext->pix_fmt, imagePixFormat); - } - av_frame_free( &mFrame ); - return image; -} diff --git a/src/zm_packet.h b/src/zm_packet.h index 75532ac7c..1ac7da650 100644 --- a/src/zm_packet.h +++ b/src/zm_packet.h @@ -41,7 +41,7 @@ class ZMPacket { public: AVPacket *av_packet() { return &packet; } AVFrame *av_frame() { return frame; } - Image *get_image( Image * ); + Image *get_image( Image *, _AVPIXELFORMAT imagePixFormat, struct SwsContext *mConvertContext ); int is_keyframe() { return keyframe; }; int decode( AVCodecContext *ctx ); ZMPacket( AVPacket *packet, struct timeval *timestamp ); diff --git a/src/zm_videostore.cpp b/src/zm_videostore.cpp index 8d4452f07..8fa29ae7e 100644 --- a/src/zm_videostore.cpp +++ b/src/zm_videostore.cpp @@ -31,10 +31,12 @@ extern "C" { #include "libavutil/time.h" } -VideoStore::VideoStore(const char *filename_in, const char *format_in, - AVStream *p_video_in_stream, - AVStream *p_audio_in_stream, - Monitor *monitor) { +VideoStore::VideoStore( + const char *filename_in, const char *format_in, + AVStream *p_video_in_stream, + AVStream *p_audio_in_stream, int64_t nStartTime, + Monitor *monitor + ) { video_in_stream = p_video_in_stream; audio_in_stream = p_audio_in_stream; @@ -50,9 +52,12 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, // store ins in variables local to class filename = filename_in; format = format_in; + packets_written = 0; + frame_count = 0; Info("Opening video storage stream %s format: %s", filename, format); +#if 0 ret = avformat_alloc_output_context2(&oc, NULL, NULL, filename); if (ret < 0) { Warning( @@ -65,6 +70,7 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, // Couldn't deduce format from filename, trying from format name if (!oc) { +#endif avformat_alloc_output_context2(&oc, NULL, format, filename); if (!oc) { Fatal( @@ -72,9 +78,11 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, " could not be assigned based on filename or format %s", filename, format); } else { - Debug(4, "Success alocateing out ctx"); + Debug(4, "Success alocating out ctx"); } +#if 0 } // end if ! oc +#endif AVDictionary *pmetadata = NULL; int dsr = @@ -84,15 +92,14 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, oc->metadata = pmetadata; out_format = oc->oformat; + in_frame = NULL; #if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) - - // Since we are not re-encoding, all we have to do is copy the parameters video_out_ctx = avcodec_alloc_context3(NULL); // Copy params from instream to ctx ret = avcodec_parameters_to_context(video_out_ctx, video_in_stream->codecpar); - if (ret < 0) { + if ( ret < 0 ) { Error("Could not initialize ctx parameteres"); return; } else { @@ -106,7 +113,7 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, Debug(2, "Success creating video out stream"); } - if (!video_out_ctx->codec_tag) { + if ( !video_out_ctx->codec_tag ) { video_out_ctx->codec_tag = av_codec_get_tag(oc->oformat->codec_tag, video_in_ctx->codec_id); Debug(2, "No codec_tag, setting to %d", video_out_ctx->codec_tag); @@ -124,22 +131,80 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, zm_dump_codecpar(video_out_stream->codecpar); #else - video_out_stream = - avformat_new_stream(oc,(const AVCodec *)(video_in_ctx->codec)); - if (!video_out_stream) { + if ( video_in_ctx->codec_id == AV_CODEC_ID_H264 ) { + // Same codec, just copy the packets, otherwise we have to decode/encode + video_out_codec = (AVCodec *)video_in_ctx->codec; + video_out_stream = avformat_new_stream(oc, video_out_codec); + if ( !video_out_stream ) { + Fatal("Unable to create video out stream\n"); + } else { + Debug(2, "Success creating video out stream"); + } + video_out_ctx = video_out_stream->codec; + // Just copy them from the in, no reason to choose different + video_out_ctx->time_base = video_in_ctx->time_base; + video_out_stream->time_base = video_in_stream->time_base; + } else { + /** Create a new frame to store the audio samples. */ + if ( !(in_frame = zm_av_frame_alloc()) ) { + Error("Could not allocate in frame"); + return; + } + video_out_codec = avcodec_find_encoder(AV_CODEC_ID_H264); + if (!video_out_codec) { + Fatal("Could not find codec for H264"); + } + Debug(2, "Have video out codec"); + } + video_out_stream = avformat_new_stream(oc, video_out_codec); + if ( !video_out_stream ) { Fatal("Unable to create video out stream\n"); } else { Debug(2, "Success creating video out stream"); } video_out_ctx = video_out_stream->codec; - ret = avcodec_copy_context(video_out_ctx, video_in_ctx); - if (ret < 0) { - Fatal("Unable to copy in video ctx to out video ctx %s\n", - av_make_error_string(ret).c_str()); + + // * Copy over the useful; parameters + video_out_ctx->height = video_in_ctx->height; + video_out_ctx->width = video_in_ctx->width; + video_out_ctx->sample_aspect_ratio = video_in_ctx->sample_aspect_ratio; + /* take first format from list of supported formats */ + video_out_ctx->pix_fmt = video_out_codec->pix_fmts[0]; + /* video time_base can be set to whatever is handy and supported by encoder */ + //video_out_ctx->time_base = video_in_ctx->time_base; + video_out_ctx->time_base = (AVRational){1, 1000}; // Milliseconds as base frame rate + video_out_ctx->framerate = (AVRational){0,1}; // Unknown framerate + + AVDictionary *opts = 0; + std::string Options = monitor->GetEncoderOptions(); + ret = av_dict_parse_string(&opts, Options.c_str(), "=", ",#\n", 0); + if ( ret < 0 ) { + Warning("Could not parse ffmpeg encoder options list '%s'\n", Options.c_str()); } else { - Debug(3, "Success copying ctx"); + AVDictionaryEntry *e = NULL; + while ( (e = av_dict_get(opts, "", e, AV_DICT_IGNORE_SUFFIX)) != NULL ) { + Debug( 3, "Encoder Option %s=%s", e->key, e->value ); + } } - if (!video_out_ctx->codec_tag) { + ret = avcodec_open2(video_out_ctx, video_out_codec, &opts ); + if (ret < 0) { + Error("Can't open video codec!"); + return; + } + AVDictionaryEntry *e = NULL; + while ( (e = av_dict_get(opts, "", e, AV_DICT_IGNORE_SUFFIX)) != NULL ) { + Warning( "Encoder Option %s not recognized by ffmpeg codec", e->key); + } + av_dict_free(&opts); + //ret = avcodec_copy_context(video_out_ctx, video_in_ctx); + //if ( ret < 0 ) { + //Fatal("Unable to copy in video ctx to out video ctx %s\n", + //av_make_error_string(ret).c_str()); + ////} else { + //Debug(3, "Success copying ctx"); + //} +#if 0 + if ( !video_out_ctx->codec_tag ) { Debug(2, "No codec_tag"); if (!oc->oformat->codec_tag || av_codec_get_id(oc->oformat->codec_tag, @@ -152,10 +217,7 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, } } #endif - - // Just copy them from the in, no reason to choose different - video_out_ctx->time_base = video_in_ctx->time_base; - video_out_stream->time_base = video_in_stream->time_base; +#endif Debug(3, "Time bases: VIDEO in stream (%d/%d) in codec: (%d/%d) out " @@ -192,7 +254,6 @@ VideoStore::VideoStore(const char *filename_in, const char *format_in, audio_out_codec = NULL; audio_in_ctx = NULL; audio_out_stream = NULL; - in_frame = NULL; out_frame = NULL; #ifdef HAVE_LIBAVRESAMPLE resample_ctx = NULL; @@ -329,17 +390,86 @@ bool VideoStore::open() { return true; } +void VideoStore::write_audio_packet( AVPacket &pkt ) { + Debug(2, "writing flushed packet pts(%d) dts(%d) duration(%d)", pkt.pts, + pkt.dts, pkt.duration); + pkt.pts = audio_next_pts; + pkt.dts = audio_next_dts; + + if (pkt.duration > 0) + pkt.duration = + av_rescale_q(pkt.duration, audio_out_ctx->time_base, + audio_out_stream->time_base); + audio_next_pts += pkt.duration; + audio_next_dts += pkt.duration; + + Debug(2, "writing flushed packet pts(%d) dts(%d) duration(%d)", pkt.pts, + pkt.dts, pkt.duration); + pkt.stream_index = audio_out_stream->index; + av_interleaved_write_frame(oc, &pkt); +} + VideoStore::~VideoStore() { + if ( video_out_ctx->codec_id != video_in_ctx->codec_id ) { + + if (video_out_ctx->codec->capabilities & AV_CODEC_CAP_DELAY) { + // The codec queues data. We need to send a flush command and out + // whatever we get. Failures are not fatal. + AVPacket pkt; + av_init_packet(&pkt); + +#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + // Put encoder into flushing mode + avcodec_send_frame(video_out_ctx, NULL); + while (1) { + ret = avcodec_receive_packet(video_out_ctx, &pkt); + if (ret < 0) { + if (AVERROR_EOF != ret) { + Error("ERror encoding audio while flushing (%d) (%s)", ret, + av_err2str(ret)); + } + break; + } +#else + while (1) { + // WIthout these we seg fault I don't know why. + pkt.data = NULL; + pkt.size = 0; + av_init_packet(&pkt); + int got_packet = 0; + ret = avcodec_encode_video2(video_out_ctx, &pkt, NULL, &got_packet); + if ( ret < 0 ) { + Error("ERror encoding video while flushing (%d) (%s)", ret, + av_err2str(ret)); + break; + } + if (!got_packet) { + break; + } +#endif +Debug(3, "(%d, %d)", video_next_dts, video_next_pts ); + pkt.dts = video_next_dts; + pkt.pts = video_next_pts; + pkt.duration = video_last_duration; + write_video_packet(pkt); + zm_av_packet_unref(&pkt); + } // while have buffered frames + } // end if have delay capability + } // end if have buffered video + if (audio_out_codec) { // The codec queues data. We need to send a flush command and out // whatever we get. Failures are not fatal. AVPacket pkt; + // WIthout these we seg fault I don't know why. + pkt.data = NULL; + pkt.size = 0; av_init_packet(&pkt); - while (1) { #if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) - // Put encoder into flushing mode - avcodec_send_frame(audio_out_ctx, NULL); + // Put encoder into flushing mode + avcodec_send_frame(audio_out_ctx, NULL); + while (1) { ret = avcodec_receive_packet(audio_out_ctx, &pkt); if (ret < 0) { if (AVERROR_EOF != ret) { @@ -349,6 +479,7 @@ VideoStore::~VideoStore() { break; } #else + while (1) { int got_packet = 0; ret = avcodec_encode_audio2(audio_out_ctx, &pkt, NULL, &got_packet); @@ -362,22 +493,7 @@ VideoStore::~VideoStore() { break; } #endif - Debug(2, "writing flushed packet pts(%d) dts(%d) duration(%d)", pkt.pts, - pkt.dts, pkt.duration); - pkt.pts = audio_next_pts; - pkt.dts = audio_next_dts; - - if (pkt.duration > 0) - pkt.duration = - av_rescale_q(pkt.duration, audio_out_ctx->time_base, - audio_out_stream->time_base); - audio_next_pts += pkt.duration; - audio_next_dts += pkt.duration; - - Debug(2, "writing flushed packet pts(%d) dts(%d) duration(%d)", pkt.pts, - pkt.dts, pkt.duration); - pkt.stream_index = audio_out_stream->index; - av_interleaved_write_frame(oc, &pkt); + write_audio_packet(pkt); zm_av_packet_unref(&pkt); } // while have buffered frames } // end if audio_out_codec @@ -402,6 +518,11 @@ VideoStore::~VideoStore() { video_out_ctx = NULL; Debug(4, "Success freeing video_out_ctx"); } +// Used by both audio and video conversions + if (in_frame) { + av_frame_free(&in_frame); + in_frame = NULL; + } if (audio_out_stream) { avcodec_close(audio_out_ctx); audio_out_ctx = NULL; @@ -410,10 +531,6 @@ VideoStore::~VideoStore() { avresample_close(resample_ctx); avresample_free(&resample_ctx); } - if (in_frame) { - av_frame_free(&in_frame); - in_frame = NULL; - } if (out_frame) { av_frame_free(&out_frame); out_frame = NULL; @@ -547,10 +664,12 @@ bool VideoStore::setup_resampler() { audio_out_ctx->channel_layout, audio_out_ctx->frame_size); /** Create a new frame to store the audio samples. */ + if ( ! in_frame ) { if (!(in_frame = zm_av_frame_alloc())) { Error("Could not allocate in frame"); return false; } + } /** Create a new frame to store the audio samples. */ if (!(out_frame = zm_av_frame_alloc())) { @@ -687,92 +806,117 @@ int VideoStore::writePacket( ZMPacket *ipkt ) { int VideoStore::writeVideoFramePacket(AVPacket *ipkt) { av_init_packet(&opkt); + frame_count += 1; + if ( video_out_ctx->codec_id != video_in_ctx->codec_id ) { + Debug(3, "Have encoding video frmae count (%d)", frame_count); - opkt.pts = video_next_pts; - opkt.dts = video_next_dts; - opkt.duration = 0; - - int duration; - if (!video_last_pts) { - duration = 0; - } else { - duration = - av_rescale_q(ipkt->pts - video_last_pts, video_in_stream->time_base, - video_out_stream->time_base); - Debug(1, "duration calc: pts(%d) - last_pts(%d) = (%d)", ipkt->pts, - video_last_pts, duration); - if (duration < 0) { - duration = ipkt->duration; +#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + ret = avcodec_send_packet(video_in_ctx, ipkt); + if (ret < 0) { + Error("avcodec_send_packet fail %s", av_make_error_string(ret).c_str()); + return 0; } - } - //#if ( 0 && video_last_pts && ( ipkt->duration == AV_NOPTS_VALUE || ! - //ipkt->duration ) ) { - // Video packets don't really have a duration. Audio does. - // opkt.duration = av_rescale_q(duration, video_in_stream->time_base, - // video_out_stream->time_base); - // opkt.duration = 0; - //} else { - // duration = opkt.duration = av_rescale_q(ipkt->duration, - // video_in_stream->time_base, video_out_stream->time_base); - //} - video_last_pts = ipkt->pts; - video_last_dts = ipkt->dts; + ret = avcodec_receive_frame(video_in_ctx, in_frame); + if ( ret < 0 ) { + Error("avcodec_receive_frame fail %s", av_make_error_string(ret).c_str()); + return 0; + } + if ((ret = avcodec_send_frame(video_out_ctx, in_frame)) < 0) { + Error("Could not send frame (error '%s')", + av_make_error_string(ret).c_str()); + zm_av_packet_unref(&opkt); + return 0; + } -#if 0 - //Scale the PTS of the outgoing packet to be the correct time base - if ( ipkt->pts != AV_NOPTS_VALUE ) { - - if ( ! video_last_pts ) { - // This is the first packet. - opkt.pts = 0; - Debug(2, "Starting video video_last_pts will become (%d)", ipkt->pts); + if ((ret = avcodec_receive_packet(video_out_ctx, &opkt)) < 0) { + if (AVERROR(EAGAIN) == ret) { + // THe codec may need more samples than it has, perfectly valid + Debug(3, "Could not recieve packet (error '%s')", + av_make_error_string(ret).c_str()); + } else { + Error("Could not recieve packet (error %d = '%s')", ret, + av_make_error_string(ret).c_str()); + } + zm_av_packet_unref(&opkt); + av_frame_unref(in_frame); + return 0; + } +#else + /** + * Decode the video frame stored in the packet. + * The in video stream decoder is used to do this. + * If we are at the end of the file, pass an empty packet to the decoder + * to flush it. + */ + if ((ret = avcodec_decode_video2(video_in_ctx, in_frame, + &data_present, ipkt)) < 0) { + Error("Could not decode frame (error '%s')\n", + av_make_error_string(ret).c_str()); + dumpPacket(ipkt); + av_frame_free(&in_frame); + return 0; } else { - if ( ipkt->pts < video_last_pts ) { - Debug(1, "Resetting video_last_pts from (%d) to (%d)", video_last_pts, ipkt->pts); - // wrap around, need to figure out the distance FIXME having this wrong should cause a jump, but then play ok? - opkt.pts = video_next_pts + av_rescale_q( ipkt->pts, video_in_stream->time_base, video_out_stream->time_base); - } else { - opkt.pts = video_next_pts + av_rescale_q( ipkt->pts - video_last_pts, video_in_stream->time_base, video_out_stream->time_base); - } + Debug(3, "Decoded frame data_present(%d)", data_present); } - Debug(3, "opkt.pts = %d from ipkt->pts(%d) - last_pts(%d)", opkt.pts, ipkt->pts, video_last_pts); - video_last_pts = ipkt->pts; - } else { - Debug(3, "opkt.pts = undef"); - opkt.pts = AV_NOPTS_VALUE; - } - // Just because the in stream wraps, doesn't mean the out needs to. Really, if we are limiting ourselves to 10min segments I can't imagine every wrapping in the out. So need to handle in wrap, without causing out wrap. - if ( !video_last_dts ) { - // This is the first packet. - opkt.dts = 0; - Debug(1, "Starting video video_last_dts will become (%lu)", ipkt->dts); - video_last_dts = ipkt->dts; - } else { - // Scale the DTS of the outgoing packet to be the correct time base - - if ( ipkt->dts == AV_NOPTS_VALUE ) { - // why are we using cur_dts instead of packet.dts? I think cur_dts is in AV_TIME_BASE_Q, but ipkt.dts is in video_in_stream->time_base - if ( video_in_stream->cur_dts < video_last_dts ) { - Debug(1, "Resetting video_last_dts from (%d) to (%d) p.dts was (%d)", video_last_dts, video_in_stream->cur_dts, ipkt->dts); - opkt.dts = video_next_dts + av_rescale_q(video_in_stream->cur_dts, AV_TIME_BASE_Q, video_out_stream->time_base); - } else { - opkt.dts = video_next_dts + av_rescale_q(video_in_stream->cur_dts - video_last_dts, AV_TIME_BASE_Q, video_out_stream->time_base); - } - Debug(3, "opkt.dts = %d from video_in_stream->cur_dts(%d) - previus_dts(%d)", opkt.dts, video_in_stream->cur_dts, video_last_dts); - video_last_dts = video_in_stream->cur_dts; - } else { - if ( ipkt->dts < video_last_dts ) { - Debug(1, "Resetting video_last_dts from (%d) to (%d)", video_last_dts, ipkt->dts); - opkt.dts = video_next_dts + av_rescale_q( ipkt->dts, video_in_stream->time_base, video_out_stream->time_base); - } else { - opkt.dts = video_next_dts + av_rescale_q( ipkt->dts - video_last_dts, video_in_stream->time_base, video_out_stream->time_base); - } - Debug(3, "opkt.dts = %d from ipkt.dts(%d) - previus_dts(%d)", opkt.dts, ipkt->dts, video_last_dts); - video_last_dts = ipkt->dts; + if ( !data_present ) { + Debug(2, "Not ready to transcode a frame yet."); + return 0; + } + if ((ret = avcodec_encode_video2(video_out_ctx, &opkt, in_frame, + &data_present)) < 0) { + Error("Could not encode frame (error '%s')", + av_make_error_string(ret).c_str()); + zm_av_packet_unref(&opkt); + return 0; + } + if (!data_present) { + Debug(2, "Not ready to out a frame yet."); + zm_av_packet_unref(&opkt); + return 0; } - } #endif + } else { + Debug(3, "Doing passthrough, just copy packet"); + // Just copy it because the codec is the same + opkt.data = ipkt->data; + opkt.size = ipkt->size; + opkt.flags = ipkt->flags; + } + + opkt.dts = video_next_dts; + opkt.pts = video_next_pts; + + int duration; + if ( !video_last_pts ) { + duration = 0; + } else { + duration = av_rescale_q( + ipkt->pts - video_last_pts, + video_in_stream->time_base, + video_out_stream->time_base + ); + Debug(1, "duration calc: pts(%d) - last_pts(%d) = (%d)", ipkt->pts, + video_last_pts, duration); + if ( duration < 0 ) { + duration = ipkt->duration; + } + } + + Debug(1, "ipkt.dts(%d) ipkt.pts(%d)", ipkt->dts, ipkt->pts); + video_last_pts = ipkt->pts; + video_last_dts = ipkt->dts; + video_last_duration = duration; + opkt.duration = duration; + + write_video_packet( opkt ); + zm_av_packet_unref(&opkt); + + return 0; +} // end int VideoStore::writeVideoFramePacket( AVPacket *ipkt ) + +void VideoStore::write_video_packet( AVPacket &opkt ) { + if (opkt.dts > opkt.pts) { Debug(1, "opkt.dts(%d) must be <= opkt.pts(%d). Decompression must happen " @@ -781,36 +925,30 @@ int VideoStore::writeVideoFramePacket(AVPacket *ipkt) { opkt.dts = opkt.pts; } - opkt.flags = ipkt->flags; int keyframe = opkt.flags & AV_PKT_FLAG_KEY; opkt.pos = -1; - - opkt.data = ipkt->data; - opkt.size = ipkt->size; - opkt.stream_index = video_out_stream->index; + video_next_dts += opkt.duration; + video_next_pts += opkt.duration; + AVPacket safepkt; memcpy(&safepkt, &opkt, sizeof(AVPacket)); Debug(1, - "writing video packet keyframe(%d) pts(%d) dts(%d) duration(%d) " - "ipkt.duration(%d)", - keyframe, opkt.pts, opkt.dts, duration, ipkt->duration); - if ((opkt.data == NULL) || (opkt.size < 1)) { + "writing video packet keyframe(%d) pts(%d) dts(%d) duration(%d) packet_count(%d)", + keyframe, opkt.pts, opkt.dts, opkt.duration, packets_written ); + if ( (opkt.data == NULL) || (opkt.size < 1) ) { Warning("%s:%d: Mangled AVPacket: discarding frame", __FILE__, __LINE__); - dumpPacket(ipkt); dumpPacket(&opkt); - } else if ((video_next_dts > 0) && (video_next_dts > opkt.dts)) { - Warning("%s:%d: DTS out of order: %lld \u226E %lld; discarding frame", - __FILE__, __LINE__, video_next_dts, opkt.dts); - video_next_dts = opkt.dts; - dumpPacket(&opkt); + //} else if ((video_next_dts > 0) && (video_next_dts > opkt.dts)) { + //Warning("%s:%d: DTS out of order: next:%lld \u226E opkt.dts %lld; discarding frame", + //__FILE__, __LINE__, video_next_dts, opkt.dts); + //video_next_dts = opkt.dts; + //dumpPacket(&opkt); } else { - video_next_dts = opkt.dts + duration; - video_next_pts = opkt.pts + duration; 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 @@ -824,13 +962,12 @@ int VideoStore::writeVideoFramePacket(AVPacket *ipkt) { zm_dump_codecpar(video_in_stream->codecpar); zm_dump_codecpar(video_out_stream->codecpar); #endif + } else { + packets_written += 1; } } - zm_av_packet_unref(&opkt); - - return 0; -} // end int VideoStore::writeVideoFramePacket( AVPacket *ipkt ) +} // end void VideoStore::write_video_packet int VideoStore::writeAudioFramePacket(AVPacket *ipkt) { Debug(4, "writeAudioFrame"); diff --git a/src/zm_videostore.h b/src/zm_videostore.h index 553604f88..a2c242fee 100644 --- a/src/zm_videostore.h +++ b/src/zm_videostore.h @@ -18,11 +18,14 @@ extern "C" { class VideoStore { private: unsigned int packets_written; + unsigned int frame_count; AVOutputFormat *out_format; AVFormatContext *oc; AVStream *video_out_stream; AVStream *audio_out_stream; + + AVCodec *video_out_codec; AVCodecContext *video_out_ctx; AVStream *video_in_stream; @@ -31,6 +34,7 @@ private: // Move this into the object so that we aren't constantly allocating/deallocating it on the stack AVPacket opkt; // we are transcoding + AVFrame *video_in_frame; AVFrame *in_frame; AVFrame *out_frame; @@ -58,12 +62,14 @@ AVAudioResampleContext* resample_ctx; // These are for in int64_t video_last_pts; int64_t video_last_dts; + int64_t video_last_duration; int64_t audio_last_pts; int64_t audio_last_dts; // These are for out, should start at zero. We assume they do not wrap because we just aren't going to save files that big. int64_t video_next_pts; int64_t video_next_dts; +; int64_t audio_next_pts; int64_t audio_next_dts; @@ -81,6 +87,8 @@ public: bool open(); ~VideoStore(); + void write_video_packet( AVPacket &pkt ); + void write_audio_packet( AVPacket &pkt ); int writeVideoFramePacket( AVPacket *pkt ); int writeAudioFramePacket( AVPacket *pkt ); int writePacket( ZMPacket *pkt ); diff --git a/web/includes/Event.php b/web/includes/Event.php index 392973a6a..9f775f039 100644 --- a/web/includes/Event.php +++ b/web/includes/Event.php @@ -198,14 +198,6 @@ class Event { } function createListThumbnail( $overwrite=false ) { - # Load the frame with the highest score to use as a thumbnail - if ( !($frame = dbFetchOne( 'SELECT * FROM Frames WHERE EventId=? AND Score=? ORDER BY FrameId LIMIT 1', NULL, array( $this->{'Id'}, $this->{'MaxScore'} ) )) ) { - Error("Unable to find a Frame matching max score " . $this->{'MaxScore'} . ' for event ' . $this->{'Id'} ); - // FIXME: What if somehow the db frame was lost or score was changed? Should probably try another search for any frame. - return( false ); - } - - $frameId = $frame['FrameId']; if ( ZM_WEB_LIST_THUMB_WIDTH ) { $thumbWidth = ZM_WEB_LIST_THUMB_WIDTH; @@ -219,12 +211,25 @@ class Event { Fatal( "No thumbnail width or height specified, please check in Options->Web" ); } - $imageData = $this->getImageSrc( $frame, $scale, false, $overwrite ); - if ( ! $imageData ) { - return ( false ); + $eventPath = $this->Path(); + if ( file_exists( $eventPath.'/snapshot.jpg' ) ) { + $frame = 'snapshot'; + $humbData = array('Path'=>$eventPath.'/snapshot.jpg'); + } else { +# Load the frame with the highest score to use as a thumbnail + if ( !($frame = dbFetchOne( 'SELECT * FROM Frames WHERE EventId=? AND Score=? ORDER BY FrameId LIMIT 1', NULL, array( $this->{'Id'}, $this->{'MaxScore'} ) )) ) { + Error("Unable to find a Frame matching max score " . $this->{'MaxScore'} . ' for event ' . $this->{'Id'} ); + // FIXME: What if somehow the db frame was lost or score was changed? Should probably try another search for any frame. + return( false ); + } + + $imageData = $this->getImageSrc( $frame, $scale, false, $overwrite ); + if ( ! $imageData ) { + return ( false ); + } + $thumbData = $frame; + $thumbData['Path'] = $imageData['thumbPath']; } - $thumbData = $frame; - $thumbData['Path'] = $imageData['thumbPath']; $thumbData['Width'] = (int)$thumbWidth; $thumbData['Height'] = (int)$thumbHeight; diff --git a/web/includes/Filter.php b/web/includes/Filter.php index 59988430d..11d47973a 100644 --- a/web/includes/Filter.php +++ b/web/includes/Filter.php @@ -12,6 +12,7 @@ public $defaults = array( 'AutoArchive' => 0, 'AutoVideo' => 0, 'AutoMessage' => 0, + 'UpdateDiskSpace' => 0, 'Background' => 0, 'Concurrent' => 0, 'limit' => 100, diff --git a/web/includes/actions.php b/web/includes/actions.php index 046b7bbf2..cbadae3f6 100644 --- a/web/includes/actions.php +++ b/web/includes/actions.php @@ -148,6 +148,7 @@ Warning("Addterm"); $sql .= ', AutoExecute = '. ( !empty($_REQUEST['filter']['AutoExecute']) ? 1 : 0); $sql .= ', AutoExecuteCmd = '.dbEscape($_REQUEST['filter']['AutoExecuteCmd']); $sql .= ', AutoDelete = '. ( !empty($_REQUEST['filter']['AutoDelete']) ? 1 : 0); + $sql .= ', UpdateDiskSpace = '. ( !empty($_REQUEST['filter']['UpdateDiskSpace']) ? 1 : 0); $sql .= ', Background = '. ( !empty($_REQUEST['filter']['Background']) ? 1 : 0); $sql .= ', Concurrent = '. ( !empty($_REQUEST['filter']['Concurrent']) ? 1 : 0); diff --git a/web/includes/database.php b/web/includes/database.php index d19bc86ac..483df64a5 100644 --- a/web/includes/database.php +++ b/web/includes/database.php @@ -127,10 +127,10 @@ function dbQuery( $sql, $params=NULL ) { } else { $result = $dbConn->query( $sql ); } - //if ( $params ) - //Warning("SQL: $sql" . implode(',',$params)); - //else - //Warning("SQL: $sql" ); + if ( $params ) + Warning("SQL: $sql" . implode(',',$params)); + else + Warning("SQL: $sql" ); } catch(PDOException $e) { Error( "SQL-ERR '".$e->getMessage()."', statement was '".$sql."' params:" . implode(',',$params) ); } diff --git a/web/includes/functions.php b/web/includes/functions.php index ea9d8de02..ed4cd4f49 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -1057,10 +1057,17 @@ function parseSort( $saveToSession=false, $querySep='&' ) { $sortColumn = 'E.Cause'; break; case 'DateTime' : - $_REQUEST['sort_field'] = 'StartTime'; + $sortColumn = 'E.StartTime'; + break; + case 'DiskSpace' : + $sortColumn = 'E.DiskSpace'; + break; case 'StartTime' : $sortColumn = 'E.StartTime'; break; + case 'EndTime' : + $sortColumn = 'E.EndTime'; + break; case 'Length' : $sortColumn = 'E.Length'; break; @@ -1126,6 +1133,7 @@ function parseFilter( &$filter, $saveToSession=false, $querySep='&' ) { case 'ServerId': $filter['sql'] .= 'M.ServerId'; break; +# Unspecified start or end, so assume start, this is to support legacy filters case 'DateTime': $filter['sql'] .= 'E.StartTime'; break; @@ -1138,8 +1146,35 @@ function parseFilter( &$filter, $saveToSession=false, $querySep='&' ) { case 'Weekday': $filter['sql'] .= 'weekday( E.StartTime )'; break; +# Starting Time + case 'StartDateTime': + $filter['sql'] .= 'E.StartTime'; + break; + case 'StartDate': + $filter['sql'] .= 'to_days( E.StartTime )'; + break; + case 'StartTime': + $filter['sql'] .= 'extract( hour_second from E.StartTime )'; + break; + case 'StartWeekday': + $filter['sql'] .= 'weekday( E.StartTime )'; + break; +# Ending Time + case 'EndDateTime': + $filter['sql'] .= 'E.EndTime'; + break; + case 'EndDate': + $filter['sql'] .= 'to_days( E.EndTime )'; + break; + case 'EndTime': + $filter['sql'] .= 'extract( hour_second from E.EndTime )'; + break; + case 'EndWeekday': + $filter['sql'] .= 'weekday( E.EndTime )'; + break; case 'Id': case 'Name': + case 'DiskSpace': case 'MonitorId': case 'StorageId': case 'Length': @@ -1853,7 +1888,8 @@ function logState() { Logger::WARNING => array( ZM_LOG_ALERT_WAR_COUNT, ZM_LOG_ALARM_WAR_COUNT ), ); - $sql = "select Level, count(Level) as LevelCount from Logs where Level < ".Logger::INFO." and TimeKey > unix_timestamp(now() - interval ".ZM_LOG_CHECK_PERIOD." second) group by Level order by Level asc"; + # This is an expensive request, as it has to hit every row of the Logs Table + $sql = 'SELECT Level, COUNT(Level) AS LevelCount FROM Logs WHERE Level < '.Logger::INFO.' AND TimeKey > unix_timestamp(now() - interval '.ZM_LOG_CHECK_PERIOD.' second) GROUP BY Level ORDER BY Level ASC'; $counts = dbFetchAll( $sql ); foreach ( $counts as $count ) { diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 8783f1ed8..59b5b8ba3 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -116,8 +116,11 @@ $SLANG = array( 'AttrArchiveStatus' => 'Archive Status', 'AttrAvgScore' => 'Avg. Score', 'AttrCause' => 'Cause', - 'AttrDate' => 'Date', - 'AttrDateTime' => 'Date/Time', + 'AttrStartDate' => 'Start Date', + 'AttrEndDate' => 'End Date', + 'AttrStartDateTime' => 'Start Date/Time', + 'AttrEndDateTime' => 'End Date/Time', + 'AttrDiskSpace' => 'Disk Space', 'AttrDiskBlocks' => 'Disk Blocks', 'AttrDiskPercent' => 'Disk Percent', 'AttrDuration' => 'Duration', @@ -132,9 +135,11 @@ $SLANG = array( 'AttrName' => 'Name', 'AttrNotes' => 'Notes', 'AttrSystemLoad' => 'System Load', - 'AttrTime' => 'Time', + 'AttrStartTime' => 'Start Time', + 'AttrEndTime' => 'End Time', 'AttrTotalScore' => 'Total Score', - 'AttrWeekday' => 'Weekday', + 'AttrStartWeekday' => 'Start Weekday', + 'AttrEndWeekday' => 'End Weekday', 'Auto' => 'Auto', 'AutoStopTimeout' => 'Auto Stop Timeout', 'Available' => 'Available', @@ -334,6 +339,7 @@ $SLANG = array( 'Ffmpeg' => 'Ffmpeg', 'File' => 'File', 'FilterArchiveEvents' => 'Archive all matches', + 'FilterUpdateDiskSpace' => 'Update used disk space', 'FilterDeleteEvents' => 'Delete all matches', 'FilterEmailEvents' => 'Email details of all matches', 'FilterExecuteEvents' => 'Execute command on all matches', @@ -413,6 +419,7 @@ $SLANG = array( 'LimitResultsPre' => 'Limit to first', // This is used at the beginning of the phrase 'Limit to first N results only' 'LinkedMonitors' => 'Linked Monitors', 'List' => 'List', + 'ListMatches' => 'List Matches', 'Load' => 'Load', 'Local' => 'Local', 'Log' => 'Log', diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index 9cae836ce..26cb1a26e 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -194,9 +194,19 @@ ZoneMinder requires Javascript. Please enable Javascript in your browser for thi
  • -
  • Logger::NOLOG ) { ?> '.translate('Log').'' ) ?>
  • - - +
  • + Logger::NOLOG ) { + if ( ! ZM_RUN_AUDIT ) { + # zmaudit can clean the logs, but if we aren't running it, then we should clecan them regularly + dbQuery("DELETE FROM Logs WHERE TimeKey < NOW()-to_days('".ZM_LOG_DATABASE_LIMIT."')"); + } + echo makePopupLink( '?view=log', 'zmLog', 'log', ''.translate('Log').'' ); + } +} // end if canview(System) +?>
  • +
  • Devices
  • >
  • diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index 4ba9e2457..679a569d4 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -258,6 +258,17 @@ echo htmlSelect( 'StorageFilter', array(''=>'All')+$StorageById, (isset($_SESSIO + +'All', + 'Unknown' => translate('Unknown'), + 'NotRunning' => translate('NotRunning'), + 'Running' => translate('Running'), + ); +echo htmlSelect( 'StatusFilter', $status_options, ( isset($_SESSION['StatusFilter']) ? $_SESSION['StatusFilter'] : '' ), array('onchange'=>'changeFilter(this);') ); +?> +
    diff --git a/web/skins/classic/views/filter.php b/web/skins/classic/views/filter.php index 33faecbc7..b62d85d4a 100644 --- a/web/skins/classic/views/filter.php +++ b/web/skins/classic/views/filter.php @@ -69,10 +69,14 @@ $attrTypes = array( 'Name' => translate('AttrName'), 'Cause' => translate('AttrCause'), 'Notes' => translate('AttrNotes'), - 'DateTime' => translate('AttrDateTime'), - 'Date' => translate('AttrDate'), - 'Time' => translate('AttrTime'), - 'Weekday' => translate('AttrWeekday'), + 'StartDateTime' => translate('AttrStartDateTime'), + 'StartDate' => translate('AttrStartDate'), + 'StartTime' => translate('AttrStartTime'), + 'StartWeekday' => translate('AttrStartWeekday'), + 'EndDateTime' => translate('AttrEndDateTime'), + 'EndDate' => translate('AttrEndDate'), + 'EndTime' => translate('AttrEndTime'), + 'EndWeekday' => translate('AttrEndWeekday'), 'Length' => translate('AttrDuration'), 'Frames' => translate('AttrFrames'), 'AlarmFrames' => translate('AttrAlarmFrames'), @@ -80,8 +84,9 @@ $attrTypes = array( 'AvgScore' => translate('AttrAvgScore'), 'MaxScore' => translate('AttrMaxScore'), 'Archived' => translate('AttrArchiveStatus'), - 'DiskPercent' => translate('AttrDiskPercent'), 'DiskBlocks' => translate('AttrDiskBlocks'), + 'DiskPercent' => translate('AttrDiskPercent'), + 'DiskSpace' => translate('AttrDiskSpace'), 'SystemLoad' => translate('AttrSystemLoad'), 'StorageId' => translate('AttrStorageArea'), 'ServerId' => translate('AttrServer'), @@ -297,18 +302,19 @@ if ( count($terms) == 0 ) { translate('AttrId'), - 'Name' => translate('AttrName'), - 'Cause' => translate('AttrCause'), - 'Notes' => translate('AttrNotes'), - 'MonitorName' => translate('AttrMonitorName'), - 'DateTime' => translate('AttrDateTime'), - 'Length' => translate('AttrDuration'), - 'Frames' => translate('AttrFrames'), - 'AlarmFrames' => translate('AttrAlarmFrames'), - 'TotScore' => translate('AttrTotalScore'), - 'AvgScore' => translate('AttrAvgScore'), - 'MaxScore' => translate('AttrMaxScore'), + 'Id' => translate('AttrId'), + 'Name' => translate('AttrName'), + 'Cause' => translate('AttrCause'), + 'DiskSpace' => translate('AttrDiskSpace'), + 'Notes' => translate('AttrNotes'), + 'MonitorName' => translate('AttrMonitorName'), + 'StartDateTime' => translate('AttrStartDateTime'), + 'Length' => translate('AttrDuration'), + 'Frames' => translate('AttrFrames'), + 'AlarmFrames' => translate('AttrAlarmFrames'), + 'TotScore' => translate('AttrTotalScore'), + 'AvgScore' => translate('AttrAvgScore'), + 'MaxScore' => translate('AttrMaxScore'), ); echo htmlSelect( 'filter[Query][sort_field]', $sort_fields, $filter->sort_field() ); $sort_dirns = array( @@ -332,6 +338,9 @@ echo htmlSelect( 'filter[Query][sort_asc]', $sort_dirns, $filter->sort_asc() ); AutoArchive()) ) { ?> checked="checked" onclick="updateButtons( this )"/>

    +

    + UpdateDiskSpace()) ? '' : ' checked="checked"' ?> onclick="updateButtons(this);"/> +

    @@ -387,7 +396,7 @@ if ( ZM_OPT_MESSAGE ) {

    - + "; - if ( form.elements['newMonitor[AnalysisFPS]'].value && !(parseFloat(form.elements['newMonitor[AnalysisFPS]'].value) > 0 ) ) + if ( form.elements['newMonitor[AnalysisFPSLimit]'].value && !(parseFloat(form.elements['newMonitor[AnalysisFPSLimit]'].value) > 0 ) ) errors[errors.length] = ""; if ( form.elements['newMonitor[MaxFPS]'].value && !(parseFloat(form.elements['newMonitor[MaxFPS]'].value) > 0 ) ) errors[errors.length] = ""; diff --git a/web/skins/classic/views/js/montagereview.js b/web/skins/classic/views/js/montagereview.js index 305de1574..561f7e6c2 100644 --- a/web/skins/classic/views/js/montagereview.js +++ b/web/skins/classic/views/js/montagereview.js @@ -84,7 +84,11 @@ function imagedone( obj, monId, success ) { if ( ! success ) { // if we had a failrue queue up the no-data image //loadImage2Monitor(monId,"no data"); // leave the staged URL if there is one, just ignore it here. - loadNoData( monId ); + if ( liveMode ) { + writeText( monId, "Camera Offline" ); + } else { + writeText( monId, "No Data" ); + } } else { if ( monitorLoadingStageURL[monId] == "" ) { console.log("Not showing image for " + monId ); diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 42891ac23..340fec26b 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -122,7 +122,7 @@ if ( ! $monitor ) { 'FrameSkip' => 0, 'MotionFrameSkip' => 0, 'EventPrefix' => 'Event-', - 'AnalysisFPS' => '', + 'AnalysisFPSLimit' => '', 'AnalysisUpdateDelay' => 0, 'MaxFPS' => '', 'AlarmMaxFPS' => '', @@ -170,8 +170,8 @@ if ( isset( $_REQUEST['newMonitor'] ) ) { } # What if it has less zeros? This is not robust code. -if ( $monitor->AnalysisFPS() == '0.00' ) - $monitor->AnalysisFPS( '' ); +if ( $monitor->AnalysisFPSLimit() == '0.00' ) + $monitor->AnalysisFPSLimit( '' ); if ( $monitor->MaxFPS() == '0.00' ) $monitor->MaxFPS( '' ); if ( $monitor->AlarmMaxFPS() == '0.00' ) @@ -538,7 +538,7 @@ if ( $tab != 'general' ) { - + - + Type() != 'Local' && $monitor->Type() != 'File' && $monitor->Type() != 'NVSocket' ) { ?>