Поделиться через


Управление видео во время звонков

Узнайте, как управлять видеозвонками с помощью пакетов SDK Службы коммуникации Azure. Мы узнаем, как управлять получением и отправкой видео в рамках звонка.

Необходимые компоненты

Установка пакета SDK

npm install Используйте команду для установки пакета SDK Службы коммуникации Azure Common and Calling SDK для JavaScript:

npm install @azure/communication-common --save
npm install @azure/communication-calling --save

Инициализация обязательных объектов

Экземпляр CallClient требуется для большинства операций вызова. При создании нового CallClient экземпляра его можно настроить с помощью пользовательских параметров, таких как Logger экземпляр.

С помощью экземпляра CallClient можно создать CallAgent экземпляр, вызвав его createCallAgent. Этот метод асинхронно возвращает объект экземпляра CallAgent.

Метод createCallAgent использует CommunicationTokenCredential в качестве аргумента. Он принимает маркер доступа пользователя.

Можно применить метод getDeviceManager для экземпляра CallClient, чтобы получить доступ к deviceManager.

const { CallClient } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential} = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");

// Set the logger's log level
setLogLevel('verbose');

// Redirect log output to console, file, buffer, REST API, or whatever location you want
AzureLogger.log = (...args) => {
    console.log(...args); // Redirect log output to console
};

const userToken = '<USER_TOKEN>';
callClient = new CallClient(options);
const tokenCredential = new AzureCommunicationTokenCredential(userToken);
const callAgent = await callClient.createCallAgent(tokenCredential, {displayName: 'optional Azure Communication Services user name'});
const deviceManager = await callClient.getDeviceManager()

Управление подключением пакета SDK к инфраструктуре Майкрософт

Экземпляр Call Agent помогает управлять вызовами (для присоединения или запуска вызовов). Для работы пакета SDK для вызова необходимо подключиться к инфраструктуре Майкрософт для получения уведомлений о входящих звонках и координации других сведений о вызове. У вас Call Agent есть два возможных состояния:

ПодключеноCall Agent значение Connected connectionStatue означает, что клиентский пакет SDK подключен и способен получать уведомления из инфраструктуры Майкрософт.

ОтключеноCall Agent значение connectionStatue состояний Disconnected возникает проблема, которая препятствует правильному подключению пакета SDK. Call Agent необходимо повторно создать.

  • invalidToken: если срок действия маркера истек или является недопустимым Call Agent экземпляром, отключается с этой ошибкой.
  • connectionIssue: если возникла проблема с клиентом, подключающимся к инфраструктуре Майкрософт, после многих повторных попыток Call Agent возникает connectionIssue ошибка.

Вы можете проверить, подключен ли локальный сервер Call Agent к инфраструктуре Майкрософт, проверив текущее значение connectionState свойства. Во время активного вызова можно прослушивать connectionStateChanged событие, чтобы определить, изменяются ли Call Agent изменения из подключенного к отключенном состоянию.

const connectionState = callAgentInstance.connectionState;
console.log(connectionState); // it may return either of 'Connected' | 'Disconnected'

const connectionStateCallback = (args) => {
    console.log(args); // it will return an object with oldState and newState, each of having a value of either of 'Connected' | 'Disconnected'
    // it will also return reason, either of 'invalidToken' | 'connectionIssue'
}
callAgentInstance.on('connectionStateChanged', connectionStateCallback);

Управление устройствами

Чтобы начать использовать видео с пакетом SDK для вызовов, необходимо иметь возможность управлять устройствами. Устройства позволяют управлять тем, что передает звук и видео в звонок.

deviceManagerС помощью функции можно перечислить локальные устройства, которые могут передавать аудио- и видеопотоки в вызове. Вы также можете запросить deviceManager разрешение на доступ к микрофонам и камерам локального устройства.

Доступ к deviceManager осуществляется путем вызова метода callClient.getDeviceManager():

const deviceManager = await callClient.getDeviceManager();

Получение локальных устройств

Для доступа к локальным устройствам можно использовать deviceManager методы getCameras() перечисления и getMicrophones. Эти методы являются асинхронными действиями.

//  Get a list of available video devices for use.
const localCameras = await deviceManager.getCameras(); // [VideoDeviceInfo, VideoDeviceInfo...]

// Get a list of available microphone devices for use.
const localMicrophones = await deviceManager.getMicrophones(); // [AudioDeviceInfo, AudioDeviceInfo...]

// Get a list of available speaker devices for use.
const localSpeakers = await deviceManager.getSpeakers(); // [AudioDeviceInfo, AudioDeviceInfo...]

Настройка устройств по умолчанию

После того как вы узнаете, какие устройства доступны для использования, можно настроить устройства по умолчанию для микрофона, динамика и камеры. Если значения по умолчанию для клиента не заданы, пакет SDK служб коммуникации использует значения по умолчанию операционной системы.

Микрофон

Доступ к используемому устройству

// Get the microphone device that is being used.
const defaultMicrophone = deviceManager.selectedMicrophone;

Настройка используемого устройства

// Set the microphone device to use.
await deviceManager.selectMicrophone(localMicrophones[0]);

Докладчик

Доступ к используемому устройству

// Get the speaker device that is being used.
const defaultSpeaker = deviceManager.selectedSpeaker;

Настройка используемого устройства

// Set the speaker device to use.
await deviceManager.selectSpeaker(localSpeakers[0]);

Камера

Доступ к используемому устройству

// Get the camera device that is being used.
const defaultSpeaker = deviceManager.selectedSpeaker;

Настройка используемого устройства

// Set the speaker device to use.
await deviceManager.selectSpeaker(localCameras[0]);

Каждый CallAgent может выбрать свой микрофон и динамики на его связанном DeviceManagerустройстве. Рекомендуется использовать разные CallAgents микрофоны и динамики. Они не должны совместно использовать одни и те же микрофоны, ни динамики. Если общий доступ происходит, то может быть активирована диагностика взаимодействия с пользователем с микрофоном, а микрофон перестает работать в зависимости от браузера или ос.

Локальный видеопоток

Чтобы отправить видео в вызове, необходимо создать LocalVideoStreamобъект.

const localVideoStream = new LocalVideoStream(camera);

Камера, передаваемая в качестве параметра, является одним из VideoDeviceInfo объектов, возвращаемых методом deviceManager.getCameras().

A LocalVideoStream имеет следующие свойства:

  • source: сведения об устройстве.
const source = localVideoStream.source;
  • mediaStreamType: может быть Video, ScreenSharingили RawMedia.
const type: MediaStreamType = localVideoStream.mediaStreamType;

Предварительный просмотр изображения с локальной камеры

С помощью deviceManager и VideoStreamRenderer вы можете реализовать отрисовку потоков из локальной камеры. LocalVideoStream После создания используйте его для настройкиVideoStreamRenderer. VideoStreamRendererПосле создания метода вызовите его createView() метод, чтобы получить представление, которое можно добавить в качестве дочернего элемента на страницу.

Этот поток не отправляется другим участникам; это локальный веб-канал предварительной версии.

// To start viewing local camera preview
const cameras = await deviceManager.getCameras();
const camera = cameras[0];
const localVideoStream = new LocalVideoStream(camera);
const videoStreamRenderer = new VideoStreamRenderer(localVideoStream);
const view = await videoStreamRenderer.createView();
htmlElement.appendChild(view.target);

Остановка локальной предварительной версии

Чтобы остановить локальный вызов предварительной версии, удалите представление, полученное из .VideoStreamRenderer После удаления VideoStreamRenderer удалите представление из html-дерева, вызвав removeChild() метод из узла DOM, содержащего предварительную версию.

// To stop viewing local camera preview
view.dispose();
htmlElement.removeChild(view.target);

Запрос разрешений на доступ к камере и микрофону

Приложение не может использовать камеру или микрофон без разрешений. С помощью deviceManager можно предложить пользователю предоставить разрешения камеры и (или) микрофона:

const result = await deviceManager.askDevicePermission({audio: true, video: true});

После разрешения обещания метод возвращает объект, указывающий DeviceAccess , были ли audio предоставлены разрешения и video разрешения:

console.log(result.audio);
console.log(result.video);

Примечания.

  • videoDevicesUpdated событие возникает при подключении и отмене подключения видеоустройств.
  • audioDevicesUpdated событие возникает при подключении звуковых устройств.
  • При создании DeviceManager сначала он не знает о каких-либо устройствах, если разрешения еще не предоставлены, поэтому изначально его имя устройства пусто, и оно не содержит подробных сведений об устройстве. Если мы вызовем API DeviceManager.askPermission(), пользователю будет предложено получить доступ к устройству. Когда пользователь выбирает "разрешить", чтобы предоставить доступ к диспетчеру устройств, узнает об устройствах в системе, обновите его списки устройств и выпустите события audioDevicesUpdated и videoDevicesUpdated. Если пользователь обновляет страницу и создает диспетчер устройств, диспетчер устройств может узнать об устройствах, так как пользователь предоставил ранее доступ. Он имеет свои списки устройств, заполненные изначально, и он не выдает события audioDevicesUpdated или videoDevicesUpdated.
  • Перечисление или выбор динамиков не поддерживается в Android Chrome, iOS Safari и macOS Safari.

Размещение звонка с видеокамерой

Внимание

В настоящее время поддерживается только один исходящий локальный видеопоток.

Чтобы осуществить видеозвонок, необходимо перечислить локальные камеры с помощью метода getCameras() в deviceManager.

После выбора камеры используйте ее для создания экземпляра LocalVideoStream. Передайте его в videoOptions виде элемента в localVideoStream массиве методу CallAgent startCall .

const deviceManager = await callClient.getDeviceManager();
const cameras = await deviceManager.getCameras();
const camera = cameras[0]
const localVideoStream = new LocalVideoStream(camera);
const placeCallOptions = {videoOptions: {localVideoStreams:[localVideoStream]}};
const userCallee = { communicationUserId: '<ACS_USER_ID>' }
const call = callAgent.startCall([userCallee], placeCallOptions);
  • Вы также можете присоединиться к вызову с помощью CallAgent.join() API и принять и позвонить с помощью Call.Accept() API.
  • Когда вызов будет осуществлен, автоматически начнется отправка видеопотока с выбранной камеры другим участникам вызова.

Запуск и остановка отправки локального видео во время вызова

Начало видео

Чтобы запустить видео во время вызова, необходимо перечислить камеры с помощью getCameras метода объекта deviceManager . Затем создайте новый экземпляр LocalVideoStream с требуемой камерой, а затем передайте LocalVideoStream объект в startVideo метод существующего объекта вызова:

const deviceManager = await callClient.getDeviceManager();
const cameras = await deviceManager.getCameras();
const camera = cameras[0]
const localVideoStream = new LocalVideoStream(camera);
await call.startVideo(localVideoStream);

Остановка видео

После успешной отправки видео LocalVideoStream экземпляр типа Video добавляется в коллекцию localVideoStreams в экземпляре вызова.

Поиск видеопотока в объекте Call

const localVideoStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'Video'} );

Остановите локальное видео , чтобы остановить локальное видео во время вызова, передайте localVideoStream экземпляр, используемый для видео, в метод CallstopVideo из :

await call.stopVideo(localVideoStream);

Вы можете переключиться на другое устройство камеры при наличии активного LocalVideoStream, вызвав switchSource на этом LocalVideoStream экземпляре:

const cameras = await callClient.getDeviceManager().getCameras();
const camera = cameras[1];
localVideoStream.switchSource(camera);

Если указанное видеоустройство недоступно:

  • При вызове, если видео отключено, и вы начинаете видео с помощью call.startVideo(), этот метод выдает SourceUnavailableError диагностику, cameraStartFailed которая сталкивается с пользователем, имеет значение true.
  • Вызов localVideoStream.switchSource() метода вызывает cameraStartFailed значение true. В нашем руководстве по диагностике звонков содержатся дополнительные сведения о том, как диагностировать связанные с вызовом проблемы.

Чтобы проверить, включена ли или отключена локальная видео, можно использовать Call метод isLocalVideoStarted, который возвращает значение true или false:

// Check if local video is on or off
call.isLocalVideoStarted;

Чтобы прослушивать изменения в локальном видео, вы можете подписаться и отменить подписку на событие isLocalVideoStartedChanged:

// Subscribe to local video event
call.on('isLocalVideoStartedChanged', () => {
    // Callback();
});
// Unsubscribe from local video event
call.off('isLocalVideoStartedChanged', () => {
    // Callback();
});

Запуск и остановка общего доступа к экрану во время звонка

Чтобы запустить общий доступ к экрану во время вызова, можно использовать асинхронный метод startScreenSharing() в объекте Call :

Начальный общий доступ к экрану

// Start screen sharing
await call.startScreenSharing();

Примечание. Отправка общего доступа к экрану поддерживается только в классическом браузере.

Поиск общего доступа к экрану в коллекции LocalVideoStream

После успешной ScreenSharingотправки общего доступа LocalVideoStream к экрану экземпляр типа добавляется в коллекцию localVideoStreams экземпляра вызова.

const localVideoStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'ScreenSharing'} );

Остановка общего доступа к экрану

Чтобы остановить общий доступ к экранам во время вызова, можно использовать асинхронный api stoptScreenSharing:

// Stop screen sharing
await call.stopScreenSharing();

Проверка состояния общего доступа к экрану

Чтобы проверить, включен ли общий доступ к экрану, можно использовать APIScreenSharingOn, который возвращает значение true или false:

// Check if screen sharing is on or off
call.isScreenSharingOn;

Чтобы слушать изменения в общей папке экрана, вы можете подписаться и отменить подписку на событие IsScreenSharingOnChanged:

// Subscribe to screen share event
call.on('isScreenSharingOnChanged', () => {
    // Callback();
});
// Unsubscribe from screen share event
call.off('isScreenSharingOnChanged', () => {
    // Callback();
});

Внимание

Эта функция Службы коммуникации Azure сейчас доступна в предварительной версии.

Предварительные версии API и пакеты SDK предоставляются без соглашения об уровне обслуживания. Рекомендуется не использовать их для рабочих нагрузок. Некоторые функции могут не поддерживаться или могут иметь ограниченные возможности.

Дополнительные сведения см . в дополнительных условиях использования для предварительных версий Microsoft Azure.

Предварительная версия локального общего экрана доступна в общедоступной предварительной версии и доступна в составе версии 1.15.1-beta.1+.

Предварительная версия общей папки локального экрана

Вы можете начать VideoStreamRenderer отрисовку потоков из общей папки локального экрана, чтобы увидеть, что вы отправляете в виде потока общего доступа к экранам.

// To start viewing local screen share preview
await call.startScreenSharing();
const localScreenSharingStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'ScreenSharing' });
const videoStreamRenderer = new VideoStreamRenderer(localScreenSharingStream);
const view = await videoStreamRenderer.createView();
htmlElement.appendChild(view.target);

// To stop viewing local screen share preview.
await call.stopScreenSharing();
view.dispose();
htmlElement.removeChild(view.target);

