From ec0bcbe57876706c286c1d2a29d167315ae64f29 Mon Sep 17 00:00:00 2001 From: Zafer Cesur Date: Thu, 6 Jun 2024 15:27:42 +0300 Subject: [PATCH] add clipper (#48) --- assets/js/app.ts | 3 + lib/algora/clipper.ex | 96 +++++++++++++ .../components/layouts/live.html.heex | 1 + lib/algora_web/live/clipper_live.ex | 136 ++++++++++++++++++ lib/algora_web/live/embed_live.ex | 1 + lib/algora_web/live/subtitle_live/index.ex | 1 + lib/algora_web/live/subtitle_live/show.ex | 1 + lib/algora_web/live/video_live.ex | 1 + 8 files changed, 240 insertions(+) create mode 100644 lib/algora/clipper.ex create mode 100644 lib/algora_web/live/clipper_live.ex diff --git a/assets/js/app.ts b/assets/js/app.ts index 5307742..4d866ca 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -161,6 +161,7 @@ const Hooks = { }); const playVideo = (opts: { + id: string; url: string; title: string; player_type: string; @@ -206,6 +207,8 @@ const Hooks = { backdrop.classList.remove("opacity-10"); backdrop.classList.add("opacity-20"); } + + this.pushEventTo("#clipper", "video_loaded", { id: opts.id }); }; this.handleEvent("play_video", playVideo); diff --git a/lib/algora/clipper.ex b/lib/algora/clipper.ex new file mode 100644 index 0000000..36e9fa2 --- /dev/null +++ b/lib/algora/clipper.ex @@ -0,0 +1,96 @@ +defmodule Algora.Clipper do + alias Algora.{Storage, Library} + + defp bucket(), do: Algora.config([:buckets, :media]) + + defp to_absolute(:video, uuid, uri), + do: "#{Storage.endpoint_url()}/#{bucket()}/#{uuid}/#{uri}" + + defp to_absolute(:clip, uuid, uri), + do: "#{Storage.endpoint_url()}/#{bucket()}/clips/#{uuid}/#{uri}" + + def clip(video, from, to) do + playlists = Algora.Admin.get_media_playlists(video) + + %{timeline: timeline, ss: ss} = + playlists.video.timeline + |> Enum.reduce(%{elapsed: 0, ss: 0, timeline: []}, fn x, acc -> + case x do + %ExM3U8.Tags.MediaInit{uri: uri} -> + %{ + acc + | timeline: [ + %ExM3U8.Tags.MediaInit{uri: to_absolute(:video, video.uuid, uri)} | acc.timeline + ] + } + + %ExM3U8.Tags.Segment{duration: duration} when acc.elapsed > to -> + %{acc | elapsed: acc.elapsed + duration} + + %ExM3U8.Tags.Segment{duration: duration} when acc.elapsed + duration < from -> + %{acc | elapsed: acc.elapsed + duration} + + %ExM3U8.Tags.Segment{duration: duration, uri: uri} + when acc.elapsed < from and acc.elapsed + duration > from -> + %{ + acc + | elapsed: acc.elapsed + duration, + ss: acc.elapsed + duration - from, + timeline: [ + %ExM3U8.Tags.Segment{ + duration: duration, + uri: to_absolute(:video, video.uuid, uri) + } + | acc.timeline + ] + } + + %ExM3U8.Tags.Segment{duration: duration, uri: uri} -> + %{ + acc + | elapsed: acc.elapsed + duration, + timeline: [ + %ExM3U8.Tags.Segment{ + duration: duration, + uri: to_absolute(:video, video.uuid, uri) + } + | acc.timeline + ] + } + + _ -> + acc + end + end) + |> then(fn clip -> %{ss: clip.ss, timeline: Enum.reverse(clip.timeline)} end) + + %{playlist: %{playlists.video | timeline: timeline}, ss: ss} + end + + def create_clip(video, from, to) do + uuid = Ecto.UUID.generate() + + %{playlist: playlist, ss: ss} = clip(video, from, to) + + manifest = "#{ExM3U8.serialize(playlist)}#EXT-X-ENDLIST\n" + + {:ok, _} = + Storage.upload(manifest, "clips/#{uuid}/g3cFdmlkZW8.m3u8", + content_type: "application/x-mpegURL" + ) + + {:ok, _} = + ExAws.S3.put_object_copy( + bucket(), + "clips/#{uuid}/index.m3u8", + bucket(), + "#{video.uuid}/index.m3u8" + ) + |> ExAws.request() + + url = to_absolute(:clip, uuid, "index.m3u8") + filename = Slug.slugify("#{video.title}-#{Library.to_hhmmss(from)}-#{Library.to_hhmmss(to)}") + + "ffmpeg -i \"#{url}\" -ss #{ss} -t #{to - from} \"#{filename}.mp4\"" + end +end diff --git a/lib/algora_web/components/layouts/live.html.heex b/lib/algora_web/components/layouts/live.html.heex index 44be891..fb02e1a 100644 --- a/lib/algora_web/components/layouts/live.html.heex +++ b/lib/algora_web/components/layouts/live.html.heex @@ -314,6 +314,7 @@
<%= live_render(@socket, AlgoraWeb.PlayerLive, id: "player", session: %{}, sticky: true) %> + <%= live_render(@socket, AlgoraWeb.ClipperLive, id: "clipper", session: %{}, sticky: true) %> <%= @inner_content %>
diff --git a/lib/algora_web/live/clipper_live.ex b/lib/algora_web/live/clipper_live.ex new file mode 100644 index 0000000..0731187 --- /dev/null +++ b/lib/algora_web/live/clipper_live.ex @@ -0,0 +1,136 @@ +defmodule AlgoraWeb.ClipperLive do + use AlgoraWeb, {:live_view, container: {:div, []}} + + alias Algora.{Library, Clipper, Accounts} + + on_mount {AlgoraWeb.UserAuth, :current_user} + + @impl true + def render(assigns) do + ~H""" +
+
+ <%= @ffmpeg_cmd %> +
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+
+ """ + end + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:video, nil) + |> assign(:ffmpeg_cmd, nil), layout: false} + end + + @impl true + def handle_event("video_loaded", %{"id" => id}, socket) do + {:noreply, + socket + |> assign(:video, Library.get_video!(id))} + end + + def handle_event( + "clip", + %{ + "from-hh" => from_hh, + "from-mm" => from_mm, + "from-ss" => from_ss, + "to-hh" => to_hh, + "to-mm" => to_mm, + "to-ss" => to_ss + }, + socket + ) do + from = to_time(from_hh, :hours) + to_time(from_mm, :minutes) + to_time(from_ss, :seconds) + to = to_time(to_hh, :hours) + to_time(to_mm, :minutes) + to_time(to_ss, :seconds) + + cmd = Clipper.create_clip(socket.assigns.video, from, to) + + {:noreply, + socket + |> assign(:ffmpeg_cmd, cmd)} + end + + defp to_time("", _timeframe), do: 0 + defp to_time(n, :seconds), do: String.to_integer(n) + defp to_time(n, :minutes), do: String.to_integer(n) * 60 + defp to_time(n, :hours), do: String.to_integer(n) * 3600 +end diff --git a/lib/algora_web/live/embed_live.ex b/lib/algora_web/live/embed_live.ex index c53246d..9993a55 100644 --- a/lib/algora_web/live/embed_live.ex +++ b/lib/algora_web/live/embed_live.ex @@ -77,6 +77,7 @@ defmodule AlgoraWeb.EmbedLive do socket = socket |> push_event("play_video", %{ + id: video.id, url: video.url, title: video.title, player_type: Library.player_type(video), diff --git a/lib/algora_web/live/subtitle_live/index.ex b/lib/algora_web/live/subtitle_live/index.ex index 9e632de..06c2814 100644 --- a/lib/algora_web/live/subtitle_live/index.ex +++ b/lib/algora_web/live/subtitle_live/index.ex @@ -46,6 +46,7 @@ defmodule AlgoraWeb.SubtitleLive.Index do {:noreply, socket |> push_event("play_video", %{ + id: video.id, url: video.url, title: video.title, player_type: Library.player_type(video), diff --git a/lib/algora_web/live/subtitle_live/show.ex b/lib/algora_web/live/subtitle_live/show.ex index 156e871..cb73c1e 100644 --- a/lib/algora_web/live/subtitle_live/show.ex +++ b/lib/algora_web/live/subtitle_live/show.ex @@ -19,6 +19,7 @@ defmodule AlgoraWeb.SubtitleLive.Show do {:noreply, socket |> push_event("play_video", %{ + id: video.id, url: video.url, title: video.title, player_type: Library.player_type(video), diff --git a/lib/algora_web/live/video_live.ex b/lib/algora_web/live/video_live.ex index fa65402..7e2d44c 100644 --- a/lib/algora_web/live/video_live.ex +++ b/lib/algora_web/live/video_live.ex @@ -606,6 +606,7 @@ defmodule AlgoraWeb.VideoLive do socket = socket |> push_event("play_video", %{ + id: video.id, url: video.url, title: video.title, player_type: Library.player_type(video),