Краткое руководство. Добавление видеозвонка 1:1 в приложение в качестве пользователя Teams

Начните работу с Службы коммуникации Azure с помощью пакета SDK для звонков служб коммуникации, чтобы добавить голосовой и видеозвонок 1:1 в приложение. Вы узнаете, как начать и ответить на вызов с помощью пакета SDK для вызовов Службы коммуникации Azure для JavaScript.

Пример кода

Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.

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

Установка

Создание нового приложения Node.js

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

mkdir calling-quickstart && cd calling-quickstart

Воспользуйтесь командой npm init -y, чтобы создать файл package.json с параметрами по умолчанию.

npm init -y

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

Используйте команду npm install, чтобы установить пакет SDK Служб коммуникации Azure для реализации вызовов на JavaScript.

Внимание

В этом кратком руководстве используется последняя версия пакета SDK для вызовов Службы коммуникации Azure.

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

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

В этом кратком руководстве для объединения ресурсов приложения используется webpack. Выполните следующую команду, чтобы установить пакеты npm webpack, webpack-cli и webpack-dev-server, а также указать их в качестве зависимостей разработки в package.json:

npm install copy-webpack-plugin@^11.0.0 webpack@^5.88.2 webpack-cli@^5.1.4 webpack-dev-server@^4.15.1 --save-dev

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

Вот этот код:

<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>Azure Communication Services - Teams Calling Web Application</title>
    </head>
    <body>
        <h4>Azure Communication Services - Teams Calling Web Application</h4>
        <input id="user-access-token"
            type="text"
            placeholder="User access token"
            style="margin-bottom:1em; width: 500px;"/>
        <button id="initialize-teams-call-agent" type="button">Login</button>
        <br>
        <br>
        <input id="callee-teams-user-id"
            type="text"
            placeholder="Microsoft Teams callee's id (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
            style="margin-bottom:1em; width: 500px; display: block;"/>
        <button id="start-call-button" type="button" disabled="true">Start Call</button>
        <button id="hangup-call-button" type="button" disabled="true">Hang up Call</button>
        <button id="accept-call-button" type="button" disabled="true">Accept Call</button>
        <button id="start-video-button" type="button" disabled="true">Start Video</button>
        <button id="stop-video-button" type="button" disabled="true">Stop Video</button>
        <br>
        <br>
        <div id="connectedLabel" style="color: #13bb13;" hidden>Call is connected!</div>
        <br>
        <div id="remoteVideoContainer" style="width: 40%;" hidden>Remote participants' video streams:</div>
        <br>
        <div id="localVideoContainer" style="width: 30%;" hidden>Local video stream:</div>
        <!-- points to the bundle generated from client.js -->
        <script src="./main.js"></script>
    </body>
</html>

объектная модель веб-пакета SDK Службы коммуникации Azure

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

Имя Описание
CallClient Основная точка входа в пакет SDK для вызовов.
AzureCommunicationTokenCredential Реализует интерфейс CommunicationTokenCredential, который используется для создания экземпляра teamsCallAgent.
TeamsCallAgent Используется для запуска вызовов Teams и управления ими.
DeviceManager Используется для управления устройствами мультимедиа.
TeamsCall Используется для представления вызова Teams
LocalVideoStream Используется для создания локального видеопотока для устройства камеры в локальной системе.
RemoteParticipant Используется для представления удаленного участника в вызове
RemoteVideoStream Используется для представления удаленного видеопотока от удаленного участника.

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

// Make sure to install the necessary dependencies
const { CallClient, VideoStreamRenderer, LocalVideoStream } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential } = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");
// Set the log level and output
setLogLevel('verbose');
AzureLogger.log = (...args) => {
    console.log(...args);
};
// Calling web sdk objects
let teamsCallAgent;
let deviceManager;
let call;
let incomingCall;
let localVideoStream;
let localVideoStreamRenderer;
// UI widgets
let userAccessToken = document.getElementById('user-access-token');
let calleeTeamsUserId = document.getElementById('callee-teams-user-id');
let initializeCallAgentButton = document.getElementById('initialize-teams-call-agent');
let startCallButton = document.getElementById('start-call-button');
let hangUpCallButton = document.getElementById('hangup-call-button');
let acceptCallButton = document.getElementById('accept-call-button');
let startVideoButton = document.getElementById('start-video-button');
let stopVideoButton = document.getElementById('stop-video-button');
let connectedLabel = document.getElementById('connectedLabel');
let remoteVideoContainer = document.getElementById('remoteVideoContainer');
let localVideoContainer = document.getElementById('localVideoContainer');
/**
 * Create an instance of CallClient. Initialize a TeamsCallAgent instance with a CommunicationUserCredential via created CallClient. TeamsCallAgent enables us to make outgoing calls and receive incoming calls. 
 * You can then use the CallClient.getDeviceManager() API instance to get the DeviceManager.
 */
initializeCallAgentButton.onclick = async () => {
    try {
        const callClient = new CallClient(); 
        tokenCredential = new AzureCommunicationTokenCredential(userAccessToken.value.trim());
        teamsCallAgent = await callClient.createTeamsCallAgent(tokenCredential)
        // Set up a camera device to use.
        deviceManager = await callClient.getDeviceManager();
        await deviceManager.askDevicePermission({ video: true });
        await deviceManager.askDevicePermission({ audio: true });
        // Listen for an incoming call to accept.
        teamsCallAgent.on('incomingCall', async (args) => {
            try {
                incomingCall = args.incomingCall;
                acceptCallButton.disabled = false;
                startCallButton.disabled = true;
            } catch (error) {
                console.error(error);
            }
        });
        startCallButton.disabled = false;
        initializeCallAgentButton.disabled = true;
    } catch(error) {
        console.error(error);
    }
}
/**
 * Place a 1:1 outgoing video call to a user
 * Add an event listener to initiate a call when the `startCallButton` is selected.
 * Enumerate local cameras using the deviceManager `getCameraList` API.
 * In this quickstart, we're using the first camera in the collection. Once the desired camera is selected, a
 * LocalVideoStream instance will be constructed and passed within `videoOptions` as an item within the
 * localVideoStream array to the call method. When the call connects, your application will be sending a video stream to the other participant. 
 */