// Screen sharing can also be stoped by clicking on the native browser's "Stop sharing" button.
// The isScreenSharingOnChanged event will be triggered where you can check the value of call.isScreenSharingOn.
// If the value is false, then that means screen sharing is turned off and so we can go ahead and dispose the screen share preview.
// This event is also triggered for the case when stopping screen sharing via Call.stopScreenSharing() API.
call.on('isScreenSharingOnChanged', () => {
    if (!call.isScreenSharingOn) {
        view.dispose();
        htmlElement.removeChild(view.target);
    }
});

Отрисовка потоков видео и экранов удаленного участника

Чтобы отобразить видео или общий доступ к экрану удаленного участника, сначала необходимо получить ссылку на RemoteVideoStream, которую вы хотите отобразить. Это можно сделать, перейдя через массив или видеопоток (videoStreams) объекта RemoteParticipant. Доступ к коллекции удаленных участников осуществляется через Call объект.

const remoteVideoStream = call.remoteParticipants[0].videoStreams[0];
const streamType = remoteVideoStream.mediaStreamType;

Для отрисовки RemoteVideoStreamнеобходимо подписаться на его isAvailableChanged событие. isAvailable Если свойство изменяетсяtrue, удаленный участник отправляет видеопоток. Когда это произойдет, создайте новый экземпляр VideoStreamRenderer, а затем новый экземпляр VideoStreamRendererView с помощью асинхронного метода createView.
Затем вы сможете присоединить view.target к любому элементу пользовательского интерфейса.

При каждом изменении доступности удаленного потока можно уничтожить все VideoStreamRenderer или конкретное VideoStreamRendererView. Если вы решите сохранить их, то в представлении отображается пустой кадр видео.

// Reference to the html's div where we would display a grid of all remote video stream from all participants.
let remoteVideosGallery = document.getElementById('remoteVideosGallery');

subscribeToRemoteVideoStream = async (remoteVideoStream) => {
    let renderer = new VideoStreamRenderer(remoteVideoStream);
    let view;
    let remoteVideoContainer = document.createElement('div');
    remoteVideoContainer.className = 'remote-video-container';

    let loadingSpinner = document.createElement('div');
    // See the css example below for styling the loading spinner.
    loadingSpinner.className = 'loading-spinner';
    remoteVideoStream.on('isReceivingChanged', () => {
        try {
            if (remoteVideoStream.isAvailable) {
                const isReceiving = remoteVideoStream.isReceiving;
                const isLoadingSpinnerActive = remoteVideoContainer.contains(loadingSpinner);
                if (!isReceiving && !isLoadingSpinnerActive) {
                    remoteVideoContainer.appendChild(loadingSpinner);
                } else if (isReceiving && isLoadingSpinnerActive) {
                    remoteVideoContainer.removeChild(loadingSpinner);
                }
            }
        } catch (e) {
            console.error(e);
        }
    });

    const createView = async () => {
        // Create a renderer view for the remote video stream.
        view = await renderer.createView();
        // Attach the renderer view to the UI.
        remoteVideoContainer.appendChild(view.target);
        remoteVideosGallery.appendChild(remoteVideoContainer);
    }

    // Remote participant has switched video on/off
    remoteVideoStream.on('isAvailableChanged', async () => {
        try {
            if (remoteVideoStream.isAvailable) {
                await createView();
            } else {
                view.dispose();
                remoteVideosGallery.removeChild(remoteVideoContainer);
            }
        } catch (e) {
            console.error(e);
        }
    });

    // Remote participant has video on initially.
    if (remoteVideoStream.isAvailable) {
        try {
            await createView();
        } catch (e) {
            console.error(e);
        }
    }
    
    console.log(`Initial stream size: height: ${remoteVideoStream.size.height}, width: ${remoteVideoStream.size.width}`);
    remoteVideoStream.on('sizeChanged', () => {
        console.log(`Remote video stream size changed: new height: ${remoteVideoStream.size.height}, new width: ${remoteVideoStream.size.width}`);
    });
}

CSS для стилизации загрузочного спиннера по удаленному видеопотоку.

.remote-video-container {
   position: relative;
}
.loading-spinner {
   border: 12px solid #f3f3f3;
   border-radius: 50%;
   border-top: 12px solid #ca5010;
   width: 100px;
   height: 100px;
   -webkit-animation: spin 2s linear infinite; /* Safari */
   animation: spin 2s linear infinite;
   position: absolute;
   margin: auto;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
   transform: translate(-50%, -50%);
}
@keyframes spin {
   0% { transform: rotate(0deg); }
   100% { transform: rotate(360deg); }
}
/* Safari */
@-webkit-keyframes spin {
   0% { -webkit-transform: rotate(0deg); }
   100% { -webkit-transform: rotate(360deg); }
}

Качество удаленного видео

Пакет SDK Службы коммуникации Azure WebJS предоставляет функцию с именем "Оптимальное число видео" (OVC), начиная с версии 1.15.1. Эта функция может использоваться для информирования приложений во время выполнения о том, сколько входящих видео от разных участников может быть оптимально отрисовано в данный момент в групповом вызове (2+ участники). Эта функция предоставляет свойство optimalVideoCount , которое динамически изменяется во время вызова на основе сетевых и аппаратных возможностей локальной конечной точки. Значение сведений о optimalVideoCount том, сколько видео из разных приложений-участников должно отображаться в данный момент. Приложения должны обрабатывать эти изменения и обновлять количество отрисованных видео соответствующим образом в соответствии с рекомендацией. Между каждым обновлением существует период разбиение (около 10 с).

optimalVideoCount Использование функции — это функция вызова. Необходимо ссылаться на функцию OptimalVideoCount с помощью feature метода Call объекта. Затем вы можете задать прослушиватель с помощью on метода уведомления при изменении оптимальной OptimalVideoCountCallFeature версииVideoCount. Чтобы отменить подписку из изменений, можно вызвать off метод. Текущее максимальное количество входящих видео , которое можно отобразить, равно 16. Для правильной поддержки 16 входящих видео компьютер должен иметь mimum из 16 ГБ ОЗУ и 4-ядра или больше ЦП, который не старше 3 лет.

const optimalVideoCountFeature = call.feature(Features.OptimalVideoCount);
optimalVideoCountFeature.on('optimalVideoCountChanged', () => {
    const localOptimalVideoCountVariable = optimalVideoCountFeature.optimalVideoCount;
})

Пример использования: приложение должно подписаться на изменения оптимального количества видео в групповых вызовах. Изменение оптимального количества видео можно обрабатывать путем создания нового отрисовщика (createView метода) или удаления представлений (dispose) и обновления макета приложения соответствующим образом.

Свойства удаленного видеопотока

Удаленные потоки видео имеют следующие свойства:

const id: number = remoteVideoStream.id;
  • id — идентификатор удаленного видеопотока;
const type: MediaStreamType = remoteVideoStream.mediaStreamType;
  • mediaStreamType может иметь значение Video или ScreenSharing;
const isAvailable: boolean = remoteVideoStream.isAvailable;
  • isAvailable: определяет, активно ли конечная точка удаленного участника отправляет поток.
const isReceiving: boolean = remoteVideoStream.isReceiving;
  • isReceiving:
    • Сообщает приложению, если данные удаленного видеопотока получены или нет.

    • Флаг переходит к false следующим сценариям:

      • Удаленный участник, работающий в мобильном браузере, переносит приложение браузера в фон.
      • Удаленный участник или пользователь, получающий видео, имеет проблему с сетью, которая влияет на качество видео резко.
      • Удаленный участник, работающий в macOS/iOS Safari, выбирает "Приостановить" в адресной строке.
      • Удаленный участник имеет сетевое отключение.
      • Удаленный участник на мобильном устройстве убивает или завершает браузер.
      • Удаленный участник на мобильном устройстве или на рабочем столе блокирует свое устройство. Этот сценарий также применяется, если удаленный участник находится на настольном компьютере и переходит в спящий режим.
    • Флаг переходит к true следующим сценариям:

      • Удаленный участник, работающий в мобильном браузере и имеющий фон браузера, возвращает его на передний план.
      • Удаленный участник, работающий в macOS/iOS Safari, выбирает "Возобновить" в адресной строке после приостановки видео.
      • Удаленный участник повторно подключается к сети после временного отключения.
      • Удаленный участник на мобильном устройстве разблокирует свое устройство и вернется к вызову в своем мобильном браузере.
    • Эта функция улучшает взаимодействие с пользователем для отрисовки удаленных видеопотоков.

    • Вы можете отобразить спиннер загрузки по удаленному видеопотоку при изменении флага false. Вам не нужно реализовать спиннер загрузки, но загрузка спиннер является наиболее распространенным использованием для лучшего взаимодействия с пользователем.

const size: StreamSize = remoteVideoStream.size;
  • size: размер потока с информацией о ширине и высоте видео.

Методы и свойства VideoStreamRenderer

await videoStreamRenderer.createView();

VideoStreamRendererView Создайте экземпляр, который можно подключить в пользовательском интерфейсе приложения для отрисовки удаленного видеопотока, используйте асинхронный createView() метод, он разрешает, когда поток готов к отрисовке и возвращает объект со target свойством, представляющим video элемент, который может быть вставлен в любое место в дереве DOM.

videoStreamRenderer.dispose();

videoStreamRenderer Удаление и все связанные VideoStreamRendererView экземпляры.

Методы и свойства VideoStreamRendererView

При создании VideoStreamRendererView можно указать свойства scalingMode и isMirrored. scalingMode может иметь значение Stretch, Crop или Fit. Если задано значение isMirrored, преобразованный для просмотра поток зеркально отражается по вертикали.

const videoStreamRendererView: VideoStreamRendererView = await videoStreamRenderer.createView({ scalingMode, isMirrored });

Каждый экземпляр VideoStreamRendererView имеет свойство target, которое представляет область отрисовки. Подключите это свойство к пользовательскому интерфейсу приложения:

htmlElement.appendChild(view.target);

Чтобы обновить scalingMode, вызовите метод updateScalingMode:

view.updateScalingMode('Crop');

Отправка видеопотоков из двух разных камер в одном вызове с одного и того же классического устройства.

Внимание

Эта функция Службы коммуникации Azure сейчас доступна в предварительной версии.

Предварительные версии API и пакеты SDK предоставляются без соглашения об уровне обслуживания. Рекомендуется не использовать их для рабочих нагрузок. Некоторые функции могут не поддерживаться или могут иметь ограниченные возможности.

Дополнительные сведения см . в дополнительных условиях использования для предварительных версий Microsoft Azure.

Отправка видеопотоков из двух разных камер в одном вызове поддерживается в рамках версии 1.17.1-beta.1+ в поддерживаемых браузерах на настольных компьютерах.

  • Видеопотоки можно отправлять из двух разных камер из одной вкладки браузера рабочего стола или приложения в одном вызове с помощью следующего фрагмента кода:
// Create your first CallAgent with identity A
const callClient1 = new CallClient();
const callAgent1 = await callClient1.createCallAgent(tokenCredentialA);
const deviceManager1 = await callClient1.getDeviceManager();

// Create your second CallAgent with identity B
const callClient2 = new CallClient();
const callAgent2 = await callClient2.createCallAgent(tokenCredentialB);
const deviceManager2 = await callClient2.getDeviceManager();

// Join the call with your first CallAgent
const camera1 = await deviceManager1.getCameras()[0];
const callObj1 = callAgent1.join({ groupId: ‘123’}, { videoOptions: { localVideoStreams: [new LocalVideoStream(camera1)] } });

// Join the same call with your second CallAgent and make it use a different camera
const camera2 = (await deviceManager2.getCameras()).filter((camera) => { return camera !== camera1 })[0];
const callObj2 = callAgent2.join({ groupId: '123' }, { videoOptions: { localVideoStreams: [new LocalVideoStream(camera2)] } });

//Mute the microphone and speakers of your second CallAgent’s Call, so that there is no echos/noises.
await callObj2.muteIncomingAudio();
await callObj2.mute();

Ограничения:

  • Это необходимо сделать с двумя разными экземплярами с использованием разных CallAgent удостоверений. Фрагмент кода показывает, что используются два агента вызова, каждый из которых имеет собственный объект Call.
  • В примере кода оба callAgents присоединяются к одному вызову (одинаковые идентификаторы вызова). Вы также можете присоединиться к разным звонкам с каждым агентом и отправить одно видео на один звонок и другое видео на другом вызове.
  • Отправка одной и той же камеры в CallAgent не поддерживается. Они должны быть двумя разными камерами.
  • Отправка двух разных камер с одним CallAgent в настоящее время не поддерживается.
  • В macOS Safari эффекты фонового размытия видео (из @azure/communication-effects)них можно применять только к одной камере, а не одновременно.

Установка пакета SDK

Найдите файл уровня build.gradle проекта и добавьте mavenCentral() в список репозиториев в buildscript разделе и allprojects:

buildscript {
    repositories {
    ...
        mavenCentral()
    ...
    }
}
allprojects {
    repositories {
    ...
        mavenCentral()
    ...
    }
}

Затем в файле уровня build.gradle модуля добавьте в раздел следующие строки dependencies :

dependencies {
    ...
    implementation 'com.azure.android:azure-communication-calling:1.0.0'
    ...
}

Инициализация обязательных объектов

Чтобы создать CallAgent экземпляр, необходимо вызвать createCallAgent метод в экземпляре CallClient . Этот вызов асинхронно возвращает объект экземпляра CallAgent .

Метод createCallAgent принимает CommunicationUserCredential в качестве аргумента, который инкапсулирует маркер доступа.

Чтобы получить доступ DeviceManager, сначала необходимо создать callAgent экземпляр. Затем можно использовать CallClient.getDeviceManager метод для получения DeviceManager.

String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an activity, for instance
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential).get();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();

Чтобы задать отображаемое имя для вызывающей стороны, используйте следующий альтернативный метод:

String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an activity, for instance
CallAgentOptions callAgentOptions = new CallAgentOptions();
callAgentOptions.setDisplayName("Alice Bob");
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential, callAgentOptions).get();

Управление устройствами

Чтобы начать работу с видео с вызовами, необходимо знать, как управлять устройствами. Устройства позволяют управлять тем, что передает звук и видео в звонок.

DeviceManager позволяет получить список локальных устройств, которые можно использовать в вызове для передачи аудио- и видеопотоков. Вы также можете запрашивать у пользователя разрешение на доступ к его микрофону и камере через собственный API браузера.

Доступ к deviceManager осуществляется путем вызова метода callClient.getDeviceManager().

Context appContext = this.getApplicationContext();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();

Перечисление локальных устройств

Для доступа к локальным устройствам можно использовать методы перечисления из Диспетчера устройств. Действие перечисления выполняется асинхронно.

//  Get a list of available video devices for use.
List<VideoDeviceInfo> localCameras = deviceManager.getCameras(); // [VideoDeviceInfo, VideoDeviceInfo...]

Предварительный просмотр изображения с локальной камеры

С помощью DeviceManager и Renderer вы можете реализовать отрисовку потоков из локальной камеры. Этот поток не отправляется другим участникам, а используется как локальный канал предварительного просмотра. Это действие выполняется асинхронно.

