feat(voice): саундборд, звуковая палитра, push-to-talk и статистика соединения
Клиентская часть Фазы 3 (AGENT.md 7.13, 7.14): звуки сервера, локальные
звуки интерфейса и телеметрия медиа.
- саундборд: `api/sounds.ts` (лимиты 512 КБ и 30 на вид, multipart-загрузка,
play, `SOUND_EVENTS`), `lib/soundboard.ts` (blob-кэш `/files/{id}`,
наложение до 8 звуков, громкость = `outputVolume`, тишина при дефене),
попап в панели комнаты (`USE_SOUNDBOARD`), таймер на `rate_limited`,
подпись «включил звук» и защита от двойного воспроизведения;
- звуковая палитра: `SOUNDBOARD_PLAY` и события сервера (вход/выход
участника, упоминание, сообщение, вход/выход из голосовой, свои
микрофон/камера/экран, серверный мьют) → `playEventSound`;
- секция «Звуки» в настройках сервера (`MANAGE_SOUNDS`): два вида со
счётчиками, drag&drop, имя, эмодзи, событие палитры, переименование,
удаление и клиентские проверки файла;
- push-to-talk: режим и клавиша (`event.code`), хук в `AppLayout`, вход в
комнату с выключенным микрофоном, удержание включает публикацию, отпускание
и потеря фокуса выключают, серверный мьют приоритетнее;
- статистика: поповер с разрешением, fps, кодеком, битрейтом, RTT, потерями
и jitter (опрос раз в секунду, остановка при закрытии), выбор качества
публикации low/medium/high, пресеты шаринга экрана 1080p60…480p30;
- данные: `guilds[].sounds` из READY, `GUILD_SOUNDS_UPDATE`, селектор
`useGuildSounds`, тумблеры звуков в «Аудио-видео» (localStorage);
- тесты: `soundboard`, `soundSettings`, `pushToTalk`, `voiceStats` и
расширенный фейковый LiveKit SDK.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { parseGuildEmojiList } from './emojis';
|
||||
import { parseGuildSoundList } from './sounds';
|
||||
import { userStatuses, type Channel, type UserStatus } from './types';
|
||||
import type { GatewayUser } from './gateway';
|
||||
import type { GuildEmoji } from './emojis';
|
||||
import type { GuildSound } from './sounds';
|
||||
import type { Message, MessageAttachment, MessageReaction } from './messages';
|
||||
|
||||
/**
|
||||
@@ -58,6 +60,22 @@ export interface GuildEmojisUpdateEvent {
|
||||
emojis: GuildEmoji[];
|
||||
}
|
||||
|
||||
export interface GuildSoundsUpdateEvent {
|
||||
guild_id: string;
|
||||
sounds: GuildSound[];
|
||||
}
|
||||
|
||||
/** Событие `SOUNDBOARD_PLAY` (AGENT.md 7.13). */
|
||||
export interface SoundboardPlayEvent {
|
||||
guild_id: string;
|
||||
channel_id: string;
|
||||
sound_id: string;
|
||||
file_id: string;
|
||||
name: string;
|
||||
user_id: string;
|
||||
played_at: string;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
@@ -203,6 +221,53 @@ export function parseGuildEmojisUpdate(value: unknown): GuildEmojisUpdateEvent |
|
||||
return { guild_id: guildId, emojis: parseGuildEmojiList(record['emojis']) };
|
||||
}
|
||||
|
||||
/**
|
||||
* `GUILD_SOUNDS_UPDATE`: набор звуков сервера изменился (AGENT.md 7.13).
|
||||
* Список приходит целиком — тем же форматом, что в снапшоте READY.
|
||||
*/
|
||||
export function parseGuildSoundsUpdate(value: unknown): GuildSoundsUpdateEvent | null {
|
||||
const record = asRecord(value);
|
||||
const guildId = asString(record?.['guild_id']);
|
||||
if (record === null || guildId === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { guild_id: guildId, sounds: parseGuildSoundList(record['sounds']) };
|
||||
}
|
||||
|
||||
/**
|
||||
* `SOUNDBOARD_PLAY`: кто-то сыграл звук в голосовой комнате. Событие
|
||||
* приходит только участникам этой комнаты, и каждый клиент играет звук сам.
|
||||
*/
|
||||
export function parseSoundboardPlay(value: unknown): SoundboardPlayEvent | null {
|
||||
const record = asRecord(value);
|
||||
if (record === null) {
|
||||
return null;
|
||||
}
|
||||
const guildId = asString(record['guild_id']);
|
||||
const channelId = asString(record['channel_id']);
|
||||
const soundId = asString(record['sound_id']);
|
||||
const fileId = asString(record['file_id']);
|
||||
const userId = asString(record['user_id']);
|
||||
if (
|
||||
guildId === undefined ||
|
||||
channelId === undefined ||
|
||||
soundId === undefined ||
|
||||
fileId === undefined ||
|
||||
userId === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
guild_id: guildId,
|
||||
channel_id: channelId,
|
||||
sound_id: soundId,
|
||||
file_id: fileId,
|
||||
name: asString(record['name']) ?? '',
|
||||
user_id: userId,
|
||||
played_at: asString(record['played_at']) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Канал из `CHANNEL_CREATE`/`CHANNEL_UPDATE` — та же форма, что в REST. */
|
||||
export function parseChannelPayload(value: unknown): Channel | null {
|
||||
const record = asRecord(value);
|
||||
|
||||
Reference in New Issue
Block a user