lib/kino/video.ex

defmodule Kino.Video do
  @moduledoc """
  A kino for rendering a binary video.

  ## Examples

      content = File.read!("/path/to/video.mp4")
      Kino.Video.new(content, :mp4)

      content = File.read!("/path/to/video.mp4")
      Kino.Video.new(content, :mp4, autoplay: true, loop: true)

  """

  use Kino.JS, assets_path: "lib/assets/video"
  use Kino.JS.Live

  @type t :: Kino.JS.Live.t()

  @type mime_type :: binary()
  @type common_video_type :: :mp4 | :ogg | :avi | :mwv | :mov

  @doc """
  Creates a new kino displaying the given binary video.

  The given type be either `:mp4`, `:ogg`, `:avi`, `:wmv`, `:mov`
  or a string with video MIME type.

  ## Options

    * `:autoplay` - whether the video should start playing as soon as
      it is rendered. Defaults to `false`

    * `:loop` - whether the video should loop. Defaults to `false`

    * `:muted` - whether the video should be muted. Defaults to `false`

  """
  @spec new(binary(), common_video_type() | mime_type(), list()) :: t()
  def new(content, type, opts \\ []) when is_binary(content) do
    opts =
      Keyword.validate!(opts,
        autoplay: false,
        loop: false,
        muted: false
      )

    Kino.JS.Live.new(__MODULE__, %{
      content: content,
      type: mime_type!(type),
      opts:
        Enum.reduce(opts, "controls", fn {opt, val}, acc ->
          if val do
            "#{acc} #{opt}"
          else
            acc
          end
        end)
    })
  end

  @impl true
  def init(assigns, ctx) do
    {:ok, assign(ctx, assigns)}
  end

  @impl true
  def handle_connect(%{assigns: %{content: content, type: type, opts: opts}} = ctx) do
    payload = {:binary, %{type: type, opts: opts}, content}
    {:ok, payload, ctx}
  end

  defp mime_type!(:mp4), do: "video/mp4"
  defp mime_type!(:ogg), do: "video/ogg"
  defp mime_type!(:avi), do: "video/x-msvideo"
  defp mime_type!(:wmv), do: "video/x-ms-wmv"
  defp mime_type!(:mov), do: "video/quicktime"
  defp mime_type!("video/" <> _ = mime_type), do: mime_type

  defp mime_type!(other) do
    raise ArgumentError,
          "expected video type to be either :mp4, :ogg, :avi, :wmv, :mov, or an video MIME type string, got: #{inspect(other)}"
  end
end