Важные замечания:

  • ряд состояний при изменении, клиенту сообщен не будет! соответвенно, какие-то из состояний участников нужно обновлять руками. имейте это ввиду, если будете искать баги;
  • приходить будут все ключи нотификации, однако, в каждой нотификации будет задана только 1 логическая цепочка. как именно определять, что хочет сказать та или иная нотификация - проще всего по коду по описанным ниже стратегиям обработки нотификаций.

Пример нотификации:

{
"muteStates": {
"VIDEO": "MUTE_PERMANENT",
"AUDIO": "MUTE_PERMANENT"
},
"unmuteOptions": [],
"mediaOptions": [
"VIDEO",
"AUDIO"
],
"muteAll": false,
"unmute": false,
"userId": {
"deviceIdx": 0,
"id": "1000002",
"type": "USER"
},
"adminId": null,
"stateUpdated": true
}

Приведу ряд примеров о том, как нужно интерпретировать нотификации

(*) помечу те поля, по которым изменения могут происходить на сервере без нотификации клиенту

  • userId - если это поле есть, то нотификация персональная, для этого юзера, если передан null и при этом muteAll = false, то это нотификация для клиента (те вас);
  • adminId - автор нотификации, те тот, что инициировал эти изменения;
  • muteAll - значит, что нотификация общая для звонка, а не персональная;
  • stateUpdated - говорит о том, что в состоянии произошли какие то изменения, в большинстве сценариев, если это поле false, то реагировать не нужно;
  • mediaOptions - это массив тех медиа (VIDEO|AUDIO...) по которым произошли изменения. на этот флаг нужно смотреть в совокупности с другими;
  • muteStates (*) - это состояние конкретных медиа, например AUDIO: MUTE_PERMANENT, говорит о том, что на аудио наложен пермамьют. Однако, если в mediaOptions нет AUDIO, значит изменения коснулись какой то другой медиа группы. Тут нужно быть особенно внимательным, тк бекенд будет обновлять это поле в ряде случаев без уведомления клиента. Например, при введении\снятии глобальных пермамьютов;
  • unmute и unmuteOptions (*) - до конца не понятно, зачем нужен unmute, а вот в unmuteOptions будет перечень MediaOption по которым есть разовая возможность включения. По идее, эта нотификация может прийти пользователю после перезахода в звонок, те тут останутся "сохраненная" информация о разовых разьмьютах. В течение звонка, разовые мьюты будут прихожить через requestedMedia. Так же тут могут приходить оставшиеся не использованные "разовые включения", по мере их использования, но приходить они будут не сами по себе, а наряду с другими нотификациями;
  • requestedMedia - это медиа, по которым приходит просьба на размьют. через неё выдаются разовые пермиссии на влючение медиа в течение активного звонка (напомню, что при перезаходе, разовые пермиссии придут юзеру, скорее, в unmuteOptions);

Возможные стратегии обработки входящих нотификаций:

  • во всех примерах state = редакс стор;
  1. Изменение глобальных пермиссий
    • На изменение каких ключей смотрим:
      • muteStates состояния мьютов;
      • mediaOptions по каким медиа пришли изменения;
    • Последствия:
      • Если клиент = (админ \ создатель) звонка, нужно получить состояния других участников, тк приходить до этого к нему они не будут!
// при входе в звонок может прилететь кеш из sdk
if (!stateUpdated) {
// стейт не обновился - пропускаем
return;
}

if (!muteAll) {
// состояние обновилось для меня - обработать позже
return;
}

// ...обновляем состояние пермиссий звонка

// если мы администратор или создатель
// то нужно получить состояния отдельно взятых участников (девайсов)
if (isAdmin(state.myUserExternalId) || isCreator(state.myUserExternalId)) {
// если нет чанков - SDK.getParticipants
// с чанками - SDK.getParticipantListChunk
}

// ...обновляем состояния участников (девайсов)
  1. Изменение персональных пермиссий
    • На изменение каких ключей смотрим:
      • muteStates состояния мьютов;
      • mediaOptions по каким медиа пришли изменения;
