快速入門:將 1 對 1 視訊通話新增為 Teams 使用者至您的應用程式

使用通訊服務的通話 SDK,在應用程式中新增 1 對 1 音訊和視訊通話,以開始使用 Azure 通訊服務。 您將了解如何使用適用於 JavaScript 的 Azure 通訊服務通話 SDK,開始並接聽通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub \(英文\) 上下載此快速入門作為範例。

必要條件

設定

建立新的 Node.js 應用程式

開啟您的終端機或命令視窗,為您的應用程式建立新的目錄,並瀏覽至該目錄。

mkdir calling-quickstart && cd calling-quickstart

執行 npm init -y 以使用預設設定建立 package.json 檔案。

npm init -y

Install the package

使用 npm install 命令,以安裝適用於 JavaScript 的 Azure 通訊服務通話 SDK。

重要

本快速入門會使用最新 Azure 通訊服務通話 SDK 版本。

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

設定應用程式架構

本快速入門會使用 webpack 來組合應用程式資產。 執行下列命令以安裝 webpackwebpack-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>

Azure 通訊服務呼叫 Web SDK 物件模型

下列類別和介面會處理 Azure 通訊服務通話 SDK 的一些主要功能:

名稱 描述
CallClient 通話 SDK 的主要進入點。
AzureCommunicationTokenCredential 實作 CommunicationTokenCredential 介面,可用來將 teamsCallAgent 具現化。
TeamsCallAgent 用來開始和管理 Teams 通話。
DeviceManager 用來管理媒體裝置。
TeamsCall 用來表示 Teams 通話
LocalVideoStream 用來在本機系統上建立相機裝置的本機視訊串流。
RemoteParticipant 用來代表通話中的遠端參與者
RemoteVideoStream 用來表示來自遠端參與者的遠端視訊串流。

index.js 專案的根目錄中建立檔案,以包含此快速入門的應用程式邏輯。 將下列程式碼新增至 index.js:

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

新增 Webpack 本地伺服器程式代碼

webpack.config.js 專案的根目錄中建立檔案,以包含此快速入門的本機伺服器邏輯。 將下列程式碼新增至 webpack.config.js

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

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

執行程式碼

使用 webpack-dev-server 來建置並執行您的應用程式。 執行下列命令,在本機 Web 伺服器上組合應用程式主機:

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

開啟瀏覽器和兩個索引標籤並巡覽至 http://localhost:8080/. 索引標籤應該會顯示類似下圖的結果:螢幕擷取畫面顯示預設檢視中的兩個索引標籤。每個索引標籤會供不同的 Teams 使用者使用。

在第一個索引標籤上,輸入有效使用者存取權杖。 在第二個索引標籤上,輸入另一個不同的有效使用者存取權杖。 如果您還沒有可使用的權杖,請參閱使用者存取權杖文件。 在這兩個索引標籤上,按一下 [初始化通話代理程式] 按鈕。 索引標籤應該會顯示類似下圖的結果:螢幕擷取畫面顯示在瀏覽器索引標籤中初始化每個 Teams 使用者的步驟。

在第一個索引標籤上,輸入第二個索引標籤的 Azure 通訊服務使用者身分識別,然後選取 [開始通話] 按鈕。 第一個索引標籤將會開始外撥給第二個索引標籤的通話,而第二個索引標籤的 [接受通話] 按鈕會變成已啟用:螢幕擷取畫面顯示 Teams 使用者初始化 SDK 時的體驗,並顯示開始呼叫第二位使用者的步驟,以及如何接受通話。

從第二個索引標籤中,選取 [接受通話] 按鈕。 通話將會接聽並連線。 索引標籤應該會顯示類似下圖的結果:螢幕擷取畫面顯示兩個索引標籤,當中兩位 Teams 使用者之間持續通話並已登入個別的索引標籤。

這兩個索引標籤現在已成功處於 1 對 1 個視訊通話中。 這兩個使用者可以聽到彼此的音訊,並看見彼此的視訊串流。

