mirror of
https://github.com/algora-io/tv.git
synced 2025-03-17 20:17:45 +02:00
244 lines
6.4 KiB
Elixir
244 lines
6.4 KiB
Elixir
import Ecto.Query, warn: false
|
|
import Ecto.Changeset
|
|
alias Algora.{Accounts, Library, Storage, Repo}
|
|
alias Algora.Library.Video
|
|
|
|
defmodule Algora.Admin do
|
|
@tracks %{
|
|
manifest: "index.m3u8",
|
|
audio: "g3cFYXVkaW8.m3u8",
|
|
video: "g3cFdmlkZW8.m3u8"
|
|
}
|
|
|
|
def whoami(), do: {System.get_env("FLY_REGION"), Node.self()}
|
|
|
|
defp get(url) do
|
|
Finch.build(:get, url) |> Finch.request(Algora.Finch)
|
|
end
|
|
|
|
def get_playlist(video) do
|
|
{:ok, resp} = get(video.url)
|
|
{:ok, playlist} = ExM3U8.deserialize_playlist(resp.body, [])
|
|
playlist
|
|
end
|
|
|
|
def get_media_playlist(video, uri) do
|
|
url = "#{video.url_root}/#{uri}"
|
|
|
|
with {:ok, resp} <- get(url),
|
|
{:ok, playlist} <- ExM3U8.deserialize_media_playlist(resp.body, []) do
|
|
playlist
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
def get_media_playlists(video) do
|
|
%{
|
|
video: get_media_playlist(video, @tracks.video),
|
|
audio: get_media_playlist(video, @tracks.audio)
|
|
}
|
|
end
|
|
|
|
def set_thumbnail!(id, path \\ nil) do
|
|
video = Library.get_video!(id)
|
|
{:ok, _} = Library.store_thumbnail_from_file(video, path || "/tmp/#{id}.png")
|
|
{:ok, _} = Library.store_og_image_from_file(video, path || "/tmp/#{id}.png")
|
|
end
|
|
|
|
def set_title!(id, title) do
|
|
video = Library.get_video!(id)
|
|
user = Accounts.get_user!(video.user_id)
|
|
{:ok, _} = Library.update_video(video, %{title: title})
|
|
{:ok, _} = Accounts.update_settings(user, %{channel_tagline: title})
|
|
end
|
|
|
|
def nodes(), do: [Node.self() | Node.list()]
|
|
|
|
def pipelines() do
|
|
nodes() |> Enum.flat_map(&Membrane.Pipeline.list_pipelines/1)
|
|
end
|
|
|
|
def broadcasts() do
|
|
pipelines() |> Enum.map(fn pid -> GenServer.call(pid, :get_video_id) end)
|
|
end
|
|
|
|
def multicast!(video_id) do
|
|
pipelines()
|
|
|> Enum.find(fn pid -> GenServer.call(pid, :get_video_id) == video_id end)
|
|
|> send(:multicast_algora)
|
|
end
|
|
|
|
def download_chunks(video, chunks, dir) do
|
|
Task.async_stream(
|
|
Enum.with_index(chunks),
|
|
fn {chunk, i} ->
|
|
IO.puts("#{rounded(100 * i / length(chunks))}%")
|
|
|
|
dl_path = "#{dir}/#{chunk}"
|
|
|
|
if not File.exists?(dl_path) do
|
|
{:ok, :saved_to_file} =
|
|
:httpc.request(:get, {~c"#{video.url_root}/#{chunk}", []}, [],
|
|
stream: ~c"#{dl_path}.part"
|
|
)
|
|
|
|
File.rename!("#{dl_path}.part", dl_path)
|
|
end
|
|
end,
|
|
max_concurrency: 100,
|
|
timeout: :infinity
|
|
)
|
|
|> Stream.map(fn {:ok, val} -> val end)
|
|
|> Enum.to_list()
|
|
end
|
|
|
|
def download_video(video, dir) do
|
|
playlists = get_media_playlists(video)
|
|
|
|
video_chunks =
|
|
for n <- playlists.video.timeline,
|
|
Map.has_key?(n, :uri),
|
|
do: n.uri
|
|
|
|
audio_chunks =
|
|
for n <- playlists.audio.timeline,
|
|
Map.has_key?(n, :uri),
|
|
do: n.uri
|
|
|
|
{time, _} = :timer.tc(&download_chunks/3, [video, video_chunks ++ audio_chunks, dir])
|
|
|
|
video_chunks
|
|
|> Enum.map(fn chunk -> "#{dir}/#{chunk}" end)
|
|
|> concatenate_files("#{dir}/video.mp4")
|
|
|
|
audio_chunks
|
|
|> Enum.map(fn chunk -> "#{dir}/#{chunk}" end)
|
|
|> concatenate_files("#{dir}/audio.mp4")
|
|
|
|
{_, 0} =
|
|
System.cmd(
|
|
"ffmpeg",
|
|
[
|
|
"-y",
|
|
"-i",
|
|
"#{dir}/video.mp4",
|
|
"-i",
|
|
"#{dir}/audio.mp4",
|
|
"-c",
|
|
"copy",
|
|
"#{dir}/output.mp4"
|
|
]
|
|
)
|
|
|
|
%File.Stat{size: size} = File.stat!("#{dir}/output.mp4")
|
|
|
|
IO.puts(
|
|
"Downloaded #{rounded(size / 1_000_000)} MB in #{rounded(time / 1_000_000)} s (#{rounded(size / time)} MB/s)"
|
|
)
|
|
end
|
|
|
|
def tmp_dir do
|
|
case Algora.config([:mode]) do
|
|
:prod ->
|
|
"/data"
|
|
|
|
_ ->
|
|
System.tmp_dir!()
|
|
end
|
|
end
|
|
|
|
def download(video) do
|
|
dir = Path.join(tmp_dir(), video.uuid)
|
|
|
|
File.mkdir_p!(dir)
|
|
|
|
{time, _} = :timer.tc(&download_video/2, [video, dir])
|
|
|
|
%File.Stat{size: size} = File.stat!("#{dir}/output.mp4")
|
|
|
|
IO.puts("Transmuxed #{rounded(size / 1_000_000)} MB in #{rounded(time / 1_000_000)} s")
|
|
end
|
|
|
|
def save_download(video, cb \\ fn _ -> nil end) do
|
|
mp4_basename = Slug.slugify("#{Date.to_string(video.inserted_at)}-#{video.title}")
|
|
|
|
mp4_video =
|
|
%Video{
|
|
title: video.title,
|
|
duration: video.duration,
|
|
type: :vod,
|
|
format: :mp4,
|
|
is_live: false,
|
|
visibility: :unlisted,
|
|
user_id: video.user_id,
|
|
transmuxed_from_id: video.id,
|
|
thumbnail_url: video.thumbnail_url
|
|
}
|
|
|> change()
|
|
|> Video.put_video_url(:mp4, mp4_basename)
|
|
|
|
%{remote_path: mp4_remote_path} = mp4_video.changes
|
|
|
|
mp4_local_path = Path.join([tmp_dir(), video.uuid, "output.mp4"])
|
|
|
|
Storage.upload_from_filename(mp4_local_path, mp4_remote_path, cb, content_type: "video/mp4")
|
|
|
|
Repo.insert!(mp4_video)
|
|
end
|
|
|
|
def concatenate_files(paths, output_file) do
|
|
paths
|
|
|> Stream.map(&stream_file/1)
|
|
|> Stream.concat()
|
|
|> Stream.into(File.stream!(output_file, [:write, :delayed]))
|
|
|> Stream.run()
|
|
end
|
|
|
|
defp stream_file(file_path) do
|
|
File.stream!(file_path, [], 2048)
|
|
end
|
|
|
|
def nearest_tigris_region() do
|
|
%{scheme: scheme, host: host} = Application.fetch_env!(:ex_aws, :s3) |> Enum.into(%{})
|
|
bucket_url = "#{scheme}#{host}/#{Algora.config([:buckets, :media])}"
|
|
|
|
src_region = System.get_env("FLY_REGION") || "local"
|
|
bytes = :crypto.strong_rand_bytes(1_000)
|
|
|
|
{_time, {:ok, _}} = :timer.tc(&Storage.upload/3, [bytes, "0/#{src_region}.bin", []])
|
|
|
|
{:ok, %{headers: headers}} = HTTPoison.get("#{bucket_url}/0/#{src_region}.bin")
|
|
|
|
headers
|
|
|> Enum.find(fn {k, _} -> k == "X-Tigris-Regions" end)
|
|
|> then(fn {_, v} -> v end)
|
|
end
|
|
|
|
def speedtest(n \\ 1, size \\ 1_000_000) do
|
|
bytes = :crypto.strong_rand_bytes(size)
|
|
src_region = System.get_env("FLY_REGION") || "local"
|
|
dst_region = nearest_tigris_region()
|
|
|
|
for _ <- 1..n do
|
|
{time, {:ok, _}} = :timer.tc(&Storage.upload/3, [bytes, "0/#{src_region}.bin", []])
|
|
|
|
IO.puts(
|
|
"Uploaded #{Float.round(size / 1.0e6, 1)} MB in #{Float.round(time / 1.0e6, 2)} s (#{Float.round(size / time, 2)} MB/s, #{src_region} -> #{dst_region})"
|
|
)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
|
|
def speedtest_par(n \\ 1, size \\ 1_000_000) do
|
|
:rpc.multicall(nodes(), Algora.Admin, :speedtest, [n, size])
|
|
end
|
|
|
|
def speedtest_seq(n \\ 1, size \\ 1_000_000) do
|
|
nodes() |> Enum.map(fn node -> :rpc.call(node, Algora.Admin, :speedtest, [n, size]) end)
|
|
end
|
|
|
|
defp rounded(num), do: Float.round(num, 1)
|
|
end
|