#!/usr/bin/perl

# (c) G. Finch (salsaman+lives@gmail.com) 2009 - 2018

# released under the GNU GPL 3 or later
# see file COPYING or www.gnu.org for details

#######################################################################
# LiVES ffmpeg plugin v2.10
# v 2.1 - add experimental 3gp support
# v 2.2 - format changed in ffmpeg from image to image2, update
# v 2.3 add youtube and flv formats
# v 2.4 allow encoding of .png images
# v 2.5 add threading support and experimental webm encoding
# v 2.6 made threading support optional because of instability problems; versioning string changed in ffmpeg
# v 2.7 fixed webm encoding support, removed asf format
# v 2.8 always prompt for threads
# v 2.9 check for avconv, change -quality to -qscale
# v 2.10 move -meta options after png/audio
# v 2.11 change fussy order of -pass option
# v 2.12 add h264/aac/mp4, wmv2, ffv1 and mjpeg; show missing libraries
# v 2.13 add qtrel, add quality settings
# v 2.14 change qscale to q:v and b to b:v
# v 2.15 code cleanup and change caller mechanism
# v 2.16 code cleanup, fix libtheora
#######################################################################

my $USE_STRICT = 1;
if ($USE_STRICT) {
    use strict;
}

my $USE_WARNINGS = 1;
if ($USE_WARNINGS) {
    use warnings;
}

use POSIX;     # Needed for setlocale()
setlocale(LC_NUMERIC, "C");

my $nulfile;
my $exe;

if ($^O eq "MSWin32") {
    $nulfile = "NUL";
    $exe = ".exe";
} else {
    $nulfile = "/dev/null";
    $exe = "";
}

if (!caller) {
    our $command;
    #our $otype;
    exit 0 if (!defined($ARGV[0]));
}

if (!defined($command)) {
    $command  = $ARGV[0];
}

if ($command eq "version") {
    print "ffmpeg encoder plugin v2.15\n";
    exit 0;
}

if ($command eq "init") {
    # perform any initialisation needed
    # On error, print error message and exit 1
    # otherwise exit 0
    
    if (&location("ffmpeg") eq "" && &location("avconv") eq "") {
        print "The ffmpeg or avconv binary was not found, please install it and try again !";
	exit 1;
    }

    ##############################
    print "initialised\n";
    exit 0;
}

if ($command eq "get_capabilities") {
    # return capabilities - this is a bitmap field
    # bit 0 - takes extra parameters (using RFX request)
    # bit 1 - unused
    # bit 2 - can encode png
    # bit 3 - not pure perl
    print "5\n";
    exit 0;
}

if ($command eq "get_format_request") {
    # return the code for how we would like audio and video delivered
    # this is a bitmap field composed of:
    # bit 0 - unset=raw pcm audio; set=pcm audio with wav header
    # bit 1 - unset=all audio; set=clipped audio
    # bit 2 - unset=all frames; set=frames for selection only

    print 7; # clipped pcm wav, frames start at selection
}

if ($command eq "get_formats") {
   # for each format: -
   # return format_name|display_name|audio_types|restrictions|default_extension|

   # audio types are: 0 - cannot encode audio, 1 - can encode using
   #  mp3, 2 - can encode using pcm, 3 - can encode using pcm and mp3
   # 4 - mp2, 8 - vorbis, 16 - AC3, 32 - AAC, 64 - AMR-NB

   # restrictions: 'fps=xx.yy' which
   # means "can only encode at xx.yy frames per second", size=x X y, arate=audio rate 
    # - otherwise set it to 'none'

    my $tool = "ffmpeg";

    if (&location("ffmpeg") eq "") {
	$tool = "avconv";
    }

    my $has_mpeg4 = (system("$tool -encoders 2>/dev/null | grep \" mpeg4 \" >/dev/null") == 0);
    my $has_theora = (system("$tool -encoders 2>/dev/null | grep \" libtheora \" >/dev/null") == 0);
    my $has_webm = (system("$tool -encoders 2>/dev/null | grep vpx >/dev/null") == 0);
    my $has_qtrle = (system("$tool -encoders 2>/dev/null | grep qtrle >/dev/null") == 0);
    my $has_wmv2 = (system("$tool -encoders 2>/dev/null | grep wmv2 >/dev/null") == 0);
    my $has_ffv1 = (system("$tool -encoders 2>/dev/null | grep ffv1 >/dev/null") == 0);
    my $has_mjpeg = (system("$tool -encoders 2>/dev/null | grep mjpeg >/dev/null") == 0);
    my $has_x264 = (system("$tool -encoders 2>/dev/null | grep x264 >/dev/null") == 0);
    my $has_flv = (system("$tool -encoders 2>/dev/null | grep flv >/dev/null") == 0);
    my $has_3gp = (system("$tool -encoders 2>/dev/null | grep h263 >/dev/null") == 0);

    my $has_aac = (system("$tool -encoders 2>/dev/null | grep aac >/dev/null") == 0);
    my $has_vorbis = (system("$tool -encoders 2>/dev/null | grep vorbis >/dev/null") == 0);
    my $has_opus = (system("$tool -encoders 2>/dev/null | grep opus >/dev/null") == 0);
    my $has_amr = (system("$tool -encoders 2>/dev/null | grep amr >/dev/null") == 0);
    my $has_mp3 = (system("$tool -encoders 2>/dev/null | grep mp3 >/dev/null") == 0);
    my $has_mp2 = (system("$tool -encoders 2>/dev/null | grep mp2 >/dev/null") == 0);
    my $has_wmav2 = (system("$tool -encoders 2>/dev/null | grep wmav2 >/dev/null") == 0);

    my $errwebm = my $errx264 = my $errflv = my $err3gp = my $errwmv2 = my $errffv1 =
	my $errmjpeg = my $errqtrle = my $errmpeg4 = my $errtheora = "";
    my $erraac = my $errvorbis = my $erropus = my $erramraac = my $errmp3aac = my $errmp3mp2 = my $errwmav2 =
	my $erropus = "";
    
    unless ($has_mpeg4) {
	$errmpeg4 = " (MISSING mpeg4 support) ";
    }
    unless ($has_theora) {
	$errtheora = " (MISSING libtheora support) ";
    }
    unless ($has_webm) {
	$errwebm = " (MISSING libvxp support) ";
    }
    unless ($has_wmv2) {
	$errwmv2 = " (MISSING wmv2 support) ";
    }
    unless ($has_ffv1) {
	$errffv1 = " (MISSING ffv1 support) ";
    }
    unless ($has_mjpeg) {
	$errmjpeg = " (MISSING mjpeg support) ";
    }
    unless ($has_x264) {
	$errx264 = " (MISSING libx264 support) ";
    }
    unless ($has_flv) {
	$errflv = " (MISSING flv support) ";
    }
    unless ($has_qtrle) {
	$errflv = " (MISSING QuickTime support) ";
    }
    unless ($has_3gp) {
	$err3gp = " (MISSING h263 support) ";
    }
    unless ($has_aac) {
	$erraac = " (MISSING aac support) ";
    }
    unless ($has_vorbis) {
	$errvorbis = " (MISSING libvorbis support) ";
    }
    unless ($has_opus) {
	$erropus = " (MISSING libopus support) ";
    }
    unless ($has_wmav2) {
	$errwmav2 = " (MISSING wmav2 support) ";
    }
    unless ($has_amr || $has_aac) {
	$erramraac = " (MISSING amr or aac support) ";
    }
    unless ($has_mp3 || $has_aac) {
	$errmp3aac = " (MISSING mp3 or aac support) ";
    }
    unless ($has_mp3 || $has_mp2) {
	$errmp3mp2 = " (MISSING mp3 or mp2 audio support) ";
    }
    
    print "x264|h264/aac/mp4 $errx264$erraac|32|vstep=2,hstep=2|mp4|frame=|\n";
    print "theora|ogg/theora/vorbis $errtheora$errvorbis|8|none|ogg|frame=|\n";

    print "webm|webm/vp9/opus $errwebm$erropus|512|none|webm|frame=|\n";

    print "qtrle|QuickTime animation$errqtrle|0|none|mov|frame=|\n";
    
    print "flv|flv$errflv$errmp3aac|33|arate=44100;22050;11025|flv|frame=|\n";
    print "flv-youtubex|flv (optimised for youtube)$errflv$errmp3aac|33|arate=44100;22050;11025,aspect=1.3333:1|flv|frame=|\n";

    print "divx|divx/avi (25 fps)$errmpeg4$errmp3mp2|5|fps=25.00|avi|frame=|\n";

    print "wmv2|wmv2/wma2/asf (low quality)$errwmv2$errwmav2|256|arate=44100;22050;11025|wmv|frame=|\n";
    print "ffv1|ffv1 (lossless)$errffv1|2|none|avi|frame=|\n";
    print "mjpeg|mjpg$errmjpeg|2|none|avi|frame=|\n";

    print "3gp_h263|3gp (h263)$err3gp$erramraac|96|size=176x144,arate=8000|3gp|frame=|\n";
    print "3gp_mp4|3gp (mp4)$err3gp$erramraac|96|size=176x144,arate=8000|3gp|frame=|\n";
    exit 0;
}


