Recently I needed to create a script to make a single movie from static images and existing movie files. I hacked this together with ruby, ffmpeg and ImageMagick. Note, it will function under windows with little code change.
Putting it up in case someone finds it of use!
#!/usr/bin/ruby
require 'fileutils'
# !! Script requires image magick to be installed (for convert) and ffmpeg !!
#
# This was *hacked* up quickly, apologies for the general cr*ppy style, its
# functional but not beautiful!
#
# Note: I pipe ffmpeg output (STDOUT/STDERR) to /dev/null - If ffmpeg fails to
# convert you might want to remove this "feature".
#
# Also note you will have to change jpg_to_mpeg() to alter how long static
# images play for.
#
# Also, also note, its hardcoded to look for specific file extensions, you
# might want to alter these as well.
#
# For a given folder, find a unique filename of random string + post_fix
def unique_filename_at ( folder , post_fix )
name = nil
begin
name = File . join ( folder , (( 1 .. 8 ). map { | i | ( 'a' .. 'z' ). to_a [ rand ( 26 )]}. join ) + post_fix )
end while File . exists? ( name )
name
end
# Resize all image files in env[:src] writing them to env[:dst]
def resize_images_as_jpg ( env )
resized_names = []
images = Dir . glob ( File . join ( env [ :src ], '*.{jpg}' ), File :: FNM_CASEFOLD )
images . each do | image |
resized_names << unique_filename_at ( env [ :dst ], ".jpg" ) `convert -scale #{ env [ :width ] } x #{ env [ :height ] } #{ image } #{ resized_names [ - 1 ] } ` `identify #{ resized_names [ - 1 ] } ` =~ /(\d+)x(\d+)/ # Scale attempts to best fit (based on image aspect ratio), it doesn't guarantee the exact size, so # add extents (pad) the image to one which has the desired dimensions if (env[:width] - $1.to_i) > 0 or (env[:height] - $2.to_i) > 0
`convert -gravity Center -extent #{ env [ :width ] } x #{ env [ :height ] } #{ resized_names [ - 1 ] } #{ resized_names [ - 1 ] } `
end
end
resized_names
end
# Resize all the movie files in env[:src] writing them to env[:dst]
def resize_movies ( env )
resized_names = []
movies = Dir . glob ( File . join ( env [ :src ], '*.{mpg,mp4,flv}' ), File :: FNM_CASEFOLD )
movies . each do | movie |
resized_names << unique_filename_at ( env [ :dst ], ".mpg" ) `ffmpeg -i #{ movie } -s #{ env [ :width ] } x #{ env [ :height ] } -sameq -r 25 #{ resized_names [ - 1 ] } > /dev/null 2>&1`
end
resized_names
end
# Convert the given file (must end .jpg) to an mpeg
# !! Hard-coded to create movies of length 10 seconds
# !! 25 frames per second and 250 frames in total
def jpg_to_mpeg ( file )
movie_name = File . join ( File . dirname ( file ), File . basename ( file , ".jpg" ) + ".mpg" )
`ffmpeg -loop_input -qscale 1 -f image2 -vframes 250 -r 25 -i #{ file } #{ movie_name } > /dev/null 2>&1`
movie_name
end
# Usage: tomov src_folder dest_folder
# src_folder contains images/movies
# dest_folder will contain the final result
if __FILE__ == $0
# Change this to set the resultant video size
env = { :width => 640 , :height => 480 }
# Get src/dest folders - I didn't care about fancy argument checks at the time...
if ARGV . length != 2
puts "tomov in_folder out_folder"
exit ( - 1 )
end
env [ :src ] = ARGV [ 0 ]
env [ :dst ] = ARGV [ 1 ]
exit ( - 1 ) unless File . exists? ( env [ :src ])
exit ( - 1 ) unless File . exists? ( env [ :dst ])
# Get the movie files to be consistent
movies = resize_movies ( env )
# Get the image files to be consistent and convert to movie files
resized_images = resize_images_as_jpg ( env )
movies . concat ( resized_images . collect { | resized_image | jpg_to_mpeg ( resized_image ) })
out_tmp = File . join ( env [ :dst ], "movie.mpg" )
out_final = File . join ( env [ :dst ], "final.avi" )
# Concat individual movies into a single movie
# (the sort tries to ensure video's and images are mingled (not guaranteed though))
`cat #{ movies . sort_by { rand } .join(" ")} > #{ out_tmp } `
# Ensure the final movie is consistent and the format I want...
`ffmpeg -i #{ out_tmp } -sameq -vcodec mpeg4 -an -r 25 #{ out_final } > /dev/null 2>&1`
# Clean-up intermediate files
File . delete ( out_tmp )
resized_images . each { | i | File . delete ( i ) }
movies . each { | i | File . delete ( i ) }
end