mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
117 lines
3.6 KiB
Elixir
117 lines
3.6 KiB
Elixir
defmodule PhilomenaMedia.GifPreview do
|
|
@moduledoc """
|
|
GIF preview generation for video files.
|
|
"""
|
|
|
|
@type duration :: float()
|
|
@type dimensions :: {pos_integer(), pos_integer()}
|
|
|
|
@type num_images :: integer()
|
|
@type target_framerate :: 1..50
|
|
@type opts :: [
|
|
num_images: num_images(),
|
|
target_framerate: target_framerate()
|
|
]
|
|
|
|
@doc """
|
|
Generate a GIF preview of the given video input with evenly-spaced sample points.
|
|
|
|
The input should have pre-computed duration `duration`. The `dimensions`
|
|
are a `{target_width, target_height}` tuple of the largest dimensions desired,
|
|
and the image will be resized to fit inside the box of those dimensions,
|
|
preserving aspect ratio.
|
|
|
|
Depending on the input file, this may take a long time to process.
|
|
|
|
Options:
|
|
- `:target_framerate` - framerate of the output GIF, must be between 1 and 50.
|
|
Default 2.
|
|
- `:num_images` - number of images to sample from the video.
|
|
Default is determined by the duration:
|
|
* 90 or above: 20 images
|
|
* 30 or above: 10 images
|
|
* 1 or above: 5 images
|
|
* otherwise: 2 images
|
|
"""
|
|
@spec preview(Path.t(), Path.t(), duration(), dimensions(), opts()) :: :ok
|
|
def preview(video, gif, duration, dimensions, opts \\ []) do
|
|
target_framerate = Keyword.get(opts, :target_framerate, 2)
|
|
|
|
num_images =
|
|
Keyword.get_lazy(opts, :num_images, fn ->
|
|
cond do
|
|
duration >= 90 -> 20
|
|
duration >= 30 -> 10
|
|
duration >= 1 -> 5
|
|
true -> 2
|
|
end
|
|
end)
|
|
|
|
{_output, 0} =
|
|
System.cmd(
|
|
"ffmpeg",
|
|
commands(video, gif, clamp(duration), dimensions, num_images, target_framerate)
|
|
)
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec commands(Path.t(), Path.t(), duration(), dimensions(), num_images(), target_framerate()) ::
|
|
[String.t()]
|
|
defp commands(video, gif, duration, {target_width, target_height}, num_images, target_framerate) do
|
|
# Compute range [0, num_images)
|
|
image_range = 0..(num_images - 1)
|
|
|
|
# Generate input list in the following form:
|
|
# -ss 0.0 -i input.webm
|
|
input_arguments =
|
|
Enum.flat_map(image_range, &["-ss", "#{&1 * duration / num_images}", "-i", video])
|
|
|
|
# Generate graph in the following form:
|
|
# [0:v] trim=end_frame=1 [t0]; [1:v] trim=end_frame=1 [t1] ...
|
|
trim_filters =
|
|
Enum.map_join(image_range, ";", &"[#{&1}:v] trim=end_frame=1 [t#{&1}]")
|
|
|
|
# Generate graph in the following form:
|
|
# [t0][t1]... concat=n=10 [concat]
|
|
concat_input_pads =
|
|
Enum.map_join(image_range, "", &"[t#{&1}]")
|
|
|
|
concat_filter =
|
|
"#{concat_input_pads} concat=n=#{num_images}, settb=1/#{target_framerate}, setpts=N [concat]"
|
|
|
|
scale_filter =
|
|
"[concat] scale=width=#{target_width}:height=#{target_height}:" <>
|
|
"force_original_aspect_ratio=decrease [scale]"
|
|
|
|
split_filter = "[scale] split [s0][s1]"
|
|
|
|
palettegen_filter =
|
|
"[s0] palettegen=stats_mode=single:max_colors=255:reserve_transparent=1 [palettegen]"
|
|
|
|
paletteuse_filter =
|
|
"[s1][palettegen] paletteuse=dither=bayer:bayer_scale=5:new=1:alpha_threshold=255"
|
|
|
|
filter_graph =
|
|
[
|
|
trim_filters,
|
|
concat_filter,
|
|
scale_filter,
|
|
split_filter,
|
|
palettegen_filter,
|
|
paletteuse_filter
|
|
]
|
|
|> Enum.join(";")
|
|
|
|
# Delay in centiseconds - otherwise it will be computed incorrectly
|
|
final_delay = 100.0 / target_framerate
|
|
|
|
["-loglevel", "0", "-y"]
|
|
|> Kernel.++(input_arguments)
|
|
|> Kernel.++(["-lavfi", filter_graph])
|
|
|> Kernel.++(["-f", "gif", "-final_delay", "#{final_delay}", gif])
|
|
end
|
|
|
|
defp clamp(duration) when duration <= 0, do: 1.0
|
|
defp clamp(duration), do: duration
|
|
end
|