total rebase
[anni] / lib / pleroma / helpers / media_helper.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Helpers.MediaHelper do
6   @moduledoc """
7   Handles common media-related operations.
8   """
9
10   alias Pleroma.HTTP
11   alias Vix.Vips.Operation
12
13   require Logger
14
15   @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
16
17   def missing_dependencies do
18     Enum.reduce([ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
19       if Pleroma.Utils.command_available?(executable) do
20         acc
21       else
22         [sym | acc]
23       end
24     end)
25   end
26
27   def image_resize(url, options) do
28     with {:ok, env} <- HTTP.get(url, [], pool: :media),
29          {:ok, resized} <-
30            Operation.thumbnail_buffer(env.body, options.max_width,
31              height: options.max_height,
32              size: :VIPS_SIZE_DOWN
33            ) do
34       if options[:format] == "png" do
35         Operation.pngsave_buffer(resized, Q: options[:quality])
36       else
37         Operation.jpegsave_buffer(resized, Q: options[:quality], interlace: true)
38       end
39     else
40       {:error, _} = error -> error
41     end
42   end
43
44   # Note: video thumbnail is intentionally not resized (always has original dimensions)
45   @spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()}
46   def video_framegrab(url) do
47     with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
48          false <- @cachex.exists?(:failed_media_helper_cache, url),
49          {:ok, env} <- HTTP.get(url, [], pool: :media),
50          {:ok, pid} <- StringIO.open(env.body) do
51       body_stream = IO.binstream(pid, 1)
52
53       task =
54         Task.async(fn ->
55           Exile.stream!(
56             [
57               executable,
58               "-i",
59               "pipe:0",
60               "-vframes",
61               "1",
62               "-f",
63               "mjpeg",
64               "pipe:1"
65             ],
66             input: body_stream,
67             ignore_epipe: true,
68             stderr: :disable
69           )
70           |> Enum.into(<<>>)
71         end)
72
73       case Task.yield(task, 5_000) do
74         nil ->
75           Task.shutdown(task)
76           @cachex.put(:failed_media_helper_cache, url, nil)
77           {:error, {:ffmpeg, :timeout}}
78
79         result ->
80           {:ok, result}
81       end
82     else
83       nil -> {:error, {:ffmpeg, :command_not_found}}
84       {:error, _} = error -> error
85     end
86   end
87 end