다음을 통해 공유


빠른 시작: 애플리케이션에 Teams 사용자로 1:1 영상 통화 추가

Communication Services 통화 SDK를 사용하여 앱에 1:1 음성 및 영상 통화를 추가하여 Azure Communication Services를 시작합니다. JavaScript용 Azure Communication Services Calling SDK를 사용하여 통화를 시작하고 답변하는 방법에 대해 알아봅니다.

예제 코드

끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.

필수 조건

설정

새 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-cliwebpack-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/.으로 이동합니다. 탭은 다음 이미지와 유사한 결과를 표시해야 합니다. 기본 보기에 두 개의 탭을 보여 주는 스크린샷. 각 탭은 다양한 Teams 사용자에게 사용됩니다.

첫 번째 탭에서 유효한 사용자 액세스 토큰을 입력합니다. 두 번째 탭에서 다른 유효한 사용자 액세스 토큰을 입력합니다. 사용할 수 있는 액세스 토큰이 아직 없는 경우 사용자 액세스 토큰 설명서를 참조하세요. 두 탭에서 "에이전트 호출 초기화" 단추를 클릭합니다. 탭은 다음 이미지와 유사한 결과를 표시해야 합니다. 브라우저 탭에서 각 Teams 사용자를 초기화하는 단계를 보여 주는 스크린샷.

첫 번째 탭에서 두 번째 탭의 Azure Communication Services 사용자 ID를 입력하고 "통화 시작" 단추를 선택합니다. 첫 번째 탭이 두 번째 탭으로 발신 전화를 걸기 시작하고 두 번째 탭의 "통화 수락" 단추가 사용하도록 설정됩니다. Teams 사용자가 SDK를 초기화할 때의 환경을 보여 주고 두 번째 사용자에 대한 통화를 시작하는 단계와 통화를 수락하는 방법을 보여 주는 스크린샷.

두 번째 탭에서 "통화 수락" 단추를 선택합니다. 전화를 받고 연결됩니다. 탭은 다음 이미지와 유사한 결과를 표시해야 합니다. 각각 개별 탭에 로그인된 두 Teams 사용자 간의 진행 중인 통화가 포함된 두 개의 탭을 보여 주는 스크린샷.

이제 두 탭 모두 1:1 영상 통화에 성공했습니다. 두 사용자 모두 서로의 오디오를 듣고 서로의 동영상 스트림을 볼 수 있습니다.

Communication Services 통화 SDK를 사용하여 앱에 1:1 음성 및 영상 통화를 추가하여 Azure Communication Services를 시작합니다. Windows용 Azure Communication Services Calling SDK를 사용하여 통화를 시작하고 답변하는 방법에 대해 알아봅니다.

예제 코드

끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.

필수 조건

이 자습서를 완료하려면 다음 필수 구성 요소가 필요합니다.

설정

프로젝트 만들기

Visual Studio에서 비어 있는 앱(유니버설 Windows) 템플릿을 사용하여 단일 페이지 UWP(유니버설 Windows 플랫폼) 앱을 설정하는 새 프로젝트를 만듭니다.

Visual Studio 내의 새 UWP 프로젝트 창을 보여주는 스크린샷.

패키지 설치

프로젝트를 마우스 오른쪽 단추로 선택하고 Manage Nuget Packages(으)로 이동하여 Azure.Communication.Calling.WindowsClient 1.2.0-beta.1 이상을 설치합니다. 시험판 포함이 선택되어 있는지 확인합니다.

액세스 요청

Package.appxmanifest로 이동하여 Capabilities를 선택합니다. 인터넷에 대한 인바운드 및 아웃바운드 액세스 권한을 얻으려면 Internet (Client)Internet (Client & Server)를 선택합니다. Microphone을 선택하여 마이크의 오디오 피드에 액세스하고 Webcam을 선택하여 카메라의 비디오 피드에 액세스합니다.

Visual Studio에서 인터넷 및 마이크에 대한 액세스 요청을 보여 주는 스크린샷.

앱 프레임워크 설정

논리를 연결하려면 기본 레이아웃을 구성해야 합니다. 아웃바운드 통화를 수행하려면 호출 수신자의 사용자 ID를 입력할 TextBox가 필요합니다. 또한 Start/Join call 단추와 Hang up 단추도 필요합니다. 오디오 상태 및 비디오 효과를 토글하는 기능을 보여 주는 MuteBackgroundBlur 확인란도 이 샘플에 포함되어 있습니다.

프로젝트의 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 CallTokenCredentialTeamsCallAgent를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다.
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 개체에 RemoteParticipantsUpdatedStateChanged 이벤트 처리기를 연결합니다.

        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, x64x86입니다.

텍스트 필드에 사용자 ID를 지정하고 Start Call/Join 단추를 클릭하여 아웃바운드 호출을 수행할 수 있습니다. 8:echo123에 전화를 걸면 에코 봇과 연결됩니다. 이 기능은 통화를 시작하고 오디오 디바이스가 작동하는지 확인하는 데 유용합니다.