使用通訊服務的通話 SDK,在應用程式中新增 1 對 1 音訊和視訊通話,以開始使用 Azure 通訊服務。 您將了解如何使用適用於 Windows 的 Azure 通訊服務通話 SDK,開始並接聽通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub \(英文\) 上下載此快速入門作為範例。

必要條件

若要完成本教學課程,您需要下列必要條件:

設定

建立專案

在 Visual Studio 中,使用 [空白應用程式 (通用 Windows)] 範本建立新專案,以設定單頁通用 Windows 平台 (UWP) 應用程式。

顯示 Visual Studio 內 [新 UWP 專案] 視窗的螢幕擷取畫面。

Install the package

以滑鼠右鍵選取您的專案,然後移至 Manage Nuget Packages 以安裝 Azure.Communication.Calling.WindowsClient 1.2.0-beta.1 或更高版本。 請確定已選取 [包含發行前版本]。

要求存取

移至 Package.appxmanifest 並選取 Capabilities。 勾選 Internet (Client)Internet (Client & Server) 以取得網際網路的輸入和輸出存取權。 勾選 Microphone 以存取麥克風的音訊來源,並勾選 Webcam 以存取相機的視訊來源。

顯示要求在 Visual Studio 中存取網際網路和麥克風的螢幕擷取畫面。

設定應用程式架構

我們必須設定基本配置來附加邏輯。 為了撥打外撥電話,我們需要 TextBox 來提供被通話者的使用者識別碼。 我們也需要 [Start/Join call] 按鈕和 [Hang up] 按鈕。 此範例中也包含 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 通訊服務通話 SDK 的一些主要功能:

名稱 描述
CallClient CallClient 是通話 SDK 的主要進入點。
TeamsCallAgent TeamsCallAgent 可用來開始和管理通話。
TeamsCommunicationCall TeamsCommunicationCall 可用來管理進行中的通話。
CallTokenCredential CallTokenCredential 可作為權杖認證用來將 TeamsCallAgent 具現化。
CallIdentifier CallIdentifier 可用來代表使用者的身分識別,其可以是下列其中一個選項:MicrosoftTeamsUserCallIdentifierUserCallIdentifierPhoneNumberCallIdentifier 等。

驗證用戶端

透過使用者存取權杖來將 TeamsCallAgent 執行個體初始化,此存取權杖可讓我們撥打和接聽通話,並選擇性地取得 DeviceManager 執行個體來查詢用戶端裝置設定。

在程式碼中,將 <AUTHENTICATION_TOKEN> 取代為使用者存取權杖。 如果您還沒有可用的權杖,請參閱使用者存取權杖文件。

新增 InitCallAgentAndDeviceManagerAsync 函式,以啟動 SDK。 您可以自訂此協助程式,以符合應用程式的需求。

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

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

            var tokenCredential = new CallTokenCredential(authToken, callTokenRefreshOptions);

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

開始通話

將實作新增至 CallButton_Click 以使用我們建立的 teamsCallAgent 物件啟動各種通話,並關聯 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 中,您可以建置並執行程式碼。 針對解決方案平台,我們支援 ARM64x64x86

您可以在文字欄位中提供使用者識別碼,並按一下 [Start Call/Join] 按鈕以向外撥打通話。 呼叫 8:echo123,會將您連線到回應聊天機器人,此功能非常適合入門,以及驗證您的音訊裝置運作正常。

顯示正在執行 UWP 快速入門應用程式的螢幕擷取畫面

使用通訊服務的通話 SDK,在應用程式中新增 1 對 1 音訊和視訊通話,以開始使用 Azure 通訊服務。 您將了解如何使用適用於 Java 的 Azure 通訊服務通話 SDK,開始並接聽通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub \(英文\) 上下載此快速入門作為範例。

必要條件

設定

建立具有空白活動的 Android 應用程式

從 Android Studio 中,選取 [啟動新的 Android Studio 專案]。

顯示在 Android Studio 中選取 [啟動新的 Android Studio 專案] 按鈕的螢幕擷取畫面。