startCallButton.onclick = async () => {
    try {
        const localVideoStream = await createLocalVideoStream();
        const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
        call = teamsCallAgent.startCall({ microsoftTeamsUserId: calleeTeamsUserId.value.trim() }, { videoOptions: videoOptions });
        // Subscribe to the call's properties and events.
        subscribeToCall(call);
    } catch (error) {
        console.error(error);
    }
}
/**
 * Accepting an incoming call with a video
 * Add an event listener to accept a call when the `acceptCallButton` is selected.
 * You can accept incoming calls after subscribing to the `TeamsCallAgent.on('incomingCall')` event.
 * You can pass the local video stream to accept the call with the following code.
 */
acceptCallButton.onclick = async () => {
    try {
        const localVideoStream = await createLocalVideoStream();
        const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
        call = await incomingCall.accept({ videoOptions });
        // Subscribe to the call's properties and events.
        subscribeToCall(call);
    } catch (error) {
        console.error(error);
    }
}
// Subscribe to a call obj.
// Listen for property changes and collection udpates.
subscribeToCall = (call) => {
    try {
        // Inspect the initial call.id value.
        console.log(`Call Id: ${call.id}`);
        //Subsribe to call's 'idChanged' event for value changes.
        call.on('idChanged', () => {
            console.log(`Call ID changed: ${call.id}`); 
        });
        // Inspect the initial call.state value.
        console.log(`Call state: ${call.state}`);
        // Subscribe to call's 'stateChanged' event for value changes.
        call.on('stateChanged', async () => {
            console.log(`Call state changed: ${call.state}`);
            if(call.state === 'Connected') {
                connectedLabel.hidden = false;
                acceptCallButton.disabled = true;
                startCallButton.disabled = true;
                hangUpCallButton.disabled = false;
                startVideoButton.disabled = false;
                stopVideoButton.disabled = false;
            } else if (call.state === 'Disconnected') {
                connectedLabel.hidden = true;
                startCallButton.disabled = false;
                hangUpCallButton.disabled = true;
                startVideoButton.disabled = true;
                stopVideoButton.disabled = true;
                console.log(`Call ended, call end reason={code=${call.callEndReason.code}, subCode=${call.callEndReason.subCode}}`);
            }   
        });
        call.on('isLocalVideoStartedChanged', () => {
            console.log(`isLocalVideoStarted changed: ${call.isLocalVideoStarted}`);
        });
        console.log(`isLocalVideoStarted: ${call.isLocalVideoStarted}`);
        call.localVideoStreams.forEach(async (lvs) => {
            localVideoStream = lvs;
            await displayLocalVideoStream();
        });
        call.on('localVideoStreamsUpdated', e => {
            e.added.forEach(async (lvs) => {
                localVideoStream = lvs;
                await displayLocalVideoStream();
            });
            e.removed.forEach(lvs => {
               removeLocalVideoStream();
            });
        });
        
        // Inspect the call's current remote participants and subscribe to them.
        call.remoteParticipants.forEach(remoteParticipant => {
            subscribeToRemoteParticipant(remoteParticipant);
        });
        // Subscribe to the call's 'remoteParticipantsUpdated' event to be
        // notified when new participants are added to the call or removed from the call.
        call.on('remoteParticipantsUpdated', e => {
            // Subscribe to new remote participants that are added to the call.
            e.added.forEach(remoteParticipant => {
                subscribeToRemoteParticipant(remoteParticipant)
            });
            // Unsubscribe from participants that are removed from the call
            e.removed.forEach(remoteParticipant => {
                console.log('Remote participant removed from the call.');
            });
        });
    } catch (error) {
        console.error(error);
    }
}
// Subscribe to a remote participant obj.
// Listen for property changes and collection udpates.
subscribeToRemoteParticipant = (remoteParticipant) => {
    try {
        // Inspect the initial remoteParticipant.state value.
        console.log(`Remote participant state: ${remoteParticipant.state}`);
        // Subscribe to remoteParticipant's 'stateChanged' event for value changes.
        remoteParticipant.on('stateChanged', () => {
            console.log(`Remote participant state changed: ${remoteParticipant.state}`);
        });
        // Inspect the remoteParticipants's current videoStreams and subscribe to them.
        remoteParticipant.videoStreams.forEach(remoteVideoStream => {
            subscribeToRemoteVideoStream(remoteVideoStream)
        });
        // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be
        // notified when the remoteParticiapant adds new videoStreams and removes video streams.
        remoteParticipant.on('videoStreamsUpdated', e => {
            // Subscribe to newly added remote participant's video streams.
            e.added.forEach(remoteVideoStream => {
                subscribeToRemoteVideoStream(remoteVideoStream)
            });
            // Unsubscribe from newly removed remote participants' video streams.
            e.removed.forEach(remoteVideoStream => {
                console.log('Remote participant video stream was removed.');
            })
        });
    } catch (error) {
        console.error(error);
    }
}
/**
 * Subscribe to a remote participant's remote video stream obj.
 * You have to subscribe to the 'isAvailableChanged' event to render the remoteVideoStream. If the 'isAvailable' property
 * changes to 'true' a remote participant is sending a stream. Whenever the availability of a remote stream changes
 * you can choose to destroy the whole 'Renderer' a specific 'RendererView' or keep them. Displaying RendererView without a video stream will result in a blank video frame. 
 */
subscribeToRemoteVideoStream = async (remoteVideoStream) => {
    // Create a video stream renderer for the remote video stream.
    let videoStreamRenderer = new VideoStreamRenderer(remoteVideoStream);
    let view;
    const renderVideo = async () => {
        try {
            // Create a renderer view for the remote video stream.
            view = await videoStreamRenderer.createView();
            // Attach the renderer view to the UI.
            remoteVideoContainer.hidden = false;
            remoteVideoContainer.appendChild(view.target);
        } catch (e) {
            console.warn(`Failed to createView, reason=${e.message}, code=${e.code}`);
        }	
    }
    
    remoteVideoStream.on('isAvailableChanged', async () => {
        // Participant has switched video on.
        if (remoteVideoStream.isAvailable) {
            await renderVideo();
        // Participant has switched video off.
        } else {
            if (view) {
                view.dispose();
                view = undefined;
            }
        }
    });
    // Participant has video on initially.
    if (remoteVideoStream.isAvailable) {
        await renderVideo();
    }
}
// Start your local video stream.
// This will send your local video stream to remote participants so they can view it.
startVideoButton.onclick = async () => {
    try {
        const localVideoStream = await createLocalVideoStream();
        await call.startVideo(localVideoStream);
    } catch (error) {
        console.error(error);
    }
}
// Stop your local video stream.
// This will stop your local video stream from being sent to remote participants.
stopVideoButton.onclick = async () => {
    try {
        await call.stopVideo(localVideoStream);
    } catch (error) {
        console.error(error);
    }
}
/**
 * To render a LocalVideoStream, you need to create a new instance of VideoStreamRenderer, and then
 * create a new VideoStreamRendererView instance using the asynchronous createView() method.
 * You may then attach view.target to any UI element. 
 */
