Last active
December 26, 2015 07:29
-
-
Save ata4/7115171 to your computer and use it in GitHub Desktop.
Video to GIF converter script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
use strict; | |
use warnings; | |
use Math::Round; | |
use JSON; | |
use Getopt::Std; | |
# program paths | |
my $ffprobePath = "ffprobe"; | |
my $ffmpegPath = "ffmpeg"; | |
my $convertPath = "imconvert"; | |
my $gifsiclePath = "gifsicle"; | |
# subroutines | |
sub remove_extension { | |
my $str = shift; | |
$str =~ s{\.[^.]+$}{}; | |
return $str; | |
} | |
sub get_file_size { | |
my $file = shift; | |
sprintf("%.2f MiB", (-s $file) / 1048576); | |
} | |
# argument parsing | |
my %opts = (); | |
getopts('ogvrq:f:w:h:s:k:', \%opts); | |
if (@ARGV < 1) { | |
print "usage: makegif.pl <options> <video file> [gif file]\n\n"; | |
print "-q <float> Sets the color fuzzing value in percent. Higher values remove more similar pixels from following frames to lower the file size at cost of more artifacts. Default is 2.\n"; | |
print "-r Re-use .rgb file from previous call and don't delete it.\n"; | |
print "-o Use ordered dithering with reduced colors. Ugly, but reduces file size significantly.\n"; | |
print "-g Create GIF file with a global color table.\n"; | |
print "-w <int> Override video width.\n"; | |
print "-h <int> Override video height.\n"; | |
print "-s <float> Override video width and height using this multiplier.\n"; | |
print "-f <float> Override video frame rate.\n"; | |
print "-k <string> Add comment to GIF file.\n"; | |
exit; | |
} | |
# set options | |
my $fuzz = defined $opts{q} ? $opts{q} : 2; | |
my $verbose = defined $opts{v} ? $opts{v} : 0; | |
my $orderedDither = defined $opts{o} ? $opts{o} : undef; | |
my $globalColors = defined $opts{g} ? $opts{g} : 0; | |
my $reuseRGB = defined $opts{r} ? $opts{r} : 0; | |
my $customFramerate = defined $opts{f} ? $opts{f} : undef; | |
my $customWidth = defined $opts{w} ? int $opts{w} : undef; | |
my $customHeight = defined $opts{h} ? int $opts{h} : undef; | |
my $sizeMulti = defined $opts{s} ? $opts{s} : undef; | |
my $resize = $customWidth || $customHeight || $sizeMulti; | |
my $comment = defined $opts{k} ? $opts{k} : undef; | |
my $colors = 256; | |
# set files | |
my $videoFile = $ARGV[0]; | |
my $videoFileName = remove_extension($videoFile); | |
my $gifFile = defined $ARGV[1] ? $ARGV[1] : $videoFileName . ".gif"; | |
my $gifFileName = remove_extension($gifFile); | |
my $rgbFile = $videoFileName . ".rgb"; | |
# probe video file | |
#if (not -f $videoFile) { | |
# die "Video file not found"; | |
#} | |
my $ffprobe = `$ffprobePath -v quiet -print_format json -show_streams "$videoFile" 2>&1`; | |
my $ffprobeJson = decode_json $ffprobe; | |
my $videoStream; | |
# scan for video stream | |
foreach my $stream (@{$ffprobeJson->{streams}}) { | |
if ($stream->{codec_type} eq 'video') { | |
$videoStream = $stream; | |
last; | |
} | |
} | |
if (!$videoStream) { | |
die "No video stream found in source file"; | |
} | |
# get final resolution | |
my $width = $customWidth ? $customWidth : int $videoStream->{width}; | |
my $height = $customHeight ? $customHeight : int $videoStream->{height}; | |
my $framerate = $customFramerate ? $customFramerate : $videoStream->{r_frame_rate}; | |
my $frames = $videoStream->{nb_frames} ? $videoStream->{nb_frames} : $videoStream->{duration_ts}; | |
if ($sizeMulti) { | |
$width *= $sizeMulti; | |
$height *= $sizeMulti; | |
} | |
# convert numerator/denominator to decimal | |
if ($framerate =~ /^\d+\/\d+$/) { | |
$framerate = eval($framerate); | |
} | |
my $delay = round (100 / $framerate); | |
# gif needs at least 1ms delay, most browsers have a minimum of 2ms | |
if ($delay < 2) { | |
$delay = 2; | |
} | |
printf "Input: %d frames, %dx%d @ %.2f FPS (%d ms)\n", $frames, $width, $height, $framerate, $delay; | |
my @args; | |
# create RGB image array | |
if (!$reuseRGB || !(-f $rgbFile)) { | |
print "Converting video to RGB...\n"; | |
@args = (); | |
push @args, "-loglevel", "quiet" if !$verbose; | |
push @args, "-y"; | |
push @args, "-i", $videoFile; | |
push @args, "-pix_fmt", "rgb24"; | |
push @args, "-s", $width . "x" . $height if $resize; | |
push @args, $rgbFile; | |
system $ffmpegPath, @args; | |
print "RGB size: " . get_file_size($rgbFile) . "\n"; | |
} | |
print "Converting RGB to GIF...\n"; | |
@args = (); | |
# memory limits | |
push @args, "-limit", "memory", "8GiB"; | |
push @args, "-limit", "map", "6GiB"; | |
# output control | |
push @args, "-verbose" if $verbose; | |
# animation | |
push @args, "-delay", $delay; | |
# input file and control | |
push @args, "-size", $width . "x" . $height; | |
push @args, "-depth", "8"; | |
push @args, "rgb:$rgbFile"; | |
# optimization | |
push @args, "-fuzz", $fuzz . "%" if $fuzz > 0; | |
push @args, "-layers", "OptimizePlus"; | |
push @args, "-layers", "OptimizeTransparency"; | |
# color reduction and dithering | |
push @args, "-quantize", "YUV"; | |
push @args, "-dither", "FloydSteinberg"; | |
push @args, "-ordered-dither", "o8x8,32" if $orderedDither; | |
if ($globalColors) { | |
push @args, "-colors", $colors ; | |
push @args, "+map"; | |
} | |
# output file | |
push @args, $gifFile; | |
system $convertPath, @args; | |
print "GIF size: " . get_file_size($gifFile) . " (raw)\n"; | |
unlink $rgbFile if !$reuseRGB; | |
# optimize and add comment | |
@args = (); | |
push @args, "--batch"; | |
push @args, "--optimize=3"; | |
push @args, "--comment", $comment if $comment; | |
push @args, $gifFile; | |
system $gifsiclePath, @args; | |
print "GIF size: " . get_file_size($gifFile) . " (optimized)\n"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment