mirror of
https://github.com/uowuo/abaddon.git
synced 2024-11-10 06:00:10 +00:00
rudimentary voice implementation
This commit is contained in:
parent
634f51fb41
commit
0fa33915da
@ -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 ()
|
||||
|
@ -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();
|
||||
|
@ -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
92
src/audio/manager.cpp
Normal 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
35
src/audio/manager.hpp
Normal 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;
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
372
src/discord/voiceclient.cpp
Normal 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
205
src/discord/voiceclient.hpp
Normal 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
29
src/discord/waiter.hpp
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user