aboutsummaryrefslogtreecommitdiff
path: root/lib/audio/server.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/audio/server.ex')
-rw-r--r--lib/audio/server.ex220
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