選取 [手機和平板電腦] 底下的 [空白活動] 專案範本。

顯示在 [專案範本] 畫面中選取 [空白活動] 選項的螢幕擷取畫面。

針對最低 SDK 選取 [API 26:Android 8.0(Oreo)] 或更新版本。

顯示在 [專案範本] 畫面 2 中選取 [空白活動] 選項的螢幕擷取畫面。

Install the package

找出您的專案層級 build.gradle,並務必將 mavenCentral() 新增至 buildscriptallprojects 下的存放庫清單

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

然後,在您的模組層級 build.gradle 中,將以下幾行新增至相依性和 android 區段

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

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

將權限新增至應用程式資訊清單

為了要求進行通話所需的權限,必須在應用程式資訊清單 (app/src/main/AndroidManifest.xml) 中宣告。 使用下列程式碼取代檔案的內容:

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

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

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

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

</manifest>
    

設定應用程式的配置

需要兩個輸入:被呼叫者識別碼的文字輸入,以及用來進行呼叫的按鈕。 這些輸入可以透過設計工具或藉由編輯版面配置 xml 來新增。 建立識別碼為 call_button、文字輸入為 callee_id 的按鈕。 瀏覽至 app/src/main/res/layout/activity_main.xml 並且以下列程式碼取代檔案的內容:

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

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

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

建立主要活動 Scaffolding 和繫結

建立版面配置之後,即可新增繫結,以及活動的基本 Scaffolding。 活動會處理要求執行階段權限、建立小組通話代理程式,並且在按下按鈕時進行通話。 每個項目都涵蓋在自己的區段中。 系統會覆寫 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 或更新版本,權限會在執行階段授與,而不是在安裝應用程式時。 為了提供支援,可實作 getAllPermissions 來呼叫 ActivityCompat.checkSelfPermissionActivityCompat.requestPermissions,以取得每個必要的權限。

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

注意

在設計您的應用程式時,請考量何時應要求這些權限。 您應該視需要要求權限,而不是提前要求。 如需詳細資訊,請參閱 Android 權限指南 \(機器翻譯\)。

物件模型

下列類別和介面會處理 Azure 通訊服務通話 SDK 的一些重大功能:

名稱 描述
CallClient CallClient 是通話 SDK 的主要進入點。
TeamsCallAgent TeamsCallAgent 可用來開始和管理通話。
TeamsCall TeamsCall 用來表示 Teams 通話。
CommunicationTokenCredential CommunicationTokenCredential 可作為權杖認證用來將 TeamsCallAgent 具現化。
CommunicationIdentifier CommunicationIdentifier 會當成可參與通話的不同參與者類型。

從使用者存取權杖建立代理程式

透過使用者權杖,可以將已驗證的呼叫代理程式具現化。 一般來說,此權杖會從具有應用程式特定驗證的服務產生。 如需使用者存取權杖的詳細資訊,請參閱使用者存取權杖指南。

在快速入門中,將 <User_Access_Token> 取代為針對您的 Azure 通訊服務資源所產生的使用者存取權杖。


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

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

使用呼叫代理程式開始通話

您可以透過小組呼叫代理程式來進行呼叫,只需要提供被呼叫者識別碼和呼叫選項的清單。 在快速入門中,會使用沒有視訊的預設呼叫選項,以及文字輸入中的單一被呼叫者識別碼。

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

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

接聽電話

使用小組通話代理程式,只使用目前內容的參考,即可接受通話。

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 Bot

應用程式現在可以使用工具列上的 [執行應用程式] 按鈕 (Shift+F10) 來啟動。 確認您可以藉由呼叫 8:echo123 來進行通話。 系統會播放預先錄製的訊息,然後為您重複訊息。

顯示已完成應用程式的螢幕擷取畫面。

使用通訊服務的通話 SDK,在您的應用程式中新增一對一視訊通話,以開始使用 Azure 通訊服務。 您將了解如何使用適用於 iOS 的 Azure 通訊服務通話 SDK,使用 Teams 身分識別開始並接聽視訊通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 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 install。

  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 通訊服務通話 SDK 的一些主要功能。