VideoDeviceInfo videoDevice = <get-video-device>; // See the `Enumerate local devices` topic above
Context appContext = this.getApplicationContext();

LocalVideoStream currentVideoStream = new LocalVideoStream(videoDevice, appContext);

LocalVideoStream[] localVideoStreams = new LocalVideoStream[1];
localVideoStreams[0] = currentVideoStream;

VideoOptions videoOptions = new VideoOptions(localVideoStreams);

RenderingOptions renderingOptions = new RenderingOptions(ScalingMode.Fit);
VideoStreamRenderer previewRenderer = new VideoStreamRenderer(currentVideoStream, appContext);

VideoStreamRendererView uiView = previewRenderer.createView(renderingOptions);

// Attach the uiView to a viewable location on the app at this point
layout.addView(uiView);

Осуществление персонального вызова с использованием видеокамеры

Предупреждение

В настоящее время поддерживается только один исходящий локальный видеопоток. Чтобы осуществить вызов с поддержкой видео, необходимо перечислить локальные камеры через API deviceManager getCameras. После выбора нужной камеры создайте на ее основе экземпляр LocalVideoStream и передайте его в videoOptions в качестве элемента массива localVideoStream в метод call. После подключения вызова он автоматически начнет отправлять видеопоток из выбранной камеры другим участникам.

Примечание.

Для защиты конфиденциальности видео не будет передаваться участникам вызова, если не просматривается локально. Дополнительные сведения см. в разделе Предварительный просмотр изображения с локальной камеры.

VideoDeviceInfo desiredCamera = <get-video-device>; // See the `Enumerate local devices` topic above
Context appContext = this.getApplicationContext();

LocalVideoStream currentVideoStream = new LocalVideoStream(desiredCamera, appContext);

LocalVideoStream[] localVideoStreams = new LocalVideoStream[1];
localVideoStreams[0] = currentVideoStream;

VideoOptions videoOptions = new VideoOptions(localVideoStreams);

// Render a local preview of video so the user knows that their video is being shared
Renderer previewRenderer = new VideoStreamRenderer(currentVideoStream, appContext);
View uiView = previewRenderer.createView(new CreateViewOptions(ScalingMode.FIT));

// Attach the uiView to a viewable location on the app at this point
layout.addView(uiView);

CommunicationUserIdentifier[] participants = new CommunicationUserIdentifier[]{ new CommunicationUserIdentifier("<acs user id>") };

StartCallOptions startCallOptions = new StartCallOptions();
startCallOptions.setVideoOptions(videoOptions);

Call call = callAgent.startCall(context, participants, startCallOptions);

Запуск и остановка отправки локального видео

Чтобы запустить видео, нужно перечислить камеры с помощью API getCameraList для объекта deviceManager. Затем создайте новый экземпляр LocalVideoStream, передав ему данные о нужной камере, и передайте этот экземпляр в качестве аргумента в API startVideo:

VideoDeviceInfo desiredCamera = <get-video-device>; // See the `Enumerate local devices` topic above
Context appContext = this.getApplicationContext();

LocalVideoStream currentLocalVideoStream = new LocalVideoStream(desiredCamera, appContext);

VideoOptions videoOptions = new VideoOptions(currentLocalVideoStream);

Future startVideoFuture = call.startVideo(appContext, currentLocalVideoStream);
startVideoFuture.get();

После успешного запуска отправки видео экземпляр LocalVideoStream будет добавлен в коллекцию localVideoStreams в экземпляре вызова.

List<LocalVideoStream> videoStreams = call.getLocalVideoStreams();
LocalVideoStream currentLocalVideoStream = videoStreams.get(0); // Please make sure there are VideoStreams in the list before calling get(0).

Чтобы остановить отправку локального видео, передайте экземпляр LocalVideoStream, доступный в коллекции localVideoStreams:

call.stopVideo(appContext, currentLocalVideoStream).get();

Вы можете переключиться на другое устройство камеры, не прекращая передачу видео, вызвав switchSource для экземпляра LocalVideoStream:

currentLocalVideoStream.switchSource(source).get();

Отрисовка видеопотоков удаленных участников

Чтобы получить список видеопотоков и потоков демонстрации экрана, получаемых от удаленных участников, проверьте коллекции videoStreams:

List<RemoteParticipant> remoteParticipants = call.getRemoteParticipants();
RemoteParticipant remoteParticipant = remoteParticipants.get(0); // Please make sure there are remote participants in the list before calling get(0).

List<RemoteVideoStream> remoteStreams = remoteParticipant.getVideoStreams();
RemoteVideoStream remoteParticipantStream = remoteStreams.get(0); // Please make sure there are video streams in the list before calling get(0).

MediaStreamType streamType = remoteParticipantStream.getType(); // of type MediaStreamType.Video or MediaStreamType.ScreenSharing

Чтобы реализовать отрисовку RemoteVideoStream, полученного от удаленного участника, нужно подписаться на событие OnVideoStreamsUpdated.

В этом событии изменение значения свойства isAvailable на true указывает на то, что удаленный участник в настоящий момент отправляет поток. Обнаружив такое изменение, создайте новый экземпляр Renderer, а затем создайте новое представление RendererView с помощью асинхронного API createView и подключите view.target к любому элементу пользовательского интерфейса в приложении.

При каждом изменении состояния доступности удаленного потока вы можете удалить отрисовщик или определенное представление RendererView либо оставить все как есть, но это приведет к отрисовке пустого видеокадра.

VideoStreamRenderer remoteVideoRenderer = new VideoStreamRenderer(remoteParticipantStream, appContext);
VideoStreamRendererView uiView = remoteVideoRenderer.createView(new RenderingOptions(ScalingMode.FIT));
layout.addView(uiView);

remoteParticipant.addOnVideoStreamsUpdatedListener(e -> onRemoteParticipantVideoStreamsUpdated(p, e));

void onRemoteParticipantVideoStreamsUpdated(RemoteParticipant participant, RemoteVideoStreamsEvent args) {
    for(RemoteVideoStream stream : args.getAddedRemoteVideoStreams()) {
        if(stream.getIsAvailable()) {
            startRenderingVideo();
        } else {
            renderer.dispose();
        }
    }
}

Свойства удаленного видеопотока

Удаленный видеопоток имеет несколько свойств:

  • Id — идентификатор удаленного видеопотока;
int id = remoteVideoStream.getId();
  • MediaStreamType — принимает значения Video или ScreenSharing;
MediaStreamType type = remoteVideoStream.getMediaStreamType();
  • isAvailable — указывает на то, что конечная точка этого удаленного участника активно отправляет поток данных.
boolean availability = remoteVideoStream.isAvailable();

Методы и свойства отрисовщика

Объект отрисовщика имеет следующие API:

  • Создайте экземпляр VideoStreamRendererView, который позже можно будет подключить к пользовательскому интерфейсу приложения для отрисовке удаленного видеопотока.
// Create a view for a video stream
VideoStreamRendererView.createView()
  • Удалите отрисовщик и все VideoStreamRendererView, связанные с ним. Вызывается при удалении всех связанных представлений из пользовательского интерфейса.
VideoStreamRenderer.dispose()
  • StreamSize — размер (ширина и высота) удаленного видеопотока;
StreamSize renderStreamSize = VideoStreamRenderer.getSize();
int width = renderStreamSize.getWidth();
int height = renderStreamSize.getHeight();

Методы и свойства RendererView

При создании VideoStreamRendererView можно указать свойства ScalingMode и mirrored, которые будут применяться к этому представлению. Поддерживаются режимы масштабирования CROP и FIT.

