summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorouwou <26526779+ouwou@users.noreply.github.com>2022-08-31 01:51:02 -0400
committerouwou <26526779+ouwou@users.noreply.github.com>2022-08-31 01:51:02 -0400
commit0fa33915da6255cf7460758197eaea7e43353543 (patch)
tree15a92a3aae2cd2647c24ce4c44f1aaca01fcf422
parent634f51fb4117c0870399e73560ac313d68d281e8 (diff)
downloadabaddon-portaudio-0fa33915da6255cf7460758197eaea7e43353543.tar.gz
abaddon-portaudio-0fa33915da6255cf7460758197eaea7e43353543.zip
rudimentary voice implementation
-rw-r--r--CMakeLists.txt9
-rw-r--r--src/abaddon.cpp18
-rw-r--r--src/abaddon.hpp5
-rw-r--r--src/audio/manager.cpp92
-rw-r--r--src/audio/manager.hpp35
-rw-r--r--src/components/channels.cpp36
-rw-r--r--src/components/channels.hpp13
-rw-r--r--src/components/channelscellrenderer.cpp43
-rw-r--r--src/components/channelscellrenderer.hpp14
-rw-r--r--src/discord/discord.cpp37
-rw-r--r--src/discord/discord.hpp39
-rw-r--r--src/discord/objects.cpp21
-rw-r--r--src/discord/objects.hpp28
-rw-r--r--src/discord/voiceclient.cpp372
-rw-r--r--src/discord/voiceclient.hpp205
-rw-r--r--src/discord/waiter.hpp29
16 files changed, 964 insertions, 32 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6d5ee2d..e779686 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -109,5 +109,14 @@ if (USE_LIBHANDY)
endif ()
if (ENABLE_VOICE)
+ target_compile_definitions(abaddon PRIVATE WITH_VOICE)
+
+ find_package(PkgConfig)
+
target_include_directories(abaddon PUBLIC subprojects/miniaudio)
+ pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus)
+ target_link_libraries(abaddon PkgConfig::Opus)
+
+ pkg_check_modules(libsodium REQUIRED IMPORTED_TARGET libsodium)
+ target_link_libraries(abaddon PkgConfig::libsodium)
endif ()
diff --git a/src/abaddon.cpp b/src/abaddon.cpp
index 343dff7..a3a228d 100644
--- a/src/abaddon.cpp
+++ b/src/abaddon.cpp
@@ -3,6 +3,7 @@
#include <string>
#include <algorithm>
#include "platform.hpp"
+#include "audio/manager.hpp"
#include "discord/discord.hpp"
#include "dialogs/token.hpp"
#include "dialogs/editmessage.hpp"
@@ -219,6 +220,14 @@ int Abaddon::StartGTK() {
return 1;
}
+ m_audio = std::make_unique<AudioManager>();
+ if (!m_audio->OK()) {
+ Gtk::MessageDialog dlg(*m_main_window, "The audio engine could not be initialized!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
+ dlg.set_position(Gtk::WIN_POS_CENTER);
+ dlg.run();
+ return 1;
+ }
+
// store must be checked before this can be called
m_main_window->UpdateComponents();
@@ -238,6 +247,7 @@ int Abaddon::StartGTK() {
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true));
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings));
+ m_main_window->GetChannelList()->signal_action_join_voice_channel().connect(sigc::mem_fun(*this, &Abaddon::ActionJoinVoiceChannel));
m_main_window->GetChatWindow()->signal_action_message_edit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatEditMessage));
m_main_window->GetChatWindow()->signal_action_chat_submit().connect(sigc::mem_fun(*this, &Abaddon::ActionChatInputSubmit));
@@ -898,6 +908,10 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) {
window->show();
}
+void Abaddon::ActionJoinVoiceChannel(Snowflake channel_id) {
+ m_discord.ConnectToVoice(channel_id);
+}
+
std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) {
TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window);
const auto code = dlg.run();
@@ -937,6 +951,10 @@ EmojiResource &Abaddon::GetEmojis() {
return m_emojis;
}
+AudioManager &Abaddon::GetAudio() {
+ return *m_audio.get();
+}
+
int main(int argc, char **argv) {
if (std::getenv("ABADDON_NO_FC") == nullptr)
Platform::SetupFonts();
diff --git a/src/abaddon.hpp b/src/abaddon.hpp
index ab80c46..d67f4ab 100644
--- a/src/abaddon.hpp
+++ b/src/abaddon.hpp
@@ -12,6 +12,8 @@
#define APP_TITLE "Abaddon"
+class AudioManager;
+
class Abaddon {
private:
Abaddon();
@@ -51,6 +53,7 @@ public:
void ActionAddRecipient(Snowflake channel_id);
void ActionViewPins(Snowflake channel_id);
void ActionViewThreads(Snowflake channel_id);
+ void ActionJoinVoiceChannel(Snowflake channel_id);
std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
@@ -59,6 +62,7 @@ public:
ImageManager &GetImageManager();
EmojiResource &GetEmojis();
+ AudioManager &GetAudio();
std::string GetDiscordToken() const;
bool IsDiscordActive() const;
@@ -137,6 +141,7 @@ private:
ImageManager m_img_mgr;
EmojiResource m_emojis;
+ std::unique_ptr<AudioManager> m_audio;
mutable std::mutex m_mutex;
Glib::RefPtr<Gtk::Application> m_gtk_app;
diff --git a/src/audio/manager.cpp b/src/audio/manager.cpp
new file mode 100644
index 0000000..af36327
--- /dev/null
+++ b/src/audio/manager.cpp
@@ -0,0 +1,92 @@
+#ifdef _WIN32
+ #include <winsock2.h>
+#endif
+
+#include "manager.hpp"
+#include <array>
+#define MINIAUDIO_IMPLEMENTATION
+#include <miniaudio.h>
+#include <opus/opus.h>
+#include <cstring>
+
+const uint8_t *StripRTPExtensionHeader(const uint8_t *buf, int num_bytes, size_t &outlen) {
+ if (buf[0] == 0xbe && buf[1] == 0xde && num_bytes > 4) {
+ uint64_t offset = 4 + 4 * ((buf[2] << 8) | buf[3]);
+
+ outlen = num_bytes - offset;
+ return buf + offset;
+ }
+ outlen = num_bytes;
+ return buf;
+}
+
+void data_callback(ma_device *pDevice, void *pOutput, const void *pInput, ma_uint32 frameCount) {
+ AudioManager *mgr = reinterpret_cast<AudioManager *>(pDevice->pUserData);
+ if (mgr == nullptr) return;
+ std::lock_guard<std::mutex> _(mgr->m_dumb_mutex);
+
+ const auto buffered_frames = std::min(static_cast<ma_uint32>(mgr->m_dumb.size() / 2), frameCount);
+ auto *pOutputCast = static_cast<ma_int16 *>(pOutput);
+ for (ma_uint32 i = 0; i < buffered_frames * 2; i++) {
+ pOutputCast[i] = mgr->m_dumb.front();
+ mgr->m_dumb.pop();
+ }
+}
+
+AudioManager::AudioManager() {
+ m_ok = true;
+
+ m_device_config = ma_device_config_init(ma_device_type_playback);
+ m_device_config.playback.format = ma_format_s16;
+ m_device_config.playback.channels = 2;
+ m_device_config.sampleRate = 48000;
+ m_device_config.dataCallback = data_callback;
+ m_device_config.pUserData = this;
+
+ if (ma_device_init(nullptr, &m_device_config, &m_device) != MA_SUCCESS) {
+ puts("open playabck fail");
+ m_ok = false;
+ return;
+ }
+
+ if (ma_device_start(&m_device) != MA_SUCCESS) {
+ puts("failed to start playback");
+ ma_device_uninit(&m_device);
+ m_ok = false;
+ return;
+ }
+
+ int err;
+ m_opus_decoder = opus_decoder_create(48000, 2, &err);
+
+ m_active = true;
+ // m_thread = std::thread(&AudioManager::testthread, this);
+}
+
+AudioManager::~AudioManager() {
+ m_active = false;
+ ma_device_uninit(&m_device);
+}
+
+void AudioManager::FeedMeOpus(const std::vector<uint8_t> &data) {
+ size_t payload_size = 0;
+ const auto *opus_encoded = StripRTPExtensionHeader(data.data(), static_cast<int>(data.size()), payload_size);
+ static std::array<opus_int16, 120 * 48 * 2 * sizeof(opus_int16)> pcm;
+ int decoded = opus_decode(m_opus_decoder, opus_encoded, static_cast<opus_int32>(payload_size), pcm.data(), 120 * 48, 0);
+ if (decoded <= 0) {
+ printf("failed decode: %d\n", decoded);
+ } else {
+ m_dumb_mutex.lock();
+ for (size_t i = 0; i < decoded * 2; i++) {
+ m_dumb.push(pcm[i]);
+ }
+ m_dumb_mutex.unlock();
+ }
+}
+
+void AudioManager::testthread() {
+}
+
+bool AudioManager::OK() const {
+ return m_ok;
+}
diff --git a/src/audio/manager.hpp b/src/audio/manager.hpp
new file mode 100644
index 0000000..ffe7ae9
--- /dev/null
+++ b/src/audio/manager.hpp
@@ -0,0 +1,35 @@
+#pragma once
+#include <atomic>
+#include <mutex>
+#include <thread>
+#include <queue>
+#include <miniaudio.h>
+#include <opus/opus.h>
+
+class AudioManager {
+public:
+ AudioManager();
+ ~AudioManager();
+
+ void FeedMeOpus(const std::vector<uint8_t> &data);
+
+ [[nodiscard]] bool OK() const;
+
+private:
+ friend void data_callback(ma_device *, void *, const void *, ma_uint32);
+
+ std::atomic<bool> m_active;
+ void testthread();
+ std::thread m_thread;
+
+ bool m_ok;
+
+ ma_engine m_engine;
+ ma_device m_device;
+ ma_device_config m_device_config;
+
+ std::mutex m_dumb_mutex;
+ std::queue<int16_t> m_dumb;
+
+ OpusDecoder *m_opus_decoder;
+};
diff --git a/src/components/channels.cpp b/src/components/channels.cpp
index 497c021..566ebd1 100644
--- a/src/components/channels.cpp
+++ b/src/components/channels.cpp
@@ -36,7 +36,7 @@ ChannelList::ChannelList()
const auto type = row[m_columns.m_type];
// text channels should not be allowed to be collapsed
// maybe they should be but it seems a little difficult to handle expansion to permit this
- if (type != RenderType::TextChannel) {
+ if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) {
if (row[m_columns.m_expanded]) {
m_view.collapse_row(path);
row[m_columns.m_expanded] = false;
@@ -161,6 +161,15 @@ ChannelList::ChannelList()
m_menu_channel.append(m_menu_channel_copy_id);
m_menu_channel.show_all();
+ m_menu_voice_channel_join.signal_activate().connect([this]() {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ printf("join voice: %llu\n", static_cast<uint64_t>(id));
+ m_signal_action_join_voice_channel.emit(id);
+ });
+
+ m_menu_voice_channel.append(m_menu_voice_channel_join);
+ m_menu_voice_channel.show_all();
+
m_menu_dm_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
@@ -579,7 +588,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel_ : *guild.Channels) {
const auto channel = discord.GetChannel(channel_.ID);
if (!channel.has_value()) continue;
- if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) {
+ if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) {
if (channel->ParentID.has_value())
categories[*channel->ParentID].push_back(*channel);
else
@@ -607,7 +616,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel : orphan_channels) {
auto channel_row = *m_model->append(guild_row.children());
- channel_row[m_columns.m_type] = RenderType::TextChannel;
+ if (IsTextChannel(channel.Type))
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ else
+ channel_row[m_columns.m_type] = RenderType::VoiceChannel;
channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
@@ -630,7 +642,10 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
for (const auto &channel : channels) {
auto channel_row = *m_model->append(cat_row.children());
- channel_row[m_columns.m_type] = RenderType::TextChannel;
+ if (IsTextChannel(channel.Type))
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ else
+ channel_row[m_columns.m_type] = RenderType::VoiceChannel;
channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position;
@@ -856,6 +871,10 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
OnChannelSubmenuPopup();
m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
+ case RenderType::VoiceChannel:
+ OnVoiceChannelSubmenuPopup();
+ m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
case RenderType::DM: {
OnDMSubmenuPopup();
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
@@ -947,6 +966,9 @@ void ChannelList::OnChannelSubmenuPopup() {
m_menu_channel_toggle_mute.set_label("Mute");
}
+void ChannelList::OnVoiceChannelSubmenuPopup() {
+}
+
void ChannelList::OnDMSubmenuPopup() {
auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
@@ -997,6 +1019,12 @@ ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new
}
#endif
+#ifdef WITH_VOICE
+ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() {
+ return m_signal_action_join_voice_channel;
+}
+#endif
+
ChannelList::ModelColumns::ModelColumns() {
add(m_type);
add(m_id);
diff --git a/src/components/channels.hpp b/src/components/channels.hpp
index 53a68c9..53afbdc 100644
--- a/src/components/channels.hpp
+++ b/src/components/channels.hpp
@@ -125,6 +125,9 @@ protected:
Gtk::MenuItem m_menu_channel_open_tab;
#endif
+ Gtk::Menu m_menu_voice_channel;
+ Gtk::MenuItem m_menu_voice_channel_join;
+
Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
@@ -145,6 +148,7 @@ protected:
void OnGuildSubmenuPopup();
void OnCategorySubmenuPopup();
void OnChannelSubmenuPopup();
+ void OnVoiceChannelSubmenuPopup();
void OnDMSubmenuPopup();
void OnThreadSubmenuPopup();
@@ -166,6 +170,11 @@ public:
type_signal_action_open_new_tab signal_action_open_new_tab();
#endif
+#ifdef WITH_VOICE
+ using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>;
+ type_signal_action_join_voice_channel signal_action_join_voice_channel();
+#endif
+
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
@@ -178,4 +187,8 @@ private:
#ifdef WITH_LIBHANDY
type_signal_action_open_new_tab m_signal_action_open_new_tab;
#endif
+
+#ifdef WITH_VOICE
+ type_signal_action_join_voice_channel m_signal_action_join_voice_channel;
+#endif
};
diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp
index 9afce8a..e9c43aa 100644
--- a/src/components/channelscellrenderer.cpp
+++ b/src/components/channelscellrenderer.cpp
@@ -65,6 +65,8 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
case RenderType::Thread:
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
+ case RenderType::VoiceChannel:
+ return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width);
case RenderType::DMHeader:
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
case RenderType::DM:
@@ -82,6 +84,8 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
case RenderType::Thread:
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
+ case RenderType::VoiceChannel:
+ return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width);
case RenderType::DMHeader:
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
case RenderType::DM:
@@ -99,6 +103,8 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
case RenderType::Thread:
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
+ case RenderType::VoiceChannel:
+ return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height);
case RenderType::DMHeader:
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
case RenderType::DM:
@@ -116,6 +122,8 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
case RenderType::Thread:
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
+ case RenderType::VoiceChannel:
+ return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height);
case RenderType::DMHeader:
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
case RenderType::DM:
@@ -133,6 +141,8 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
case RenderType::Thread:
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
+ case RenderType::VoiceChannel:
+ return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags);
case RenderType::DMHeader:
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
case RenderType::DM:
@@ -499,6 +509,39 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Contex
}
}
+// voice channel
+
+void CellRendererChannels::get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
+ m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width);
+}
+
+void CellRendererChannels::get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
+}
+
+void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
+ m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
+}
+
+void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ Gtk::Requisition minimum_size, natural_size;
+ m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
+
+ const int text_x = background_area.get_x() + 21;
+ const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
+ const int text_w = natural_size.width;
+ const int text_h = natural_size.height;
+
+ Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
+ m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#0f0");
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+ m_renderer_text.property_foreground_set() = false;
+}
+
// dm header
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp
index e2be9b2..e77cf47 100644
--- a/src/components/channelscellrenderer.hpp
+++ b/src/components/channelscellrenderer.hpp
@@ -10,6 +10,7 @@ enum class RenderType : uint8_t {
Category,
TextChannel,
Thread,
+ VoiceChannel,
DMHeader,
DM,
@@ -83,6 +84,19 @@ protected:
const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags);
+#ifdef WITH_VOICE
+ // voice channel
+ void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
+ void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
+ void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
+ void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
+ void render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr,
+ Gtk::Widget &widget,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags);
+#endif
+
// dm header
void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp
index 561b25b..ed9b999 100644
--- a/src/discord/discord.cpp
+++ b/src/discord/discord.cpp
@@ -1169,6 +1169,16 @@ void DiscordClient::AcceptVerificationGate(Snowflake guild_id, VerificationGateI
});
}
+void DiscordClient::ConnectToVoice(Snowflake channel_id) {
+ auto channel = GetChannel(channel_id);
+ if (!channel.has_value() || !channel->GuildID.has_value()) return;
+ VoiceStateUpdateMessage m;
+ m.GuildID = *channel->GuildID;
+ m.ChannelID = channel_id;
+ m.PreferredRegion = "newark";
+ m_websocket.Send(m);
+}
+
void DiscordClient::SetReferringChannel(Snowflake id) {
if (!id.IsValid()) {
m_http.SetPersistentHeader("Referer", "https://discord.com/channels/@me");
@@ -1488,6 +1498,12 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::GUILD_MEMBERS_CHUNK: {
HandleGatewayGuildMembersChunk(m);
} break;
+ case GatewayEvent::VOICE_STATE_UPDATE: {
+ HandleGatewayVoiceStateUpdate(m);
+ } break;
+ case GatewayEvent::VOICE_SERVER_UPDATE: {
+ HandleGatewayVoiceServerUpdate(m);
+ } break;
}
} break;
default:
@@ -2098,6 +2114,25 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) {
m_store.EndTransaction();
}
+void DiscordClient::HandleGatewayVoiceStateUpdate(const GatewayMessage &msg) {
+ VoiceStateUpdateData data = msg.Data;
+ if (data.UserID == m_user_data.ID) {
+ printf("voice session id: %s\n", data.SessionID.c_str());
+ m_voice.SetSessionID(data.SessionID);
+ }
+}
+
+void DiscordClient::HandleGatewayVoiceServerUpdate(const GatewayMessage &msg) {
+ VoiceServerUpdateData data = msg.Data;
+ printf("endpoint: %s\n", data.Endpoint.c_str());
+ printf("token: %s\n", data.Token.c_str());
+ m_voice.SetEndpoint(data.Endpoint);
+ m_voice.SetToken(data.Token);
+ m_voice.SetServerID(data.GuildID);
+ m_voice.SetUserID(m_user_data.ID);
+ m_voice.Start();
+}
+
void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) {
ReadySupplementalData data = msg.Data;
for (const auto &p : data.MergedPresences.Friends) {
@@ -2589,6 +2624,8 @@ void DiscordClient::LoadEventMap() {
m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK;
m_event_map["USER_GUILD_SETTINGS_UPDATE"] = GatewayEvent::USER_GUILD_SETTINGS_UPDATE;
m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK;
+ m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE;
+ m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE;
}
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {
diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp
index 70c2d82..a6eabd9 100644
--- a/src/discord/discord.hpp
+++ b/src/discord/discord.hpp
@@ -1,9 +1,11 @@
#pragma once
-#include "websocket.hpp"
+#include "chatsubmitparams.hpp"
+#include "waiter.hpp"
#include "httpclient.hpp"
#include "objects.hpp"
#include "store.hpp"
-#include "chatsubmitparams.hpp"
+#include "voiceclient.hpp"
+#include "websocket.hpp"
#include <sigc++/sigc++.h>
#include <nlohmann/json.hpp>
#include <thread>
@@ -18,31 +20,6 @@
#undef GetMessage
#endif
-class HeartbeatWaiter {
-public:
- template<class R, class P>
- bool wait_for(std::chrono::duration<R, P> const &time) const {
- std::unique_lock<std::mutex> lock(m);
- return !cv.wait_for(lock, time, [&] { return terminate; });
- }
-
- void kill() {
- std::unique_lock<std::mutex> lock(m);
- terminate = true;
- cv.notify_all();
- }
-
- void revive() {
- std::unique_lock<std::mutex> lock(m);
- terminate = false;
- }
-
-private:
- mutable std::condition_variable cv;
- mutable std::mutex m;
- bool terminate = false;
-};
-
class Abaddon;
class DiscordClient {
friend class Abaddon;
@@ -204,6 +181,8 @@ public:
void GetVerificationGateInfo(Snowflake guild_id, const sigc::slot<void(std::optional<VerificationGateInfoObject>)> &callback);
void AcceptVerificationGate(Snowflake guild_id, VerificationGateInfoObject info, const sigc::slot<void(DiscordError code)> &callback);
+ void ConnectToVoice(Snowflake channel_id);
+
void SetReferringChannel(Snowflake id);
void SetBuildNumber(uint32_t build_number);
@@ -283,6 +262,8 @@ private:
void HandleGatewayMessageAck(const GatewayMessage &msg);
void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg);
void HandleGatewayGuildMembersChunk(const GatewayMessage &msg);
+ void HandleGatewayVoiceStateUpdate(const GatewayMessage &msg);
+ void HandleGatewayVoiceServerUpdate(const GatewayMessage &msg);
void HandleGatewayReadySupplemental(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg);
@@ -338,13 +319,15 @@ private:
std::thread m_heartbeat_thread;
std::atomic<int> m_last_sequence = -1;
std::atomic<int> m_heartbeat_msec = 0;
- HeartbeatWaiter m_heartbeat_waiter;
+ Waiter m_heartbeat_waiter;
std::atomic<bool> m_heartbeat_acked = true;
bool m_reconnecting = false; // reconnecting either to resume or reidentify
bool m_wants_resume = false; // reconnecting specifically to resume
std::string m_session_id;
+ DiscordVoiceClient m_voice;
+
mutable std::mutex m_msg_mutex;
Glib::Dispatcher m_msg_dispatch;
std::queue<std::string> m_msg_queue;
diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp
index e43e05a..e4c61c5 100644
--- a/src/discord/objects.cpp
+++ b/src/discord/objects.cpp
@@ -640,3 +640,24 @@ void from_json(const nlohmann::json &j, GuildMembersChunkData &m) {
JS_D("members", m.Members);
JS_D("guild_id", m.GuildID);
}
+
+void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m) {
+ j["op"] = GatewayOp::VoiceStateUpdate;
+ j["d"]["guild_id"] = m.GuildID;
+ j["d"]["channel_id"] = m.ChannelID;
+ j["d"]["self_mute"] = m.SelfMute;
+ j["d"]["self_deaf"] = m.SelfDeaf;
+ j["d"]["self_video"] = m.SelfVideo;
+ j["d"]["preferred_region"] = m.PreferredRegion;
+}
+
+void from_json(const nlohmann::json &j, VoiceStateUpdateData &m) {
+ JS_ON("user_id", m.UserID);
+ JS_ON("session_id", m.SessionID);
+}
+
+void from_json(const nlohmann::json &j, VoiceServerUpdateData &m) {
+ JS_D("token", m.Token);
+ JS_D("guild_id", m.GuildID);
+ JS_D("endpoint", m.Endpoint);
+}
diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp
index 9db9369..240b4c5 100644
--- a/src/discord/objects.hpp
+++ b/src/discord/objects.hpp
@@ -100,6 +100,8 @@ enum class GatewayEvent : int {
MESSAGE_ACK,
USER_GUILD_SETTINGS_UPDATE,
GUILD_MEMBERS_CHUNK,
+ VOICE_STATE_UPDATE,
+ VOICE_SERVER_UPDATE,
};
enum class GatewayCloseCode : uint16_t {
@@ -864,3 +866,29 @@ struct GuildMembersChunkData {
friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m);
};
+
+struct VoiceStateUpdateMessage {
+ Snowflake GuildID;
+ Snowflake ChannelID;
+ bool SelfMute = false;
+ bool SelfDeaf = false;
+ bool SelfVideo = false;
+ std::string PreferredRegion;
+
+ friend void to_json(nlohmann::json &j, const VoiceStateUpdateMessage &m);
+};
+
+struct VoiceStateUpdateData {
+ Snowflake UserID;
+ std::string SessionID;
+
+ friend void from_json(const nlohmann::json &j, VoiceStateUpdateData &m);
+};
+
+struct VoiceServerUpdateData {
+ std::string Token;
+ Snowflake GuildID;
+ std::string Endpoint;
+
+ friend void from_json(const nlohmann::json &j, VoiceServerUpdateData &m);
+};
diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp
new file mode 100644
index 0000000..162a5a1
--- /dev/null
+++ b/src/discord/voiceclient.cpp
@@ -0,0 +1,372 @@
+#include "voiceclient.hpp"
+#include "json.hpp"
+#include <sodium.h>
+#include "abaddon.hpp"
+#include "audio/manager.hpp"
+
+UDPSocket::UDPSocket() {
+ m_socket = socket(AF_INET, SOCK_DGRAM, 0);
+}
+
+UDPSocket::~UDPSocket() {
+ Stop();
+}
+
+void UDPSocket::Connect(std::string_view ip, uint16_t port) {
+ std::memset(&m_server, 0, sizeof(m_server));
+ m_server.sin_family = AF_INET;
+ m_server.sin_addr.S_un.S_addr = inet_addr(ip.data());
+ m_server.sin_port = htons(port);
+ bind(m_socket, reinterpret_cast<sockaddr *>(&m_server), sizeof(m_server));
+}
+
+void UDPSocket::Run() {
+ m_running = true;
+ m_thread = std::thread(&UDPSocket::ReadThread, this);
+}
+
+void UDPSocket::SetSecretKey(std::array<uint8_t, 32> key) {
+ m_secret_key = key;
+}
+
+void UDPSocket::SetSSRC(uint32_t ssrc) {
+ m_ssrc = ssrc;
+}
+
+void UDPSocket::SendEncrypted(const std::vector<uint8_t> &data) {
+ m_sequence++;
+ m_timestamp += (48000 / 100) * 2;
+
+ std::vector<uint8_t> rtp(12, 0);
+ rtp[0] = 0x80; // ver 2
+ rtp[1] = 0x78; // payload type 0x78
+ rtp[2] = (m_sequence >> 8) & 0xFF;
+ rtp[3] = (m_sequence >> 0) & 0xFF;
+ rtp[4] = (m_timestamp >> 24) & 0xFF;
+ rtp[5] = (m_timestamp >> 16) & 0xFF;
+ rtp[6] = (m_timestamp >> 8) & 0xFF;
+ rtp[7] = (m_timestamp >> 0) & 0xFF;
+ rtp[8] = (m_ssrc >> 24) & 0xFF;
+ rtp[9] = (m_ssrc >> 16) & 0xFF;
+ rtp[10] = (m_ssrc >> 8) & 0xFF;
+ rtp[11] = (m_ssrc >> 0) & 0xFF;
+
+ static std::array<uint8_t, 24> nonce = {};
+ std::memcpy(nonce.data(), rtp.data(), 12);
+
+ std::vector<uint8_t> ciphertext(crypto_secretbox_MACBYTES + rtp.size(), 0);
+ crypto_secretbox_easy(ciphertext.data(), rtp.data(), rtp.size(), nonce.data(), m_secret_key.data());
+ rtp.insert(rtp.end(), ciphertext.begin(), ciphertext.end());
+
+ Send(rtp.data(), rtp.size());
+}
+
+void UDPSocket::Send(const uint8_t *data, size_t len) {
+ sendto(m_socket, reinterpret_cast<const char *>(data), static_cast<int>(len), 0, reinterpret_cast<sockaddr *>(&m_server), sizeof(m_server));
+}
+
+std::vector<uint8_t> UDPSocket::Receive() {
+ while (true) {
+ sockaddr_in from;
+ int fromlen = sizeof(from);
+ static std::array<uint8_t, 4096> buf;
+ int n = recvfrom(m_socket, reinterpret_cast<char *>(buf.data()), sizeof(buf), 0, reinterpret_cast<sockaddr *>(&from), &fromlen);
+ if (n < 0) {
+ return {};
+ } else if (from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) {
+ return { buf.begin(), buf.begin() + n };
+ }
+ }
+}
+
+void UDPSocket::Stop() {
+ m_running = false;
+ shutdown(m_socket, SD_BOTH);
+ if (m_thread.joinable()) m_thread.join();
+}
+
+void UDPSocket::ReadThread() {
+ while (m_running) {
+ static std::array<uint8_t, 4096> buf;
+ sockaddr_in from;
+ int addrlen = sizeof(from);
+ int n = recvfrom(m_socket, reinterpret_cast<char *>(buf.data()), sizeof(buf), 0, reinterpret_cast<sockaddr *>(&from), &addrlen);
+ if (n > 0 && from.sin_addr.S_un.S_addr == m_server.sin_addr.S_un.S_addr && from.sin_port == m_server.sin_port) {
+ m_signal_data.emit({ buf.begin(), buf.begin() + n });
+ }
+ }
+}
+
+UDPSocket::type_signal_data UDPSocket::signal_data() {
+ return m_signal_data;
+}
+
+DiscordVoiceClient::DiscordVoiceClient() {
+ sodium_init();
+
+ m_ws.signal_open().connect([this]() {
+ puts("vws open");
+ });
+
+ m_ws.signal_close().connect([this](uint16_t code) {
+ printf("vws close %u\n", code);
+ });
+
+ m_ws.signal_message().connect([this](const std::string &str) {
+ std::lock_guard<std::mutex> _(m_dispatch_mutex);
+ m_message_queue.push(str);
+ m_dispatcher.emit();
+ });
+
+ m_udp.signal_data().connect([this](const std::vector<uint8_t> &data) {
+ std::lock_guard<std::mutex> _(m_udp_dispatch_mutex);
+ m_udp_message_queue.push(data);
+ m_udp_dispatcher.emit();
+ });
+
+ m_dispatcher.connect([this]() {
+ m_dispatch_mutex.lock();
+ if (m_message_queue.empty()) {
+ m_dispatch_mutex.unlock();
+ return;
+ }
+ auto msg = std::move(m_message_queue.front());
+ m_message_queue.pop();
+ m_dispatch_mutex.unlock();
+ OnGatewayMessage(msg);
+ });
+
+ m_udp_dispatcher.connect([this]() {
+ m_udp_dispatch_mutex.lock();
+ if (m_udp_message_queue.empty()) {
+ m_udp_dispatch_mutex.unlock();
+ return;
+ }
+ auto data = std::move(m_udp_message_queue.front());
+ m_udp_message_queue.pop();
+ m_udp_dispatch_mutex.unlock();
+ OnUDPData(data);
+ });
+}
+
+DiscordVoiceClient::~DiscordVoiceClient() {
+ m_ws.Stop();
+ m_udp.Stop();
+ m_heartbeat_waiter.kill();
+ if (m_heartbeat_thread.joinable()) m_heartbeat_thread.join();
+}
+
+void DiscordVoiceClient::Start() {
+ m_ws.StartConnection("wss://" + m_endpoint + "/?v=7");
+}
+
+void DiscordVoiceClient::SetSessionID(std::string_view session_id) {
+ m_session_id = session_id;
+}
+
+void DiscordVoiceClient::SetEndpoint(std::string_view endpoint) {
+ m_endpoint = endpoint;
+}
+
+void DiscordVoiceClient::SetToken(std::string_view token) {
+ m_token = token;
+}
+
+void DiscordVoiceClient::SetServerID(Snowflake id) {
+ m_server_id = id;
+}
+
+void DiscordVoiceClient::SetUserID(Snowflake id) {
+ m_user_id = id;
+}
+
+void DiscordVoiceClient::OnGatewayMessage(const std::string &str) {
+ VoiceGatewayMessage msg = nlohmann::json::parse(str);
+ puts(msg.Data.dump(4).c_str());
+ switch (msg.Opcode) {
+ case VoiceGatewayOp::Hello: {
+ HandleGatewayHello(msg);
+ } break;
+ case VoiceGatewayOp::Ready: {
+ HandleGatewayReady(msg);
+ } break;
+ case VoiceGatewayOp::SessionDescription: {
+ HandleGatewaySessionDescription(msg);
+ } break;
+ default: break;
+ }
+}
+
+void DiscordVoiceClient::HandleGatewayHello(const VoiceGatewayMessage &m) {
+ VoiceHelloData d = m.Data;
+ m_heartbeat_msec = d.HeartbeatInterval;
+ m_heartbeat_thread = std::thread(&DiscordVoiceClient::HeartbeatThread, this);
+
+ Identify();
+}
+
+void DiscordVoiceClient::HandleGatewayReady(const VoiceGatewayMessage &m) {
+ VoiceReadyData d = m.Data;
+ m_ip = d.IP;
+ m_port = d.Port;
+ m_ssrc = d.SSRC;
+ if (std::find(d.Modes.begin(), d.Modes.end(), "xsalsa20_poly1305") == d.Modes.end()) {
+ puts("xsalsa20_poly1305 not in encryption modes");
+ }
+ printf("connect to %s:%u ssrc %u\n", m_ip.c_str(), m_port, m_ssrc);
+
+ m_udp.Connect(m_ip, m_port);
+
+ Discovery();
+}
+
+void DiscordVoiceClient::HandleGatewaySessionDescription(const VoiceGatewayMessage &m) {
+ VoiceSessionDescriptionData d = m.Data;
+ printf("receiving with %s secret key: ", d.Mode.c_str());
+ for (auto b : d.SecretKey) {
+ printf("%02X", b);
+ }
+ printf("\n");
+ m_secret_key = d.SecretKey;
+ m_udp.SetSSRC(m_ssrc);
+ m_udp.SetSecretKey(m_secret_key);
+ m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE });
+ m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE });
+ m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE });
+ m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE });
+ m_udp.SendEncrypted({ 0xF8, 0xFF, 0xFE });
+ m_udp.Run();
+}
+
+void DiscordVoiceClient::Identify() {
+ VoiceIdentifyMessage msg;
+ msg.ServerID = m_server_id;
+ msg.UserID = m_user_id;
+ msg.SessionID = m_session_id;
+ msg.Token = m_token;
+ msg.Video = true;
+ m_ws.Send(msg);
+}
+
+void DiscordVoiceClient::Discovery() {
+ std::vector<uint8_t> payload;
+ // 2 bytes = 1, request
+ payload.push_back(0x00);
+ payload.push_back(0x01);
+ // 2 bytes = 70, pl length
+ payload.push_back(0x00);
+ payload.push_back(70);
+ // 4 bytes = ssrc
+ payload.push_back((m_ssrc >> 24) & 0xFF);
+ payload.push_back((m_ssrc >> 16) & 0xFF);
+ payload.push_back((m_ssrc >> 8) & 0xFF);
+ payload.push_back((m_ssrc >> 0) & 0xFF);
+ // address and port
+ for (int i = 0; i < 66; i++)
+ payload.push_back(0);
+ m_udp.Send(payload.data(), payload.size());
+ auto response = m_udp.Receive();
+ if (response.size() >= 74 && response[0] == 0x00 && response[1] == 0x02) {
+ const char *our_ip = reinterpret_cast<const char *>(&response[8]);
+ uint16_t our_port = (response[73] << 8) | response[74];
+ printf("we are %s:%u\n", our_ip, our_port);
+ SelectProtocol(our_ip, our_port);
+ } else {
+ puts("received non-discovery packet after discovery");
+ }
+}
+
+void DiscordVoiceClient::SelectProtocol(std::string_view ip, uint16_t port) {
+ VoiceSelectProtocolMessage msg;
+ msg.Mode = "xsalsa20_poly1305";
+ msg.Address = ip;
+ msg.Port = port;
+ msg.Protocol = "udp";
+ m_ws.Send(msg);
+}
+
+void DiscordVoiceClient::OnUDPData(std::vector<uint8_t> data) {
+ uint8_t *payload = data.data() + 12;
+ static std::array<uint8_t, 24> nonce = {};
+ std::memcpy(nonce.data(), data.data(), 12);
+ if (crypto_secretbox_open_easy(payload, payload, data.size() - 12, nonce.data(), m_secret_key.data())) {
+ puts("decrypt fail");
+ } else {
+ Abaddon::Get().GetAudio().FeedMeOpus({ payload, payload + data.size() - 12 - crypto_box_MACBYTES });
+ }
+}
+
+void DiscordVoiceClient::HeartbeatThread() {
+ while (true) {
+ if (!m_heartbeat_waiter.wait_for(std::chrono::milliseconds(m_heartbeat_msec)))
+ break;
+
+ const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::system_clock::now().time_since_epoch())
+ .count();
+
+ VoiceHeartbeatMessage msg;
+ msg.Nonce = static_cast<uint64_t>(ms);
+ m_ws.Send(msg);
+ }
+}
+
+void from_json(const nlohmann::json &j, VoiceGatewayMessage &m) {
+ JS_D("op", m.Opcode);
+ m.Data = j.at("d");
+}
+
+void from_json(const nlohmann::json &j, VoiceHelloData &m) {
+ JS_D("heartbeat_interval", m.HeartbeatInterval);
+}
+
+void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m) {
+ j["op"] = VoiceGatewayOp::Heartbeat;
+ j["d"] = m.Nonce;
+}
+
+void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m) {
+ j["op"] = VoiceGatewayOp::Identify;
+ j["d"]["server_id"] = m.ServerID;
+ j["d"]["user_id"] = m.UserID;
+ j["d"]["session_id"] = m.SessionID;
+ j["d"]["token"] = m.Token;
+ j["d"]["video"] = m.Video;
+ j["d"]["streams"][0]["type"] = "video";
+ j["d"]["streams"][0]["rid"] = "100";
+ j["d"]["streams"][0]["quality"] = 100;
+}
+
+void from_json(const nlohmann::json &j, VoiceReadyData::VoiceStream &m) {
+ JS_D("active", m.IsActive);
+ JS_D("quality", m.Quality);
+ JS_D("rid", m.RID);
+ JS_D("rtx_ssrc", m.RTXSSRC);
+ JS_D("ssrc", m.SSRC);
+ JS_D("type", m.Type);
+}
+
+void from_json(const nlohmann::json &j, VoiceReadyData &m) {
+ JS_ON("experiments", m.Experiments);
+ JS_D("ip", m.IP);
+ JS_D("modes", m.Modes);
+ JS_D("port", m.Port);
+ JS_D("ssrc", m.SSRC);
+ JS_ON("streams", m.Streams);
+}
+
+void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m) {
+ j["op"] = VoiceGatewayOp::SelectProtocol;
+ j["d"]["address"] = m.Address;
+ j["d"]["port"] = m.Port;
+ j["d"]["protocol"] = m.Protocol;
+ j["d"]["mode"] = m.Mode;
+ j["d"]["data"]["address"] = m.Address;
+ j["d"]["data"]["port"] = m.Port;
+ j["d"]["data"]["mode"] = m.Mode;
+}
+
+void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m) {
+ JS_D("mode", m.Mode);
+ JS_D("secret_key", m.SecretKey);
+}
diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp
new file mode 100644
index 0000000..615bbde
--- /dev/null
+++ b/src/discord/voiceclient.hpp
@@ -0,0 +1,205 @@
+#pragma once
+#include "snowflake.hpp"
+#include "waiter.hpp"
+#include "websocket.hpp"
+#include <mutex>
+#include <queue>
+#include <string>
+#include <glibmm/dispatcher.h>
+
+enum class VoiceGatewayCloseCode : uint16_t {
+ UnknownOpcode = 4001,
+ InvalidPayload = 4002,
+ NotAuthenticated = 4003,
+ AuthenticationFailed = 4004,
+ AlreadyAuthenticated = 4005,
+ SessionInvalid = 4006,
+ SessionTimedOut = 4009,
+ ServerNotFound = 4011,
+ UnknownProtocol = 4012,
+ Disconnected = 4014,
+ ServerCrashed = 4015,
+ UnknownEncryption = 4016,
+};
+
+enum class VoiceGatewayOp : int {
+ Identify = 0,
+ SelectProtocol = 1,
+ Ready = 2,
+ Heartbeat = 3,
+ SessionDescription = 4,
+ Speaking = 5,
+ HeartbeatAck = 6,
+ Resume = 7,
+ Hello = 8,
+ Resumed = 9,
+ ClientDisconnect = 13,
+};
+
+struct VoiceGatewayMessage {
+ VoiceGatewayOp Opcode;
+ nlohmann::json Data;
+
+ friend void from_json(const nlohmann::json &j, VoiceGatewayMessage &m);
+};
+
+struct VoiceHelloData {
+ int HeartbeatInterval;
+
+ friend void from_json(const nlohmann::json &j, VoiceHelloData &m);
+};
+
+struct VoiceHeartbeatMessage {
+ uint64_t Nonce;
+
+ friend void to_json(nlohmann::json &j, const VoiceHeartbeatMessage &m);
+};
+
+struct VoiceIdentifyMessage {
+ Snowflake ServerID;
+ Snowflake UserID;
+ std::string SessionID;
+ std::string Token;
+ bool Video;
+ // todo streams i guess?
+
+ friend void to_json(nlohmann::json &j, const VoiceIdentifyMessage &m);
+};
+
+struct VoiceReadyData {
+ struct VoiceStream {
+ bool IsActive;
+ int Quality;
+ std::string RID;
+ int RTXSSRC;
+ int SSRC;
+ std::string Type;
+
+ friend void from_json(const nlohmann::json &j, VoiceStream &m);
+ };
+
+ std::vector<std::string> Experiments;
+ std::string IP;
+ std::vector<std::string> Modes;
+ uint16_t Port;
+ uint32_t SSRC;
+ std::vector<VoiceStream> Streams;
+
+ friend void from_json(const nlohmann::json &j, VoiceReadyData &m);
+};
+
+struct VoiceSelectProtocolMessage {
+ std::string Address;
+ uint16_t Port;
+ std::string Mode;
+ std::string Protocol;
+
+ friend void to_json(nlohmann::json &j, const VoiceSelectProtocolMessage &m);
+};
+
+struct VoiceSessionDescriptionData {
+ // std::string AudioCodec;
+ // std::string VideoCodec;
+ // std::string MediaSessionID;
+ std::string Mode;
+ std::array<uint8_t, 32> SecretKey;
+
+ friend void from_json(const nlohmann::json &j, VoiceSessionDescriptionData &m);
+};
+
+class UDPSocket {
+public:
+ UDPSocket();
+ ~UDPSocket();
+
+ void Connect(std::string_view ip, uint16_t port);
+ void Run();
+ void SetSecretKey(std::array<uint8_t, 32> key);
+ void SetSSRC(uint32_t ssrc);
+ void SendEncrypted(const std::vector<uint8_t> &data);
+ void Send(const uint8_t *data, size_t len);
+ std::vector<uint8_t> Receive();
+ void Stop();
+
+private:
+ void ReadThread();
+
+#ifdef _WIN32
+ SOCKET m_socket;
+#else
+ int m_socket;
+#endif
+ sockaddr_in m_server;
+
+ std::atomic<bool> m_running = false;
+
+ std::thread m_thread;
+
+ std::array<uint8_t, 32> m_secret_key;
+ uint32_t m_ssrc;
+
+ uint16_t m_sequence = 0;
+ uint32_t m_timestamp = 0;
+
+public:
+ using type_signal_data = sigc::signal<void, std::vector<uint8_t>>;
+ type_signal_data signal_data();
+
+private:
+ type_signal_data m_signal_data;
+};
+
+class DiscordVoiceClient {
+public:
+ DiscordVoiceClient();
+ ~DiscordVoiceClient();
+
+ void Start();
+
+ void SetSessionID(std::string_view session_id);
+ void SetEndpoint(std::string_view endpoint);
+ void SetToken(std::string_view token);
+ void SetServerID(Snowflake id);
+ void SetUserID(Snowflake id);
+
+private:
+ void OnGatewayMessage(const std::string &str);
+ void HandleGatewayHello(const VoiceGatewayMessage &m);
+ void HandleGatewayReady(const VoiceGatewayMessage &m);
+ void HandleGatewaySessionDescription(const VoiceGatewayMessage &m);
+
+ void Identify();
+ void Discovery();
+ void SelectProtocol(std::string_view ip, uint16_t port);
+
+ void OnUDPData(std::vector<uint8_t> data);
+
+ void HeartbeatThread();
+
+ std::string m_session_id;
+ std::string m_endpoint;
+ std::string m_token;
+ Snowflake m_server_id;
+ Snowflake m_user_id;
+
+ std::string m_ip;
+ uint16_t m_port;
+ uint32_t m_ssrc;
+
+ std::array<uint8_t, 32> m_secret_key;
+
+ Websocket m_ws;
+ UDPSocket m_udp;
+
+ Glib::Dispatcher m_dispatcher;
+ std::queue<std::string> m_message_queue;
+ std::mutex m_dispatch_mutex;
+
+ Glib::Dispatcher m_udp_dispatcher;
+ std::queue<std::vector<uint8_t>> m_udp_message_queue;
+ std::mutex m_udp_dispatch_mutex;
+
+ int m_heartbeat_msec;
+ Waiter m_heartbeat_waiter;
+ std::thread m_heartbeat_thread;
+};
diff --git a/src/discord/waiter.hpp b/src/discord/waiter.hpp
new file mode 100644
index 0000000..0d5ae92
--- /dev/null
+++ b/src/discord/waiter.hpp
@@ -0,0 +1,29 @@
+#pragma once
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+
+class Waiter {
+public:
+ template<class R, class P>
+ bool wait_for(std::chrono::duration<R, P> const &time) const {
+ std::unique_lock<std::mutex> lock(m);
+ return !cv.wait_for(lock, time, [&] { return terminate; });
+ }
+
+ void kill() {
+ std::unique_lock<std::mutex> lock(m);
+ terminate = true;
+ cv.notify_all();
+ }
+
+ void revive() {
+ std::unique_lock<std::mutex> lock(m);
+ terminate = false;
+ }
+
+private:
+ mutable std::condition_variable cv;
+ mutable std::mutex m;
+ bool terminate = false;
+};