// Create a local video stream for your camera device
createLocalVideoStream = async () => {
    const camera = (await deviceManager.getCameras())[0];
    if (camera) {
        return new LocalVideoStream(camera);
    } else {
        console.error(`No camera device found on the system`);
    }
}
// Display your local video stream preview in your UI
displayLocalVideoStream = async () => {
    try {
        localVideoStreamRenderer = new VideoStreamRenderer(localVideoStream);
        const view = await localVideoStreamRenderer.createView();
        localVideoContainer.hidden = false;
        localVideoContainer.appendChild(view.target);
    } catch (error) {
        console.error(error);
    } 
}
// Remove your local video stream preview from your UI
removeLocalVideoStream = async() => {
    try {
        localVideoStreamRenderer.dispose();
        localVideoContainer.hidden = true;
    } catch (error) {
        console.error(error);
    } 
}
// End the current call
hangUpCallButton.addEventListener("click", async () => {
    // end the current call
    await call.hangUp();
});

Добавление кода локального сервера webpack

Создайте файл в корневом каталоге проекта с именем webpack.config.js , чтобы содержать логику локального сервера для этого краткого руководства. Добавьте следующий код в webpack.config.js:

const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
    mode: 'development',
    entry: './index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        static: {
            directory: path.join(__dirname, './')
        },
    },
    plugins: [
        new CopyPlugin({
            patterns: [
                './index.html'
            ]
        }),
    ]
};

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

Чтобы создать и запустить приложение, используйте webpack-dev-server. Выполните следующую команду, чтобы создать пакет узла приложения на локальном веб-сервере.

`npx webpack serve --config webpack.config.js`

Откройте браузер и перейдите на две вкладки. http://localhost:8080/. Вкладки должны показать аналогичный результат, как на следующем рисунке: Снимок экрана: две вкладки в представлении по умолчанию. Каждая вкладка будет использоваться для разных пользователей Teams.

На первой вкладке введите допустимый маркер доступа пользователя. На второй вкладке введите другой допустимый маркер доступа пользователя. Обратитесь к документации по маркерам доступа пользователя, если у вас еще нет маркеров доступа, доступных для использования. На обеих вкладках нажмите кнопки "Инициализация агента вызова". Вкладки должны показать аналогичный результат, как на следующем рисунке: Снимок экрана: шаги по инициализации каждого пользователя Teams на вкладке браузера.

На первой вкладке введите удостоверение пользователя Службы коммуникации Azure второй вкладки и нажмите кнопку "Начать звонок". Первая вкладка запустит исходящий вызов на второй вкладке, а кнопка "Принять звонок" второй вкладки будет включена: Снимок экрана: при инициализации пакета SDK для пользователей Teams показано, как начать вызов второго пользователя и как принять вызов.

На второй вкладке нажмите кнопку "Принять звонок". Вызов будет отвечать и подключаться. Вкладки должны показать аналогичный результат, как на следующем рисунке: Снимок экрана: две вкладки с текущим вызовом между двумя пользователями Teams, каждый вошедший на отдельную вкладку.

Обе вкладки теперь успешно находятся в видеозвонке 1:1. Оба пользователя могут слышать звук друг друга и видеть друг друга видеопоток.

Начните работу с Службы коммуникации Azure с помощью пакета SDK для звонков служб коммуникации, чтобы добавить голосовой и видеозвонок 1:1 в приложение. Вы узнаете, как начать и ответить на вызов с помощью пакета SDK для вызовов Службы коммуникации Azure для Windows.

Пример кода

Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.

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

Для работы с данным руководством вам потребуется:

Установка

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

Создайте в Visual Studio новый проект с помощью шаблона Пустое приложение (универсальное приложение Windows), чтобы настроить одностраничное приложение универсальной платформы Windows (UWP).

Снимок экрана: окно нового проекта UWP в Visual Studio.

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

Выберите проект правой кнопкой мыши и перейдите к Manage Nuget Packages установке Azure.Communication.Calling.WindowsClientверсии 1.2.0-beta.1 или более поздней версии. Убедитесь, что предварительная подготовка включена проверка.

Запрос на доступ

Перейдите Package.appxmanifest и выберите Capabilities. Проверьте Internet (Client) и Internet (Client & Server) получите входящий и исходящий доступ к Интернету. Проверьте Microphone доступ к звуковому каналу микрофона и Webcam чтобы получить доступ к видео-каналу камеры.

Снимок экрана, показывающий запрос доступа к Интернету и микрофону в Visual Studio

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

Необходимо настроить базовую структуру для подключения нашей логики. Чтобы разместить исходящий вызов, необходимо TextBox указать идентификатор пользователя вызываемого абонента. Будут также необходимы кнопки Start/Join call и Hang up. В Mute этом примере также включены проверка BackgroundBlur boxs, чтобы продемонстрировать функции переключения состояний звука и эффектов видео.

Откройте MainPage.xaml проекта и добавьте узел Grid в Page.

<Page
    x:Class="CallingQuickstart.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CallingQuickstart"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Width="800" Height="600">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="16*"/>
            <RowDefinition Height="30*"/>
            <RowDefinition Height="200*"/>
            <RowDefinition Height="60*"/>
            <RowDefinition Height="16*"/>
        </Grid.RowDefinitions>
        <TextBox Grid.Row="1" x:Name="CalleeTextBox" PlaceholderText="Who would you like to call?" TextWrapping="Wrap" VerticalAlignment="Center" Height="30" Margin="10,10,10,10" />

        <Grid x:Name="AppTitleBar" Background="LightSeaGreen">
            <TextBlock x:Name="QuickstartTitle" Text="Calling Quickstart sample title bar" Style="{StaticResource CaptionTextBlockStyle}" Padding="7,7,0,0"/>
        </Grid>

        <Grid Grid.Row="2">
            <Grid.RowDefinitions>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <MediaPlayerElement x:Name="LocalVideo" HorizontalAlignment="Center" Stretch="UniformToFill" Grid.Column="0" VerticalAlignment="Center" AutoPlay="True" />
            <MediaPlayerElement x:Name="RemoteVideo" HorizontalAlignment="Center" Stretch="UniformToFill" Grid.Column="1" VerticalAlignment="Center" AutoPlay="True" />
        </Grid>
        <StackPanel Grid.Row="3" Orientation="Vertical" Grid.RowSpan="2">
            <StackPanel Orientation="Horizontal">
                <Button x:Name="CallButton" Content="Start/Join call" Click="CallButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="123"/>
                <Button x:Name="HangupButton" Content="Hang up" Click="HangupButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="123"/>
                <CheckBox x:Name="MuteLocal" Content="Mute" Margin="10,0,0,0" Click="MuteLocal_Click" Width="74"/>
            </StackPanel>
        </StackPanel>
        <TextBox Grid.Row="5" x:Name="Stats" Text="" TextWrapping="Wrap" VerticalAlignment="Center" Height="30" Margin="0,2,0,0" BorderThickness="2" IsReadOnly="True" Foreground="LightSlateGray" />
    </Grid>
</Page>

Откройте файл MainPage.xaml.cs и замените его содержимое следующей реализацией.

using Azure.Communication.Calling.WindowsClient;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using Windows.Media.Core;
using Windows.Networking.PushNotifications;
using Windows.UI;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

namespace CallingQuickstart
{
    public sealed partial class MainPage : Page
    {
        private const string authToken = "<AUTHENTICATION_TOKEN>";

        private CallClient callClient;
        private CallTokenRefreshOptions callTokenRefreshOptions = new CallTokenRefreshOptions(false);
        private TeamsCallAgent teamsCallAgent;
        private TeamsCommunicationCall teamsCall;

        private LocalOutgoingAudioStream micStream;
        private LocalOutgoingVideoStream cameraStream;

        #region Page initialization
        public MainPage()
        {
            this.InitializeComponent();
            // Additional UI customization code goes here
        }

        protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
        }
        #endregion

        #region UI event handlers
        private async void CallButton_Click(object sender, RoutedEventArgs e)
        {
            // Start a call
        }

        private async void HangupButton_Click(object sender, RoutedEventArgs e)
        {
            // Hang up a call
        }

        private async void MuteLocal_Click(object sender, RoutedEventArgs e)
        {
            // Toggle mute/unmute audio state of a call
        }
        #endregion

        #region API event handlers
        private async void OnIncomingCallAsync(object sender, TeamsIncomingCallReceivedEventArgs args)
        {
            // Handle incoming call event
        }

        private async void OnStateChangedAsync(object sender, PropertyChangedEventArgs args)
        {
            // Handle connected and disconnected state change of a call
        }
        #endregion
    }
}

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

В следующей таблице перечислены классы и интерфейсы, которые обрабатывают некоторые основные функции пакета SDK для вызовов Службы коммуникации Azure:

Имя Описание
CallClient Это CallClient основная точка входа в пакет SDK для вызова.
TeamsCallAgent Используется TeamsCallAgent для запуска вызовов и управления ими.
TeamsCommunicationCall Используется TeamsCommunicationCall для управления текущим вызовом.
CallTokenCredential Используется CallTokenCredential в качестве учетных данных маркера для создания экземпляра TeamsCallAgent.
CallIdentifier Используется CallIdentifier для представления удостоверения пользователя, который может быть одним из следующих вариантов: MicrosoftTeamsUserCallIdentifier, UserCallIdentifierи PhoneNumberCallIdentifier т. д.

аутентификация клиента;

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

В коде замените <AUTHENTICATION_TOKEN> маркер доступа пользователем. Если у вас еще нет доступного маркера, см. документацию по маркеру доступа пользователя.

Добавьте InitCallAgentAndDeviceManagerAsync функцию, которая загружает пакет SDK. Этот вспомогательный элемент можно настроить в соответствии с требованиями приложения.

        private async Task InitCallAgentAndDeviceManagerAsync()
        {
            this.callClient = new CallClient(new CallClientOptions() {
                Diagnostics = new CallDiagnosticsOptions() { 
                    AppName = "CallingQuickstart",
                    AppVersion="1.0",
                    Tags = new[] { "Calling", "CTE", "Windows" }
                    }
                });

            // Set up local video stream using the first camera enumerated
            var deviceManager = await this.callClient.GetDeviceManagerAsync();
            var camera = deviceManager?.Cameras?.FirstOrDefault();
            var mic = deviceManager?.Microphones?.FirstOrDefault();
            micStream = new LocalOutgoingAudioStream();

            var tokenCredential = new CallTokenCredential(authToken, callTokenRefreshOptions);

            this.teamsCallAgent = await this.callClient.CreateTeamsCallAgentAsync(tokenCredential);
            this.teamsCallAgent.IncomingCallReceived += OnIncomingCallAsync;
        }

Инициирование вызова

Добавьте реализацию для CallButton_Click запуска различных видов вызовов с teamsCallAgent созданным объектом и перехватчиками RemoteParticipantsUpdatedStateChanged событий в TeamsCommunicationCall объекте.

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

            teamsCall = await StartCteCallAsync(callString);
            if (teamsCall != null)
            {
                teamsCall.StateChanged += OnStateChangedAsync;
            }
        }

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

Текущий вызов завершается при нажатии кнопки Hang up. Добавьте реализацию в HangupButton_Click для завершения вызова и остановите предварительный просмотр и видеопотоки.

        private async void HangupButton_Click(object sender, RoutedEventArgs e)
        {
            var teamsCall = this.teamsCallAgent?.Calls?.FirstOrDefault();
            if (teamsCall != null)
            {
                await teamsCall.HangUpAsync(new HangUpOptions() { ForEveryone = false });
            }
        }

Переключатель выключения или отмены звука

Отключение исходящего звука при Mute нажатии кнопки. Добавьте реализацию в MuteLocal_Click для отключения вызова.

        private async void MuteLocal_Click(object sender, RoutedEventArgs e)
        {
            var muteCheckbox = sender as CheckBox;

            if (muteCheckbox != null)
            {
                var teamsCall = this.teamsCallAgent?.Calls?.FirstOrDefault();
                if (teamsCall != null)
                {
                    if ((bool)muteCheckbox.IsChecked)
                    {
                        await teamsCall.MuteOutgoingAudioAsync();
                    }
                    else
                    {
                        await teamsCall.UnmuteOutgoingAudioAsync();
                    }
                }

                // Update the UI to reflect the state
            }
        }

Запуск вызова