名稱 描述
CallClient CallClient 是通話 SDK 的主要進入點。
TeamsCallAgent TeamsCallAgent 可用來開始和管理通話。
TeamsIncomingCall TeamsIncomingCall 用來接受或拒絕來電。
CommunicationTokenCredential CommunicationTokenCredential 可作為權杖認證用來將 TeamsCallAgent 具現化。
CommunicationIdentifier CommunicationIdentifier 可用來代表使用者的身分識別,其可以是下列其中一個選項:CommunicationUserIdentifierPhoneNumberIdentifierCallingApplication

建立 Teams 通話代理程式

使用一些簡單的 UI 控制項來取代 ContentView struct 的實作,讓使用者可以起始和結束通話。 在此快速入門中,我們會將商務邏輯附加至這些控制項。

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

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

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

//Functions and Observers

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

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

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

驗證用戶端

為了將 TeamsCallAgent 執行個體初始化,您需要使用者存取權杖,才能撥打和接聽通話。 如果您沒有可用的權杖,請參閱使用者存取權杖文件。

當您具有權杖後,將下列程式碼新增至 ContentView.swift 中的 onAppear 回呼。 您必須將 <USER ACCESS TOKEN> 取代為資源的有效使用者存取權杖

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

將 Teams CallAgent 初始化並存取裝置管理員

若要從 CallClient 建立 TeamsCallAgent 執行個體,使用 callClient.createTeamsCallAgent 方法,在 TeamsCallAgent 物件初始化後以非同步方式傳回該物件。 DeviceManager 可讓您列舉可於通話中用來傳輸音訊/視訊串流的本機裝置。 這也可讓您向使用者要求權限以存取麥克風/相機。

self.callClient = CallClient()
let options = TeamsCallAgentOptions()
// Enable CallKit in the SDK
options.callKitOptions = CallKitOptions(with: createCXProvideConfiguration())
self.callClient?.createTeamsCallAgent(userCredential: userCredential, options: options) { (agent, error) in
    if error != nil {
        print("ERROR: It was not possible to create a Teams call agent.")
        return
    } else {
        self.teamsCallAgent = agent
        print("Teams Call agent successfully created.")
        self.teamsCallAgent!.delegate = teamsIncomingCallHandler
        self.callClient?.getDeviceManager { (deviceManager, error) in
            if (error == nil) {
                print("Got device manager instance")
                self.deviceManager = deviceManager
            } else {
                print("Failed to get device manager instance")
            }
        }
    }
}

要求權限

我們需要將下列程式碼新增至 onAppear 回呼,以要求音訊和視訊的權限。

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

撥出電話

startCall 方法會設定為點選 [開始通話] 按鈕時要執行的動作。 在此快速入門中,撥出的通話預設為僅限音訊。 若要透過視訊開始通話,則需要使用 LocalVideoStream 設定 VideoOptions,並使用 startCallOptions 傳遞它,以設定通話的初始選項。

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

加入 Teams 會議

join 方法可讓使用者加入小組會議。

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

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

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

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

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.swift 中的 onAppear 回呼,以建立 TeamsIncomingCallHandler 的執行個體:

成功建立 TeamsCallAgent 後,將委派設定為 TeamsCallAgent

self.teamsCallAgent!.delegate = incomingCallHandler

一旦有來電,TeamsIncomingCallHandler 就會呼叫 showIncomingCallBanner 函式,以顯示 answerdecline 按鈕。

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

附加至 answerdecline 的動作會以下列程式碼來實作。 為了透過視訊接聽通話,需要開啟本機視訊,並使用 localVideoStream 設定 AcceptCallOptions 選項。

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 模擬器上建置並執行應用程式。

清除資源

如果您想要清除並移除通訊服務訂用帳戶,您可以刪除資源或資源群組。 刪除資源群組也會刪除與其相關聯的任何其他資源。 深入了解如何清除資源

下一步

如需詳細資訊,請參閱下列文章: