Skip to content

updateDisplayLayout

Для того чтобы приходили видео-стримы от собеседников, нужно отправлять команду updateDisplayLayout. В противном случае видео от других участников звонка может не приходить вообще.

Основной принцип

Команда updateDisplayLayout указывает, от каких собеседников нужно получать видео и в каких размерах. Также она позволяет остановить видео от собеседника.

Важно

updateDisplayLayout ожидает только изменения (diff) относительно предыдущего состояния, а не полный список всех участников. Отправляйте только те layouts, которые изменились.

Параметры layout

typescript
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)
}

Базовое использование

javascript
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. Для этого нужно:

  1. Хранить предыдущее состояние каждого участника
  2. Сравнивать новое состояние с предыдущим
  3. Отправлять только разницу
  4. Использовать throttle для ограничения частоты вызовов

Пример оптимизированной реализации

javascript
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

javascript
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 при их выходе из звонка, чтобы избежать утечек памяти.