StartTeamsCallOptions После получения TeamsCallAgent объекта можно использовать для запуска вызова Teams:

        private async Task<TeamsCommunicationCall> StartCteCallAsync(string cteCallee)
        {
            var options = new StartTeamsCallOptions();
            var teamsCall = await this.teamsCallAgent.StartCallAsync( new MicrosoftTeamsUserCallIdentifier(cteCallee), options);
            return call;
        }

Прием входящего вызова

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

    this.teamsCallAgent.IncomingCallReceived += OnIncomingCallAsync;

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

        private async void OnIncomingCallAsync(object sender, TeamsIncomingCallReceivedEventArgs args)
        {
            var teamsIncomingCall = args.IncomingCall;

            var acceptteamsCallOptions = new AcceptTeamsCallOptions() { };

            teamsCall = await teamsIncomingCall.AcceptAsync(acceptteamsCallOptions);
            teamsCall.StateChanged += OnStateChangedAsync;
        }

Присоединение к вызову Teams

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

TeamsMeetingLinkLocator link = new TeamsMeetingLinkLocator("meetingLink");
JoinTeamsCallOptions options = new JoinTeamsCallOptions();
TeamsCall call = await teamsCallAgent.JoinAsync(link, options);

Мониторинг и реагирование на событие изменения состояния вызова

StateChanged событие на TeamsCommunicationCall объекте запускается при выполнении транзакций вызова из одного состояния в другое. Приложение предоставляет возможности отражения изменений состояния пользовательского интерфейса или вставки бизнес-логики.

        private async void OnStateChangedAsync(object sender, PropertyChangedEventArgs args)
        {
            var teamsCall = sender as TeamsCommunicationCall;

            if (teamsCall != null)
            {
                var state = teamsCall.State;

                // Update the UI

                switch (state)
                {
                    case CallState.Connected:
                        {
                            await teamsCall.StartAudioAsync(micStream);

                            break;
                        }
                    case CallState.Disconnected:
                        {
                            teamsCall.StateChanged -= OnStateChangedAsync;

                            teamsCall.Dispose();

                            break;
                        }
                    default: break;
                }
            }
        }

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

Вы можете выполнить сборку и запустить код в Visual Studio. Для платформ решений мы поддерживаем ARM64x64 и x86.

Вы можете выполнить исходящий вызов, указав идентификатор пользователя в текстовом поле и нажав кнопку Start Call/Join. Вызов 8:echo123 подключает вас к эхо-боту, эта функция отлично подходит для начала работы и проверки работы звуковых устройств.

Снимок экрана: запуск приложения быстрого запуска UWP

Начните работу с Службы коммуникации Azure с помощью пакета SDK для звонков служб коммуникации, чтобы добавить голосовой и видеозвонок 1:1 в приложение. Вы узнаете, как начать и ответить на вызов с помощью пакета SDK для вызовов Службы коммуникации Azure для Java.

Пример кода

Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.

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

Установка

Создание приложения Android с пустым действием

В Android Studio щелкните Start a new Android Studio project (Создать проект Android Studio).

Снимок экрана с выбранной кнопкой создания нового проекта в Android Studio.

Выберите шаблон проекта Empty Activity (Пустое действие) из раздела Phone and Tablet (Телефон и планшет).

Снимок экрана с экраном шаблона проекта, где выбран вариант Empty Activity (Пустое действие).

Выберите минимальную версию пакета SDK: "API 26: Android 8.0 (Oreo)" или более позднюю.

Снимок экрана с экраном шаблона проекта, где выбран вариант Empty Activity (Пустое действие) (2).

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

Выберите build.gradle на уровне проекта и добавьте mavenCentral() в список репозиториев в разделах buildscript и allprojects.

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

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

android {
    ...
    packagingOptions {
        pickFirst  'META-INF/*'
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

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

Добавление разрешений в манифест приложения

Чтобы запрашивать разрешения, необходимые для вызова, они должны быть объявлены в манифесте приложения (app/src/main/AndroidManifest.xml). Замените содержимое файла следующим кодом:

    <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.contoso.ctequickstart">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!--Our Calling SDK depends on the Apache HTTP SDK.
When targeting Android SDK 28+, this library needs to be explicitly referenced.
See https://developer.android.com/about/versions/pie/android-9.0-changes-28#apache-p-->
        <uses-library android:name="org.apache.http.legacy" android:required="false"/>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
    

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

Нам нужны два элемента: текстовое поле для идентификатора вызываемого участника и кнопка для начала вызова. Эти входные данные можно добавить через конструктор или изменить xml макета. Создайте кнопку с идентификатором call_button и текстовое поле callee_id. Перейдите к (app/src/main/res/layout/activity_main.xml) и замените содержимое файла следующим кодом:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/call_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="Call"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <EditText
        android:id="@+id/callee_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="Callee Id"
        android:inputType="textPersonName"
        app:layout_constraintBottom_toTopOf="@+id/call_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Создание шаблонов и привязок для основных событий

После создания макета вы можете добавить в действие привязки и основные шаблоны. Действие обрабатывает запрос разрешений среды выполнения, создание агента вызова teams и размещение вызова при нажатии кнопки. Каждый из них рассматривается в собственном разделе. Метод onCreate переопределяется для вызова getAllPermissions и createTeamsAgent добавления привязок для кнопки вызова. Это событие происходит только один раз при создании действия. Дополнительные сведения onCreateсм. в руководстве по жизненному циклу действий.

Перейдите к файлу MainActivity.java и замените его содержимое следующим кодом:

package com.contoso.ctequickstart;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.media.AudioManager;
import android.Manifest;
import android.content.pm.PackageManager;

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.azure.android.communication.common.CommunicationUserIdentifier;
import com.azure.android.communication.common.CommunicationTokenCredential;
import com.azure.android.communication.calling.TeamsCallAgent;
import com.azure.android.communication.calling.CallClient;
import com.azure.android.communication.calling.StartTeamsCallOptions;


import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    
    private TeamsCallAgent teamsCallAgent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getAllPermissions();
        createTeamsAgent();
        
        // Bind call button to call `startCall`
        Button callButton = findViewById(R.id.call_button);
        callButton.setOnClickListener(l -> startCall());
        
        setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
    }

    /**
     * Request each required permission if the app doesn't already have it.
     */
    private void getAllPermissions() {
        // See section on requesting permissions
    }

    /**
      * Create the call agent for placing calls
      */
    private void createTeamsAgent() {
        // See section on creating the call agent
    }

    /**
     * Place a call to the callee id provided in `callee_id` text input.
     */
    private void startCall() {
        // See section on starting the call
    }
}

Запрос разрешений во время выполнения

В Android 6.0 и более поздних версий (API уровня 23) или targetSdkVersion версии 23 или более поздней разрешения предоставляются не при установке приложения, а во время выполнения. Для поддержки его getAllPermissions можно реализовать для вызова ActivityCompat.checkSelfPermission и ActivityCompat.requestPermissions для каждого требуемого разрешения.

/**
 * Request each required permission if the app doesn't already have it.
 */
private void getAllPermissions() {
    String[] requiredPermissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_PHONE_STATE};
    ArrayList<String> permissionsToAskFor = new ArrayList<>();
    for (String permission : requiredPermissions) {
        if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
            permissionsToAskFor.add(permission);
        }
    }
    if (!permissionsToAskFor.isEmpty()) {
        ActivityCompat.requestPermissions(this, permissionsToAskFor.toArray(new String[0]), 1);
    }
}

Примечание.

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

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

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

Имя Описание
CallClient Это CallClient основная точка входа в пакет SDK для вызова.
TeamsCallAgent Используется TeamsCallAgent для запуска вызовов и управления ими.
TeamsCall Используется TeamsCall для представления вызова Teams.
CommunicationTokenCredential Используется CommunicationTokenCredential в качестве учетных данных маркера для создания экземпляра TeamsCallAgent.
CommunicationIdentifier Используется CommunicationIdentifier в качестве другого типа участника, который может быть частью вызова.

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

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

Для целей этого краткого руководства замените <User_Access_Token> маркером доступа пользователя, который был создан для вашего ресурса Службы коммуникации Azure.


/**
 * Create the teams call agent for placing calls
 */
private void createAgent() {
    String userToken = "<User_Access_Token>";

    try {
        CommunicationTokenCredential credential = new CommunicationTokenCredential(userToken);
        teamsCallAgent = new CallClient().createTeamsCallAgent(getApplicationContext(), credential).get();
    } catch (Exception ex) {
        Toast.makeText(getApplicationContext(), "Failed to create teams call agent.", Toast.LENGTH_SHORT).show();
    }
}

Инициирование вызова с помощью агента вызовов

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

/**
 * Place a call to the callee id provided in `callee_id` text input.
 */
private void startCall() {
    EditText calleeIdView = findViewById(R.id.callee_id);
    String calleeId = calleeIdView.getText().toString();
    
    StartTeamsCallOptions options = new StartTeamsCallOptions();

    teamsCallAgent.startCall(
        getApplicationContext(),
        new MicrosoftTeamsUserCallIdentifier(calleeId),
        options);
}

Ответ на звонок

Прием вызова можно выполнить с помощью агента вызова teams, используя только ссылку на текущий контекст.

public void acceptACall(TeamsIncomingCall teamsIncomingCall){
	teamsIncomingCall.accept(this);
}

Присоединение к вызову Teams

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

/**
 * Join a call using a teams meeting link.
 */
public TeamsCall joinTeamsCall(TeamsCallAgent teamsCallAgent){
	TeamsMeetingLinkLocator link = new TeamsMeetingLinkLocator("meetingLink");
	TeamsCall call = teamsCallAgent.join(this, link);
}

Присоединение к вызову Teams с параметрами

Мы также можем присоединить существующий вызов с предварительно настроенными параметрами, такими как отключение.

/**
 * Join a call using a teams meeting link while muted.
 */
public TeamsCall joinTeamsCall(TeamsCallAgent teamsCallAgent){
	TeamsMeetingLinkLocator link = new TeamsMeetingLinkLocator("meetingLink");
	OutgoingAudioOptions audioOptions = new OutgoingAudioOptions().setMuted(true);
	JoinTeamsCallOptions options = new JoinTeamsCallOptions().setAudioOptions(audioOptions);
	TeamsCall call = teamsCallAgent.join(this, link, options);
}

Настройка прослушивателя входящих вызовов

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

private TeamsIncomingCall teamsincomingCall;
teamsCallAgent.addOnIncomingCallListener(this::handleIncomingCall);

private void handleIncomingCall(TeamsIncomingCall incomingCall) {
	this.teamsincomingCall = incomingCall;
}

Запуск приложения и вызов эхо-бота

Теперь вы можете запустить приложение с помощью кнопки Run App (Запустить приложение) на панели инструментов или нажав клавиши SHIFT+F10. Убедитесь, что вы можете размещать звонки путем вызова 8:echo123. Предварительно записанное сообщение воспроизводится, а затем повторите сообщение.

Снимок экрана с готовым приложением.

Приступая к работе с Службы коммуникации Azure с помощью пакета SDK для вызова служб коммуникации, чтобы добавить один на один видеозвонок в приложение. Вы узнаете, как начать и ответить на видеозвонок с помощью пакета SDK для вызовов Службы коммуникации Azure для iOS с помощью удостоверения Teams.

Пример кода

Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.

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

Установка

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

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

Снимок экрана с окном New Project (Новый проект) в Xcode.

Установка CocoaPods

Используйте это руководство для установки CocoaPods на компьютере Mac.

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

  1. Чтобы создать Podfile приложение, откройте терминал и перейдите в папку проекта и запустите pod init.

  2. Добавьте следующий код в и сохраните Podfile его. Ознакомьтесь с версиями поддержки пакета SDK.

platform :ios, '13.0'
use_frameworks!

target 'VideoCallingQuickstart' do
  pod 'AzureCommunicationCalling', '~> 2.10.0'
end
  1. Выполните команду pod install.

  2. Откройте файл .xcworkspace с помощью Xcode.

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

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

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

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

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

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

import AzureCommunicationCalling
import AVFoundation

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

Следующие классы и интерфейсы реализуют некоторые основные функции пакета SDK Служб коммуникации Azure для iOS.

Имя Описание
CallClient Это CallClient основная точка входа в пакет SDK для вызова.
TeamsCallAgent Используется TeamsCallAgent для запуска вызовов и управления ими.
TeamsIncomingCall Используется TeamsIncomingCall для принятия или отклонения входящих вызовов команд.
CommunicationTokenCredential Используется CommunicationTokenCredential в качестве учетных данных маркера для создания экземпляра TeamsCallAgent.
CommunicationIdentifier Используется CommunicationIdentifier для представления удостоверения пользователя, который может быть одним из следующих параметров: CommunicationUserIdentifierPhoneNumberIdentifier или CallingApplication.

Создание агента вызовов Teams

