你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
快速入门:以 Teams 用户的身份在应用中添加一对一视频通话功能
使用通信服务通话 SDK 向应用添加一对一语音和视频通话,开启 Azure 通信服务使用旅程。 你将了解如何使用适用于 JavaScript 的 Azure 通信服务呼叫 SDK 发起和接听呼叫。
代码示例
如果你要向前跳转到末尾,可以从 GitHub 下载此快速入门示例。
必备条件
- 获取具有有效订阅的 Azure 帐户。 免费创建帐户。
- 需要有 Node.js 18。 可以使用 msi 安装程序安装它。
- 创建活动的通信服务资源。 创建通信服务资源。
- 创建用于实例化呼叫客户端的用户访问令牌。 了解如何创建和管理用户访问令牌。
- 使用 Graph 浏览器,为呼叫操作获取 Teams 线程 ID。 阅读有关 如何创建聊天线程 ID 的详细信息。
设置
创建新的 Node.js 应用程序
打开终端或命令窗口,为应用创建一个新目录,并导航到该目录。
mkdir calling-quickstart && cd calling-quickstart
运行 npm init -y
以使用默认设置创建 package.json 文件。
npm init -y
安装包
使用 npm install
命令安装适用于 JavaScript 的 Azure 通信服务通话 SDK。
重要
本快速入门使用最新的 Azure 通信服务呼叫 SDK 版本。
npm install @azure/communication-common --save
npm install @azure/communication-calling --save
设置应用框架
此快速入门使用 webpack 捆绑应用程序资产。 运行以下命令以安装 webpack
、webpack-cli
和 webpack-dev-server
npm 包并在 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 文件。 我们将使用此文件来配置可让用户发起 1:1 视频呼叫的基本布局。
代码如下:
<!-- 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>
Azure 通信服务呼叫 Web SDK 对象模型
以下类和接口用于处理 Azure 通信服务通话 SDK 的某些主要功能:
名称 | 说明 |
---|---|
CallClient |
通话 SDK 的主入口点。 |
AzureCommunicationTokenCredential |
实现用于实例化 teamsCallAgent 的 CommunicationTokenCredential 接口。 |
TeamsCallAgent |
用于启动和管理 Teams 通话。 |
DeviceManager |
用于管理媒体设备。 |
TeamsCall |
用于表示 Teams 通话 |
LocalVideoStream |
用于为本地系统上的相机设备创建本地视频流。 |
RemoteParticipant |
用于表示通话中的远程参与者 |
RemoteVideoStream |
用于表示来自远程参与者的远程视频流。 |
在名为 client.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 updates.
subscribeToCall = (call) => {
try {
// Inspect the initial call.id value.
console.log(`Call Id: ${call.id}`);
//Subscribe 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 updates.
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 remoteParticipant 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
生成并运行应用。 运行以下命令,在本地 Web 服务器中捆绑应用程序主机:
`npx webpack serve --config webpack.config.js`
打开浏览器,并在两个选项卡上导航到 http://localhost:8080/. 选项卡应显示类似的结果,如下图所示:
在第一个选项卡上,输入有效的用户访问令牌。 在第二个选项卡上,输入另一个有效的用户访问令牌。 如果还没有可用的令牌,请参阅用户访问令牌文档。 在两个选项卡上,单击“初始化通话代理”按钮。 选项卡应显示类似的结果,如下图所示:
第一个选项卡上,输入第二个选项卡的 Azure 通信服务用户标识,然后选择“开始通话”按钮。 第一个选项卡将启动对第二个选项卡的传出呼叫,并且第二个选项卡的“接受呼叫”按钮变为可启用状态:
在第二个选项卡中,选择“接受呼叫”按钮。 将应答并连接呼叫。 选项卡应显示类似的结果,如下图所示:
现在,两个选项卡已成功连接到一对一视频通话中。 两个用户可以听到彼此的音频,并查看彼此的视频流。
使用通信服务通话 SDK 向应用添加一对一语音和视频通话,开启 Azure 通信服务使用旅程。 你将了解如何使用适用于 Windows 的 Azure 通信服务通话 SDK 发起和接听通话。
代码示例
如果你要向前跳转到末尾,可以从 GitHub 下载此快速入门示例。
先决条件
若要完成本教程,需要具备以下先决条件:
- 具有活动订阅的 Azure 帐户。 免费创建帐户。
- 安装带有通用 Windows 平台开发工作负载的 Visual Studio 2022。
- 已部署的通信服务资源。 创建通信服务资源。 需要为此快速入门记录连接字符串。
- Azure 通信服务的用户访问令牌。
- 使用 Graph 浏览器,为呼叫操作获取 Teams 线程 ID。 阅读有关 如何创建聊天线程 ID 的详细信息。
设置
创建项目
在 Visual Studio 中,使用“空白应用(通用 Windows)”模板创建一个新项目,设置单页通用 Windows 平台 (UWP) 应用。
安装包
右键选择项目,转到 Manage Nuget Packages
来安装 Azure.Communication.Calling.WindowsClient
1.2.0-beta.1 或更高版本。 请务必选中“包括预发行版”。
请求访问权限
转到 Package.appxmanifest
并选择 Capabilities
。
选中 Internet (Client)
和 Internet (Client & Server)
,以获取 Internet 的入站和出站访问权限。 选中 Microphone
以访问麦克风的音频源,然后勾选 Webcam
以访问相机的视频源。
设置应用框架
我们需要配置基本布局来附加逻辑。 为了发起出站呼叫,需要通过 TextBox
提供被呼叫方的用户 ID。 还需要一个 Start/Join call
按钮和一个 Hang up
按钮。 Mute
和 BackgroundBlur
还包括在此示例中,用于演示切换音频状态和视频效果的功能。
打开你的项目的 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
}
}
对象模型
下表列出了用于处理 Azure 通信服务通话 SDK 的某些主要功能的类和接口:
名称 | 说明 |
---|---|
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
”对象启动各种呼叫,并将“RemoteParticipantsUpdated
”和“StateChanged
”事件处理程序挂接到“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
事件接收器是在 SDK 启动帮助程序 InitCallAgentAndDeviceManagerAsync
中设置的。
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);
监视和响应呼叫状态更改事件
当正在进行的通话从一种状态转换到其他状态时,将会触发 TeamsCommunicationCall
对象上的 StateChanged
事件。 应用程序有机会反映 UI 上的状态更改或插入业务逻辑。
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 中生成并运行代码。 对于解决方案平台,我们支持 ARM64
、x64
和 x86
。
可以通过在“文本”字段中提供用户 ID 并单击 Start Call/Join
按钮,启动出站呼叫。 呼叫 8:echo123
会将你连接到回显机器人,此功能对于入门和验证音频设备是否正常运行非常有用。
使用通信服务通话 SDK 向应用添加一对一语音和视频通话,开启 Azure 通信服务使用旅程。 你将了解如何使用适用于 Java 的 Azure 通信服务呼叫 SDK 发起和接听呼叫。
代码示例
如果你要向前跳转到末尾,可以从 GitHub 下载此快速入门示例。
先决条件
- 具有活动订阅的 Azure 帐户。 免费创建帐户。
- Android Studio,用于创建 Android 应用程序。
- 已部署的通信服务资源。 创建通信服务资源。 需要为此快速入门记录连接字符串。
- Azure 通信服务的用户访问令牌。
- 使用 Graph 浏览器,为呼叫操作获取 Teams 线程 ID。 阅读有关 如何创建聊天线程 ID 的详细信息。
设置
使用空活动创建 Android 应用
在 Android Studio 中,选择“启动新的 Android Studio 项目”。
在“手机和平板电脑”下选择“空活动”项目模板。
选择“API 26: Android 8.0 (Oreo)”最低 SDK 版本或更高版本。
安装包
找到项目级别 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>
为应用设置布局
需要两个输入:被叫方 ID 的文本输入,以及用于进行呼叫的按钮。 可以通过设计器或通过编辑布局 xml 来添加这些输入。 使用 ID 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>
创建主活动基架和绑定
创建布局后,可以添加绑定以及活动的基本基架。 活动会处理请求运行时权限、创建团队呼叫代理以及在按下按钮时进行呼叫。 每项功能都在各自的部分中介绍。 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 权限指南。
对象模型
以下类和接口用于处理 Azure 通信服务通话 SDK 的某些主要功能:
名称 | 说明 |
---|---|
CallClient |
CallClient 是通话 SDK 的主入口点。 |
TeamsCallAgent |
TeamsCallAgent 用于启动和管理通话。 |
TeamsCall |
用于表示 Teams 通话的TeamsCall 。 |
CommunicationTokenCredential |
CommunicationTokenCredential 用作实例化 TeamsCallAgent 的令牌凭据。 |
CommunicationIdentifier |
CommunicationIdentifier 用作可参与通话的不同类型的参与者。 |
从用户访问令牌创建代理
借助用户令牌,可以实例化经过身份验证的呼叫代理。 通常,此令牌从具有特定于应用程序的身份验证的服务生成。 有关用户访问令牌的详细信息,请查看用户访问令牌指南。
对于快速入门,请使用为你的 Azure 通信服务资源生成的用户访问令牌替换 <User_Access_Token>
。
/**
* 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();
}
}
使用呼叫代理开始呼叫
可以通过团队呼叫代理来进行呼叫,只需提供被叫方 ID 的列表和呼叫选项。 对于快速入门,将使用不带视频的默认呼叫选项和来自文本输入的单个被叫方 ID。
/**
* 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;
}
启动应用并呼叫回显机器人
现在可以使用工具栏上的“运行应用”按钮 (Shift+F10) 启动应用。 通过呼叫 8:echo123
来验证是否能够进行通话。 播放预先录制的消息,然后重复你的消息并回复给你。
使用通信服务通话 SDK 向应用添加一对一视频通话,开启 Azure 通信服务使用旅程。 你会了解如何通过 Teams 标识使用适用于 iOS 的 Azure 通信服务通话 SDK 发起和接听视频通话。
代码示例
如果你要向前跳转到末尾,可以从 GitHub 下载此快速入门示例。
必备条件
- 获取具有有效订阅的 Azure 帐户。 免费创建帐户。
- 一部运行 Xcode 的 Mac,以及一个安装到密钥链的有效开发人员证书。
- 创建活动的通信服务资源。 创建通信服务资源。 需要为此快速入门记录连接字符串。
- Azure 通信服务的用户访问令牌。
- 使用 Graph 浏览器,为呼叫操作获取 Teams 线程 ID。 阅读有关如何创建聊天线程 ID的详细信息
设置
创建 Xcode 项目
在 Xcode 中,创建新的 iOS 项目,并选择“单视图应用”模板。 本教程使用 SwiftUI 框架,因此应将“语言”设置为“Swift”,并将“用户界面”设置为“SwiftUI”。 在此快速入门过程中,不会创建测试。 可以取消选中“包括测试”。
安装 CocoaPods
使用此指南在 Mac 上安装 CocoaPods。
使用 CocoaPods 安装包和依赖项
若要为应用程序创建
Podfile
,请打开终端并导航到项目文件夹,然后运行 pod init。将以下代码添加到
Podfile
并保存。 请参阅 SDK 支持版本。
platform :ios, '13.0'
use_frameworks!
target 'VideoCallingQuickstart' do
pod 'AzureCommunicationCalling', '~> 2.10.0'
end
运行 pod install。
使用 Xcode 打开
.xcworkspace
。
请求访问麦克风和摄像头
若要访问设备的麦克风和摄像头,需要使用 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
文件,然后将 import 声明添加到文件顶部以导入 AzureCommunicationCalling
库和 AVFoundation
。 AVFoundation 用于从代码中捕获音频权限。
import AzureCommunicationCalling
import AVFoundation
对象模型
以下类和接口处理适用于 iOS 的 Azure 通信服务呼叫 SDK 的某些主要功能。
名称 | 说明 |
---|---|
CallClient |
CallClient 是通话 SDK 的主入口点。 |
TeamsCallAgent |
TeamsCallAgent 用于启动和管理通话。 |
TeamsIncomingCall |
TeamsIncomingCall 用于接受或拒绝传入的团队呼叫。 |
CommunicationTokenCredential |
CommunicationTokenCredential 用作实例化 TeamsCallAgent 的令牌凭据。 |
CommunicationIdentifier |
CommunicationIdentifier 用于表示用户的身份,可以是以下选项之一:CommunicationUserIdentifier 、PhoneNumberIdentifier 或 CallingApplication 。 |
创建 Teams 呼叫代理
将 ContentView struct
的实现替换为使用户可以发起和结束通话的一些简单 UI 控件。 我们会在本快速入门中将业务逻辑添加到这些控件。
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
实例,你需要用户访问令牌使其可以发出和接收呼叫。 如果你没有可用的令牌,请参阅用户访问令牌文档。
如果你有一个令牌,请将以下代码添加到 ContentView.swift
中的 onAppear
回调。 需要将 <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
}
初始化 Teams CallAgent 并访问设备管理器
要从CallClient
创建TeamsCallAgent
实例,请使用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
方法设置为在点击“开始呼叫”按钮时执行的操作。 在本快速入门中,传出呼叫默认仅为音频呼叫。 若要开始视频通话,我们需要使用 LocalVideoStream
设置 VideoOptions
,并将其连同 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
和 RemoteParticipantObserver
用于管理呼叫中事件和远程参与者。 我们在 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, didReceiveIncomingCall 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
}
}
}
需要通过将以下代码添加到 ContentView.swift
中的 onAppear
回调来创建 TeamsIncomingCallHandler
的实例:
成功创建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
}
}
}
运行代码
可以通过选择“产品”>“运行”或使用 (⌘-R) 键盘快捷方式,在 iOS 模拟器上生成并运行应用。
清理资源
如果想要清理并删除通信服务订阅,可以删除资源或资源组。 删除资源组同时也会删除与之相关联的任何其他资源。 了解有关清理资源的详细信息。
后续步骤
有关详细信息,请参阅以下文章: