#!/usr/bin/perl -w
#
# makempeg2.pl
#
# Generate an MPEG-2 movie with the MSSG mpeg2encode utility out of JPEG
# snapshots.  Since the MPEG-2 standard doesn't support less than 23.976
# frames/second, we fudge lower frame rates by specifying each JPEG image
# multiple times.  It's a lousy hack, but it works. :-)
#
# This program makes use of the following utilities:
#
# mpeg2encode
# from the MSSG MPEG-2 Video Codec
# http://www.mpeg.org/MSSG/
#
# identify and convert
# from ImageMagick
# http://www.simplesystems.org/ImageMagick/
#
# Copyright (C) 2000
# All rights reserved.
#
# BackWatcher, Inc.
# Information Security Solutions
# http://www.backwatcher.com/
# support@backwatcher.com
# 813-979-1633
#
# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Change history:
#
# 0.10    12/27/00        Alpha release

require 5.004;
use strict;
use Getopt::Long;
use POSIX qw(locale_h);
use POSIX qw(strftime);

#
# User defined variables
#

my $encode		= "/usr/local/bin/mpeg2encode";		# mpeg2encode binary
my $paramfile	= "mpeg2encode.par";			# mpeg2encode parameter file
my $statfile	= "/dev/null";					# mpeg2encode statistics file
my $ident		= "/usr/local/bin/identify";		# identify binary
my $identopts	= "-ping";						# identify options
my $convert		= "/usr/local/bin/convert";		# convert binary
my $tmp			= "/tmp";						# tmp dir
my $path		= "/bin:/usr/bin:/usr/sbin";	# path
my $locale		= "en_US.ISO-8859-1";			# locale

#
# Options requiring arguments
#

my $src		= '';
my $dst		= '';
my $cam		= '';
my $ips	= '';					# encode 1,2,3,4,6,8,12 and 24 ips @ 24 fps
                   				# encode 5 and 25 ips @ 25 fps
                   				# encode 10,15 and 30 ips @ 30 fps

# Frame rates available in MPEG-1 and the currently defined set of
# Profiles and Levels in MPEG-2 are: 23.976 Hz (3-2 pulldown NTSC),
# 24 Hz (Film), 25 Hz (PAL/SECAM or 625/60 video), 29.97 (NTSC),
# 30 Hz (drop-frame NTSC or component 525/60), 50 Hz (double-rate PAL),
# 59.97 Hz (double rate NTSC), and 60 Hz (double-rate, drop-frame
# NTSC/component 525/60 video). Only 23.976, 24, 25, 29.97, and 30 Hz
# are within the conformance space of Constrained Parameter Bitstreams
# and Main Level.

#
# Main symbol declerations
#

my (
     $fps, $frc, $today, @files, $file, $dir,
     $from, $to,
     $seq, $frame, $count, $modulus,
     $dims, $width, $height, $dstcpy,
     $date, $help, $debug
);

main();
exit;

#
# Main subroutine
#

sub main {

  #
  # Secure the environment
  #

  delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};		# make %ENV safer
  $ENV{PATH} = "$path";							# set PATH

  #
  # Process command line arguments
  #

  getopts();

  #
  # Get local date
  #

  getdate();

  #
  # Generate mpeg2encode parameter file
  #

  genparamfile();

  #
  # Encode movie
  #

  encode();

  #
  # Tidy up
  #

  tidyup();

}

