빠른 시작: 애플리케이션에 Teams 사용자로 1:1 영상 통화 추가
Communication Services 통화 SDK를 사용하여 앱에 1:1 음성 및 영상 통화를 추가하여 Azure Communication Services를 시작합니다. JavaScript용 Azure Communication Services Calling SDK를 사용하여 통화를 시작하고 답변하는 방법에 대해 알아봅니다.
예제 코드
끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.
필수 조건
- 활성 구독이 있는 Azure 계정을 얻습니다. 체험 계정을 만듭니다.
- Node.js 18이 있어야 합니다. msi 설치 관리자를 사용하여 설치할 수 있습니다.
- 활성 Communication Services 리소스를 만듭니다. Communication Services 리소스 만들기
- 호출 클라이언트를 인스턴스화하기 위한 사용자 액세스 토큰을 만듭니다. 사용자 액세스 토큰 만들기 및 관리 방법에 대해 알아봅니다.
- Graph 탐색기를 사용하여 호출 작업을 위해 Teams 스레드 ID를 가져옵니다. 채팅 스레드 ID를 만드는 방법에 대해 자세히 알아봅니다.
설정
새 Node.js 애플리케이션 만들기
터미널 또는 명령 창을 열고 앱의 새 디렉터리를 만들고 해당 디렉터리로 이동합니다.
mkdir calling-quickstart && cd calling-quickstart
npm init -y
를 실행하여 기본 설정으로 package.json 파일을 만듭니다.
npm init -y
패키지 설치
npm install
명령을 사용하여 JavaScript용 Azure Communication Services Calling SDK를 설치합니다.
Important
이 빠른 시작에서는 최신 Azure Communication Services Calling 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>
웹 SDK 개체 모델을 호출하는 Azure Communication Services
Azure Communication Services Calling SDK의 주 기능 중 일부를 처리하는 클래스와 인터페이스는 다음과 같습니다.
이름 | 설명 |
---|---|
CallClient |
통화 SDK의 주 진입점입니다. |
AzureCommunicationTokenCredential |
teamsCallAgent 를 인스턴스화하는 데 사용되는 CommunicationTokenCredential 인터페이스를 구현합니다. |
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/.으로 이동합니다. 탭은 다음 이미지와 유사한 결과를 표시해야 합니다.
첫 번째 탭에서 유효한 사용자 액세스 토큰을 입력합니다. 두 번째 탭에서 다른 유효한 사용자 액세스 토큰을 입력합니다. 사용할 수 있는 액세스 토큰이 아직 없는 경우 사용자 액세스 토큰 설명서를 참조하세요. 두 탭에서 "에이전트 호출 초기화" 단추를 클릭합니다. 탭은 다음 이미지와 유사한 결과를 표시해야 합니다.
첫 번째 탭에서 두 번째 탭의 Azure Communication Services 사용자 ID를 입력하고 "통화 시작" 단추를 선택합니다. 첫 번째 탭이 두 번째 탭으로 발신 전화를 걸기 시작하고 두 번째 탭의 "통화 수락" 단추가 사용하도록 설정됩니다.
두 번째 탭에서 "통화 수락" 단추를 선택합니다. 전화를 받고 연결됩니다. 탭은 다음 이미지와 유사한 결과를 표시해야 합니다.
이제 두 탭 모두 1:1 영상 통화에 성공했습니다. 두 사용자 모두 서로의 오디오를 듣고 서로의 동영상 스트림을 볼 수 있습니다.
Communication Services 통화 SDK를 사용하여 앱에 1:1 음성 및 영상 통화를 추가하여 Azure Communication Services를 시작합니다. Windows용 Azure Communication Services Calling SDK를 사용하여 통화를 시작하고 답변하는 방법에 대해 알아봅니다.
예제 코드
끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.
필수 조건
이 자습서를 완료하려면 다음 필수 구성 요소가 필요합니다.
- 활성 구독이 있는 Azure 계정. 체험 계정을 만듭니다.
- 유니버설 Windows 플랫폼 개발 워크로드가 있는 Visual Studio 2022를 설치합니다.
- 배포된 Communication Services 리소스. Communication Services 리소스 만들기 이 빠른 시작에 대한 연결 문자열을 기록해야 합니다.
- 사용자 액세스 토큰(Azure Communication Service용)
- Graph 탐색기를 사용하여 호출 작업을 위해 Teams 스레드 ID를 가져옵니다. 채팅 스레드 ID를 만드는 방법에 대해 자세히 알아봅니다.
설정
프로젝트 만들기
Visual Studio에서 비어 있는 앱(유니버설 Windows) 템플릿을 사용하여 단일 페이지 UWP(유니버설 Windows 플랫폼) 앱을 설정하는 새 프로젝트를 만듭니다.
패키지 설치
프로젝트를 마우스 오른쪽 단추로 선택하고 Manage Nuget Packages
(으)로 이동하여 Azure.Communication.Calling.WindowsClient
1.2.0-beta.1 이상을 설치합니다. 시험판 포함이 선택되어 있는지 확인합니다.
액세스 요청
Package.appxmanifest
로 이동하여 Capabilities
를 선택합니다.
인터넷에 대한 인바운드 및 아웃바운드 액세스 권한을 얻으려면 Internet (Client)
및 Internet (Client & Server)
를 선택합니다. Microphone
을 선택하여 마이크의 오디오 피드에 액세스하고 Webcam
을 선택하여 카메라의 비디오 피드에 액세스합니다.
앱 프레임워크 설정
논리를 연결하려면 기본 레이아웃을 구성해야 합니다. 아웃바운드 통화를 수행하려면 호출 수신자의 사용자 ID를 입력할 TextBox
가 필요합니다. 또한 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 Communication Services Calling SDK의 주요 기능 중 일부를 처리하는 클래스 및 인터페이스가 나열되어 있습니다.
이름 | 설명 |
---|---|
CallClient |
CallClient 는 Calling SDK의 기본 진입점입니다. |
TeamsCallAgent |
TeamsCallAgent 는 통화를 시작하고 관리하는 데 사용됩니다. |
TeamsCommunicationCall |
TeamsCommunicationCall 는 진행 중인 통화를 관리하는 데 사용됩니다. |
CallTokenCredential |
CallTokenCredential 은 TeamsCallAgent 를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다. |
CallIdentifier |
CallIdentifier 는 사용자의 ID를 나타내는 데 사용되며 다음 옵션(MicrosoftTeamsUserCallIdentifier , UserCallIdentifier , PhoneNumberCallIdentifier 등) 중 하나일 수 있습니다. |
클라이언트 인증
전화를 걸고 받을 수 있는 사용자 액세스 토큰을 사용하여 TeamsCallAgent
인스턴스를 초기화하고 필요에 따라 DeviceManager 인스턴스를 가져와 클라이언트 디바이스 구성을 쿼리합니다.
코드에서 <AUTHENTICATION_TOKEN>
을 사용자 액세스 토큰으로 바꿉니다. 사용할 수 있는 토큰이 아직 없는 경우 사용자 액세스 토큰 설명서를 참조하세요.
SDK를 부트스트랩하는 InitCallAgentAndDeviceManagerAsync
함수를 추가합니다. 이 도우미는 애플리케이션의 요구 사항을 충족하도록 사용자 지정할 수 있습니다.
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
개체를 사용하여 다양한 종류의 호출을 시작하고 TeamsCommunicationCall
개체에 RemoteParticipantsUpdated
및 StateChanged
이벤트 처리기를 연결합니다.
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
에 전화를 걸면 에코 봇과 연결됩니다. 이 기능은 통화를 시작하고 오디오 디바이스가 작동하는지 확인하는 데 유용합니다.
Communication Services 통화 SDK를 사용하여 앱에 1:1 음성 및 영상 통화를 추가하여 Azure Communication Services를 시작합니다. Java용 Azure Communication Services Calling SDK를 사용하여 통화를 시작하고 답변하는 방법에 대해 알아봅니다.
예제 코드
끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.
필수 조건
- 활성 구독이 있는 Azure 계정. 체험 계정을 만듭니다.
- Android 스튜디오(Android 애플리케이션 만들기용)
- 배포된 Communication Services 리소스. Communication Services 리소스 만들기 이 빠른 시작에 대한 연결 문자열을 기록해야 합니다.
- 사용자 액세스 토큰(Azure Communication Service용)
- Graph 탐색기를 사용하여 호출 작업을 위해 Teams 스레드 ID를 가져옵니다. 채팅 스레드 ID를 만드는 방법에 대해 자세히 알아봅니다.
설정
빈 활동을 사용하여 Android 앱 만들기
Android 스튜디오에서 [새 Android 스튜디오 프로젝트 시작]을 선택합니다.
"휴대폰 및 태블릿" 아래에서 "빈 활동" 프로젝트 템플릿을 선택합니다.
"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>
기본 활동 스캐폴딩 및 바인딩 만들기
레이아웃이 만들어지면 바인딩과 활동의 기본 스캐폴딩을 추가할 수 있습니다. 이 활동은 런타임 권한을 요청하고 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 이상에서는 앱이 설치될 때가 아니라 런타임에 권한이 부여됩니다. 이를 지원하기 위해 필요한 각 권한에 대해 ActivityCompat.checkSelfPermission
및 ActivityCompat.requestPermissions
를 호출하도록 getAllPermissions
를 구현할 수 있습니다.
/**
* 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 Communication Services 통화 SDK의 주요 기능 중 일부를 처리하는 클래스와 인터페이스는 다음과 같습니다.
이름 | 설명 |
---|---|
CallClient |
CallClient 는 Calling SDK의 기본 진입점입니다. |
TeamsCallAgent |
TeamsCallAgent 는 통화를 시작하고 관리하는 데 사용됩니다. |
TeamsCall |
TeamsCall 은 Teams 통화를 나타내는 데 사용됩니다. |
CommunicationTokenCredential |
CommunicationTokenCredential 은 TeamsCallAgent 를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다. |
CommunicationIdentifier |
CommunicationIdentifier 는 통화의 일부가 될 수 있는 다른 유형의 참가자로 사용됩니다. |
사용자 액세스 토큰에서 에이전트 만들기
사용자 토큰을 사용하면 인증된 통화 에이전트를 인스턴스화할 수 있습니다. 일반적으로 이 토큰은 애플리케이션과 관련된 인증을 사용하는 서비스에서 생성됩니다. 사용자 액세스 토큰에 대한 자세한 내용은 사용자 액세스 토큰 가이드를 확인하세요.
빠른 시작에서는 <User_Access_Token>
을 Azure Communication Services 리소스에 대해 생성된 사용자 액세스 토큰으로 바꿉니다.
/**
* 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();
}
}
통화 에이전트를 사용하여 통화 시작
전화를 거는 것은 Teams 통화 에이전트를 통해 수행할 수 있으며 수신자 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);
}
통화에 응답
현재 컨텍스트에 대한 참조만 사용하여 팀 통화 에이전트를 사용하여 통화를 수락할 수 있습니다.
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;
}
앱 시작 및 echo 봇 호출
이제 도구 모음의 "앱 실행" 단추(Shift+F10)를 사용하여 앱을 시작할 수 있습니다. 8:echo123
을 호출하여 전화를 걸 수 있는지 확인합니다. 미리 녹음된 메시지가 재생된 다음, 해당 메시지를 다시 반복합니다.
Communication Services Calling SDK를 사용하여 앱에 1:1 영상 통화를 추가하여 Azure Communication Services를 시작하세요. Teams ID를 사용하여 iOS용 Azure Communication Services Calling SDK를 사용하여 영상 통화를 시작하고 응답하는 방법에 대해 알아봅니다.
예제 코드
끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.
필수 조건
- 활성 구독이 있는 Azure 계정을 얻습니다. 체험 계정을 만듭니다.
- 키 집합에 설치된 유효한 개발자 인증서와 함께 Xcode를 실행하는 Mac
- 활성 Communication Services 리소스를 만듭니다. Communication Services 리소스 만들기 이 빠른 시작에 대한 연결 문자열을 기록해야 합니다.
- 사용자 액세스 토큰(Azure Communication Service용)
- 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 설치를 실행합니다.
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
파일을 열고 파일 맨 위에 가져오기 선언을 추가하여 AzureCommunicationCalling
라이브러리 및 AVFoundation
을 가져옵니다. AvFoundation은 코드에서 오디오 권한을 캡처하는 데 사용됩니다.
import AzureCommunicationCalling
import AVFoundation
개체 모델
iOS용 Azure Communication Services Calling SDK의 주요 기능 중 일부를 처리하는 클래스와 인터페이스는 다음과 같습니다.
이름 | 설명 |
---|---|
CallClient |
CallClient 는 Calling SDK의 기본 진입점입니다. |
TeamsCallAgent |
TeamsCallAgent 는 통화를 시작하고 관리하는 데 사용됩니다. |
TeamsIncomingCall |
TeamsIncomingCall 은 수신 Teams 통화를 수락하거나 거부하는 데 사용됩니다. |
CommunicationTokenCredential |
CommunicationTokenCredential 은 TeamsCallAgent 를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다. |
CommunicationIdentifier |
CommunicationIdentifier 는 사용자의 ID를 나타내는 데 사용되며 다음 옵션(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
인스턴스를 만들려면 초기화된 후 비동기적으로 TeamsCallAgent
개체를 반환하는 callClient.createTeamsCallAgent
메서드를 사용합니다. 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
}
}
}
다음 코드를 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 시뮬레이터에서 앱을 빌드하고 실행할 수 있습니다.
리소스 정리
Communication Services 구독을 정리하고 제거하려면 리소스 또는 리소스 그룹을 삭제하면 됩니다. 리소스 그룹을 삭제하면 해당 리소스 그룹에 연결된 다른 모든 리소스가 함께 삭제됩니다. 리소스 정리에 대해 자세히 알아보세요.
다음 단계
자세한 내용은 다음 문서를 참조하세요.