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:
parent
bc933d65c0
commit
ec0bcbe578
@ -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
96
lib/algora/clipper.ex
Normal 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
|
@ -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>
|
||||
|
136
lib/algora_web/live/clipper_live.ex
Normal file
136
lib/algora_web/live/clipper_live.ex
Normal 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
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user