sub getopts {

  #
  # Process arguments
  #

  GetOptions ('src=s' => \$src,
              'dst=s' => \$dst,
              'cam=s' => \$cam,
              'ips=s' => \$ips,
              'date' => \$date,
              'help' => \$help,
              'debug' => \$debug);

  #
  # Ensure that --src, --dst, --cam and --ips were specified
  #

  if (($src eq '') || ($dst eq '') || ($cam eq '') || ($ips eq '')) {
    help();
  }

  #
  # Ensure that the argument to --ips is a valid value
  #

  use integer;							# use integer operations to get modulus

  print "m24 = ", (24 % $ips), " ; m25 = ", (25 % $ips), " ; m30 = ", (30 % $ips), "\n" if ($debug);	# debug info

  if ((30 % $ips) != 0) {
    if ((24 % $ips) != 0) {
      if ((25 % $ips) != 0) {
        print "error: argument to --ips must be on of 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 24, 25 or 30.\n";
        exit;
      }
    }
  }

  no integer;							# don't use integer operations anymore

  print "src = $src\n" if ($debug);				# debug info
  print "dst = $dst\n" if ($debug);				# debug info
  print "cam = $cam\n" if ($debug);				# debug info
  print "ips = $ips\n" if ($debug);				# debug info

}

sub getdate {

  #
  # Get local date
  #

  setlocale(LC_TIME, "$locale");
  $today = ( strftime "%m%d%y", localtime);

  print "today = $today\n" if ($debug);			# debug info

}

