updateDisplayLayout
Для того чтобы приходили видео-стримы от собеседников, нужно отправлять команду updateDisplayLayout. В противном случае видео от других участников звонка может не приходить вообще.
Основной принцип
Команда updateDisplayLayout указывает, от каких собеседников нужно получать видео и в каких размерах. Также она позволяет остановить видео от собеседника.
Важно
updateDisplayLayout ожидает только изменения (diff) относительно предыдущего состояния, а не полный список всех участников. Отправляйте только те layouts, которые изменились.
Параметры layout
interface ParticipantLayout {
uid: ExternalParticipantId; // ID участника
mediaType?: MediaType; // CAMERA | SCREEN | MOVIE | ANIMOJI
width?: number; // Желаемая ширина видео
height?: number; // Желаемая высота видео
fit?: 'cv' | 'cn'; // cv = cover, cn = contain
stopStream?: boolean; // Остановить получение видео
streamName?: string; // Для совместного просмотра (movieId)
}Базовое использование
import { MediaType } from '@vkontakte/calls-sdk';
// Запросить видео от участника
SDK.updateDisplayLayout([{
uid: userId,
mediaType: MediaType.CAMERA,
width: 1920,
height: 1080,
fit: 'cn' // contain
}]);
// Остановить получение видео
SDK.updateDisplayLayout([{
uid: userId,
mediaType: MediaType.CAMERA,
stopStream: true
}]);Оптимизация: отправка только изменений
SDK ожидает, что вы будете отправлять только изменившиеся layouts. Для этого нужно:
- Хранить предыдущее состояние каждого участника
- Сравнивать новое состояние с предыдущим
- Отправлять только разницу
- Использовать throttle для ограничения частоты вызовов
Пример оптимизированной реализации
import { MediaType } from '@vkontakte/calls-sdk';
import { throttle } from 'throttle-debounce';
class UpdateDisplayLayoutManager {
constructor() {
// Хранилище предыдущих layouts для каждого участника
this.prevLayouts = new Map();
}
// Throttle: не чаще чем раз в 250мс
throttledUpdate = throttle(250, (layouts) => {
if (layouts.length > 0) {
SDK.updateDisplayLayout(layouts);
}
});
// Создать layout для запроса видео
getStartLayout(userId, mediaType, width, height, cover = false) {
return {
uid: userId,
mediaType,
width,
height,
fit: cover ? 'cv' : 'cn'
};
}
// Создать layout для остановки видео
getStopLayout(userId, mediaType) {
return {
uid: userId,
mediaType,
stopStream: true
};
}
// Проверить, является ли layout запросом на видео (не stop)
isStartLayout(layout) {
return 'width' in layout && 'height' in layout;
}
// Вычислить разницу между текущим и предыдущим состоянием
computeDiff(participantId, newLayouts) {
const prev = this.prevLayouts.get(participantId) || {};
const diff = [];
for (const [mediaType, layout] of Object.entries(newLayouts)) {
const prevLayout = prev[mediaType];
// Нет предыдущего — добавляем
if (!prevLayout) {
diff.push(layout);
continue;
}
// Изменился тип (start <-> stop)
const wasStart = this.isStartLayout(prevLayout);
const isStart = this.isStartLayout(layout);
if (wasStart !== isStart) {
diff.push(layout);
continue;
}
// Изменились размеры или fit
if (isStart && wasStart) {
if (prevLayout.width !== layout.width ||
prevLayout.height !== layout.height ||
prevLayout.fit !== layout.fit) {
diff.push(layout);
}
}
}
return diff;
}
// Обновить layouts для видимых участников
update(visibleParticipants, allParticipants) {
const allDiffs = [];
for (const participant of allParticipants) {
const visibleInfo = visibleParticipants[participant.id];
const prev = this.prevLayouts.get(participant.id) || {};
const newLayouts = {};
// Камера
if (visibleInfo && participant.mediaSettings.isVideoEnabled) {
newLayouts.CAMERA = this.getStartLayout(
participant.externalId,
MediaType.CAMERA,
visibleInfo.width,
visibleInfo.height,
visibleInfo.cover
);
} else if (prev.CAMERA && this.isStartLayout(prev.CAMERA)) {
// Был активен, нужно остановить
newLayouts.CAMERA = this.getStopLayout(
participant.externalId,
MediaType.CAMERA
);
}
// Демонстрация экрана
if (visibleInfo && participant.mediaSettings.isScreenSharingEnabled) {
newLayouts.SCREEN = this.getStartLayout(
participant.externalId,
MediaType.SCREEN,
visibleInfo.width,
visibleInfo.height,
visibleInfo.cover
);
} else if (prev.SCREEN && this.isStartLayout(prev.SCREEN)) {
newLayouts.SCREEN = this.getStopLayout(
participant.externalId,
MediaType.SCREEN
);
}
// Вычислить diff
const diff = this.computeDiff(participant.id, newLayouts);
allDiffs.push(...diff);
// Обновить сохранённое состояние
this.prevLayouts.set(participant.id, {
CAMERA: newLayouts.CAMERA || prev.CAMERA,
SCREEN: newLayouts.SCREEN || prev.SCREEN
});
}
// Отправить только изменения
this.throttledUpdate(allDiffs);
}
// Очистить данные участника при его выходе
removeParticipant(participantId) {
this.prevLayouts.delete(participantId);
}
// Очистить данные вышедших участников
cleanup(activeParticipants) {
const activeIds = new Set(activeParticipants.map(p => p.id));
for (const id of this.prevLayouts.keys()) {
if (!activeIds.has(id)) {
this.prevLayouts.delete(id);
}
}
}
}
// Использование
const udlManager = new UpdateDisplayLayoutManager();
// При изменении видимых участников или их размеров
function onVisibilityChange(visibleParticipants, allParticipants) {
udlManager.update(visibleParticipants, allParticipants);
}
// При выходе участника
function onParticipantLeft(participantId) {
udlManager.removeParticipant(participantId);
}Интеграция с коллбэками SDK
const udlManager = new UpdateDisplayLayoutManager();
SDK.init({
// ...
onRemoteMediaSettings: (userId, mediaSettings) => {
// Пересчитать layouts при изменении медиа участника
updateVisibleLayouts();
},
onConversation: (externalId, mediaModifiers, muteStates, participants) => {
// Запросить видео при входе в звонок
updateVisibleLayouts();
},
onRemoteRemoved: (userId) => {
// Очистить данные вышедшего участника
udlManager.removeParticipant(userId);
}
});
function updateVisibleLayouts() {
// visibleParticipants — участники, видимые в UI, с их размерами
// participants — все участники звонка
udlManager.update(visibleParticipants, participants);
}Что вызывает изменение layout
Layout нужно обновлять когда:
| Событие | Действие |
|---|---|
| Участник включил камеру | Отправить start layout |
| Участник выключил камеру | Отправить stop layout |
| Участник начал демонстрацию экрана | Отправить start layout (SCREEN) |
| Участник закончил демонстрацию | Отправить stop layout (SCREEN) |
| Изменился размер видео в UI | Отправить layout с новыми размерами |
| Участник вышел из зоны видимости (скролл) | Отправить stop layout |
| Участник появился в зоне видимости | Отправить start layout |
Особенности
Throttle
Используйте throttle (например, 250мс) чтобы не отправлять слишком много запросов при быстрых изменениях (скролл, ресайз).
Не отправляйте лишнее
Если layout не изменился — не отправляйте его повторно. SDK ожидает только изменения.
Очистка памяти
Не забывайте удалять данные участников из prevLayouts при их выходе из звонка, чтобы избежать утечек памяти.