rudimentary voice implementation

This commit is contained in:
ouwou 2022-08-31 01:51:02 -04:00
parent 634f51fb41
commit 0fa33915da
16 changed files with 964 additions and 32 deletions

View File

@ -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 ()

View File

@ -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();

View File

@ -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;

92
src/audio/manager.cpp Normal file
View File

@ -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;
}

35
src/audio/manager.hpp Normal file
View File

@ -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;
};

View File

@ -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);

View File

@ -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
};

View File

@ -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 {

View File

@ -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;

View File

@ -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() {

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
};

372
src/discord/voiceclient.cpp Normal file
View File

@ -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);
}

205
src/discord/voiceclient.hpp Normal file
View File

@ -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;
};

29
src/discord/waiter.hpp Normal file
View File

@ -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;
};