sub genparamfile {

  #
  # Generate mpeg2encode parameter file
  #

  open (PARAMS, "> $tmp/$paramfile") || die "cannot open: $!";

  @files = scandir ("$src");					# get source directory file list

  #
  # Disregard matching files that don't end in .jpg
  #

  foreach $file (@files) {
	$_ = $file;
    if (/\.jpg/) {
      # noop!
    } else {
      shift @files;
    }
  }

  getdims();									# get source image dimensions

  #
  # Get fps and frame_rate_code
  #

  use integer;							# use integer operations to get modulus

  if (($ips == 10) || ($ips == 15) || ($ips == 30)) {
    $fps = 30;
    $frc = 5;
  } elsif (($ips == 5) || ($ips == 25)) {
    $fps = 25;
    $frc = 3;
  } else {
    $fps = 24;
    $frc = 2;
  }

  no integer;							# don't use integer operations anymore

  print "fps = $fps\n" if ($debug);				# debug info
  print "frc = $frc\n" if ($debug);				# debug info

  print PARAMS "$cam - $today - MPEG-2 @ $fps fps ($ips ips)\n";

  #
  # Convert .jpg files to .yuv files and fudgs fps
  #

  $seq = 0;										# JPEG image sequence counter
  $frame = 0;									# frame counter

  foreach $file (@files) {

    print "\nseq = $seq\n" if ($debug);			# debug info
    print "file = $file\n" if ($debug);			# debug info

    $from = $file;								# save original filename
    $to = $file;								# set new filename
    $to =~ s/\.jpg/\.yuv/;						# change extension to .yuv
    $to =~ s/_\d+\./_$frame\./;			# replace sequence number with $frame
    use integer;						# use integer operations to get modulus

    $modulus = $frame % ($fps / $ips);			# get modulus for debug info
    print "modulus = $modulus\n" if ($debug);	# debug info

    if (($frame % ($fps / $ips)) == 0) {		# if modulus returns true
      no integer;						# don't use integer operations anymore

      print "\nconverting frame $frame: $from to $to\n\n" if ($debug);	# debug info

      system "$convert", "$src/$from", "$tmp/$to"	# convert jpeg to yuv
        || die "$convert failed: $!\n";
      $frame++;									# increment frame counter

    } else {

      for ($count = 1; $count < ($fps / $ips); $count++) {	# fps fudge
        $to =~ s/_\d+.yuv//;					# remove from sequence number on

        print "symlinking frame $frame: $to", "_", ($frame - $count), ".yuv to $to", "_$frame.yuv\n" if ($debug);	# debug info

        symlink("$tmp/$to" . "_" . ($frame - $count) . ".yuv","$tmp/$to" . "_" . $frame . ".yuv");
        $frame++;								# increment frame counter
      }

      $seq++;									# increment sequence counter
      $from =~ s/_\d+\.jpg/_$seq\.jpg/;			# get last JPEG file name
      unshift (@files, $from);			# push last JPEG file back onto array
    }
  }

  #
  # Generate parameter file
  #

  print PARAMS "$tmp/$cam", "_%d	/* name of source files */\n";
  print PARAMS "-		/* name of reconstructed images ('-': don't store) */\n";
  print PARAMS "-		/* name of intra quant matrix file ('-': default matrix) */\n";
  print PARAMS "-		/* name of non intra quant matrix file ('-': default matrix) */\n";
  print PARAMS "$statfile		/* name of statistics file ('-': stdout ) */\n";

  print PARAMS "1		/* input picture file format: 0=*.Y,*.U,*.V, 1=*.yuv, 2=*.ppm */\n";
  print PARAMS "$frame		/* number of frames */\n";
  print PARAMS "0		/* number of first frame */\n";
  print PARAMS "00:00:00:00	/* timecode of first frame (hh:mm:ss:ff)*/\n";
  print PARAMS "12		/* N (# of frames in GOP) */\n";
  print PARAMS "3		/* M (I/P frame distance) */\n";
  print PARAMS "0		/* ISO/IEC 11172-2 stream (0=MPEG-2, 1=MPEG-1)*/\n";
  print PARAMS "0		/* 0:frame pictures, 1:field pictures */\n";
  print PARAMS "$width		/* horizontal_size */\n";
  print PARAMS "$height		/* vertical_size */\n";
  print PARAMS "2		/* aspect_ratio_information 1=square pel, 2=4:3, 3=16:9, 4=2.11:1 */\n";
  print PARAMS "$frc	/* frame_rate_code 1=23.976, 2=24, 3=25, 4=29.97, 5=30 frames/sec. */\n";
  print PARAMS "1856000	/* bit_rate (bits/s) */\n";
  print PARAMS "32		/* vbv_buffer_size (in multiples of 16 kbit) */\n";
  print PARAMS "0		/* low_delay  (not yet implemented)*/\n";
  print PARAMS "0		/* constrained_parameters_flag (always 0 for MPEG-2)*/\n";
  print PARAMS "4		/* Profile ID: Simple = 5, Main = 4, SNR = 3, Spatial = 2, High = 1 */\n";
  print PARAMS "8		/* Level ID:   Low = 10, Main = 8, High 1440 = 6, High = 4          */\n";
  print PARAMS "1		/* progressive_sequence */\n";
  print PARAMS "1		/* chroma_format: 1=4:2:0, 2=4:2:2, 3=4:4:4 */\n";
  print PARAMS "2		/* video_format: 0=comp., 1=PAL, 2=NTSC, 3=SECAM, 4=MAC, 5=unspec. */\n";
  print PARAMS "5		/* color_primaries */\n";
  print PARAMS "5		/* transfer_characteristics */\n";
  print PARAMS "4		/* matrix_coefficients */\n";
  print PARAMS "$width		/* display_horizontal_size */\n";
  print PARAMS "$height		/* display_vertical_size */\n";
  print PARAMS "2		/* intra_dc_precision (0: 8 bit, 1: 9 bit, 2: 10 bit, 3: 11 bit */\n";
  print PARAMS "0		/* top_field_first (0=bottom, 1=top)*/\n";
  print PARAMS "1 1 1	/* frame_pred_frame_dct (I P B) */\n";
  print PARAMS "0 0 0	/* concealment_motion_vectors (I P B) (not yet implemented)*/\n";
  print PARAMS "1 1 1	/* q_scale_type  (I P B) (0=linear, 1=non-linear)*/\n";
  print PARAMS "1 0 0	/* intra_vlc_format (I P B)*/\n";
  print PARAMS "0 0 0	/* alternate_scan (I P B) */\n";
  print PARAMS "0		/* repeat_first_field */\n";
  print PARAMS "1		/* progressive_frame */\n";
  print PARAMS "0		/* P distance between complete intra slice refresh (not yet implemented)*/\n";
  print PARAMS "0		/* rate control: r (reaction parameter) */\n";
  print PARAMS "0		/* rate control: avg_act (initial average activity) */\n";
  print PARAMS "0		/* rate control: Xi (initial I frame global complexity measure) */\n";
  print PARAMS "0		/* rate control: Xp (initial P frame global complexity measure) */\n";
  print PARAMS "0		/* rate control: Xb (initial B frame global complexity measure) */\n";
  print PARAMS "0		/* rate control: d0i (initial I frame virtual buffer fullness) */\n";
  print PARAMS "0		/* rate control: d0p (initial P frame virtual buffer fullness) */\n";
  print PARAMS "0		/* rate control: d0b (initial B frame virtual buffer fullness) */\n";
  print PARAMS "2 2 11 11	/* P:  forw_hor_f_code forw_vert_f_code search_width/height */\n";
  print PARAMS "1 1 3  3	/* B1: forw_hor_f_code forw_vert_f_code search_width/height */\n";
  print PARAMS "1 1 7  7	/* B1: back_hor_f_code back_vert_f_code search_width/height */\n";
  print PARAMS "1 1 7  7	/* B2: forw_hor_f_code forw_vert_f_code search_width/height */\n";
  print PARAMS "1 1 3  3	/* B2: back_hor_f_code back_vert_f_code search_width/height */\n";

  close PARAMS;

}