VideoStreamRenderer remoteVideoRenderer = new VideoStreamRenderer(remoteVideoStream, appContext);
VideoStreamRendererView rendererView = remoteVideoRenderer.createView(new CreateViewOptions(ScalingMode.Fit));

Созданный RendererView можно затем подключить к пользовательскому интерфейсу приложения, используя следующий фрагмент кода:

layout.addView(rendererView);

Вы можете позже изменить режим масштабирования, вызвав API updateScalingMode для объекта RendererView с аргументом ScalingMode.CROP или ScalingMode.FIT.

// Update the scale mode for this view.
rendererView.updateScalingMode(ScalingMode.CROP)

Настройка системы

Выполните следующие действия, чтобы настроить систему.

Создайте проект Xcode

В Xcode создайте новый проект iOS и выберите шаблон Single View App (Приложение с одним представлением). В этой статье используется платформа SwiftUI, поэтому необходимо задать для языка значение Swift и задать для интерфейса значение SwiftUI.

Вы не собираетесь создавать тесты в этой статье. Снимите флажок "Включить тесты ".

Снимок экрана, на котором показано окно для создания проекта в Xcode.

Установка пакета и зависимостей с помощью CocoaPods

  1. Создайте Podfile для приложения, как показано в следующем примере:

    platform :ios, '13.0'
    use_frameworks!
    target 'AzureCommunicationCallingSample' do
        pod 'AzureCommunicationCalling', '~> 1.0.0'
    end
    
  2. Запустите pod install.

  3. Откройте .xcworkspace с помощью Xcode.

Запрос доступа к микрофону

Чтобы получить доступ к микрофону устройства, необходимо обновить список свойств приложения с помощью NSMicrophoneUsageDescription. Задайте связанное значение строке, включенной в диалоговое окно, которое система использует для запроса доступа от пользователя.

Щелкните правой кнопкой мыши запись Info.plist дерева проекта и выберите "Открыть как>исходный код". Добавьте в раздел верхнего уровня <dict> следующие строки, а затем сохраните файл.

<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>

Настройка платформы приложения

Откройте файл проекта ContentView.swift . import Добавьте объявление в начало файла для импорта библиотекиAzureCommunicationCalling. Кроме того, импортируйте AVFoundation. Он необходим для запросов на разрешение звука в коде.

import AzureCommunicationCalling
import AVFoundation

Инициализация CallAgent

Чтобы создать экземпляр CallAgent из CallClient, необходимо использовать метод callClient.createCallAgent, который асинхронно возвращает объект CallAgent после его инициализации.

Чтобы создать клиент вызова, передайте CommunicationTokenCredential объект:

import AzureCommunication

let tokenString = "token_string"
var userCredential: CommunicationTokenCredential?
do {
    let options = CommunicationTokenRefreshOptions(initialToken: token, refreshProactively: true, tokenRefresher: self.fetchTokenSync)
    userCredential = try CommunicationTokenCredential(withOptions: options)
} catch {
    updates("Couldn't created Credential object", false)
    initializationDispatchGroup!.leave()
    return
}

// tokenProvider needs to be implemented by Contoso, which fetches a new token
public func fetchTokenSync(then onCompletion: TokenRefreshOnCompletion) {
    let newToken = self.tokenProvider!.fetchNewToken()
    onCompletion(newToken, nil)
}

CommunicationTokenCredential Передайте созданный CallClientобъект и задайте отображаемое имя:

self.callClient = CallClient()
let callAgentOptions = CallAgentOptions()
options.displayName = " iOS Azure Communication Services User"

self.callClient!.createCallAgent(userCredential: userCredential!,
    options: callAgentOptions) { (callAgent, error) in
        if error == nil {
            print("Create agent succeeded")
            self.callAgent = callAgent
        } else {
            print("Create agent failed")
        }
})

Управление устройствами

Чтобы начать работу с видео с вызовами, необходимо знать, как управлять устройствами. Устройства позволяют управлять тем, что передает звук и видео в звонок.

DeviceManager позволяет получить список локальных устройств, которые можно использовать в вызове для передачи аудио- и видеопотоков. Он также позволяет запросить у пользователя разрешение на доступ к микрофону или камере. Вы можете получить доступ к deviceManager через объект callClient.

self.callClient!.getDeviceManager { (deviceManager, error) in
        if (error == nil) {
            print("Got device manager instance")
            self.deviceManager = deviceManager
        } else {
            print("Failed to get device manager instance")
        }
    }

Перечисление локальных устройств

Для доступа к локальным устройствам можно использовать методы перечисления из диспетчера устройств. Действие перечисления выполняется асинхронно.

// enumerate local cameras
var localCameras = deviceManager.cameras // [VideoDeviceInfo, VideoDeviceInfo...]

Предварительный просмотр изображения с локальной камеры

С помощью Renderer можно начать отрисовку потока с локальной камеры. Этот поток не отправляется другим участникам, а используется как локальный канал предварительного просмотра. Это действие выполняется асинхронно.

let camera: VideoDeviceInfo = self.deviceManager!.cameras.first!
let localVideoStream = LocalVideoStream(camera: camera)
let localRenderer = try! VideoStreamRenderer(localVideoStream: localVideoStream)
self.view = try! localRenderer.createView()

Получение свойств предварительного просмотра изображения с локальной камеры

Отрисовщик предоставляет ряд свойств и методов для управления отрисовкой.

// Constructor can take in LocalVideoStream or RemoteVideoStream
let localRenderer = VideoStreamRenderer(localVideoStream:localVideoStream)
let remoteRenderer = VideoStreamRenderer(remoteVideoStream:remoteVideoStream)

// [StreamSize] size of the rendering view
localRenderer.size

// [VideoStreamRendererDelegate] an object you provide to receive events from this Renderer instance
localRenderer.delegate

// [Synchronous] create view
try! localRenderer.createView()

// [Synchronous] create view with rendering options
try! localRenderer!.createView(withOptions: CreateViewOptions(scalingMode: ScalingMode.fit))

// [Synchronous] dispose rendering view
localRenderer.dispose()

Осуществление персонального вызова с поддержкой видео

Чтобы получить экземпляр диспетчера устройств, см. раздел об управлении устройствами.

let firstCamera = self.deviceManager!.cameras.first
self.localVideoStreams = [LocalVideoStream]()
self.localVideoStreams!.append(LocalVideoStream(camera: firstCamera!))
let videoOptions = VideoOptions(localVideoStreams: self.localVideoStreams!)

let startCallOptions = StartCallOptions()
startCallOptions.videoOptions = videoOptions

let callee = CommunicationUserIdentifier('UserId')
self.callAgent?.startCall(participants: [callee], options: startCallOptions) { (call, error) in
    if error == nil {
        print("Successfully started outgoing video call")
        self.call = call
    } else {
        print("Failed to start outgoing video call")
    }
}

Отрисовка видеопотоков удаленных участников

Удаленные участники могут предоставить общий доступ к видео или экрану во время вызова.

Обработка потоков для совместного использования видео или экрана удаленных участников

Чтобы получить список потоков от удаленных участников, изучите коллекции videoStreams.

var remoteParticipantVideoStream = call.remoteParticipants[0].videoStreams[0]

Получение свойства удаленного видеопотока

var type: MediaStreamType = remoteParticipantVideoStream.type // 'MediaStreamTypeVideo'
var isAvailable: Bool = remoteParticipantVideoStream.isAvailable // indicates if remote stream is available
var id: Int = remoteParticipantVideoStream.id // id of remoteParticipantStream

Отрисовка потоков удаленного участника

