1
0
mirror of https://github.com/algora-io/tv.git synced 2025-01-25 01:32:39 +02:00

add clipper (#48)

This commit is contained in:
Zafer Cesur 2024-06-06 15:27:42 +03:00 committed by GitHub
parent bc933d65c0
commit ec0bcbe578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 240 additions and 0 deletions

View File

@ -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);

96
lib/algora/clipper.ex Normal file
View File

@ -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

View File

@ -314,6 +314,7 @@
<main class="flex-1 relative z-0 overflow-y-auto focus:outline-none">
<%= live_render(@socket, AlgoraWeb.PlayerLive, id: "player", session: %{}, sticky: true) %>
<%= live_render(@socket, AlgoraWeb.ClipperLive, id: "clipper", session: %{}, sticky: true) %>
<%= @inner_content %>
</main>
</div>

View File

@ -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"""
<div
:if={@video && @current_user && Accounts.admin?(@current_user)}
class="max-w-sm fixed left-1 bottom-1 z-[1000] bg-gray-800 p-2 rounded-xl"
>
<div
:if={@ffmpeg_cmd}
class="pb-2 whitespace-nowrap font-semibold text-sm text-green-300 font-mono overflow-x-auto"
>
<%= @ffmpeg_cmd %>
</div>
<form class="mx-auto" phx-submit="clip">
<div class="relative space-y-4">
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-3 rounded-lg ring-2 ring-purple-500 overflow-hidden">
<input
inputmode="numeric"
pattern="[0-9]*"
id="from-hh"
name="from-hh"
autocomplete="off"
class="w-full pl-4 text-sm bg-gray-950/75 placeholder-purple-300 text-white focus:outline-none"
placeholder="hh"
/>
<input
inputmode="numeric"
pattern="[0-9]*"
id="from-mm"
name="from-mm"
autocomplete="off"
class="w-full pl-2 text-sm bg-gray-950/75 placeholder-purple-300 text-white focus:outline-none"
placeholder="mm"
/>
<input
inputmode="numeric"
pattern="[0-9]*"
id="from-ss"
name="from-ss"
autocomplete="off"
class="w-full pl-2 text-sm bg-gray-950/75 placeholder-purple-300 text-white focus:outline-none"
placeholder="ss"
/>
</div>
<div class="grid grid-cols-3 rounded-lg ring-2 ring-purple-500 overflow-hidden">
<input
inputmode="numeric"
pattern="[0-9]*"
id="to-hh"
name="to-hh"
autocomplete="off"
class="w-full pl-4 text-sm bg-gray-950/75 placeholder-purple-300 text-white focus:outline-none"
placeholder="hh"
/>
<input
inputmode="numeric"
pattern="[0-9]*"
id="to-mm"
name="to-mm"
autocomplete="off"
class="w-full pl-2 text-sm bg-gray-950/75 placeholder-purple-300 text-white focus:outline-none"
placeholder="mm"
/>
<input
inputmode="numeric"
pattern="[0-9]*"
id="to-ss"
name="to-ss"
autocomplete="off"
class="w-full pl-2 text-sm bg-gray-950/75 placeholder-purple-300 text-white focus:outline-none"
placeholder="ss"
/>
</div>
<button
type="submit"
class="w-full text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2 bg-purple-600 hover:bg-purple-700 focus:ring-purple-800"
>
Clip
</button>
</div>
</div>
</form>
</div>
"""
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

View File

@ -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),

View File

@ -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),

View File

@ -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),

View File

@ -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),