mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-04 00:15:20 -04:00
The scripts run under -T, so they cannot trust the caller's PATH and set their own. Sixteen of them hardcoded /bin:/usr/bin:/usr/local/bin, which assumes everything they shell out to lives under /usr or /usr/local. ZoneMinder::General::findDbCommand looks for a database client on that PATH, and zmupdate.pl and zmcamtool.pl run what it finds. So an install whose client sits anywhere else cannot apply schema changes: sh: mysql: command not found Command 'mysql -u'zmuser' ... ' exited with status: 127 even with the client on the caller's PATH. Homebrew on Apple Silicon is the case that surfaced it - the client is in /opt/homebrew/bin - but a --prefix=/opt install on Linux has the same shape, as does anything that keeps its database client outside the FHS locations. Replaced the literal with @ZM_SCRIPT_PATH@, defaulting to the same three directories plus wherever cmake actually found a client, and overridable for packagers who want to pin it. Warn at configure time when no client is found at all, since that failure otherwise appears much later and says something unrelated. Nothing changes for an install whose client is already under /usr/bin: the directory is only appended when it is not in the list, so the default stays exactly as it was. Memory.pm is deliberately left alone. It has a narrower PATH of /bin:/usr/bin, and the only command it runs is uname through an absolute path from ZM_PATH_UNAME, so it does not need widening. Verified on macOS across the three cases: with the client in /opt/homebrew/bin the directory is appended; -DZM_SCRIPT_PATH= is respected verbatim; and pointing detection at /usr/bin/mariadb leaves the default untouched. Build clean, suite 146 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5KL9Xbi7K5aGsauLtd8tG
272 lines
8.6 KiB
Plaintext
272 lines
8.6 KiB
Plaintext
#!@PERL_EXECUTABLE@ -wT
|
|
#
|
|
# ==========================================================================
|
|
#
|
|
# ZoneMinder Video Creation Script, $Date$, $Revision$
|
|
# Copyright (C) 2001-2008 Philip Coombes
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
# ==========================================================================
|
|
|
|
=head1 NAME
|
|
|
|
zmvideo.pl - ZoneMinder Video Creation Script
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
zmvideo.pl [ -e <event_id>,--event=<event_id> | --filter_name=<filter name> | --filter_id=<filter id> ]
|
|
[--concat=filename]
|
|
[--format <format>]
|
|
[--rate=<rate>]
|
|
[--scale=<scale>]
|
|
[--fps=<fps>]
|
|
[--size=<size>]
|
|
[--overwrite]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This script is used to create MPEG videos of events for the web pages
|
|
or as email attachments.
|
|
|
|
=head1 OPTIONS
|
|
-c[=filename], --concat[=filename] - When creating videos for multiple events, create a concatenated video as well.
|
|
- If not specified, filename is taken from filter name.
|
|
-e<event_id>, --event=<event_id> - What event to create the video for
|
|
--filter_name=<filter name> - The name of a saved filter to generate a video for all events returned by it.
|
|
--filter_id=<filter id> - The id of a saved filter to generate a video for all events returned by it.
|
|
-f<format>, --format=<format> - What format to create the video in, default is mpg. For ffmpeg only.
|
|
-r<rate>, --rate=<rate> - Relative rate, 1 = realtime, 2 = double speed, 0.5 = half speed etc.
|
|
-s<scale>, --scale=<scale> - Scale, 1 = normal, 2 = double size, 0.5 = half size etc.
|
|
-t<transforms>, --transform=<transforms> - Transform video. Applies -vf option to ffmpeg.
|
|
-F<fps>, --fps=<fps> - Absolute frame rate, in frames per second
|
|
-S<size>, --size=<size> - Absolute video size, WxH or other specification supported by ffmpeg
|
|
-o, --overwrite - Whether to overwrite an existing file, off by default.
|
|
-v, --version - Outputs the currently installed version of ZoneMinder
|
|
|
|
=cut
|
|
use strict;
|
|
use bytes;
|
|
|
|
# ==========================================================================
|
|
#
|
|
# You shouldn't need to change anything from here downwards
|
|
#
|
|
# ==========================================================================
|
|
|
|
@EXTRA_PERL_LIB@
|
|
use ZoneMinder;
|
|
require ZoneMinder::Filter;
|
|
require ZoneMinder::Event;
|
|
use DBI;
|
|
use autouse 'Data::Dumper'=>qw(Dumper);
|
|
use POSIX qw(strftime);
|
|
use Getopt::Long qw(:config no_ignore_case );
|
|
use Cwd;
|
|
use File::Temp qw(tempfile);
|
|
use autouse 'Pod::Usage'=>qw(pod2usage);
|
|
|
|
$| = 1;
|
|
|
|
$ENV{PATH} = '@ZM_SCRIPT_PATH@';
|
|
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
|
|
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
|
|
|
|
logInit();
|
|
|
|
my $event_id;
|
|
my $concat_name;
|
|
my $filter_name;
|
|
my $filter_id;
|
|
my $format = 'mpg';
|
|
my $rate = '';
|
|
my $scale = '';
|
|
my $fps = '';
|
|
my $size = '';
|
|
my $transforms = '';
|
|
my $overwrite = 0;
|
|
my $version = 0;
|
|
|
|
GetOptions(
|
|
'concat|c:s' =>\$concat_name,
|
|
'event|e=i' =>\$event_id,
|
|
'filter_name=s' =>\$filter_name,
|
|
'filter_id=i' =>\$filter_id,
|
|
'format|f=s' =>\$format,
|
|
'rate|r=f' =>\$rate,
|
|
'scale|s=f' =>\$scale,
|
|
'fps|F=f' =>\$fps,
|
|
'size|S=s' =>\$size,
|
|
'transforms|t=s'=>\$transforms,
|
|
'overwrite' =>\$overwrite,
|
|
'version' =>\$version
|
|
) or pod2usage(-exitstatus => -1);
|
|
|
|
if ( $version ) {
|
|
print ZoneMinder::Base::ZM_VERSION . "\n";
|
|
exit(0);
|
|
}
|
|
|
|
if ( !( $filter_id or $filter_name or $event_id ) || ($event_id and ( $event_id < 0 ) ) ) {
|
|
print( STDERR "Please give a valid event id or filter name\n" );
|
|
pod2usage(-exitstatus => -1);
|
|
}
|
|
|
|
if ( ! $Config{ZM_OPT_FFMPEG} ) {
|
|
print( STDERR "Mpeg encoding is not currently enabled\n" );
|
|
exit(-1);
|
|
}
|
|
|
|
my @formats = split( /\s+/, $Config{ZM_FFMPEG_FORMATS} );
|
|
for ( my $i = 0; $i < @formats; $i++ ) {
|
|
if ( $i =~ /^(.+)\*$/ ) {
|
|
$format = $formats[$i] = $1;
|
|
}
|
|
}
|
|
|
|
if ( !$rate && !$fps ) {
|
|
$rate = 1;
|
|
}
|
|
|
|
if ( !$scale && !$size ) {
|
|
$scale = 1;
|
|
}
|
|
|
|
if ( $rate && ($rate < 0.25 || $rate > 100) ) {
|
|
print( STDERR "Rate is out of range, 0.25 >= rate <= 100\n" );
|
|
pod2usage(-exitstatus => -1);
|
|
}
|
|
|
|
if ( $scale && ($scale < 0.25 || $scale > 4) ) {
|
|
print( STDERR "Scale is out of range, 0.25 >= scale <= 4\n" );
|
|
pod2usage(-exitstatus => -1);
|
|
}
|
|
|
|
if ( $fps && ($fps > 30) ) {
|
|
print( STDERR "FPS is out of range, <= 30\n" );
|
|
pod2usage(-exitstatus => -1);
|
|
}
|
|
|
|
my ( $detaint_format ) = $format =~ /^(\w+)$/;
|
|
my ( $detaint_rate ) = $rate =~ /^(-?\d+(?:\.\d+)?)$/;
|
|
my ( $detaint_scale ) = $scale =~ /^(-?\d+(?:\.\d+)?)$/;
|
|
my ( $detaint_fps ) = $fps =~ /^(-?\d+(?:\.\d+)?)$/;
|
|
my ( $detaint_size ) = $size =~ /^(\w+)$/;
|
|
|
|
$format = $detaint_format;
|
|
$rate = $detaint_rate;
|
|
$scale = $detaint_scale;
|
|
$fps = $detaint_fps;
|
|
$size = $detaint_size;
|
|
($transforms) = $transforms =~ /^([,\w=]+)$/;
|
|
|
|
my $dbh = zmDbConnect();
|
|
|
|
my $cwd = getcwd;
|
|
|
|
my $video_name;
|
|
my @event_ids;
|
|
|
|
# Fail if the path to a valid ffmpeg binary is not set
|
|
if ( ! -x $Config{ZM_PATH_FFMPEG} ) {
|
|
Fatal("Ffmpeg binary not found or not executable. Verify ZM_PATH_FFMPEG points to ffmpeg, avconv, or a compatible binary.");
|
|
}
|
|
|
|
if ( $event_id ) {
|
|
@event_ids = ( $event_id );
|
|
|
|
} elsif ( $filter_name or $filter_id ) {
|
|
my $Filter = ZoneMinder::Filter->find_one(
|
|
($filter_name ? ( Name => $filter_name ) : () ),
|
|
($filter_id ? ( Id => $filter_name ) : () ),
|
|
);
|
|
if ( ! $Filter ) {
|
|
Fatal("Filter $filter_name $filter_id not found.");
|
|
}
|
|
@event_ids = map { $_->{Id} } $Filter->Execute();
|
|
if (!@event_ids) {
|
|
Fatal("No events found for $filter_name")
|
|
} else {
|
|
Debug(@event_ids . " events found for $filter_name");
|
|
}
|
|
$concat_name = $filter_name if $concat_name eq '';
|
|
}
|
|
|
|
my $sql = " SELECT (SELECT max(Delta) FROM Frames WHERE EventId=Events.Id)-(SELECT min(Delta) FROM Frames WHERE EventId=Events.Id) as FullLength,
|
|
Events.*,
|
|
unix_timestamp(Events.StartDateTime) as Time,
|
|
M.Name as MonitorName,
|
|
M.Palette
|
|
FROM Events
|
|
INNER JOIN Monitors as M on Events.MonitorId = M.Id
|
|
WHERE Events.Id = ?
|
|
";
|
|
my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
|
|
Debug($sql);
|
|
|
|
my @video_files;
|
|
foreach my $event_id ( @event_ids ) {
|
|
my $res = $sth->execute( $event_id )
|
|
or Fatal( "Can't execute: ".$sth->errstr() );
|
|
my $event = $sth->fetchrow_hashref();
|
|
|
|
my $Event = new ZoneMinder::Event( $$event{Id}, $event );
|
|
my $video_file = $Event->GenerateVideo( $rate, $fps, $scale, $size, $overwrite, $format, $transforms );
|
|
if ( $video_file ) {
|
|
push @video_files, $video_file;
|
|
print( STDOUT $video_file."\n" );
|
|
} else {
|
|
Warning("No video file generated for event $event_id");
|
|
}
|
|
} # end foreach event_id
|
|
|
|
if ( $concat_name ) {
|
|
($cwd) = $cwd =~ /(.*)/; # detaint
|
|
chdir( $cwd );
|
|
($concat_name ) = $concat_name =~ /([\-A-Za-z0-9_\.]*)/;
|
|
|
|
my $video_file = $concat_name . '.'. $detaint_format;
|
|
|
|
# Create the ffmpeg concat list with a randomized name (atomic O_EXCL) inside
|
|
# ZoneMinder's own temp dir, rather than a predictable /tmp/<name>.concat.lst
|
|
# in world-writable /tmp, which was open to a symlink/race and leaked
|
|
# monitor/event names. The list entries are absolute paths, so its location
|
|
# does not affect ffmpeg's resolution.
|
|
my ($fd, $concat_list_file) = tempfile('zmconcat-XXXXXX', SUFFIX => '.concat.lst', DIR => '@ZM_TMPDIR@');
|
|
foreach ( @video_files ) {
|
|
print $fd "file '$_'\n";
|
|
}
|
|
close $fd;
|
|
my $command = $Config{ZM_PATH_FFMPEG}
|
|
. " -f concat -safe 0 -i $concat_list_file -c copy "
|
|
.$Config{ZM_FFMPEG_OUTPUT_OPTIONS}
|
|
." '$video_file' > $Config{ZM_PATH_LOGS}/ffmpeg_${concat_name}.log 2>&1"
|
|
;
|
|
Debug( $command."\n" );
|
|
my $output = qx($command);
|
|
|
|
my $status = $? >> 8;
|
|
|
|
unlink $concat_list_file;
|
|
if ( $status ) {
|
|
Error( "Unable to generate video, check $Config{ZM_PATH_LOGS}/ffmpeg_${concat_name}.log for details");
|
|
exit(-1);
|
|
}
|
|
print( STDOUT $video_file."\n" );
|
|
}
|
|
exit( 0 );
|
|
|
|
__END__
|