Чтобы начать отрисовку потоков удаленных участников, используйте следующий код.

let renderer = VideoStreamRenderer(remoteVideoStream: remoteParticipantVideoStream)
let targetRemoteParticipantView = renderer?.createView(withOptions: CreateViewOptions(scalingMode: ScalingMode.crop))
// To update the scaling mode later
targetRemoteParticipantView.update(scalingMode: ScalingMode.fit)

Получение методов и свойств отрисовщика удаленного видео

// [Synchronous] dispose() - dispose renderer and all `RendererView` associated with this renderer. To be called when you have removed all associated views from the UI.
remoteVideoRenderer.dispose()

Настройка системы

Выполните следующие действия, чтобы настроить систему.

Создание проекта Visual Studio

Для приложения универсальная платформа Windows в Visual Studio 2022 создайте проект пустого приложения (универсального приложения Windows). После ввода имени проекта вы можете выбрать любой пакет SDK для Windows позже 10.0.17763.0.

Для приложения WinUI 3 создайте проект с шаблоном "Пустое приложение" (WinUI 3 в классическом приложении) для настройки одностраничного приложения WinUI 3. Требуется пакет SDK для приложений Windows версии 1.3 или более поздней.

Установка пакета и зависимостей с помощью NuGet диспетчер пакетов

Api и библиотеки пакета SDK для вызовов общедоступны через пакет NuGet.

Чтобы найти, скачать и установить пакет NuGet пакета Sdk для вызовов:

  1. Откройте nuGet диспетчер пакетов, выбрав инструменты>NuGet диспетчер пакетов> Manage NuGet Packages for Solution.
  2. Выберите "Обзор" и введите Azure.Communication.Calling.WindowsClient в поле поиска.
  3. Убедитесь, что установлен флажок "Включить предварительную версию ".
  4. Выберите пакет Azure.Communication.Calling.WindowsClient, а затем выберите Azure.Communication.Calling.WindowsClient 1.4.0-beta.1 или более позднюю версию.
  5. Установите флажок, соответствующий проекту Службы коммуникации Azure на правой панели.
  6. Выберите Установить.

Запрос доступа к микрофону

Приложению требуется доступ к камере для правильного выполнения. В приложениях UWP возможность камеры должна быть объявлена в файле манифеста приложения.

В следующих шагах показано, как это сделать.

  1. На панели Solution Explorer дважды щелкните файл с расширением .appxmanifest.
  2. Щелкните вкладку Capabilities.
  3. Установите флажок Camera в списке возможностей.

Создание кнопок пользовательского интерфейса для совершения и завершения вызова

Это простое пример приложения содержит две кнопки. одну для выполнения вызова, а другую для его завершения. Следующие шаги демонстрируют, как добавить такие кнопки в приложение.

  1. Solution Explorer На панели дважды щелкните файл с именем MainPage.xaml UWP или MainWindows.xaml WinUI 3.
  2. На центральной панели найдите код XAML в предварительной версии пользовательского интерфейса.
  3. Измените код XAML следующим фрагментом:
<TextBox x:Name="CalleeTextBox" PlaceholderText="Who would you like to call?" />
<StackPanel>
    <Button x:Name="CallButton" Content="Start/Join call" Click="CallButton_Click" />
    <Button x:Name="HangupButton" Content="Hang up" Click="HangupButton_Click" />
</StackPanel>

Настройка приложения с помощью API пакета SDK для вызовов

API пакета SDK для вызовов находятся в двух разных пространствах имен. С помощью следующих шагов можно сообщить компилятору C# об этих пространствах имен, что позволяет IntelliSense в Visual Studio упростить разработку кода.

  1. Solution Explorer На панели щелкните стрелку в левой части файла с именем MainPage.xaml UWP или MainWindows.xaml WinUI 3.
  2. Дважды щелкните файл с именем MainPage.xaml.cs или MainWindows.xaml.cs.
  3. Добавьте следующие команды в конце текущих инструкций using.
using Azure.Communication.Calling.WindowsClient;

Сохранение MainPage.xaml.cs или MainWindows.xaml.cs открытие. Вы добавите в него дополнительный код с помощью последующих действий.

Разрешение взаимодействий с приложением

Добавленные ранее кнопки пользовательского интерфейса должны работать на основе размещенного вызова (CommunicationCall). Это означает, что CommunicationCall член данных должен быть добавлен в MainPage класс или MainWindow класс. Кроме того, чтобы обеспечить успешное выполнение асинхронной операции для создания CallAgent, элемент данных CallAgent также необходимо добавить в тот же класс.

Добавьте в или MainWindow класс следующие элементы MainPage данных:

CallAgent callAgent;
CommunicationCall call;

Создание обработчиков кнопок

Ранее в код XAML были добавлены две кнопки пользовательского интерфейса. Следующий код добавляет обработчики, которые будут выполняться при нажатии кнопки. Следующий код нужно добавить после элементов данных из предыдущего раздела.

private async void CallButton_Click(object sender, RoutedEventArgs e)
{
    // Start call
}

private async void HangupButton_Click(object sender, RoutedEventArgs e)
{
    // End the current call
}

Объектная модель

Следующие классы и интерфейсы реализуют некоторые основные функции клиентской библиотеки вызовов в Службах коммуникации Azure для UWP:

Имя Описание
CallClient Это CallClient основная точка входа в клиентную библиотеку вызовов.
CallAgent Используется CallAgent для запуска и присоединения вызовов.
CommunicationCall Используется CommunicationCall для управления размещенными или присоединенными вызовами.
CommunicationTokenCredential Используется CommunicationTokenCredential в качестве учетных данных маркера для создания экземпляра CallAgent.
CallAgentOptions Содержит CallAgentOptions сведения для идентификации вызывающего объекта.
HangupOptions Сообщает HangupOptions , следует ли прервать звонок всем участникам.

Регистрация обработчика схемы видео

Компонент пользовательского интерфейса, например MediaElement или MediaPlayerElement XAML, требуется, чтобы приложение зарегистрировало конфигурацию для отрисовки локальных и удаленных видеопотоков. Добавьте следующее содержимое между тегами Package Package.appxmanifest:

<Extensions>
    <Extension Category="windows.activatableClass.inProcessServer">
        <InProcessServer>
            <Path>RtmMvrUap.dll</Path>
            <ActivatableClass ActivatableClassId="VideoN.VideoSchemeHandler" ThreadingModel="both" />
        </InProcessServer>
    </Extension>
</Extensions>

Инициализация CallAgent

Чтобы создать CallAgent экземпляр, CallClientнеобходимо использовать CallClient.CreateCallAgentAsync метод, который асинхронно возвращает CallAgent объект после инициализации.

Для создания CallAgent необходимо передать объект CallTokenCredential и объект CallAgentOptions. Помните, что CallTokenCredential выдает исключение при передаче неправильного маркера.

Следующий код должен быть добавлен внутри вспомогательной функции, которую необходимо вызвать в инициализации приложения.

var callClient = new CallClient();
this.deviceManager = await callClient.GetDeviceManagerAsync();

var tokenCredential = new CallTokenCredential("<AUTHENTICATION_TOKEN>");
var callAgentOptions = new CallAgentOptions()
{
    DisplayName = "<DISPLAY_NAME>"
};

this.callAgent = await callClient.CreateCallAgentAsync(tokenCredential, callAgentOptions);
this.callAgent.CallsUpdated += Agent_OnCallsUpdatedAsync;
this.callAgent.IncomingCallReceived += Agent_OnIncomingCallAsync;

Измените <AUTHENTICATION_TOKEN> допустимый маркер учетных данных для ресурса. Если вам необходимо получить маркер учетных данных, см. документацию по маркеру доступа пользователя.

