feat(web): второй ряд дашборда и пинг участников в голосовой комнате
- дашборд инстанса: два ряда полукругов с подписями сверху — «Контейнер инстанса» (лимиты профиля) и «Сервер целиком» (вся машина); - голосовая комната: рядом с качеством связи видно пинг участника («Отличное соединение, 42 мс») и он обновляется раз в секунду. Свой RTT измеряется по статистике WebRTC, чужой приходит по data-каналу LiveKit (`glchat.latency`, lossy), поэтому пинг виден для каждого участника; - статистика берётся с любой своей дорожки: в аудио-сессии RTT раньше был недоступен (отчёт читался только с видео); - в «Аудио-видео» появился тумблер «Показывать пинг в голосовой комнате»: выключенный не опрашивает SDK и не публикует своё значение; - тесты: ряд хоста и подписи, пинг в плитке своего и чужого участника, публикация по служебной теме, выключенный пинг не дёргает SDK.
This commit is contained in:
@@ -59,6 +59,11 @@ export interface VoicePreferences {
|
||||
eventSoundsEnabled: boolean;
|
||||
/** Саундборд включён. */
|
||||
soundboardEnabled: boolean;
|
||||
/**
|
||||
* Пинг участников: показывать задержку в комнате и делиться своей
|
||||
* (AGENT.md 7.14). Выключено — SDK не опрашивается вовсе.
|
||||
*/
|
||||
latencySharingEnabled: boolean;
|
||||
/** Качество своей публикации видео. */
|
||||
videoQuality: VoicePublishQuality;
|
||||
/** Пресет разрешения и fps для демонстрации экрана. */
|
||||
@@ -81,6 +86,7 @@ interface VoiceState extends VoicePreferences {
|
||||
setPttKey: (key: PushToTalkKey) => void;
|
||||
setEventSoundsEnabled: (enabled: boolean) => void;
|
||||
setSoundboardEnabled: (enabled: boolean) => void;
|
||||
setLatencySharingEnabled: (enabled: boolean) => void;
|
||||
setVideoQuality: (quality: VoicePublishQuality) => void;
|
||||
setScreenSharePreset: (preset: ScreenSharePresetId) => void;
|
||||
/** Вернуть исходные предпочтения (используется в тестах). */
|
||||
@@ -103,6 +109,7 @@ const defaults: VoicePreferences = {
|
||||
pttKey: 'Space',
|
||||
eventSoundsEnabled: true,
|
||||
soundboardEnabled: true,
|
||||
latencySharingEnabled: true,
|
||||
videoQuality: 'high',
|
||||
screenSharePreset: '1080p30',
|
||||
};
|
||||
@@ -184,6 +191,10 @@ function initialPreferences(): VoicePreferences {
|
||||
pttKey: asChoice(stored['pttKey'], PUSH_TO_TALK_KEYS, defaults.pttKey),
|
||||
eventSoundsEnabled: asBoolean(stored['eventSoundsEnabled'], defaults.eventSoundsEnabled),
|
||||
soundboardEnabled: asBoolean(stored['soundboardEnabled'], defaults.soundboardEnabled),
|
||||
latencySharingEnabled: asBoolean(
|
||||
stored['latencySharingEnabled'],
|
||||
defaults.latencySharingEnabled,
|
||||
),
|
||||
videoQuality: asChoice(stored['videoQuality'], PUBLISH_QUALITIES, defaults.videoQuality),
|
||||
screenSharePreset: asChoice(
|
||||
stored['screenSharePreset'],
|
||||
@@ -213,6 +224,7 @@ function persist(state: VoicePreferences): void {
|
||||
pttKey: state.pttKey,
|
||||
eventSoundsEnabled: state.eventSoundsEnabled,
|
||||
soundboardEnabled: state.soundboardEnabled,
|
||||
latencySharingEnabled: state.latencySharingEnabled,
|
||||
videoQuality: state.videoQuality,
|
||||
screenSharePreset: state.screenSharePreset,
|
||||
} satisfies VoicePreferences),
|
||||
@@ -264,6 +276,7 @@ export const useVoiceStore = create<VoiceState>((set, get) => {
|
||||
setPttKey: (key) => patch({ pttKey: key }),
|
||||
setEventSoundsEnabled: (enabled) => patch({ eventSoundsEnabled: enabled }),
|
||||
setSoundboardEnabled: (enabled) => patch({ soundboardEnabled: enabled }),
|
||||
setLatencySharingEnabled: (enabled) => patch({ latencySharingEnabled: enabled }),
|
||||
setVideoQuality: (quality) => patch({ videoQuality: quality }),
|
||||
setScreenSharePreset: (preset) => patch({ screenSharePreset: preset }),
|
||||
|
||||
@@ -292,6 +305,7 @@ function preferencesOf(state: VoiceState): VoicePreferences {
|
||||
pttKey: state.pttKey,
|
||||
eventSoundsEnabled: state.eventSoundsEnabled,
|
||||
soundboardEnabled: state.soundboardEnabled,
|
||||
latencySharingEnabled: state.latencySharingEnabled,
|
||||
videoQuality: state.videoQuality,
|
||||
screenSharePreset: state.screenSharePreset,
|
||||
};
|
||||
|
||||
@@ -53,6 +53,11 @@ export interface VoiceConnectionState {
|
||||
channelId: string | null;
|
||||
/** Участники комнаты глазами SDK: речь, качество, дорожки. */
|
||||
participants: VoiceParticipant[];
|
||||
/**
|
||||
* Пинг участников до инстанса, мс: своё значение измеряем по статистике
|
||||
* WebRTC, чужие приходят по data-каналу (AGENT.md 7.14).
|
||||
*/
|
||||
latencies: Record<string, number>;
|
||||
/** Опубликован ли свой микрофон. */
|
||||
micEnabled: boolean;
|
||||
/** Локальное глушение: чужой звук не слышен. */
|
||||
@@ -105,6 +110,7 @@ const defaults = {
|
||||
guildId: null,
|
||||
channelId: null,
|
||||
participants: [] as VoiceParticipant[],
|
||||
latencies: {} as Record<string, number>,
|
||||
micEnabled: true,
|
||||
deafened: false,
|
||||
cameraEnabled: false,
|
||||
@@ -118,6 +124,8 @@ const defaults = {
|
||||
|
||||
/** Активная комната SDK. Живёт вне стора: подписки и таймеры не сериализуются. */
|
||||
let room: VoiceRoomHandle | null = null;
|
||||
/** Остановка публикации пинга: живёт вместе с активной комнатой. */
|
||||
let stopLatencyReporting: (() => void) | null = null;
|
||||
/** Канал, о котором уже знает сервер: только для него шлём PATCH и leave. */
|
||||
let joinedChannelId: string | null = null;
|
||||
/** Номер подключения: ответы устаревших операций не должны менять состояние. */
|
||||
@@ -184,6 +192,9 @@ export const useVoiceConnectionStore = create<VoiceConnectionState>((set, get) =
|
||||
|
||||
/** Отключение от SDK-комнаты без запросов: используется и в leave, и в reset. */
|
||||
const closeRoom = async (): Promise<void> => {
|
||||
stopLatencyReporting?.();
|
||||
stopLatencyReporting = null;
|
||||
set({ latencies: {} });
|
||||
const handle = room;
|
||||
room = null;
|
||||
if (handle === null) {
|
||||
@@ -396,6 +407,20 @@ export const useVoiceConnectionStore = create<VoiceConnectionState>((set, get) =
|
||||
void finishSession();
|
||||
},
|
||||
onSpeakingWhileMuted: warnSpeakingWhileMuted,
|
||||
onLatency: (identity, latency) => {
|
||||
if (mine !== generation) {
|
||||
return;
|
||||
}
|
||||
set((state) => {
|
||||
const latencies = { ...state.latencies };
|
||||
if (latency === null) {
|
||||
delete latencies[identity];
|
||||
} else {
|
||||
latencies[identity] = latency;
|
||||
}
|
||||
return { latencies };
|
||||
});
|
||||
},
|
||||
});
|
||||
if (mine !== generation) {
|
||||
handle.dispose();
|
||||
@@ -410,6 +435,10 @@ export const useVoiceConnectionStore = create<VoiceConnectionState>((set, get) =
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Пинг публикуется раз в секунду и виден всем участникам комнаты.
|
||||
stopLatencyReporting = handle.startLatencyReporting(
|
||||
() => useVoiceStore.getState().latencySharingEnabled,
|
||||
);
|
||||
await handle.setMicrophoneEnabled(preferences.micEnabled && !pushToTalk);
|
||||
await handle.setCameraEnabled(preferences.cameraEnabled);
|
||||
// Качество публикации — из предпочтений клиента.
|
||||
|
||||
Reference in New Issue
Block a user