Замените реализацию ContentView struct простыми элементами управления пользовательского интерфейса, которые позволяют пользователю инициировать и завершить вызов. Мы добавим бизнес-логику в эти элементы управления в этом кратком руководстве.

struct ContentView: View {
    @State var callee: String = ""
    @State var callClient: CallClient?
    @State var teamsCallAgent: TeamsCallAgent?
    @State var teamsCall: TeamsCall?
    @State var deviceManager: DeviceManager?
    @State var localVideoStream:[LocalVideoStream]?
    @State var teamsIncomingCall: TeamsIncomingCall?
    @State var sendingVideo:Bool = false
    @State var errorMessage:String = "Unknown"

    @State var remoteVideoStreamData:[Int32:RemoteVideoStreamData] = [:]
    @State var previewRenderer:VideoStreamRenderer? = nil
    @State var previewView:RendererView? = nil
    @State var remoteRenderer:VideoStreamRenderer? = nil
    @State var remoteViews:[RendererView] = []
    @State var remoteParticipant: RemoteParticipant?
    @State var remoteVideoSize:String = "Unknown"
    @State var isIncomingCall:Bool = false
    
    @State var callObserver:CallObserver?
    @State var remoteParticipantObserver:RemoteParticipantObserver?

    var body: some View {
        NavigationView {
            ZStack{
                Form {
                    Section {
                        TextField("Who would you like to call?", text: $callee)
                        Button(action: startCall) {
                            Text("Start Teams Call")
                        }.disabled(teamsCallAgent == nil)
                        Button(action: endCall) {
                            Text("End Teams Call")
                        }.disabled(teamsCall == nil)
                        Button(action: toggleLocalVideo) {
                            HStack {
                                Text(sendingVideo ? "Turn Off Video" : "Turn On Video")
                            }
                        }
                    }
                }
                // Show incoming call banner
                if (isIncomingCall) {
                    HStack() {
                        VStack {
                            Text("Incoming call")
                                .padding(10)
                                .frame(maxWidth: .infinity, alignment: .topLeading)
                        }
                        Button(action: answerIncomingCall) {
                            HStack {
                                Text("Answer")
                            }
                            .frame(width:80)
                            .padding(.vertical, 10)
                            .background(Color(.green))
                        }
                        Button(action: declineIncomingCall) {
                            HStack {
                                Text("Decline")
                            }
                            .frame(width:80)
                            .padding(.vertical, 10)
                            .background(Color(.red))
                        }
                    }
                    .frame(maxWidth: .infinity, alignment: .topLeading)
                    .padding(10)
                    .background(Color.gray)
                }
                ZStack{
                    VStack{
                        ForEach(remoteViews, id:\.self) { renderer in
                            ZStack{
                                VStack{
                                    RemoteVideoView(view: renderer)
                                        .frame(width: .infinity, height: .infinity)
                                        .background(Color(.lightGray))
                                }
                            }
                            Button(action: endCall) {
                                Text("End Call")
                            }.disabled(teamsCall == nil)
                            Button(action: toggleLocalVideo) {
                                HStack {
                                    Text(sendingVideo ? "Turn Off Video" : "Turn On Video")
                                }
                            }
                        }
                        
                    }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                    VStack{
                        if(sendingVideo)
                        {
                            VStack{
                                PreviewVideoStream(view: previewView!)
                                    .frame(width: 135, height: 240)
                                    .background(Color(.lightGray))
                            }
                        }
                    }.frame(maxWidth:.infinity, maxHeight:.infinity,alignment: .bottomTrailing)
                }
            }
     .navigationBarTitle("Video Calling Quickstart")
        }.onAppear{
            // Authenticate the client
            
            // Initialize the TeamsCallAgent and access Device Manager
            
            // Ask for permissions
        }
    }
}

//Functions and Observers

struct PreviewVideoStream: UIViewRepresentable {
    let view:RendererView
    func makeUIView(context: Context) -> UIView {
        return view
    }
    func updateUIView(_ uiView: UIView, context: Context) {}
}

struct RemoteVideoView: UIViewRepresentable {
    let view:RendererView
    func makeUIView(context: Context) -> UIView {
        return view
    }
    func updateUIView(_ uiView: UIView, context: Context) {}
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

аутентификация клиента;

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

Получив маркер, добавьте приведенный ниже код в обратный вызов onAppear в ContentView.swift. Необходимо заменить <USER ACCESS TOKEN> допустимым маркером доступа пользователя для ресурса:

var userCredential: CommunicationTokenCredential?
do {
    userCredential = try CommunicationTokenCredential(token: "<USER ACCESS TOKEN>")
} catch {
    print("ERROR: It was not possible to create user credential.")
    return
}

Инициализация CallAgent Teams и доступ диспетчер устройств

Чтобы создать TeamsCallAgent экземпляр из CallClientобъекта, используйте callClient.createTeamsCallAgent метод, который асинхронно возвращает TeamsCallAgent объект после инициализации. DeviceManager позволяет получить список локальных устройств, которые можно использовать в вызове для передачи аудио- и видеопотоков. Он также позволяет запросить у пользователя разрешение на доступ к микрофону или камере.

self.callClient = CallClient()
let options = TeamsCallAgentOptions()
// Enable CallKit in the SDK
options.callKitOptions = CallKitOptions(with: createCXProvideConfiguration())
self.callClient?.createTeamsCallAgent(userCredential: userCredential, options: options) { (agent, error) in
    if error != nil {
        print("ERROR: It was not possible to create a Teams call agent.")
        return
    } else {
        self.teamsCallAgent = agent
        print("Teams Call agent successfully created.")
        self.teamsCallAgent!.delegate = teamsIncomingCallHandler
        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")
            }
        }
    }
}

Запрос разрешений

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

AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
    if granted {
        AVCaptureDevice.requestAccess(for: .video) { (videoGranted) in
            /* NO OPERATION */
        }
    }
}

Осуществление исходящего вызова

Метод startCall задается в качестве действия, выполняемого при нажатии кнопки "Пуск вызова". В этом кратком руководстве исходящие вызовы по умолчанию являются аудиовызовами. Чтобы начать вызов с видео, необходимо задать VideoOptions его LocalVideoStream и передать с startCallOptions ним, чтобы задать начальные параметры для вызова.

