aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/helpers/media_helper.ex
blob: e44114d9daf1449ca4d38ada9453b25c2858f1db (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Helpers.MediaHelper do
  @moduledoc """
  Handles common media-related operations.
  """

  alias Pleroma.HTTP
  alias Vix.Vips.Operation

  require Logger

  @cachex Pleroma.Config.get([:cachex, :provider], Cachex)

  def missing_dependencies do
    Enum.reduce([ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
      if Pleroma.Utils.command_available?(executable) do
        acc
      else
        [sym | acc]
      end
    end)
  end

  def image_resize(url, options) do
    with {:ok, env} <- HTTP.get(url, [], pool: :media),
         {:ok, resized} <-
           Operation.thumbnail_buffer(env.body, options.max_width,
             height: options.max_height,
             size: :VIPS_SIZE_DOWN
           ) do
      if options[:format] == "png" do
        Operation.pngsave_buffer(resized, Q: options[:quality])
      else
        Operation.jpegsave_buffer(resized, Q: options[:quality], interlace: true)
      end
    else
      {:error, _} = error -> error
    end
  end

  # Note: video thumbnail is intentionally not resized (always has original dimensions)
  @spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()}
  def video_framegrab(url) do
    with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
         false <- @cachex.exists?(:failed_media_helper_cache, url),
         {:ok, env} <- HTTP.get(url, [], pool: :media),
         {:ok, pid} <- StringIO.open(env.body) do
      body_stream = IO.binstream(pid, 1)

      task =
        Task.async(fn ->
          Exile.stream!(
            [
              executable,
              "-i",
              "pipe:0",
              "-vframes",
              "1",
              "-f",
              "mjpeg",
              "pipe:1"
            ],
            input: body_stream,
            ignore_epipe: true,
            stderr: :disable
          )
          |> Enum.into(<<>>)
        end)

      case Task.yield(task, 5_000) do
        nil ->
          Task.shutdown(task)
          @cachex.put(:failed_media_helper_cache, url, nil)
          {:error, {:ffmpeg, :timeout}}

        result ->
          {:ok, result}
      end
    else
      nil -> {:error, {:ffmpeg, :command_not_found}}
      {:error, _} = error -> error
    end
  end
end