diff options
author | ouwou <26526779+ouwou@users.noreply.github.com> | 2022-08-31 01:51:02 -0400 |
---|---|---|
committer | ouwou <26526779+ouwou@users.noreply.github.com> | 2022-08-31 01:51:02 -0400 |
commit | 0fa33915da6255cf7460758197eaea7e43353543 (patch) | |
tree | 15a92a3aae2cd2647c24ce4c44f1aaca01fcf422 | |
parent | 634f51fb4117c0870399e73560ac313d68d281e8 (diff) | |
download | abaddon-portaudio-0fa33915da6255cf7460758197eaea7e43353543.tar.gz abaddon-portaudio-0fa33915da6255cf7460758197eaea7e43353543.zip |
rudimentary voice implementation
-rw-r--r-- | CMakeLists.txt | 9 | ||||
-rw-r--r-- | src/abaddon.cpp | 18 | ||||
-rw-r--r-- | src/abaddon.hpp | 5 | ||||
-rw-r--r-- | src/audio/manager.cpp | 92 | ||||
-rw-r--r-- | src/audio/manager.hpp | 35 | ||||
-rw-r--r-- | src/components/channels.cpp | 36 | ||||
-rw-r--r-- | src/components/channels.hpp | 13 | ||||
-rw-r--r-- | src/components/channelscellrenderer.cpp | 43 | ||||
-rw-r--r-- | src/components/channelscellrenderer.hpp | 14 | ||||
-rw-r--r-- | src/discord/discord.cpp | 37 | ||||
-rw-r--r-- | src/discord/discord.hpp | 39 | ||||
-rw-r--r-- | src/discord/objects.cpp | 21 | ||||
-rw-r--r-- | src/discord/objects.hpp | 28 | ||||
-rw-r--r-- | src/discord/voiceclient.cpp | 372 | ||||
-rw-r--r-- | src/discord/voiceclient.hpp | 205 | ||||
-rw-r--r-- | src/discord/waiter.hpp | 29 |
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; +}; |