let startTeamsCallOptions = StartTeamsCallOptions()
if sendingVideo  {
    if self.localVideoStream == nil  {
        self.localVideoStream = [LocalVideoStream]()
    }
    let videoOptions = VideoOptions(localVideoStreams: localVideoStream!)
    startTeamsCallOptions.videoOptions = videoOptions
}
let callees: [CommunicationIdentifier] = [CommunicationUserIdentifier(self.callee)]
self.teamsCallAgent?.startCall(participants: callees, options: startTeamsCallOptions) { (call, error) in
    // Handle call object if successful or an error.
}

Подключение к собранию Teams

Этот join метод позволяет пользователю присоединяться к собранию команд.

let joinTeamsCallOptions = JoinTeamsCallOptions()
if sendingVideo
{
    if self.localVideoStream == nil {
        self.localVideoStream = [LocalVideoStream]()
    }
    let videoOptions = VideoOptions(localVideoStreams: localVideoStream!)
    joinTeamsCallOptions.videoOptions = videoOptions
}

// Join the Teams meeting muted
if isMuted
{
    let outgoingAudioOptions = OutgoingAudioOptions()
    outgoingAudioOptions.muted = true
    joinTeamsCallOptions.outgoingAudioOptions = outgoingAudioOptions
}

let teamsMeetingLinkLocator = TeamsMeetingLinkLocator(meetingLink: "https://meeting_link")

self.teamsCallAgent?.join(with: teamsMeetingLinkLocator, options: joinTeamsCallOptions) { (call, error) in
    // Handle call object if successful or an error.
}

TeamsCallObserver и RemotePariticipantObserver используются для управления событиями в процессе вызова и удаленными участниками. Мы устанавливаем наблюдателей setTeamsCallAndObserver в функции.

func setTeamsCallAndObserver(call:TeamsCall, error:Error?) {
    if (error == nil) {
        self.teamsCall = call
        self.teamsCallObserver = TeamsCallObserver(self)
        self.teamsCall!.delegate = self.teamsCallObserver
        // Attach a RemoteParticipant observer
        self.remoteParticipantObserver = RemoteParticipantObserver(self)
    } else {
        print("Failed to get teams call object")
    }
}

Ответ на входящий вызов

Чтобы ответить на входящий вызов, реализуйте интерфейс TeamsIncomingCallHandler, чтобы отобразить баннер входящего вызова для ответа или отклонения вызова. Добавьте следующую реализацию в TeamsIncomingCallHandler.swift.

final class TeamsIncomingCallHandler: NSObject, TeamsCallAgentDelegate, TeamsIncomingCallDelegate {
    public var contentView: ContentView?
    private var teamsIncomingCall: TeamsIncomingCall?

    private static var instance: TeamsIncomingCallHandler?
    static func getOrCreateInstance() -> TeamsIncomingCallHandler {
        if let c = instance {
            return c
        }
        instance = TeamsIncomingCallHandler()
        return instance!
    }

    private override init() {}
    
    func teamsCallAgent(_ teamsCallAgent: TeamsCallAgent, didRecieveIncomingCall incomingCall: TeamsIncomingCall) {
        self.teamsIncomingCall = incomingCall
        self.teamsIncomingCall.delegate = self
        contentView?.showIncomingCallBanner(self.teamsIncomingCall!)
    }
    
    func teamsCallAgent(_ teamsCallAgent: TeamsCallAgent, didUpdateCalls args: TeamsCallsUpdatedEventArgs) {
        if let removedCall = args.removedCalls.first {
            contentView?.callRemoved(removedCall)
            self.teamsIncomingCall = nil
        }
    }
}

Необходимо создать экземплярTeamsIncomingCallHandler, добавив следующий код в обратный onAppear вызов:ContentView.swift

Задайте делегат после TeamsCallAgent успешного TeamsCallAgent создания:

self.teamsCallAgent!.delegate = incomingCallHandler

После входящего вызова TeamsIncomingCallHandler вызывает функцию showIncomingCallBanner для отображения answer и decline кнопки.

func showIncomingCallBanner(_ incomingCall: TeamsIncomingCall) {
    self.teamsIncomingCall = incomingCall
}

Действия, присоединенные к answer и decline реализованные в виде следующего кода. Чтобы ответить на звонок с видео, необходимо включить локальное видео и задать параметры AcceptCallOptions с localVideoStreamпомощью .

func answerIncomingCall() {
    let options = AcceptTeamsCallOptions()
    guard let teamsIncomingCall = self.teamsIncomingCall else {
      print("No active incoming call")
      return
    }

    guard let deviceManager = deviceManager else {
      print("No device manager instance")
      return
    }

    if self.localVideoStreams == nil {
        self.localVideoStreams = [LocalVideoStream]()
    }

    if sendingVideo
    {
        guard let camera = deviceManager.cameras.first else {
            // Handle failure
            return
        }
        self.localVideoStreams?.append( LocalVideoStream(camera: camera))
        let videoOptions = VideoOptions(localVideoStreams: localVideosStreams!)
        options.videoOptions = videoOptions
    }

    teamsIncomingCall.accept(options: options) { (call, error) in
        // Handle call object if successful or an error.
    }
}

func declineIncomingCall() {
    self.teamsIncomingCall?.reject { (error) in 
        // Handle if rejection was successfully or not.
    }
}

Оформление подписки на события

Можно реализовать класс TeamsCallObserver для подписки на коллекцию событий для уведомления при изменении значений во время вызова.

public class TeamsCallObserver: NSObject, TeamsCallDelegate, TeamsIncomingCallDelegate {
    private var owner: ContentView
    init(_ view:ContentView) {
            owner = view
    }
        
    public func teamsCall(_ teamsCall: TeamsCall, didChangeState args: PropertyChangedEventArgs) {
        if(teamsCall.state == CallState.connected) {
            initialCallParticipant()
        }
    }

    // render remote video streams when remote participant changes
    public func teamsCall(_ teamsCall: TeamsCall, didUpdateRemoteParticipant args: ParticipantsUpdatedEventArgs) {
        for participant in args.addedParticipants {
            participant.delegate = self.remoteParticipantObserver
        }
    }

    // Handle remote video streams when the call is connected
    public func initialCallParticipant() {
        for participant in owner.teamsCall.remoteParticipants {
            participant.delegate = self.remoteParticipantObserver
            for stream in participant.videoStreams {
                renderRemoteStream(stream)
            }
            owner.remoteParticipant = participant
        }
    }
}

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

Вы можете создать и запустить приложение в симуляторе iOS, выбрав "Запуск продукта > " или с помощью сочетания клавиш ('-R).

Очистка ресурсов

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

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

Дополнительные сведения см. в следующих статьях: