diff options
Diffstat (limited to 'lib/audio/server.ex')
| -rw-r--r-- | lib/audio/server.ex | 220 |
1 files changed, 191 insertions, 29 deletions
diff --git a/lib/audio/server.ex b/lib/audio/server.ex index 220f898..63ea7ea 100644 --- a/lib/audio/server.ex +++ b/lib/audio/server.ex @@ -1,55 +1,99 @@ +require Logger + alias Nostrum.Struct -defmodule Audio.Server do +alias Thulani.Audio.Util + +defmodule Thulani.Audio.Server do use GenServer @type which :: String.t() - @type audio_ref :: {} + @type audio_ref :: Nostrum.Voice.play_input() + + def start_link(guild_id) when is_integer(guild_id) do + GenServer.start_link(__MODULE__, guild_id, + name: {:via, Registry, {Thulani.Audio.Registry, guild_id}} + ) + end - def start_link(arg) do - {:ok, _} = DynamicSupervisor.start_link(__MODULE__, arg, name: __MODULE__.Supervisor) - {:ok, _} = Registry.start_link(keys: :unique, name: __MODULE__.Registry) + @spec play_now(Struct.Guild.id(), audio_ref, keyword()) :: which + def play_now(guild_id, channel, audio_ref, opts \\ []) do + dispatch(guild_id, {:now, {channel, audio_ref, opts}}) end - @spec play_now(Struct.Guild.id(), audio_ref) :: which - def play_now(guild, audio_ref) do - {:ok, which} = GenServer.call(__MODULE__, {:now, guild, audio_ref}) - which + @spec enqueue(Struct.Guild.id(), audio_ref, keyword()) :: which + def enqueue(guild_id, channel, audio_ref, opts \\ []) do + dispatch(guild_id, {:enqueue, {channel, audio_ref, opts}}) end - @spec enqueue(Struct.Guild.id(), audio_ref) :: which - def enqueue(_guild, _audio_ref) do - GenServer.call(__MODULE__, {:enqueue}) + @spec cancel(Struct.Guild.id(), which) :: GenServer.call() + def cancel(guild_id, which) do + dispatch(guild_id, {:cancel, which}) end - @spec cancel(which) :: GenServer.call() - def cancel(which) do - GenServer.call(__MODULE__, {:cancel, which}) + @spec stop(Struct.Guild.id()) :: GenServer.call() + def stop(guild_id) do + dispatch(guild_id, :stop) end + #### + @impl true - def init(guild) do - {:ok, {guild, :queue.new()}} + def init(guild_id) do + Nostrum.Voice.leave_channel(guild_id) + schedule_tick() + + guild = Nostrum.Cache.GuildCache.get!(guild_id) + Logger.info("starting voice for guild '#{guild.name}' (#{guild_id})") + {:ok, {guild_id, :queue.new()}} end @impl true - def handle_call({:now, {type, ref, opts}}, _from, {guild, q}) do - if Nostrum.Voice.playing?(guild) do - :ok = Nostrum.Voice.stop(guild) - end + def handle_call({:now, {channel, ref, opts}}, _from, {guild, q}) do + guild_ = Nostrum.Cache.GuildCache.get!(guild) + channel_ = guild_.channels[channel] - id = UUID.uuid4() - Nostrum.Voice.play(guild, ref, type, opts) + {result, q} = + with :ok <- play(guild, channel, ref, opts) do + id = UUID.uuid4() + q = :queue.in_r({id, channel, ref, opts}, q) - q = :queue.in_r({id, ref}, q) - {:reply, :ok, {guild, q}} + Logger.info( + "started playback #{id} on '#{guild_.name}'/##{channel_.name}", + guild: guild, + channel: channel, + playback: id + ) + + {{:ok, id}, q} + else + e -> {e, q} + end + + {:reply, result, {guild, q}} end @impl true - def handle_call({:enqueue, audio_ref}, _from, {_guild, q}) do - id = UUID.uuid4() - q = :queue.in({id, audio_ref}, q) - {:reply, {:ok, id}, {id, q}} + def handle_call({:enqueue, {channel, audio_ref, opts}}, from, {guild, q}) do + if :queue.is_empty(q) do + # start playback immediately if queue is empty + handle_call({:now, {channel, audio_ref, opts}}, from, {guild, q}) + else + id = UUID.uuid4() + q = :queue.in({id, channel, audio_ref, opts}, q) + + guild_ = Nostrum.Cache.GuildCache.get!(guild) + channel_ = guild_.channels[channel] + + Logger.info( + "queued playback #{id} for '#{guild_.name}'/##{channel_.name}", + guild: guild, + channel: channel, + playback: id + ) + + {:reply, {:ok, id}, {guild, q}} + end end @impl true @@ -57,4 +101,122 @@ defmodule Audio.Server do q = :queue.delete_with(fn _elem -> false end, q) {:reply, :ok, {guild, q}} end + + @impl true + def handle_call(:stop, _from, {guild, _q}) do + result = Nostrum.Voice.stop(guild) + + {:reply, result, {guild, :queue.new()}} + end + + @impl true + def handle_info(:tick, {guild, q} = state) do + state = + if Nostrum.Voice.playing?(guild) do + state + else + q = + if not :queue.is_empty(q) do + {{:value, {id, _channel, ref, opts}}, q} = :queue.out(q) + Logger.info("completed playback #{id}", id: id, ref: ref, opts: opts) + + q + else + q + end + + if :queue.is_empty(q) do + if not Util.disconnected?(guild) do + Logger.debug("no audio in queue, disconnecting from voice") + + Util.disconnect(guild) + Logger.debug("waiting for disconnect") + + if Util.wait_disconnected(guild) do + Logger.debug("disconnected") + else + Logger.debug("disconnect timed out") + end + end + else + {id, channel, ref, opts} = :queue.head(q) + + Logger.debug("starting queued playback #{id}") + play(guild, channel, ref, opts) + end + + {guild, q} + end + + schedule_tick() + {:noreply, state} + end + + ### + + defp tick_interval, do: 100 + + defp schedule_tick(interval \\ tick_interval()) when is_integer(interval) do + Process.send_after(self(), :tick, tick_interval()) + end + + defp play(guild, channel, ref, opts) do + {:ok, args} = play_args(ref, opts) + + if Nostrum.Voice.playing?(guild) do + Nostrum.Voice.stop(guild) + end + + :ok = Nostrum.Voice.join_channel(guild, channel, false, true) + + if Util.wait_ready(guild) do + Logger.debug("connected") + apply(Nostrum.Voice, :play, [guild | args]) + + Logger.debug("waiting for playing back") + + if not Util.wait_playing(guild) do + {:error, :playback_timed_out} + else + Logger.debug("playback started") + :ok + end + else + {:error, :voice_unready} + end + end + + defp play_args(ref, opts) do + with {:ok, type} <- Util.stream_type(ref) do + {:ok, [ref, type, opts]} + else + {:error, _} = err -> + err + + otherwise -> + dbg(otherwise) + {:error, :unknown} + end + end + + defp dispatch(guild_id, args) do + ensure(guild_id) + call(guild_id, args) + end + + defp call(guild_id, args), do: GenServer.call(address(guild_id), args) + defp address(guild_id), do: {:via, Registry, {Thulani.Audio.Registry, guild_id}} + + defp ensure(guild_id) do + case Registry.lookup(Thulani.Audio.Registry, guild_id) do + [{pid, _}] when is_pid(pid) -> + nil + + [] -> + {:ok, _} = Thulani.Audio.Supervisor.start_child(guild_id) + + otherwise -> + dbg(otherwise) + end + end end |
