Creating a single movie/video from a mix of static images and movies

Creating a single movie/video from a mix of static images and movies

June 3, 2010  |  Coding

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

Leave a Reply