快速入門:將 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使用 命令來安裝 Azure 通訊服務呼叫 SDK for JavaScript。

重要

本快速入門使用最新的 Azure 通訊服務呼叫 SDK 版本。

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

設定應用程式架構

本快速入門會使用 webpack 來組合應用程式資產。 執行下列命令以安裝webpack、 和 webpack-dev-server npm 套件,webpack-cli並將其列為開發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 的視訊通話中。 兩位使用者可以聽到彼此的音訊,並查看彼此的視訊串流。

開始使用 Azure 通訊服務,方法是使用通訊服務通話 SDK,將 1:1 語音和視訊通話新增至您的應用程式。 您將瞭解如何使用適用於 Windows 的 Azure 通訊服務 通話 SDK 來啟動和接聽通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub下載本快速入門作為範例。

必要條件

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

設定

建立專案

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

顯示 Visual Studio 內 [新增 UWP 專案] 視窗的螢幕快照。

Install the package

以滑鼠右鍵按兩下您的項目,然後移至安裝 Manage Nuget PackagesAzure.Communication.Calling.WindowsClient1.2.0-beta.1 或更上層。 請確定已核取 [包含預先發行]。

要求存取

移至 並Package.appxmanifest選擇 。 Capabilities 檢查 Internet (Client)Internet (Client & Server) 取得因特網的輸入和輸出存取權。 檢查 Microphone 以存取麥克風的音訊摘要,以及 Webcam 存取相機的視訊摘要。

顯示要求在Visual Studio中存取因特網和麥克風的螢幕快照。

設定應用程式架構

我們需要設定基本版面配置來附加邏輯。 若要撥打輸出通話,我們需要 提供 TextBox 被呼叫者的使用者標識碼。 我們也需要按鈕 Start/Join callHang up 按鈕。 此範例中也包含 A 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);

監視和回應呼叫狀態變更事件

StateChanged 當進行中的呼叫交易從某個狀態到另一個狀態時,就會引發物件上的 TeamsCommunicationCall 事件。 應用程式有機會反映 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 會使用 Echo Bot 來連線您,這項功能非常適合開始使用並驗證音訊裝置是否正常運作。

顯示執行 UWP 快速入門應用程式的螢幕快照

開始使用 Azure 通訊服務,方法是使用通訊服務通話 SDK,將 1:1 語音和視訊通話新增至您的應用程式。 您將瞭解如何使用適用於 Java 的 Azure 通訊服務 呼叫 SDK 來啟動和接聽通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub下載本快速入門作為範例。

必要條件

設定

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

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

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

選取 [電話 和平板計算機] 底下的 [空白活動] 項目範本。

顯示 [項目範本畫面] 中選取 [空白活動] 選項的螢幕快照。

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

顯示 [項目範本畫面 2] 中選取 [空白活動] 選項的螢幕快照。

Install the package

找出您的專案層級 build.gradle,並確定將 新增 mavenCentral() 至 和 下的 buildscript 存放庫清單 allprojects

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 會覆寫以叫 getAllPermissions 用 和 , createTeamsAgent 並新增呼叫按鈕的系結。 只有在建立活動時,才會發生此事件。 如需 詳細資訊,onCreate請參閱瞭解活動生命週期指南

瀏覽至 MainActivity.java ,並以下列程式代碼取代內容:

package com.contoso.ctequickstart;

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

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

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


import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    
    private TeamsCallAgent teamsCallAgent;

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

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

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

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

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

在運行時間要求許可權

針對 Android 6.0 和更新版本 (API 層級 23) 和 targetSdkVersion 23 或更高版本,許可權會在運行時間授與,而不是安裝應用程式時授與。 為了支援它, getAllPermissions 可以實作來呼叫 ActivityCompat.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來確認您能夠撥打電話。 預先錄製的訊息會播放,然後重複訊息給您。

顯示已完成應用程式的螢幕快照。

開始使用 Azure 通訊服務,方法是使用通訊服務呼叫 SDK,將一個視訊通話新增至您的應用程式。 您將瞭解如何使用 teams 身分識別,使用適用於 iOS 的 Azure 通訊服務 通話 SDK 來啟動和接聽視訊通話。

範例程式碼

如果您想要直接跳到結尾,您可以在 GitHub下載本快速入門作為範例。

必要條件

設定

建立 Xcode 專案

在 Xcode 中,建立新的 iOS 專案,然後選取 [單一檢視應用程式] 範本。 本教學課程使用 SwiftUI 架構,因此您應該將 Language 設定為 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. .xcworkspace使用 Xcode 開啟 。

要求存取麥克風和相機

若要存取裝置的麥克風和相機,您必須使用 NSMicrophoneUsageDescriptionNSCameraUsageDescription來更新您 app 的資訊屬性清單。 您可以將相關聯的值設定為字串,其中包含系統用來要求使用者存取的對話方塊。

以滑鼠右鍵按兩下 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 通話代理程式

將 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 實例,您需要一個使用者存取令牌,讓其能夠進行並接聽呼叫。 如果您沒有可用的令牌,請參閱使用者存取令牌檔。

一旦您有令牌,請在 中將下列程式代碼新增至 回onAppearContentView.swift呼。 您必須將 取代 <USER ACCESS TOKEN> 為資源的有效 使用者存取權杖

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

初始化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 會設定為點選 [開始呼叫] 按鈕時所執行的動作。 在本快速入門中,傳出通話預設為音訊。 若要使用視訊啟動通話,我們需要使用 來設定 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
        }
    }
}

我們需要將下列程式代碼新增至 onAppear 中的ContentView.swift回呼,以建立 的實例TeamsIncomingCallHandler

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

self.teamsCallAgent!.delegate = incomingCallHandler

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

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

附加至 answerdecline 的動作會實作為下列程序代碼。 若要使用視訊接聽通話,我們需要開啟本機視訊,並使用 來設定 的選項AcceptCallOptionslocalVideoStream

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

清除資源

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

下一步

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