if (userId && !isSameExternalId(userId, state.externalId)) {
// нотификация не для меня, обработаем в другом месте
return;
}

if (adminId && isSameExternalId(adminId, state.externalId)) {
// я = отправитель, не обрабатываем
return;
}

if (!stateUpdated) {
// стейт не обновился - пропускаем
return;
}

if (!muteAll && !!requestedMedia) {
// признак, что в нотификации пришло "привлечение внимания" - пропускаем
return;
}

// ...выставляем персональные пермиссии
  1. Изменение чужих персональных пермиссий
    • На изменение каких ключей смотрим:
      • userId юзер, для которого пришел апдейт;
      • muteStates состояния мьютов;
      • mediaOptions по каким медиа пришли изменения;
      • unmuteOptions тут могут лежать разовые пермиссии по участнику (актуально при перезаходе в звонок);
    • Какие последствия:
      • клиенту выдают разовые пермиссии по выбранным медиа;
      • клиенту выдают состояния мьютов по выбранным медиа;
if (!userId || isSameExternalId(userId, state.externalId)) {
// нотификация не относится к другому участнику - skip
return;
}

if (!stateUpdated) {
return;
}

// ...выставляем пермиссии участника
  1. Изменение персональных разовых пермиссий
    • На изменение каких ключей смотрим:
      • unmuteOptions тут могут лежать разовые пермиссии по участнику
    • Какие последствия:
      • клиенту выдают разовые пермиссии по выбранным медиа;
if (userId && !isSameExternalId(userId, state.externalId)) {
// нотификация не для меня - skip
return;
}

if (adminId && isSameExternalId(adminId, state.externalId)) {
// я = отправитель команды - skip
return;
}

if (requestedMedia?.length) {
// нотификация с привлечением внимания - skip
return;
}

if (!(stateUpdated && unmuteOptions.length)) {
// стейт не обновился или unmuteOptions пустой
return;
}

// ...выставляем персональные разовые пермиссии
  1. Привлечение внимания
    • На изменение каких ключей смотрим:
      • requestedMedia по каким медиа привлечено внимание;
      • muteAll если true - глобальное, false - персональное;
    • Какие последствия применения:
      • По выбранным медиа выдается разовая пермиссия;
      • Если была поднята рука при получении пермиссии - она опустится;
if (userId && !isSameExternalId(userId, state.externalId)) {
// нотификация не для меня - skip
return;
}

if (adminId && isSameExternalId(adminId, state.externalId)) {
// я = отправитель команды - skip
return;
}

if (!requestedMedia || !requestedMedia.length) {
// внимание не было привлечено - skip
return;
}

// ...выставляем привлечение внимания

Особенности применения:

  • при отправке muteStates нельзя объединять разные значение, например, MUTE и MUTE_PERMANENT, те если вам нужно выдать пермамьют и разово отключить медиа - отправляем 2 сигнала;
  • есть события, на которых нужно снижать разовую пермиссию по переданным mediaSettings.(isAudioEnabled|isVideoEnabled|isScreenSharingEnabled):
    • onScreenStream
    • onLocalStreamUpdate
    • onRemoteMediaSettings
  • события, в которых могут приходить состояния мьютов участников:
    • onMuteStates
    • onConversation - в массивеparticipants
    • SDK.getParticipants - в массивеparticipants
    • onConversationParticipantListChunk - в массивеchunk

Как понять, что у меня разблокировано медиа устройство?

// если я создатель или админ - на меня не действуют ограничения
if (isAdmin(me) || isCreator(me)) {
return true;
}

// если на меня или на звонок выдан MUTE_PERMANENT
if (
callPermissions[mediaOption] === MuteState.MUTE_PERMANENT ||
myPermissions?.[mediaOption] === MuteState.MUTE_PERMANENT
) {
// проверяем наличие разовой пермиссии
return !!permissionsOnce?.has(mediaOption);
}

// если мы тут, значит - можно
return true;