if ($command eq "get_rfx") {
    if (!defined($ARGV[1])) {
	print STDERR "ffmpeg_encoder: missing otype\n";
	exit 1;
    }

    if (!defined($ARGV[2])) {
	print STDERR "ffmpeg_encoder: missing atype\n";
	exit 1;
    }

    if (!defined($ARGV[3])) {
	print STDERR "ffmpeg_encoder: missing hsize\n";
	exit 1;
    }

    if (!defined($ARGV[4])) {
	print STDERR "ffmpeg_encoder: missing vsize\n";
	exit 1;
    }

    my $otype = $ARGV[1];
    my $aq = $ARGV[2];
    my $hsize = $ARGV[3];
    my $vsize = $ARGV[4];

    if ($otype eq "flv" || $otype eq "flv-youtube" || $otype eq "flv-youtubex" || $otype eq "divx") {
	if ($aq == 0) {
	    ## mp3 codec
	    # mandatory section
	    print "<define>\n";
	    print "|1.7\n";
	    print "</define>\n";

	    # optional section
	    print "<params>\n";

	    if ($otype eq "divx") {
		print "quality|Video Quality (1 = Best quality, largest file, 31 = Lowest quality, smallest file)|num0|1|3|31|\n";
		print "quality|Audio Quality (1 = Best quality, largest file, 31 = Lowest quality, smallest file)|num0|1|4|31|\n";
	    }
	    print "mp3|Use _mp3 audio codec|bool|1|1\n";
	    print "mp3lame|Use mp3_lame audio codec|bool|0|1\n";
	    print "</params>\n";
	}
	else {
	    # mandatory section
	    print "<define>\n";
	    print "|1.7\n";
	    print "</define>\n";
	    
	    # optional section
	    print "<params>\n";
	    if ($otype eq "divx") {
		print "quality|Video Quality (1 = Best quality, largest file, 31 = Lowest quality, smallest file)|num0|1|3|31|\n";
		print "quality|Audio Quality (1 = Best quality, largest file, 31 = Lowest quality, smallest file)|num0|1|4|31|\n";
	    }
	    print "</params>\n";

	    print "<param_window>\n";
	    print "layout|p0|\n";
	    print "</param_window>\n";
	}
    }
    else {
	# mandatory section
	print "<define>\n";
	print "|1.7\n";
	print "</define>\n";
	
	# optional section
	print "<params>\n";

	if ($otype eq "x264" || $otype eq "qtrle") {
	    print "quality|Quality (1 = higher quality, larger files, 31 = lowest quality, smallest files, " .
		"0 = Ultra quality, slow)|num0|1|0|31|\n";
	}
	elsif ($otype eq "theora") {
	    print "quality|Quality (1 = highest quality, largest files; 10 = lowest quality, smallest files|num0|4|1|10|\n";
	    print "yuv444|Use YUV444P format (checking this may result in higher quality, " .
		"but may not play on all players)|bool|0|0|\n";
	}
	if ($otype eq "x264") {
	    if ($USE_WARNINGS) {
		no warnings 'once';
	    }
	    print "opwidth|Width|num0|$hsize|4|7680|\n";
	    print "opheight|Height|num0|$vsize|4|4096|\n";
	    print "yuv444|Use YUV444P format (checking this may result in higher quality, " .
		"but may not play on all players)|bool|0|0|\n";
	    print "blacke|Black enhancement (recommended for DARK clips only)|bool|0|0|\n";
	    if ($USE_WARNINGS) {
		use warnings 'once';
	    }
	}
	print "</params>\n";

	print "<param_window>\n";
	if ($otype eq "x264") {
	    print "layout|\"Override output frame size\"|p1|p2|\n";
	}
	print "</param_window>\n";
    }
    exit 0;
}


if ($command eq "encode") {
    ## need our $otype, our $audiofile
    my $exe;
    my $pass;
    my $passf;
    my $fwidth = my $fheight = 0;
    my $syscom;
    my $metadata;
    my $format;
    my $aquality;
    my $usemp3;
    my $usemp3lame;
    my $use444 = 0;
    my $ultra = 0;
    my $blacke = 0;

    # test first for avconv; otherwise Ubuntu complains
    my $encoder_command = "avconv$exe";
    my $line;
    my @all;
    my $fh;

    open ($fh, "$encoder_command -version 2>$nulfile |");
    while (defined($line = <$fh>)) {
	unless ($line eq "") {
	    push (@all, $line);
	}
    }
    close $fh;

    if (scalar @all == 0) {
	$encoder_command = "ffmpeg$exe";
	open ($fh, "$encoder_command -version 2>$nulfile |");
	while (defined($line = <$fh>)) {
	    push (@all, $line);
	}
	close $fh;
    }
    @all = grep(/libavformat/, @all);
    @all = grep(!/configuration/, @all);

    print STDERR "ffmpeg_encoder: libavformat version string is $all[0]\n";

    $ffver = (split(/\./, $all[0]))[0];
    $ffver = (split(" ", $ffver))[-1];

    print STDERR "ffmpeg_encoder: libavformat detected as $ffver\n";

    if ($otype eq "x264" || $otype eq "qtrle" || $otype eq "divx") {
	$quality = $ARGV[13];
	if ($otype eq "divx") {
	    $aquality = $ARGV[14];
	    $usemp3 = $ARGV[15];
	    $usemp3lame = $ARGV[16];
	}
	elsif ($otype eq "x264") {
	    $fwidth = $ARGV[14];
	    $fheight = $ARGV[15];
	    $use444 = $ARGV[16];
	    $blacke = $ARGV[17];
	    if ($quality == 0) {
		$quality = 1;
		$ultra = 1;
	    }}}
    else {
	$usemp3 = $ARGV[15];
	$usemp3lame = $ARGV[16];
    }
    
    my $vid_length = ($end - $start + 1) / $fps;

    $DEBUG_ENCODERS = 1;
    my $err = ">/dev/null 2>&1";
    if (defined($DEBUG_ENCODERS)) {
	$err = "1>&2";
    }

    if ($otype eq "") {
	$otype = "divx";
    }

    my $bug1 = "";

    if ($ffver >= 57) {
	$bug1 = "-auto-alt-ref 0";
    }
    
    # default seems to be divx
    my $vcodec = "";
    
    if ($otype eq "asf") {
	$vcodec = "-f asf";
    }
    elsif ($otype eq "3gp_h263") {
	$vcodec = "-f h263";
    }
    elsif ($otype eq "3gp_mp4") {
	$vcodec = "-f mp4";
    }
    elsif ($otype eq "wmv2") {
	$vcodec = "-c:v wmv2";
    }
    elsif ($otype eq "divx") {
	$vcodec = "-c:v mpeg4 -vtag divx -q:v $quality";
    }
    elsif ($otype eq "ffv1") {
	if ($img_ext eq ".png") {
	    $format = "bgr24";
	}
	else {
	    #jpeg
	    $format = "yuvj420p";
	}
	$vcodec = "-c:v ffv1 -pix_fmt $format";
    }
    elsif ($otype eq "mjpeg") {
	$vcodec = "-c:v mjpeg";
    }
    elsif ($otype eq "flv" || $otype eq "flv-youtubex") {
	$vcodec = "-f flv";
    }
    elsif ($otype eq "webm") {
	$vcodec = "-f webm -c:v libvpx -g 60 -q:v 1 -b:v 1G $bug1";
    }
    elsif ($otype eq "theora") {
	$quality = $ARGV[13];
	$vcodec = "-c:v libtheora -q:v $quality";
	$use444 = $ARGV[14];
	if ($use444) {
	    $vcodec .= " -pix_fmt yuv444p";
	}
    }
    elsif ($otype eq "x264") {
	$vcodec = "-c:v libx264 -q:v $quality -s $fwidth"."X$fheight";
	if ($use444) {
	    $vcodec .= " -pix_fmt yuvj444p -profile high444";
	}
	else {
	    $vcodec .= " -pix_fmt yuv420p -profile main";
	}
	if ($blacke) {
	    $vcodec .= " -aq-mode 2";
	}
	if ($ultra) {
	    $vcodec .= " -preset placebo";
	}
	else {
	    $vcodec .= " -preset medium";
	}
    }
    elsif ($otype eq "qtrle") { 
	$vcodec = "-c:v qtrle -q:v $quality";
    }

    if ($otype eq "flv-youtubex") {
	$vcodec .= " -b:v 1024k -bt 256k -maxrate 1024k -minrate 1024k -bufsize 8192k";
    }

    # video stream
    my $audio_com = "";

    unless ($audiofile eq "") {
	$audio_com = "-i $audiofile";
	if ($aq == 1) {
	    # pcm
	    $audio_com .= " -acodec copy";
	}
	elsif ($aq == 5) {
	    # aac
	    $audio_com .= " -acodec aac -strict experimental -channels $achans";
	}
	elsif ($aq == 6) {
	    # amr_mb
	    $audio_com .= " -acodec amr_nb";
	}
	elsif ($aq == 8) {
	    # wma2
	    $audio_com .= " -acodec wmav2";
	}
	elsif ($aq == 0) {
	    # mp3
	    if ($usemp3lame) {
		$audio_com .= " -acodec libmp3lame";
	    }
	    else {
		$audio_com .= " -acodec mp3";
	    }
	}
	elsif ($aq == 9) {
	    # opus
	    $audio_com .= " -acodec libopus";
	    if ($otype eq "webmh") {
		$audio_com .= " -aq 10";
	    }
	    elsif ($otype eq "webmm") {
		$audio_com .= " -aq 5";
	    }
	    elsif ($otype eq "webml") {
		$audio_com .= " -aq 1";
	    }
	}
	elsif ($aq == 3) {
	    #vorbis
	    $audio_com .= " -acodec libvorbis";
	}
	else {
	    # mp2
	    $audio_com .= " -acodec mp2";
	}
	if ($otype eq "flv-youtubex") {
	    $audio_com .= " -ab 128k -ar $arate";
	}
	elsif ($otype eq "flv") {
	    $audio_com .= " -ab 192k -ar $arate";
	}
	elsif ($otype eq "3gp_h263" || $otype eq "3gp_mp4") {
	    $audio_com .= " -ac 1 -ab 12 -ar 8000";
	}
	elsif ($otype eq "wmv2") {
	    $audio_com .= " -acodec wmav2";
	}
	elsif ($otype eq "divx") {
	    $audio_com .= " -q:a $aquality";
	}
    }

    if ($ffver >= 52) {
	$metadata = "-metadata comment=\"$comment\" -metadata author=\"$author\" -metadata title=\"$title\"";
    } else {
	$metadata = "-comment \"$comment\" -author \"$author\" -title \"$title\"";
    }
    
    if ($otype eq "3gp") {
	for $pass (1,2) {
	    $passf = "-pass $pass -passlogfile passfile";
	    $syscom = "$encoder_command -strict 1 -y -f image2 -i %8d$img_ext $audio_com -t $vid_length " .
		"$vcodec $metadata $passf -r $fps \"$nfile\" $err";
	    if (defined($DEBUG_ENCODERS)) {
		print STDERR "ffmpeg_encoder command is: $syscom\n";
	    }
	    system($syscom);
	}
    }
    else {
	if ($otype eq "webm") {
	    for $pass (1,2) {
		$passf = "-pass $pass -passlogfile passfile";
		$syscom = "$encoder_command  -y  -f image2 -i %8d$img_ext $audio_com -t $vid_length " .
		    "$vcodec $metadata $passf -r $fps \"$nfile\" $err";
		print STDERR "ffmpeg_encoder command is: $syscom\n";
		system($syscom);
	    }
	}
	else {
	    $syscom = "$encoder_command -y -f image2 -i %8d$img_ext $audio_com -t $vid_length " .
		"$vcodec $metadata -r $fps \"$nfile\" $err";
	    print STDERR "ffmpeg_encoder command is: $syscom\n";
	    system($syscom);
	}
    }
}


if ($command eq "clear") {
    # this is called after "encode"
    # note that encoding could have been stopped at any time
    unlink "temp.vid";
    unlink glob "passfile*";
    exit 0;
}


if ($command eq "finalise") {
    # do any finalising code
    # end finalising code
    print "finalised\n";
    exit 0;
}


###### subroutines #######

sub get_num_procs {
    my $result=`cat /proc/cpuinfo|grep processor`;

    $result = (split (/\n/,$result))[-1];
    $result = (split (/: /,$result))[1];
    return ++$result;
}


sub location {
    # return the location of an executable
    my ($command)=shift;

    if ($^O eq "MSWin32") {
	return "$command.exe";
    }

    my ($location) = `which \"$command\" 2>$nulfile`;
    chomp($location);

    $location;
}


##### required
1;
