cgrimmett

cgrimmett

Generate an image thumbnail grid using FFmpex

Hello there,

I would like to generate a storyboard image using elixir. Firstly, I found a ffmpeg incantation which does what I want.

ffmpeg -y -i ~/Videos/moose-encounter_75.mp4 -frames:v 1 -vf 'select=not(mod(n\,257)),scale=160:-1,tile=5x5' -update 1 -fps_mode passthrough ~/Videos/thumb.jpg

The output looks like the following

I found there’s a ffmpeg library for Elixir called FFmpex. There aren’t many ffmpex examples in the wild, but I was able to hack something together which got me 90% there. Following is the code I have so far.

  def create_thumbnail(input_file, output_file) do

    case get_video_framecount(input_file) do
      {:error, reason} -> {:error, reason}
      {:ok, framecount} ->

        frame_interval = div(framecount, 25)
        scale_width = 160
        tile_grid = "5x5"

        command =
          FFmpex.new_command
          |> add_global_option(option_y())
          |> add_input_file(input_file)
          |> add_output_file(output_file)
            |> add_file_option(option_vframes(1))
            |> add_file_option(option_filter_complex("select=not(mod(n\\,#{frame_interval})),scale=#{scale_width}:-1,tile=#{tile_grid}"))
            |> add_file_option(option_vsync(1))                  # -vsync is deprecated in ffmpeg but ffmpex doesn't have it's modern replacement func
            # |> add_file_option(option_update(1))               # ffmpeg complains but it doesn't necessarily need this. I'm omitting because ffmpex doesn't know this function
            # |> add_file_option(option_fps_mode("passthrough")) # -fps_mode is the modern replacement for -vsync, but ffmpex doesn't have that func

        execute(command)
    end
  end

Little side note, get_video_framecount/1 is a function which uses ffmpex to call ffprobe and ultimately get the number of frames from the video stream.

  def get_video_framecount(file_path) do
    case FFprobe.streams(file_path) do
      {:ok, streams} ->
        streams
        |> Enum.find(fn stream -> stream["codec_type"] == "video" end)
        |> case do
          nil -> {:error, "No video stream found"}
          video_stream ->
            nb_frames =
              video_stream
              |> Map.get("nb_frames", %{})

            case nb_frames do
              nil -> {:error, "nb_frames not found"}
              nb_frames ->
                case Integer.parse(nb_frames) do
                  {number, _} -> {:ok, number}
                end
            end
        end

      {:error, reason} -> {:error, reason}
    end
  end

The problem with create_thumbnail/2 is on the ffmpex option_filter_complex/1 line. I don’t think I have the syntax correct, because I see an error when I run the code.

** (ArgumentError) argument error
    (ffmpex 0.11.0) lib/ffmpex.ex:193: FFmpex.validate_contexts!/2
    (ffmpex 0.11.0) lib/ffmpex.ex:117: FFmpex.add_file_option/2
    (bright 0.1.0) lib/bright/images.ex:80: Bright.Images.create_thumbnail/2
    iex:1: (file)

I’m still learning Elixir, so I wasn’t able to make sense of FFmpex’s source code. It looks like the codebase is dynamically generating the options functions (very cool!), but there isn’t much documented usage for the actual option_filter_complex/1 function. The docs linked me to ffmpex/lib/ffmpex/options/advanced.ex at a190c03e706a6c770d7f7fbd3e681803933d0927 · talklittle/ffmpex · GitHub which got me started, but I’ll have to do some more digging in to understand.

My biggest question right now is what type of argument option_filter_complex/1 accepts. Is it supposed to be a string? A map? Not sure yet.

I’m just putting this here for now, documenting what I learn.

Marked As Solved

cgrimmett

cgrimmett

Success! The key was to use option_vf/1, not option_filter_complex/1 (and it accepts a string as argument).

Full command as follows


  def get_video_framecount(file_path) do
    case FFprobe.streams(file_path) do
      {:ok, streams} ->
        streams
        |> Enum.find(fn stream -> stream["codec_type"] == "video" end)
        |> case do
          nil -> {:error, "No video stream found"}
          video_stream ->
            nb_frames =
              video_stream
              |> Map.get("nb_frames", %{})

            case nb_frames do
              nil -> {:error, "nb_frames not found"}
              %{} -> {:error, "nb_frames not found. (empty map)"}
              nb_frames ->
                case Integer.parse(nb_frames) do
                  {number, _} -> {:ok, number}
                end
            end
        end

      {:error, reason} -> {:error, reason}
    end
  end


  def create_thumbnail(input_file, output_file) do

    case get_video_framecount(input_file) do
      {:error, reason} -> {:error, reason}
      {:ok, framecount} ->

        frame_interval = div(framecount, 25)
        scale_width = 160
        tile_grid = "5x5"

        # ffmpeg -y -i ~/Videos/moose-encounter_75.mp4 -frames:v 1 -vf 'select=not(mod(n\,257)),scale=160:-1,tile=5x5' -update 1 -fps_mode passthrough ~/Videos/thumb.jpg
        command =
          FFmpex.new_command
          |> add_global_option(option_y())
          |> add_input_file(input_file)
          |> add_output_file(output_file)
            |> add_file_option(option_vframes(1))
            |> add_file_option(option_vf("select=not(mod(n\\,#{frame_interval})),scale=#{scale_width}:-1,tile=#{tile_grid}"))
            |> add_file_option(option_vsync(1))                  # -vsync is deprecated in ffmpeg but ffmpex doesn't have the modern replacement, -fps_mode
            # |> add_file_option(option_update(1))               # ffmpeg complains but it doesn't necessarily need this. I'm omitting because ffmpex doesn't know this function
            # |> add_file_option(option_fps_mode("passthrough")) # -fps_mode is the modern replacement for -vsync

        execute(command)
    end
  end

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

We're in Beta

About us Mission Statement