sub scandir {

  #
  # Scan directory for image files
  #

  $dir = $_[0];
  opendir(DIR, $dir) || die "can't opendir $dir: $!";
  @files = grep { /$cam/ && -f "$dir/$_" } readdir(DIR);
  closedir DIR;
  return @files;

}

sub encode {

  #
  # Encode movie
  #

  $dstcpy = $dst if ($date);					# copy original destination
  $dstcpy =~ s/\/.*\/// if ($date);				# get destination file
  $dst = ($dst . $today . "_" . $dstcpy) if ($date);	# add datestamped file
  $dst =~ s/$dstcpy// if ($date);				# strip original file

  print "dst = $dst\n" if ($debug);				# debug info

  system "$encode", "$tmp/$paramfile", "$dst"
    || die "$encode failed: $!\n";

}

sub getdims {

  #
  # Get source image dimensions
  #

  chomp($dims = qx($ident $identopts $src/$files[0]))
    || die "$ident failed: $!\n";

  $dims =~ s/^.*? //;
  $dims =~ s/ .*$//;

  ($width, $height) = split(/x/, $dims);

  print "\$files[0] = $files[0]\n" if ($debug);	# debug info
  print "dims = $dims\n" if ($debug);			# debug info
  print "width = $width\n" if ($debug);			# debug info
  print "height = $height\n" if ($debug);		# debug info

}

sub tidyup {

  #
  # Be tidy
  #

  unlink "$tmp/$paramfile" if !($debug);	# remove paramater file if no debug	

  @files = scandir ("$tmp");
  foreach $file (@files) {				# remove .yuv files from tmp directory
    unlink "$tmp/$file";
  }

}

sub help {
  print "usage: makempeg2.pl --src sourcedir --dst destfile --cam camstring\n";
  print "	      --ips {1-4,5-6,8,10,12,15,24,25,30} [--date] [--help] [--debug]\n\n";
  print "	--src		source directory\n";
  print "	--dst		destination file\n";
  print "	--cam		string to match for input file selection\n";
  print "	--ips		images/second source JPEG files generated at\n";
  print "	--date		date stamp destination file\n";
  print "	--help		display this help\n";
  print "	--debug		print debugging information\n\n";
  print "  makempeg2.pl makes MPEG-2 movies from the JPEG snapshots of Axis\n";
  print "  network cameras.\n\n";
  print "  Example:\n\n";
  print "  makempeg2.pl --src /some/dir --dst /tmp/movie.mp2v --cam cam1 --ips 4 --date\n\n";
  print "  would make an MPEG-2 movie in file, /tmp/mmddyy_movie.mp2v, out of\n";
  print "  JPEG files beginning with the string, cam1, in directory, /some/dir,\n";
  print "  where the source JPEG files were generated at 4 images/second.\n";
  exit;
}