UWP 빠른 시작 앱 실행을 보여주는 스크린샷

Communication Services 통화 SDK를 사용하여 앱에 1:1 음성 및 영상 통화를 추가하여 Azure Communication Services를 시작합니다. Java용 Azure Communication Services Calling SDK를 사용하여 통화를 시작하고 답변하는 방법에 대해 알아봅니다.

예제 코드

끝으로 건너뛰려면 GitHub에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.

필수 조건

설정

빈 활동을 사용하여 Android 앱 만들기

Android 스튜디오에서 [새 Android 스튜디오 프로젝트 시작]을 선택합니다.

Android Studio에서 선택한 '새 Android Studio 프로젝트 시작' 단추를 보여 주는 스크린샷.

"휴대폰 및 태블릿" 아래에서 "빈 활동" 프로젝트 템플릿을 선택합니다.

프로젝트 템플릿 화면에서 선택한 '빈 활동' 옵션을 보여 주는 스크린샷

"API 26: Android 8.0(Oreo)" 이상의 최소 SDK를 선택합니다.

프로젝트 템플릿 화면에서 선택한 '빈 활동' 옵션을 보여 주는 스크린샷 2.

패키지 설치

프로젝트 수준 build.gradle을 찾고, mavenCentral()buildscriptallprojects 아래의 리포지토리 목록에 추가해야 합니다.

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 메서드는 getAllPermissionscreateTeamsAgent를 호출하고 통화 단추에 대한 바인딩을 추가하도록 재정의됩니다. 이 이벤트는 활동이 만들어질 때 한 번만 발생합니다. 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.checkSelfPermissionActivityCompat.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 CommunicationTokenCredentialTeamsCallAgent를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다.
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에서 이 빠른 시작을 샘플로 다운로드할 수 있습니다.

필수 조건

설정

Xcode 프로젝트 만들기

Xcode에서 새 iOS 프로젝트를 만들고 단일 보기 앱 템플릿을 선택합니다. 이 자습서에서는 SwiftUI 프레임워크를 사용하므로 언어를 Swift로, 사용자 인터페이스는 SwiftUI로 설정해야 합니다. 이 빠른 시작 중에는 테스트를 만들지 않습니다. 테스트 포함을 선택 취소합니다.

Xcode 내에서 새 프로젝트 창을 보여 주는 스크린샷

CocoaPods 설치

이 가이드를 사용하여 Mac에 CocoaPods를 설치합니다.

CocoaPods를 사용하여 패키지 및 종속성 설치

  1. 애플리케이션에 대한 Podfile을 만들려면 터미널을 열고 프로젝트 폴더로 이동한 후 pod init을 실행합니다.

  2. 다음 코드를 Podfile에 추가하고 저장합니다. SDK 지원 버전을 참조하세요.

platform :ios, '13.0'
use_frameworks!

target 'VideoCallingQuickstart' do
  pod 'AzureCommunicationCalling', '~> 2.10.0'
end
  1. Pod 설치를 실행합니다.

  2. Xcode로 .xcworkspace를 엽니다.

마이크, 카메라에 대한 액세스 요청

디바이스의 마이크 및 카메라에 액세스하려면 앱의 정보 속성 목록을 NSMicrophoneUsageDescriptionNSCameraUsageDescription으로 업데이트해야 합니다. 시스템이 사용자의 액세스를 요청하는 데 사용하는 대화 상자에 포함될 문자열로 연결된 값을 설정합니다.

프로젝트 트리의 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 CommunicationTokenCredentialTeamsCallAgent를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다.
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.swiftonAppear 콜백에 다음 코드를 추가합니다. <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 메서드는 통화 시작 단추를 누르면 수행되는 작업으로 설정됩니다. 이 빠른 시작에서 발신 전화는 기본적으로 오디오 전용입니다. 영상 통화를 시작하려면 VideoOptionsLocalVideoStream으로 설정하고 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.
}

TeamsCallObserverRemotePariticipantObserver는 통화 중 이벤트 및 원격 참가자를 관리하는 데 사용됩니다. 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.swiftonAppear 콜백에 추가하여 TeamsIncomingCallHandler의 인스턴스를 만들어야 합니다.

TeamsCallAgent가 성공적으로 생성된 후 TeamsCallAgent에 대리자를 설정합니다.

self.teamsCallAgent!.delegate = incomingCallHandler

수신 전화가 발생하면 TeamsIncomingCallHandlershowIncomingCallBanner 기능을 호출하여 answerdecline 단추를 표시합니다.

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

answerdecline에 연결된 작업은 다음 코드와 같이 구현됩니다. 영상 통화에 응답하기 위해서는 로컬 비디오를 켜고 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 구독을 정리하고 제거하려면 리소스 또는 리소스 그룹을 삭제하면 됩니다. 리소스 그룹을 삭제하면 해당 리소스 그룹에 연결된 다른 모든 리소스가 함께 삭제됩니다. 리소스 정리에 대해 자세히 알아보세요.

다음 단계

자세한 내용은 다음 문서를 참조하세요.