Осуществление персонального вызова с использованием видеокамеры

Объекты, необходимые для создания CallAgent, теперь готовы. Пришло время асинхронно создавать CallAgent и размещать видеозвонок.

private async void CallButton_Click(object sender, RoutedEventArgs e)
{
    var callString = CalleeTextBox.Text.Trim();

    if (!string.IsNullOrEmpty(callString))
    {
        if (callString.StartsWith("8:")) // 1:1 Azure Communication Services call
        {
            this.call = await StartAcsCallAsync(callString);
        }
    }

    if (this.call != null)
    {
        this.call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
        this.call.StateChanged += OnStateChangedAsync;
    }
}

private async Task<CommunicationCall> StartAcsCallAsync(string acsCallee)
{
    var options = await GetStartCallOptionsAsynnc();
    var call = await this.callAgent.StartCallAsync( new [] { new UserCallIdentifier(acsCallee) }, options);
    return call;
}

var micStream = new LocalOutgoingAudioStream(); // Create a default local audio stream
var cameraStream = new LocalOutgoingVideoStreamde(this.viceManager.Cameras.FirstOrDefault() as VideoDeviceDetails); // Create a default video stream

private async Task<StartCallOptions> GetStartCallOptionsAsynnc()
{
    return new StartCallOptions() {
        OutgoingAudioOptions = new OutgoingAudioOptions() { IsMuted = true, Stream = micStream  },
        OutgoingVideoOptions = new OutgoingVideoOptions() { Streams = new OutgoingVideoStream[] { cameraStream } }
    };
}

Предварительный просмотр изображения с локальной камеры

При необходимости можно настроить предварительную версию локальной камеры. Видео можно отобразить с помощью MediaPlayerElement:

<Grid>
    <MediaPlayerElement x:Name="LocalVideo" AutoPlay="True" />
    <MediaPlayerElement x:Name="RemoteVideo" AutoPlay="True" />
</Grid>

Чтобы инициализировать локальную предварительную версию MediaPlayerElement:

private async void CameraList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (cameraStream != null)
    {
        await cameraStream?.StopPreviewAsync();
        if (this.call != null)
        {
            await this.call?.StopVideoAsync(cameraStream);
        }
    }
    var selectedCamerea = CameraList.SelectedItem as VideoDeviceDetails;
    cameraStream = new LocalOutgoingVideoStream(selectedCamerea);

    var localUri = await cameraStream.StartPreviewAsync();
    LocalVideo.Source = MediaSource.CreateFromUri(localUri);

    if (this.call != null) {
        await this.call?.StartVideoAsync(cameraStream);
    }
}

Отрисовка потока удаленной камеры

Настройте даже обработчик в ответ на OnCallsUpdated событие:

private async void OnCallsUpdatedAsync(object sender, CallsUpdatedEventArgs args)
{
    var removedParticipants = new List<RemoteParticipant>();
    var addedParticipants = new List<RemoteParticipant>();

    foreach(var call in args.RemovedCalls)
    {
        removedParticipants.AddRange(call.RemoteParticipants.ToList<RemoteParticipant>());
    }

    foreach (var call in args.AddedCalls)
    {
        addedParticipants.AddRange(call.RemoteParticipants.ToList<RemoteParticipant>());
    }

    await OnParticipantChangedAsync(removedParticipants, addedParticipants);
}

private async void OnRemoteParticipantsUpdatedAsync(object sender, ParticipantsUpdatedEventArgs args)
{
    await OnParticipantChangedAsync(
        args.RemovedParticipants.ToList<RemoteParticipant>(),
        args.AddedParticipants.ToList<RemoteParticipant>());
}

private async Task OnParticipantChangedAsync(IEnumerable<RemoteParticipant> removedParticipants, IEnumerable<RemoteParticipant> addedParticipants)
{
    foreach (var participant in removedParticipants)
    {
        foreach(var incomingVideoStream in  participant.IncomingVideoStreams)
        {
            var remoteVideoStream = incomingVideoStream as RemoteIncomingVideoStream;
            if (remoteVideoStream != null)
            {
                await remoteVideoStream.StopPreviewAsync();
            }
        }
        participant.VideoStreamStateChanged -= OnVideoStreamStateChanged;
    }

    foreach (var participant in addedParticipants)
    {
        participant.VideoStreamStateChanged += OnVideoStreamStateChanged;
    }
}

private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs e)
{
    CallVideoStream callVideoStream = e.CallVideoStream;

    switch (callVideoStream.StreamDirection)
    {
        case StreamDirection.Outgoing:
            OnOutgoingVideoStreamStateChanged(callVideoStream as OutgoingVideoStream);
            break;
        case StreamDirection.Incoming:
            OnIncomingVideoStreamStateChanged(callVideoStream as IncomingVideoStream);
            break;
    }
}

Запуск отрисовки удаленного видеопотока в MediaPlayerElement:

private async void OnIncomingVideoStreamStateChanged(IncomingVideoStream incomingVideoStream)
{
    switch (incomingVideoStream.State)
    {
        case VideoStreamState.Available:
            {
                switch (incomingVideoStream.Kind)
                {
                    case VideoStreamKind.RemoteIncoming:
                        var remoteVideoStream = incomingVideoStream as RemoteIncomingVideoStream;
                        var uri = await remoteVideoStream.StartPreviewAsync();

                        await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                        {
                            RemoteVideo.Source = MediaSource.CreateFromUri(uri);
                        });

                        /* Or WinUI 3
                        this.DispatcherQueue.TryEnqueue(() => {
                            RemoteVideo.Source = MediaSource.CreateFromUri(uri);
                            RemoteVideo.MediaPlayer.Play();
                        });
                        */

                        break;

                    case VideoStreamKind.RawIncoming:
                        break;
                }

                break;
            }
        case VideoStreamState.Started:
            break;
        case VideoStreamState.Stopping:
            break;
        case VideoStreamState.Stopped:
            if (incomingVideoStream.Kind == VideoStreamKind.RemoteIncoming)
            {
                var remoteVideoStream = incomingVideoStream as RemoteIncomingVideoStream;
                await remoteVideoStream.StopPreviewAsync();
            }
            break;
        case VideoStreamState.NotAvailable:
            break;
    }
}

Завершение вызова

После совершения вызова используйте метод HangupAsync объекта CommunicationCall, чтобы завершить вызов.

Экземпляр HangupOptions также следует использовать для сообщения всем участникам о необходимости завершения вызова.

Следующий код нужно добавить в HangupButton_Click.

var call = this.callAgent?.Calls?.FirstOrDefault();
if (call != null)
{
    var call = this.callAgent?.Calls?.FirstOrDefault();
    if (call != null)
    {
        foreach (var localVideoStream in call.OutgoingVideoStreams)
        {
            await call.StopVideoAsync(localVideoStream);
        }

        try
        {
            if (cameraStream != null)
            {
                await cameraStream.StopPreviewAsync();
            }

            await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
        }
        catch(Exception ex) 
        { 
            var errorCode = unchecked((int)(0x0000FFFFU & ex.HResult));
            if (errorCode != 98) // Sample error code, sam_status_failed_to_hangup_for_everyone (98)
            {
                throw;
            }
        }
    }
}

Выполнение кода

Убедитесь, что Visual Studio создает приложение для x64, x86 или ARM64нажмите, F5 чтобы запустить приложение. После этого нажмите кнопку CommunicationCall, чтобы совершить вызов к определенному вызываемому.

Помните, что при первом запуске приложения система предложит пользователю предоставить доступ к микрофону.

Следующие шаги