mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-17 22:47:59 +01:00
Merge pull request #295 from philomena-dev/gif-preview
Timebase independent video GIF previewer
This commit is contained in:
commit
1d81e35b68
2 changed files with 123 additions and 49 deletions
117
lib/philomena_media/gif_preview.ex
Normal file
117
lib/philomena_media/gif_preview.ex
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
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
|
|
@ -3,6 +3,7 @@ defmodule PhilomenaMedia.Processors.Webm do
|
||||||
|
|
||||||
alias PhilomenaMedia.Intensities
|
alias PhilomenaMedia.Intensities
|
||||||
alias PhilomenaMedia.Analyzers.Result
|
alias PhilomenaMedia.Analyzers.Result
|
||||||
|
alias PhilomenaMedia.GifPreview
|
||||||
alias PhilomenaMedia.Processors.Processor
|
alias PhilomenaMedia.Processors.Processor
|
||||||
alias PhilomenaMedia.Processors
|
alias PhilomenaMedia.Processors
|
||||||
import Bitwise
|
import Bitwise
|
||||||
|
@ -28,12 +29,11 @@ defmodule PhilomenaMedia.Processors.Webm do
|
||||||
duration = analysis.duration
|
duration = analysis.duration
|
||||||
stripped = strip(file)
|
stripped = strip(file)
|
||||||
preview = preview(duration, stripped)
|
preview = preview(duration, stripped)
|
||||||
palette = gif_palette(stripped, duration)
|
|
||||||
mp4 = scale_mp4_only(stripped, dimensions, dimensions)
|
mp4 = scale_mp4_only(stripped, dimensions, dimensions)
|
||||||
|
|
||||||
{:ok, intensities} = Intensities.file(preview)
|
{:ok, intensities} = Intensities.file(preview)
|
||||||
|
|
||||||
scaled = Enum.flat_map(versions, &scale(stripped, palette, duration, dimensions, &1))
|
scaled = Enum.flat_map(versions, &scale(stripped, duration, dimensions, &1))
|
||||||
mp4 = [{:copy, mp4, "full.mp4"}]
|
mp4 = [{:copy, mp4, "full.mp4"}]
|
||||||
|
|
||||||
[
|
[
|
||||||
|
@ -82,12 +82,12 @@ defmodule PhilomenaMedia.Processors.Webm do
|
||||||
stripped
|
stripped
|
||||||
end
|
end
|
||||||
|
|
||||||
defp scale(file, palette, duration, dimensions, {thumb_name, target_dimensions}) do
|
defp scale(file, duration, dimensions, {thumb_name, target_dimensions}) do
|
||||||
{webm, mp4} = scale_videos(file, dimensions, target_dimensions)
|
{webm, mp4} = scale_videos(file, dimensions, target_dimensions)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
thumb_name in [:thumb, :thumb_small, :thumb_tiny] ->
|
thumb_name in [:thumb, :thumb_small, :thumb_tiny] ->
|
||||||
gif = scale_gif(file, palette, duration, target_dimensions)
|
gif = scale_gif(file, duration, target_dimensions)
|
||||||
|
|
||||||
[
|
[
|
||||||
{:copy, webm, "#{thumb_name}.webm"},
|
{:copy, webm, "#{thumb_name}.webm"},
|
||||||
|
@ -199,53 +199,14 @@ defmodule PhilomenaMedia.Processors.Webm do
|
||||||
mp4
|
mp4
|
||||||
end
|
end
|
||||||
|
|
||||||
defp scale_gif(file, palette, duration, {width, height}) do
|
defp scale_gif(file, duration, dimensions) do
|
||||||
gif = Briefly.create!(extname: ".gif")
|
gif = Briefly.create!(extname: ".gif")
|
||||||
scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease"
|
|
||||||
palette_filter = "paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle"
|
|
||||||
rate_filter = rate_filter(duration)
|
|
||||||
filter_graph = "[0:v]#{scale_filter},#{rate_filter}[x];[x][1:v]#{palette_filter}"
|
|
||||||
|
|
||||||
{_output, 0} =
|
GifPreview.preview(file, gif, duration, dimensions)
|
||||||
System.cmd("ffmpeg", [
|
|
||||||
"-loglevel",
|
|
||||||
"0",
|
|
||||||
"-y",
|
|
||||||
"-i",
|
|
||||||
file,
|
|
||||||
"-i",
|
|
||||||
palette,
|
|
||||||
"-lavfi",
|
|
||||||
filter_graph,
|
|
||||||
"-r",
|
|
||||||
"2",
|
|
||||||
gif
|
|
||||||
])
|
|
||||||
|
|
||||||
gif
|
gif
|
||||||
end
|
end
|
||||||
|
|
||||||
defp gif_palette(file, duration) do
|
|
||||||
palette = Briefly.create!(extname: ".png")
|
|
||||||
palette_filter = "palettegen=stats_mode=diff"
|
|
||||||
rate_filter = rate_filter(duration)
|
|
||||||
filter_graph = "#{rate_filter},#{palette_filter}"
|
|
||||||
|
|
||||||
{_output, 0} =
|
|
||||||
System.cmd("ffmpeg", [
|
|
||||||
"-loglevel",
|
|
||||||
"0",
|
|
||||||
"-y",
|
|
||||||
"-i",
|
|
||||||
file,
|
|
||||||
"-vf",
|
|
||||||
filter_graph,
|
|
||||||
palette
|
|
||||||
])
|
|
||||||
|
|
||||||
palette
|
|
||||||
end
|
|
||||||
|
|
||||||
# x264 requires image dimensions to be a multiple of 2
|
# x264 requires image dimensions to be a multiple of 2
|
||||||
# -2 = ~1
|
# -2 = ~1
|
||||||
def box_dimensions({width, height}, {target_width, target_height}) do
|
def box_dimensions({width, height}, {target_width, target_height}) do
|
||||||
|
@ -255,8 +216,4 @@ defmodule PhilomenaMedia.Processors.Webm do
|
||||||
|
|
||||||
{new_width, new_height}
|
{new_width, new_height}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Avoid division by zero
|
|
||||||
def rate_filter(duration) when duration > 0.5, do: "fps=1/#{duration / 10},settb=1/2,setpts=N"
|
|
||||||
def rate_filter(_duration), do: "setpts=N/TB/2"
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue