fix(voice): читать ответ GetParticipant в формате LiveKit 1.x

RoomService отвечает объектом участника на верхнем уровне, а не в обёртке
`{"participant": …}`: дорожка микрофона не находилась и серверный мьют молча
пропускался (AGENT.md 7.14). Обёртку тоже принимаем — на случай смены формы
ответа в новых версиях.

- `participantEnvelope` разбирает оба варианта;
- тесты `internal/voice/admin_test.go`: выбор дорожки микрофона (не камеры),
  тело MutePublishedTrack, отсутствие микрофона, обёрнутый ответ,
  RemoveParticipant и выключенный клиент; в каждом вызове проверяется
  Bearer-токен, привязанный к комнате.
This commit is contained in:
2026-09-20 19:59:04 +03:00
parent a6d23a295e
commit 4ba449670a
2 changed files with 167 additions and 5 deletions
+15 -5
View File
@@ -48,16 +48,26 @@ type Participant struct {
} `json:"tracks"`
}
// participantEnvelope принимает оба формата ответа RoomService: LiveKit 1.x
// отдаёт объект участника на верхнем уровне (`GetParticipant`), а обёртку
// `{"participant": {...}}` поддерживаем на случай смены формы в новых версиях.
type participantEnvelope struct {
Participant
Wrapped *Participant `json:"participant"`
}
// ParticipantInfo отдаёт данные участника комнаты.
func (c *AdminClient) ParticipantInfo(ctx context.Context, room, identity string) (*Participant, error) {
var response struct {
Participant Participant `json:"participant"`
}
err := c.call(ctx, room, "GetParticipant", map[string]any{"room": room, "identity": identity}, &response)
var envelope participantEnvelope
err := c.call(ctx, room, "GetParticipant", map[string]any{"room": room, "identity": identity}, &envelope)
if err != nil {
return nil, err
}
return &response.Participant, nil
if envelope.Wrapped != nil && envelope.Identity == "" && len(envelope.Tracks) == 0 {
return envelope.Wrapped, nil
}
participant := envelope.Participant
return &participant, nil
}
// SetMicrophoneMuted включает или выключает микрофон участника на стороне SFU: