feat(web): второй ряд дашборда и пинг участников в голосовой комнате
- дашборд инстанса: два ряда полукругов с подписями сверху — «Контейнер инстанса» (лимиты профиля) и «Сервер целиком» (вся машина); - голосовая комната: рядом с качеством связи видно пинг участника («Отличное соединение, 42 мс») и он обновляется раз в секунду. Свой RTT измеряется по статистике WebRTC, чужой приходит по data-каналу LiveKit (`glchat.latency`, lossy), поэтому пинг виден для каждого участника; - статистика берётся с любой своей дорожки: в аудио-сессии RTT раньше был недоступен (отчёт читался только с видео); - в «Аудио-видео» появился тумблер «Показывать пинг в голосовой комнате»: выключенный не опрашивает SDK и не публикует своё значение; - тесты: ряд хоста и подписи, пинг в плитке своего и чужого участника, публикация по служебной теме, выключенный пинг не дёргает SDK.
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
import { makeVoiceState, voiceRoutes, type VoiceRoutesOptions } from './voiceRoutes';
|
||||
import type { InstanceMeta } from '@/api/meta';
|
||||
import { useVoiceConnectionStore } from '@/stores/voiceConnection';
|
||||
import { useVoiceStore } from '@/stores/voice';
|
||||
|
||||
const ALL_PERMISSIONS = [
|
||||
'VIEW_GUILD',
|
||||
@@ -197,6 +198,78 @@ describe('голосовая комната: вход', () => {
|
||||
expect(voice.joinCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('показывает пинг участника рядом с качеством связи', { timeout: 20_000 }, async () => {
|
||||
await bootVoice({ seated: [{ user_id: 'user-2', channel_id: 'c-voice' }] });
|
||||
|
||||
await clickChannel('Голосовая');
|
||||
await waitConnected();
|
||||
|
||||
// Свой пинг приходит из отчёта WebRTC (candidate-pair) и публикуется
|
||||
// в комнату; у чужого участника он приходит по data-каналу.
|
||||
const room = FakeRoom.latest();
|
||||
room.setStats([
|
||||
['pair', { type: 'candidate-pair', state: 'succeeded', currentRoundTripTime: 0.042 }],
|
||||
]);
|
||||
// Первый такт может прийтись на момент, когда микрофон ещё не опубликован,
|
||||
// поэтому ждём следующего (интервал публикации пинга — секунда).
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
within(screen.getByTestId('voice-tile-user-1')).getByTestId('voice-quality-label'),
|
||||
).toHaveTextContent('42 мс');
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
const ownLabel = within(screen.getByTestId('voice-tile-user-1')).getByTestId(
|
||||
'voice-quality-label',
|
||||
);
|
||||
expect(ownLabel).toHaveTextContent('Отличное соединение, 42 мс');
|
||||
|
||||
// Второй участник подключается к комнате и присылает свой пинг.
|
||||
const remote = room.addRemote('user-2', 'Bob');
|
||||
act(() => {
|
||||
room.emitData(
|
||||
new TextEncoder().encode(JSON.stringify({ ms: 128 })),
|
||||
remote,
|
||||
'glchat.latency',
|
||||
);
|
||||
});
|
||||
const remoteTile = await screen.findByTestId('voice-tile-user-2');
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(within(remoteTile).getByTestId('voice-quality-label')).toHaveTextContent(
|
||||
'Отличное соединение, 128 мс',
|
||||
);
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
|
||||
// Свой пинг уходит в комнату по служебной теме и без гарантии доставки.
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(room.localParticipant.data.length).toBeGreaterThan(0);
|
||||
},
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
const sent = room.localParticipant.data.at(-1);
|
||||
expect(sent?.options).toMatchObject({ topic: 'glchat.latency', reliable: false });
|
||||
expect(new TextDecoder().decode(sent?.payload ?? new Uint8Array())).toContain('"ms"');
|
||||
});
|
||||
|
||||
it('выключенный пинг участников не опрашивает SDK', async () => {
|
||||
useVoiceStore.setState({ latencySharingEnabled: false });
|
||||
await bootVoice({ seated: [{ user_id: 'user-2', channel_id: 'c-voice' }] });
|
||||
|
||||
await clickChannel('Голосовая');
|
||||
await waitConnected();
|
||||
|
||||
const track = FakeRoom.latest().localParticipant.getTrackPublication('microphone')?.track;
|
||||
const before = track?.statsRequests ?? 0;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_500));
|
||||
expect(track?.statsRequests).toBe(before);
|
||||
expect(useVoiceConnectionStore.getState().latencies).toEqual({});
|
||||
});
|
||||
|
||||
it('переходит в другую комнату через выход из старой', async () => {
|
||||
const voice = await bootVoice();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user