在通話期間管理影片

瞭解如何使用 Azure 通訊服務 SDK 管理視訊通話。 我們將瞭解如何管理通話內的接收和傳送視訊。

必要條件

安裝 SDK

使用 npm install 命令安裝適用於 JavaScript 的 Azure 通訊服務通用和通話 SDK:

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

初始化必要的物件

大部分通話作業都需要 CallClient 執行個體。 當您建立新的 CallClient 實例時,您可以使用實例之類的 Logger 自定義選項進行設定。

CallClient透過 實例,您可以藉由呼叫 createCallAgent來建立 CallAgent 實例。 此方法會以非同步的方式傳回 CallAgent 執行個體物件。

createCallAgent 方法會使用 CommunicationTokenCredential 作為引數。 其接受使用者存取權杖

您可以使用 CallClient 執行個體上的 getDeviceManager 方法來存取 deviceManager

const { CallClient } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential} = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");

// Set the logger's log level
setLogLevel('verbose');

// Redirect log output to console, file, buffer, REST API, or whatever location you want
AzureLogger.log = (...args) => {
    console.log(...args); // Redirect log output to console
};

const userToken = '<USER_TOKEN>';
callClient = new CallClient(options);
const tokenCredential = new AzureCommunicationTokenCredential(userToken);
const callAgent = await callClient.createCallAgent(tokenCredential, {displayName: 'optional Azure Communication Services user name'});
const deviceManager = await callClient.getDeviceManager()

管理連入通話的 ACS SDK 連線能力

Call Agent實例可讓您啟動/加入及管理來電。 您的 Call Agent 實例必須連線到 ACS 基礎結構,才能接收來電。 建立實例時 Call Agent 會建立此連線,但有時,例如當網路不穩定、無法設定連線,或可能會在生命週期 Call Agent期間中斷。 ACS SDK 一律會嘗試與 ACS 基礎結構保持連線。 它會在連線遺失時持續重試以連線。

您可以藉由查看 屬性的connectionState目前值並接connectionStateChanged聽 來自Call Agent的事件,來檢查是否已Call Agent連線到 ACS 基礎結構。

const connectionState = callAgentInstance.connectionState;
console.log(connectionState); // it may return either of 'Connected' | 'Disconnected'

const connectionStateCallback = (args) => {
    console.log(args); // it will return an object with oldState and newState, each of having a value of either of 'Connected' | 'Disconnected'
    // it will also return reason, either of 'invalidToken' | 'connectionIssue'
}
callAgentInstance.on('connectionStateChanged', connectionStateCallback);

上述範例說明如何在連接狀態為時管理連線狀態:

  • Connected - Call Agent 實例已連線且能夠從 ACS 基礎結構接收通知。 例如,接收來電通知。
  • Disconnected - Call Agent 實例已中斷連線。 它是終端機狀態。 Call Agent 應該重新建立。 用戶應該確定它沒有網路問題。 -- 原因 invalidToken - 如果 ACS 令牌過期或無效,且應用程式無法提供新的有效令牌。 Call Agent 實例會因為這個原因而中斷連線。 -- 原因 connectionIssue - 如果網路已關閉,而且在多次重試 Call Agent 無法重新連線之後,它會中斷連線,並會因為這個原因而中斷連線。 它通常表示用戶端網路問題,而且可以在連線問題解決後立即重新儲存。

裝置管理

若要開始使用通話 SDK 的視訊,您必須能夠管理裝置。 裝置可讓您控制將音訊和視訊傳輸至通話的內容。

deviceManager透過,您可以列舉本機裝置,以在通話中傳輸音訊和視訊串流。 您也可以使用 deviceManager 來要求許可權來存取本機裝置的麥克風和相機。

您可以藉由呼叫 callClient.getDeviceManager() 方法來存取deviceManager

const deviceManager = await callClient.getDeviceManager();

取得本機裝置

若要存取本機裝置,您可以使用 deviceManager 列舉方法和 getCameras()getMicrophones。 這些方法是異步動作。

//  Get a list of available video devices for use.
const localCameras = await deviceManager.getCameras(); // [VideoDeviceInfo, VideoDeviceInfo...]

// Get a list of available microphone devices for use.
const localMicrophones = await deviceManager.getMicrophones(); // [AudioDeviceInfo, AudioDeviceInfo...]

// Get a list of available speaker devices for use.
const localSpeakers = await deviceManager.getSpeakers(); // [AudioDeviceInfo, AudioDeviceInfo...]

設定預設裝置

一旦您知道有哪些裝置可供使用,您可以設定麥克風、喇叭和相機的默認裝置。 如果未設定客戶端預設值,通訊服務 SDK 會使用作業系統預設值。

麥克風

存取使用的裝置

// Get the microphone device that is being used.
const defaultMicrophone = deviceManager.selectedMicrophone;

將裝置設定為使用

// Set the microphone device to use.
await deviceManager.selectMicrophone(localMicrophones[0]);

演講者

存取使用的裝置

// Get the speaker device that is being used.
const defaultSpeaker = deviceManager.selectedSpeaker;

將裝置設定為使用

// Set the speaker device to use.
await deviceManager.selectSpeaker(localSpeakers[0]);

相機

存取使用的裝置

// Get the camera device that is being used.
const defaultSpeaker = deviceManager.selectedSpeaker;

將裝置設定為使用

// Set the speaker device to use.
await deviceManager.selectSpeaker(localCameras[0]);

每個 CallAgent 都可以在其相關聯的 DeviceManager上選擇自己的麥克風和喇叭。 我們建議使用不同的 CallAgents 麥克風和喇叭。 他們不應該共用相同的麥克風和喇叭。 如果共享發生,則可能會觸發麥克風使用者面向診斷,而且麥克風會根據瀏覽器/ os 停止運作。

本機視訊串流

若要能夠在通話中傳送視訊,您必須建立 LocalVideoStream物件。

const localVideoStream = new LocalVideoStream(camera);

傳遞為 參數的相機是 方法所deviceManager.getCameras()傳回的VideoDeviceInfo其中一個物件。

LocalVideoStream具有下列屬性:

  • source:裝置資訊。
const source = localVideoStream.source;
  • mediaStreamType:可以是 VideoScreenSharingRawMedia
const type: MediaStreamType = localVideoStream.mediaStreamType;

本機相機預覽

您可以使用 deviceManagerVideoStreamRenderer 開始從本機相機轉譯數據流。 LocalVideoStream建立 之後,請使用它來設定VideoStreamRendererVideoStreamRenderer建立 之後,會呼叫其 createView() 方法,以取得檢視,您可以將它新增為子系至您的頁面。

此數據流不會傳送給其他參與者;這是本機預覽摘要。

// To start viewing local camera preview
const cameras = await deviceManager.getCameras();
const camera = cameras[0];
const localVideoStream = new LocalVideoStream(camera);
const videoStreamRenderer = new VideoStreamRenderer(localVideoStream);
const view = await videoStreamRenderer.createView();
htmlElement.appendChild(view.target);

停止本機預覽

若要停止本機預覽呼叫,請在衍生自 的 VideoStreamRenderer檢視上處置 。 處置 VideoStreamRenderer 之後,請從包含預覽的 DOM 節點呼叫 removeChild() 方法,從 HTML 樹狀結構移除檢視。

// To stop viewing local camera preview
view.dispose();
htmlElement.removeChild(view.target);

要求相機和麥克風的許可權

應用程式無法在沒有許可權的情況下使用相機或麥克風。 您可以使用 deviceManager 來提示使用者授與相機和/或麥克風許可權:

const result = await deviceManager.askDevicePermission({audio: true, video: true});

一旦解決承諾,方法會傳回 ,並傳 DeviceAccess 回 物件,指出是否已 audio 授與 和 video 許可權:

console.log(result.audio);
console.log(result.video);

備註

  • videoDevicesUpdated 當視訊裝置插入/取消插入時,就會引發事件。
  • audioDevicesUpdated 插入音訊裝置時引發事件。
  • 建立 DeviceManager 時,一開始不會知道任何裝置是否尚未授與許可權,因此一開始它的裝置名稱是空的,而且不包含詳細的裝置資訊。 如果我們接著呼叫 DeviceManager.askPermission() API,系統會提示使用者存取裝置。 當使用者選取 [允許] 以授與裝置管理員了解系統上裝置的存取權時,請更新其裝置清單併發出 'audioDevicesUpdated' 和 'videoDevicesUpdated' 事件。 如果使用者重新整理頁面並建立設備管理器,設備管理器就能夠瞭解裝置,因為使用者先前已授與存取權。 其裝置清單一開始已填滿,且不會發出 'audioDevicesUpdated' 或 'videoDevicesUpdated' 事件。
  • Android Chrome、iOS Safari 或 macOS Safari 不支持說話者列舉/選取範圍。

使用視訊相機撥打電話

重要

目前僅支援一個傳出本機視訊串流。

若要進行視訊呼叫,您必須在 getCameras() 中使用 deviceManager方法列舉本機相機。

選取相機之後,請使用它來建構 LocalVideoStream 實例。 將它 videoOptions 當做陣列內的 localVideoStream 項目傳遞至 CallAgentstartCall 方法。

const deviceManager = await callClient.getDeviceManager();
const cameras = await deviceManager.getCameras();
const camera = cameras[0]
const localVideoStream = new LocalVideoStream(camera);
const placeCallOptions = {videoOptions: {localVideoStreams:[localVideoStream]}};
const userCallee = { communicationUserId: '<ACS_USER_ID>' }
const call = callAgent.startCall([userCallee], placeCallOptions);
  • 您也可以使用 API 加入 CallAgent.join() 影片通話,並使用 API 接受並使用視訊 Call.Accept() 來呼叫。
  • 當您的通話連線時,它會自動開始將視訊串流從選取的相機傳送給其他參與者。

在通話時啟動和停止傳送本機視訊

啟動視訊

若要在呼叫時啟動視訊,您必須在 getCameras 物件上使用 deviceManager 方法列舉相機。 然後使用所需的相機建立 的新實例 LocalVideoStream ,然後將 對象傳遞 LocalVideoStreamstartVideo 現有呼叫物件的 方法:

const deviceManager = await callClient.getDeviceManager();
const cameras = await deviceManager.getCameras();
const camera = cameras[0]
const localVideoStream = new LocalVideoStream(camera);
await call.startVideo(localVideoStream);

停止視訊

成功開始傳送視訊之後, LocalVideoStream 呼叫實例上的 類型 Video 實例會新增至 localVideoStreams 集合。

在 Call 物件中尋找影片串流

const localVideoStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'Video'} );

停止本機視訊 若要在呼叫時停止本機視 訊,請將用於視訊的實例傳遞 localVideoStream 至 的 CallstopVideo 方法:

await call.stopVideo(localVideoStream);

您可以在該LocalVideoStream實例上叫switchSource用 ActiveVideoStream 時切換至不同的相機裝置:

const cameras = await callClient.getDeviceManager().getCameras();
const camera = cameras[1];
localVideoStream.switchSource(camera);

如果指定的視訊裝置無法使用:

  • 在通話中時,如果您的視訊已關閉,而且您使用 啟動視訊 call.startVideo(),這個方法會 SourceUnavailableError 擲回 ,且 cameraStartFailed 使用者面向診斷會設定為 true。
  • 呼叫 localVideoStream.switchSource() 方法會導致將 cameraStartFailed 設定為 True。 我們的通話診斷指南提供有關如何診斷通話相關問題的其他資訊。

若要確認本機視訊是否開啟或關閉,您可以使用 Call 方法 isLocalVideoStarted,此方法會傳回 true 或 false:

// Check if local video is on or off
call.isLocalVideoStarted;

若要接聽本機視訊的變更,您可以訂閱並取消訂閱 isLocalVideoStartedChanged 事件:

// Subscribe to local video event
call.on('isLocalVideoStartedChanged', () => {
    // Callback();
});
// Unsubscribe from local video event
call.off('isLocalVideoStartedChanged', () => {
    // Callback();
});

在通話時啟動和停止螢幕共用

若要在呼叫時啟動螢幕共用,您可以在 物件上使用Call異步方法startScreenSharing()

開始畫面共用

// Start screen sharing
await call.startScreenSharing();

在LocalVideoStream的集合中尋找螢幕共用

成功開始傳送螢幕共享之後, LocalVideoStream 類型 ScreenSharing為的實例會新增至 localVideoStreams 呼叫實例上的集合。

const localVideoStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'ScreenSharing'} );

停止螢幕共用

若要在呼叫時停止螢幕共用,您可以使用異步 API stoptScreenSharing:

// Stop screen sharing
await call.stopScreenSharing();

檢查螢幕共享狀態

若要確認螢幕共用是否開啟或關閉,您可以使用 isScreenSharingOn API,這會傳回 true 或 false:

// Check if screen sharing is on or off
call.isScreenSharingOn;

若要接聽螢幕共用的變更,您可以訂閱和取消訂閱 isScreenSharingOnChanged 事件:

// Subscribe to screen share event
call.on('isScreenSharingOnChanged', () => {
    // Callback();
});
// Unsubscribe from screen share event
call.off('isScreenSharingOnChanged', () => {
    // Callback();
});

重要

此 Azure 通訊服務功能目前處於預覽狀態。

提供的預覽 API 和 SDK 並無服務等級協定。 建議您不要將其用於生產工作負載。 部分功能可能不受支援,或是在功能上有所限制。

如需詳細資訊,請參閱 Microsoft Azure 預覽版增補使用規定

本機螢幕共用預覽處於公開預覽狀態,並可在 1.15.1-beta.1+ 版中取得。

本機畫面共用預覽

您可以使用 VideoStreamRenderer 來開始從本機螢幕共用轉譯數據流,以便查看要傳送為螢幕共用數據流的內容。

// To start viewing local screen share preview
await call.startScreenSharing();
const localScreenSharingStream = call.localVideoStreams.find( (stream) => { return stream.mediaStreamType === 'ScreenSharing' });
const videoStreamRenderer = new VideoStreamRenderer(localScreenSharingStream);
const view = await videoStreamRenderer.createView();
htmlElement.appendChild(view.target);

// To stop viewing local screen share preview.
await call.stopScreenSharing();
view.dispose();
htmlElement.removeChild(view.target);

// Screen sharing can also be stoped by clicking on the native browser's "Stop sharing" button.
// The isScreenSharingOnChanged event will be triggered where you can check the value of call.isScreenSharingOn.
// If the value is false, then that means screen sharing is turned off and so we can go ahead and dispose the screen share preview.
// This event is also triggered for the case when stopping screen sharing via Call.stopScreenSharing() API.
call.on('isScreenSharingOnChanged', () => {
    if (!call.isScreenSharingOn) {
        view.dispose();
        htmlElement.removeChild(view.target);
    }
});

轉譯遠程參與者影片/畫面共用串流

若要轉譯遠端參與者視訊或屏幕共用,第一個步驟是在您想要轉譯的 RemoteVideoStream 上取得參考。 這可以透過的數位或視訊資料流 (videoStreamsRemoteParticipant來完成。 遠端參與者集合是透過 Call 物件存取。

const remoteVideoStream = call.remoteParticipants[0].videoStreams[0];
const streamType = remoteVideoStream.mediaStreamType;

若要轉譯 RemoteVideoStream,您必須訂閱其 isAvailableChanged 事件。 isAvailable如果屬性變更為 true,遠端參與者會傳送視訊串流。 在發生此情況後,建立新的 VideoStreamRenderer 執行個體,然後使用非同步的 createView 方法建立新的 VideoStreamRendererView 執行個體。
接著,您可以將 view.target 連結至任何 UI 元素。

每當遠端資料流的可用性變更時,您可以終結整個 VideoStreamRenderer 或特定的 VideoStreamRendererView。 如果您決定保留它們,則檢視會顯示空白視訊畫面。

// Reference to the html's div where we would display a grid of all remote video stream from all participants.
let remoteVideosGallery = document.getElementById('remoteVideosGallery');

subscribeToRemoteVideoStream = async (remoteVideoStream) => {
    let renderer = new VideoStreamRenderer(remoteVideoStream);
    let view;
    let remoteVideoContainer = document.createElement('div');
    remoteVideoContainer.className = 'remote-video-container';

    let loadingSpinner = document.createElement('div');
    // See the css example below for styling the loading spinner.
    loadingSpinner.className = 'loading-spinner';
    remoteVideoStream.on('isReceivingChanged', () => {
        try {
            if (remoteVideoStream.isAvailable) {
                const isReceiving = remoteVideoStream.isReceiving;
                const isLoadingSpinnerActive = remoteVideoContainer.contains(loadingSpinner);
                if (!isReceiving && !isLoadingSpinnerActive) {
                    remoteVideoContainer.appendChild(loadingSpinner);
                } else if (isReceiving && isLoadingSpinnerActive) {
                    remoteVideoContainer.removeChild(loadingSpinner);
                }
            }
        } catch (e) {
            console.error(e);
        }
    });

    const createView = async () => {
        // Create a renderer view for the remote video stream.
        view = await renderer.createView();
        // Attach the renderer view to the UI.
        remoteVideoContainer.appendChild(view.target);
        remoteVideosGallery.appendChild(remoteVideoContainer);
    }

    // Remote participant has switched video on/off
    remoteVideoStream.on('isAvailableChanged', async () => {
        try {
            if (remoteVideoStream.isAvailable) {
                await createView();
            } else {
                view.dispose();
                remoteVideosGallery.removeChild(remoteVideoContainer);
            }
        } catch (e) {
            console.error(e);
        }
    });

    // Remote participant has video on initially.
    if (remoteVideoStream.isAvailable) {
        try {
            await createView();
        } catch (e) {
            console.error(e);
        }
    }
    
    console.log(`Initial stream size: height: ${remoteVideoStream.size.height}, width: ${remoteVideoStream.size.width}`);
    remoteVideoStream.on('sizeChanged', () => {
        console.log(`Remote video stream size changed: new height: ${remoteVideoStream.size.height}, new width: ${remoteVideoStream.size.width}`);
    });
}

CSS 用來設定透過遠端視訊串流載入微調程序樣式。

.remote-video-container {
   position: relative;
}
.loading-spinner {
   border: 12px solid #f3f3f3;
   border-radius: 50%;
   border-top: 12px solid #ca5010;
   width: 100px;
   height: 100px;
   -webkit-animation: spin 2s linear infinite; /* Safari */
   animation: spin 2s linear infinite;
   position: absolute;
   margin: auto;
   top: 0;
   bottom: 0;
   left: 0;
   right: 0;
   transform: translate(-50%, -50%);
}
@keyframes spin {
   0% { transform: rotate(0deg); }
   100% { transform: rotate(360deg); }
}
/* Safari */
@-webkit-keyframes spin {
   0% { -webkit-transform: rotate(0deg); }
   100% { -webkit-transform: rotate(360deg); }
}

遠端視訊品質

Azure 通訊服務 WebJS SDK 提供從 1.15.1 版開始的「最佳視訊計數」(OVC)功能。 這項功能可用來在運行時間通知應用程式,瞭解在不同參與者的傳入視訊在群組通話(2 個以上參與者)的指定時刻可以最佳轉譯。 這項功能會根據本機端點的網路和硬體功能,公開在呼叫期間動態變更的屬性 optimalVideoCountoptimalVideoCount詳細數據來自不同參與者應用程式的影片數目應該在指定時間轉譯的值。 應用程式應該根據建議處理這些變更,並更新轉譯影片的數目。 每個更新之間有一個 debounce 期間 (大約 10 秒)。

使用方式 功能 optimalVideoCount 是呼叫功能。 您必須透過 feature 物件的 方法Call參考功能OptimalVideoCount。 接著,您可以透過 on 的方法來 OptimalVideoCountCallFeature 設定接聽程式,以在最佳VideoCount 變更時收到通知。 若要取消訂閱變更,您可以呼叫 off 方法。

const optimalVideoCountFeature = call.feature(Features.OptimalVideoCount);
optimalVideoCountFeature.on('optimalVideoCountChanged', () => {
    const localOptimalVideoCountVariable = optimalVideoCountFeature.optimalVideoCount;
})

範例使用方式:應用程式應該訂閱群組通話中最佳視訊計數的變更。 最佳視訊計數的變更可以藉由建立新的轉譯器(createView 方法)或處置檢視處理dispose,並據以更新應用程序配置。

遠端視訊串流屬性

遠端視訊串流具有下列屬性:

const id: number = remoteVideoStream.id;
  • id:遠端視訊數據流的標識碼。
const type: MediaStreamType = remoteVideoStream.mediaStreamType;
  • mediaStreamType:可以是 VideoScreenSharing
const isAvailable: boolean = remoteVideoStream.isAvailable;
  • isAvailable:定義遠端參與者端點是否主動傳送數據流。
const isReceiving: boolean = remoteVideoStream.isReceiving;
  • isReceiving:
    • 通知應用程式是否接收遠端視訊串流數據。

    • 旗標會在下列案例中移至 false

      • 在行動瀏覽器上的遠端參與者會將瀏覽器應用程式帶入背景。
      • 遠端參與者或接收視訊的用戶有影響視訊品質的網路問題。
      • 在macOS/iOS Safari上的遠端參與者,會從網址列選取 [暫停]。
      • 遠端參與者有網路中斷連線。
      • 行動裝置上的遠端參與者會終止或終止瀏覽器。
      • 行動裝置或桌面上的遠端參與者會鎖定其裝置。 如果遠端參與者位於桌面電腦上且進入睡眠狀態,則此案例也適用於此案例。
    • 旗標會在下列案例中移至 true

      • 位於行動瀏覽器上的遠端參與者,且其瀏覽器背景已將其帶回前景。
      • 在macOS/iOS Safari上的遠端參與者在暫停視訊之後,會從網址列選取 [繼續]。
      • 遠端參與者會在暫時中斷連線之後重新連線到網路。
      • 行動裝置上的遠端參與者會解除鎖定其裝置,並返回其行動瀏覽器上的通話。
    • 這項功能可改善轉譯遠端視訊串流的用戶體驗。

    • 當 isReceiving 旗標變更為 false 時,您可以在遠端視訊串流上顯示載入微調程式。 您不需要實作載入微調程式,但載入微調器是較佳用戶體驗最常見的用法。

const size: StreamSize = remoteVideoStream.size;
  • size:數據流大小,其中包含影片寬度和高度的相關信息。

VideoStreamRenderer 方法和屬性

await videoStreamRenderer.createView();

建立VideoStreamRendererView可在應用程式 UI 中附加的實例來轉譯遠端視訊數據流、使用異步createView()方法,它會解析數據流準備好轉譯時,並傳回具有 屬性的物件,該物件表示targetvideo可在 DOM 樹狀結構中的任何位置插入的專案。

videoStreamRenderer.dispose();

處置 videoStreamRenderer 和 所有相關聯的 VideoStreamRendererView 實例。

VideoStreamRendererView 方法和屬性

當您建立 VideoStreamRendererView時,可以指定 scalingModeisMirrored 屬性。 scalingMode 可以是 StretchCropFit。 如果 isMirrored 指定 ,則會垂直翻轉轉轉譯的數據流。

const videoStreamRendererView: VideoStreamRendererView = await videoStreamRenderer.createView({ scalingMode, isMirrored });

每個 VideoStreamRendererView 實例都有一個 target 屬性,表示轉譯介面。 在應用程式 UI 中附加此屬性:

htmlElement.appendChild(view.target);

您可以叫用 updateScalingMode 方法來更新scalingMode

view.updateScalingMode('Crop');

從兩個不同的相機傳送視訊串流,並在同一部桌面裝置的相同通話中。

重要

此 Azure 通訊服務功能目前處於預覽狀態。

提供的預覽 API 和 SDK 並無服務等級協定。 建議您不要將其用於生產工作負載。 部分功能可能不受支援,或是在功能上有所限制。

如需詳細資訊,請參閱 Microsoft Azure 預覽版增補使用規定

在桌面支持的瀏覽器上,支援從相同通話中的兩部不同相機傳送視訊串流,作為 1.17.1-beta.1+ 版的一部分。

  • 您可以使用下列代碼段,從單一桌面瀏覽器索引標籤/應用程式,從兩個不同的相機傳送視訊串流:
// Create your first CallAgent with identity A
const callClient1 = new CallClient();
const callAgent1 = await callClient1.createCallAgent(tokenCredentialA);
const deviceManager1 = await callClient1.getDeviceManager();

// Create your second CallAgent with identity B
const callClient2 = new CallClient();
const callAgent2 = await callClient2.createCallAgent(tokenCredentialB);
const deviceManager2 = await callClient2.getDeviceManager();

// Join the call with your first CallAgent
const camera1 = await deviceManager1.getCameras()[0];
const callObj1 = callAgent1.join({ groupId: ‘123’}, { videoOptions: { localVideoStreams: [new LocalVideoStream(camera1)] } });

// Join the same call with your second CallAgent and make it use a different camera
const camera2 = (await deviceManager2.getCameras()).filter((camera) => { return camera !== camera1 })[0];
const callObj2 = callAgent2.join({ groupId: '123' }, { videoOptions: { localVideoStreams: [new LocalVideoStream(camera2)] } });

//Mute the microphone and speakers of your second CallAgent’s Call, so that there is no echos/noises.
await callObj2.muteIncomingAudio();
await callObj2.mute();

限制:

  • 這必須使用不同身分識別的兩個不同的 CallAgent 實例來完成。 代碼段會顯示兩個正在使用的呼叫代理程式,每個代理程式都有自己的 Call 物件。
  • 在程式代碼範例中,兩個 CallAgents 都會聯結相同的呼叫(相同的呼叫標識符)。 您也可以加入每個代理程式的不同通話,並在一次通話上傳送一個視訊,並在另一個通話上傳送不同的視訊。
  • 不支援在 CallAgent 中傳送相同的相機。 它們必須是兩個不同的相機。
  • 目前不支援使用一個 CallAgent 傳送兩個不同的相機。
  • 在macOS Safari上,背景模糊視訊效果(從 @azure/communication-effects),只能套用至一個相機,而不是同時套用兩個相機。

安裝 SDK

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

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

然後,在您的模組層級 build.gradle 檔案中,將以下幾行新增至 dependencies 區段:

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

初始化必要的物件

若要建立 CallAgent 執行個體,您必須在 CallClient 執行個體上呼叫 createCallAgent 方法。 此通話會以非同步方式傳回 CallAgent 執行個體物件。

createCallAgent 方法會採用 CommunicationUserCredential 作為引數,用來封裝存取權杖

若要存取 DeviceManager,您必須先建立 callAgent 執行個體。 然後,您可以使用 CallClient.getDeviceManager 方法取得 DeviceManager

String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an activity, for instance
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential).get();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();

若要設定來電者的顯示名稱,請使用下列替代方法:

String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an activity, for instance
CallAgentOptions callAgentOptions = new CallAgentOptions();
callAgentOptions.setDisplayName("Alice Bob");
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential, callAgentOptions).get();

裝置管理

若要開始使用影片與通話,您必須知道如何管理裝置。 裝置可讓您控制將音訊和視訊傳輸至通話的內容。

DeviceManager 可讓您列舉可用於通話中的本機裝置,以傳輸音訊/視訊串流。 它也可讓您要求使用者使用原生瀏覽器 API 存取其麥克風和相機的許可權。

您可以呼叫 callClient.getDeviceManager() 方法來存取deviceManager

Context appContext = this.getApplicationContext();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();

列舉本機裝置

若要存取本機裝置,您可以在 裝置管理員 上使用列舉方法。 列舉是同步動作。

//  Get a list of available video devices for use.
List<VideoDeviceInfo> localCameras = deviceManager.getCameras(); // [VideoDeviceInfo, VideoDeviceInfo...]

本機相機預覽

您可以使用 DeviceManagerRenderer 開始從本機相機轉譯數據流。 此數據流不會傳送給其他參與者;這是本機預覽摘要。 這是異步動作。

VideoDeviceInfo videoDevice = <get-video-device>; // See the `Enumerate local devices` topic above
Context appContext = this.getApplicationContext();

LocalVideoStream currentVideoStream = new LocalVideoStream(videoDevice, appContext);

LocalVideoStream[] localVideoStreams = new LocalVideoStream[1];
localVideoStreams[0] = currentVideoStream;

VideoOptions videoOptions = new VideoOptions(localVideoStreams);

RenderingOptions renderingOptions = new RenderingOptions(ScalingMode.Fit);
VideoStreamRenderer previewRenderer = new VideoStreamRenderer(currentVideoStream, appContext);

VideoStreamRendererView uiView = previewRenderer.createView(renderingOptions);

// Attach the uiView to a viewable location on the app at this point
layout.addView(uiView);

使用攝影機撥打 1:1 通話

警告

目前僅支援一個傳出本機視訊串流:若要使用視訊撥打通話,您必須使用 deviceManagergetCameras API 列舉本機相機。 選取所需的相機之後,請使用它來建構 LocalVideoStream 實例,並將它 videoOptions 當做陣列中的 localVideoStream 項目傳遞至 call 方法。 一旦通話連線,它會自動開始將視訊串流從選取的相機傳送給其他參與者。

注意

由於隱私權考慮,如果影片未在本機預覽,將不會與通話共用。 如需詳細資訊,請參閱 本機相機預覽

VideoDeviceInfo desiredCamera = <get-video-device>; // See the `Enumerate local devices` topic above
Context appContext = this.getApplicationContext();

LocalVideoStream currentVideoStream = new LocalVideoStream(desiredCamera, appContext);

LocalVideoStream[] localVideoStreams = new LocalVideoStream[1];
localVideoStreams[0] = currentVideoStream;

VideoOptions videoOptions = new VideoOptions(localVideoStreams);

// Render a local preview of video so the user knows that their video is being shared
Renderer previewRenderer = new VideoStreamRenderer(currentVideoStream, appContext);
View uiView = previewRenderer.createView(new CreateViewOptions(ScalingMode.FIT));

// Attach the uiView to a viewable location on the app at this point
layout.addView(uiView);

CommunicationUserIdentifier[] participants = new CommunicationUserIdentifier[]{ new CommunicationUserIdentifier("<acs user id>") };

StartCallOptions startCallOptions = new StartCallOptions();
startCallOptions.setVideoOptions(videoOptions);

Call call = callAgent.startCall(context, participants, startCallOptions);

啟動和停止傳送本機影片

若要啟動影片,您必須在 getCameraList 物件上使用 deviceManager API 列舉相機。 然後建立傳遞所需相機的新實例 LocalVideoStream ,並將它 startVideo 傳入 API 作為自變數:

VideoDeviceInfo desiredCamera = <get-video-device>; // See the `Enumerate local devices` topic above
Context appContext = this.getApplicationContext();

LocalVideoStream currentLocalVideoStream = new LocalVideoStream(desiredCamera, appContext);

VideoOptions videoOptions = new VideoOptions(currentLocalVideoStream);

Future startVideoFuture = call.startVideo(appContext, currentLocalVideoStream);
startVideoFuture.get();

成功開始傳送視訊之後, LocalVideoStream 實例就會新增至 localVideoStreams 呼叫實例上的集合。

List<LocalVideoStream> videoStreams = call.getLocalVideoStreams();
LocalVideoStream currentLocalVideoStream = videoStreams.get(0); // Please make sure there are VideoStreams in the list before calling get(0).

若要停止本機視訊,請傳遞 LocalVideoStream 集合中 localVideoStreams 可用的實例:

call.stopVideo(appContext, currentLocalVideoStream).get();

您可以在視訊傳送時切換至不同的相機裝置,方法是在 實體上LocalVideoStream叫用switchSource

currentLocalVideoStream.switchSource(source).get();

轉譯遠程參與者影片串流

若要列出遠端參與者的視訊串流和螢幕共用串流,請檢查 videoStreams 集合:

List<RemoteParticipant> remoteParticipants = call.getRemoteParticipants();
RemoteParticipant remoteParticipant = remoteParticipants.get(0); // Please make sure there are remote participants in the list before calling get(0).

List<RemoteVideoStream> remoteStreams = remoteParticipant.getVideoStreams();
RemoteVideoStream remoteParticipantStream = remoteStreams.get(0); // Please make sure there are video streams in the list before calling get(0).

MediaStreamType streamType = remoteParticipantStream.getType(); // of type MediaStreamType.Video or MediaStreamType.ScreenSharing

若要從遠端參與者轉譯 RemoteVideoStream ,您必須訂閱 OnVideoStreamsUpdated 事件。

在事件中,將屬性變更 isAvailable 為 true 表示遠端參與者目前正在傳送數據流。 一旦發生這種情況,請建立 的新實例,然後使用異步 createView API 建立新的 RendererView 實例Renderer,並在view.target應用程式的 UI 中附加任何位置。

每當遠端串流的可用性變更時,您可以選擇終結整個轉譯器、特定 RendererView 或保留它們,但這會導致顯示空白視訊畫面。

VideoStreamRenderer remoteVideoRenderer = new VideoStreamRenderer(remoteParticipantStream, appContext);
VideoStreamRendererView uiView = remoteVideoRenderer.createView(new RenderingOptions(ScalingMode.FIT));
layout.addView(uiView);

remoteParticipant.addOnVideoStreamsUpdatedListener(e -> onRemoteParticipantVideoStreamsUpdated(p, e));

void onRemoteParticipantVideoStreamsUpdated(RemoteParticipant participant, RemoteVideoStreamsEvent args) {
    for(RemoteVideoStream stream : args.getAddedRemoteVideoStreams()) {
        if(stream.getIsAvailable()) {
            startRenderingVideo();
        } else {
            renderer.dispose();
        }
    }
}

遠端視訊串流屬性

遠端視訊串流有數個屬性

  • Id - 遠端視訊串流的識別碼
int id = remoteVideoStream.getId();
  • MediaStreamType - 可以是 'Video' 或 'ScreenSharing'
MediaStreamType type = remoteVideoStream.getMediaStreamType();
  • isAvailable - 指出遠端參與者端點是否正在主動傳送數據流
boolean availability = remoteVideoStream.isAvailable();

轉譯器方法和屬性

在 API 之後的轉譯器物件

  • 建立 VideoStreamRendererView 實例,稍後可在應用程式UI中附加,以轉譯遠端視訊串流。
// Create a view for a video stream
VideoStreamRendererView.createView()
  • 處置轉譯器和與此轉譯器相關聯的所有 VideoStreamRendererView 。 當您已從 UI 移除所有相關聯的檢視時呼叫。
VideoStreamRenderer.dispose()
  • StreamSize - 遠端視訊串流的大小(寬度/高度)
StreamSize renderStreamSize = VideoStreamRenderer.getSize();
int width = renderStreamSize.getWidth();
int height = renderStreamSize.getHeight();

RendererView 方法和屬性

建立 VideoStreamRendererView 時,您可以指定 ScalingMode 將套用至此檢視的 和 mirrored 屬性:調整模式可以是 'CROP' |'FIT'

VideoStreamRenderer remoteVideoRenderer = new VideoStreamRenderer(remoteVideoStream, appContext);
VideoStreamRendererView rendererView = remoteVideoRenderer.createView(new CreateViewOptions(ScalingMode.Fit));

然後,您可以使用下列代碼段,將建立的 RendererView 附加至應用程式 UI:

layout.addView(rendererView);

您稍後可以使用其中一個 ScalingMode.CROP 在 RendererView 物件上叫 updateScalingMode 用 API 來更新縮放模式 |將Mode.FIT 調整為自變數。

// Update the scale mode for this view.
rendererView.updateScalingMode(ScalingMode.CROP)

設定系統

建立 Xcode 專案

在 Xcode 中建立新的 iOS 專案,並選取 [單一檢視應用程式] 範本。 本快速入門使用 SwiftUI 架構,因此您應將 [語言] 設定為 [Swift],並將 [使用者介面] 設定為 [SwiftUI]

進行本快速入門期間,您不會建立測試。 您可以隨意清除 [包含測試] 核取方塊。

此螢幕快照顯示用於在 Xcode 內建立項目的視窗。

使用 CocoaPods 安裝套件和相依性

  1. 為您的應用程式建立 Podfile,如以下範例所示:

    platform :ios, '13.0'
    use_frameworks!
    target 'AzureCommunicationCallingSample' do
        pod 'AzureCommunicationCalling', '~> 1.0.0'
    end
    
  2. 執行 pod install

  3. 使用 Xcode 開啟 .xcworkspace

要求存取麥克風

若要存取裝置的麥克風,您必須使用 NSMicrophoneUsageDescription 更新應用程式的資訊屬性清單。 您可以將相關聯的值設定為字串,此值會包含在系統用來向使用者要求存取權的對話中。

以滑鼠右鍵按一下專案樹狀結構的 Info.plist 項目,接著選取 [開啟形式...]>[原始程式碼]。 將以下幾行新增至最上層 <dict> 區段中,然後儲存檔案。

<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>

設定應用程式架構

開啟專案的 ContentView.swift 檔案。 將 import 宣告新增至檔案頂端,以匯入 AzureCommunicationCalling 程式庫。 此外,匯入 AVFoundation。 您將需要程式庫,才能在程式碼中要求音訊權限。

import AzureCommunicationCalling
import AVFoundation

初始化 CallAgent

若要從 CallClient 建立 CallAgent 執行個體,您必須使用 callClient.createCallAgent 方法,在 CallAgent 物件初始化後以非同步方式傳回該物件。

若要建立通話用戶端,請傳遞 CommunicationTokenCredential 物件:

import AzureCommunication

let tokenString = "token_string"
var userCredential: CommunicationTokenCredential?
do {
    let options = CommunicationTokenRefreshOptions(initialToken: token, refreshProactively: true, tokenRefresher: self.fetchTokenSync)
    userCredential = try CommunicationTokenCredential(withOptions: options)
} catch {
    updates("Couldn't created Credential object", false)
    initializationDispatchGroup!.leave()
    return
}

// tokenProvider needs to be implemented by Contoso, which fetches a new token
public func fetchTokenSync(then onCompletion: TokenRefreshOnCompletion) {
    let newToken = self.tokenProvider!.fetchNewToken()
    onCompletion(newToken, nil)
}

將您建立的 CommunicationTokenCredential 物件傳遞至 CallClient,並設定顯示名稱:

self.callClient = CallClient()
let callAgentOptions = CallAgentOptions()
options.displayName = " iOS Azure Communication Services User"

self.callClient!.createCallAgent(userCredential: userCredential!,
    options: callAgentOptions) { (callAgent, error) in
        if error == nil {
            print("Create agent succeeded")
            self.callAgent = callAgent
        } else {
            print("Create agent failed")
        }
})

管理裝置

若要開始使用影片與通話,您必須知道如何管理裝置。 裝置可讓您控制將音訊和視訊傳輸至通話的內容。

DeviceManager 可讓您列舉可用於傳輸音訊或視訊串流之呼叫中的本機裝置。 它也可讓您要求使用者存取麥克風或相機的許可權。 您可以在物件上callClient存取deviceManager

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")
        }
    }

列舉本機裝置

若要存取本機裝置,您可以在設備管理器上使用列舉方法。 列舉是同步動作。

// enumerate local cameras
var localCameras = deviceManager.cameras // [VideoDeviceInfo, VideoDeviceInfo...]

取得本機相機預覽

您可以使用 Renderer 來開始從本機相機轉譯數據流。 此數據流不會傳送給其他參與者;這是本機預覽摘要。 這是異步動作。

let camera: VideoDeviceInfo = self.deviceManager!.cameras.first!
let localVideoStream = LocalVideoStream(camera: camera)
let localRenderer = try! VideoStreamRenderer(localVideoStream: localVideoStream)
self.view = try! localRenderer.createView()

取得本機相機預覽屬性

轉譯器有一組屬性和方法,可讓您控制轉譯。

// Constructor can take in LocalVideoStream or RemoteVideoStream
let localRenderer = VideoStreamRenderer(localVideoStream:localVideoStream)
let remoteRenderer = VideoStreamRenderer(remoteVideoStream:remoteVideoStream)

// [StreamSize] size of the rendering view
localRenderer.size

// [VideoStreamRendererDelegate] an object you provide to receive events from this Renderer instance
localRenderer.delegate

// [Synchronous] create view
try! localRenderer.createView()

// [Synchronous] create view with rendering options
try! localRenderer!.createView(withOptions: CreateViewOptions(scalingMode: ScalingMode.fit))

// [Synchronous] dispose rendering view
localRenderer.dispose()

使用視訊撥打 1:1 通話

若要取得設備管理器實例,請參閱管理裝置的相關章節。

let firstCamera = self.deviceManager!.cameras.first
self.localVideoStreams = [LocalVideoStream]()
self.localVideoStreams!.append(LocalVideoStream(camera: firstCamera!))
let videoOptions = VideoOptions(localVideoStreams: self.localVideoStreams!)

let startCallOptions = StartCallOptions()
startCallOptions.videoOptions = videoOptions

let callee = CommunicationUserIdentifier('UserId')
self.callAgent?.startCall(participants: [callee], options: startCallOptions) { (call, error) in
    if error == nil {
        print("Successfully started outgoing video call")
        self.call = call
    } else {
        print("Failed to start outgoing video call")
    }
}

轉譯遠程參與者影片串流

遠端參與者可以在通話期間起始視訊或屏幕共用。

處理遠端參與者的視訊共用或螢幕共用串流

若要列出遠端參與者的數據流,請檢查 videoStreams 集合。

var remoteParticipantVideoStream = call.remoteParticipants[0].videoStreams[0]

取得遠端視訊串流屬性

var type: MediaStreamType = remoteParticipantVideoStream.type // 'MediaStreamTypeVideo'
var isAvailable: Bool = remoteParticipantVideoStream.isAvailable // indicates if remote stream is available
var id: Int = remoteParticipantVideoStream.id // id of remoteParticipantStream

轉譯遠程參與者串流

若要開始轉譯遠端參與者串流,請使用下列程序代碼。

let renderer = VideoStreamRenderer(remoteVideoStream: remoteParticipantVideoStream)
let targetRemoteParticipantView = renderer?.createView(withOptions: CreateViewOptions(scalingMode: ScalingMode.crop))
// To update the scaling mode later
targetRemoteParticipantView.update(scalingMode: ScalingMode.fit)

取得遠端視訊轉譯器方法和屬性

// [Synchronous] dispose() - dispose renderer and all `RendererView` associated with this renderer. To be called when you have removed all associated views from the UI.
remoteVideoRenderer.dispose()

設定系統

建立 Visual Studio 專案

針對 UWP 應用程式,在 Visual Studio 2022 中建立新的空白應用程式 (通用 Windows) 專案。 在輸入專案名稱之後,請隨意選擇高於 10.0.17763.0 的任何 Windows SDK。

針對 WinUI 3 應用程式,使用空白應用程式、封裝 (桌面中的 WinUI 3) 範本來建立新專案,以設定單頁 WinUI 3 應用程式。 需要 Windows App SDK 1.3 版或更新版本。

使用 NuGet 套件管理員來安裝套件和相依性

通話 SDK API 和程式庫可透過 NuGet 套件公開取得。

下列步驟示範如何尋找、下載及安裝通話 SDK NuGet 套件:

  1. 藉由選取 [工具]> [NuGet 套件管理員]> [管理解決方案的 NuGet 套件],開啟 NuGet 套件管理員。
  2. 選取 [瀏覽],然後在搜尋方塊中輸入 Azure.Communication.Calling.WindowsClient
  3. 確定已選取 [包含發行前版本] 核取方塊。
  4. 選取 Azure.Communication.Calling.WindowsClient 套件,然後選取 Azure.Communication.Calling.WindowsClient1.4.0-beta.1 或更新版本。
  5. 選取對應至右側索引標籤上「通訊服務」專案的核取方塊。
  6. 選取 [安裝] 按鈕。

要求存取麥克風

應用程式需要存取相機才能正常執行。 在 UWP 應用程式中,相機功能應該在應用程式指令清單檔案中宣告。

下列步驟會示範如何達成此目的。

  1. Solution Explorer 面板中,按兩下副檔名為 .appxmanifest 的檔案。
  2. 按一下 Capabilities 索引標籤。
  3. 從功能清單中選取 Camera 核取方塊。

建立 UI 按鈕以撥打和掛斷通話

這個簡單的範例應用程式包含兩個按鈕。 一個用於撥打電話,另一個用於掛斷通話。 下列步驟示範如何將這兩個按鈕新增至應用程式。

  1. Solution Explorer 面板中,按兩下名為 MainPage.xaml 的檔案 (若為 UWP) 或名為 MainWindows.xaml 的檔案 (若為 WinUI 3)。
  2. 在中央面板中,尋找 UI 預覽下的 XAML 程式碼。
  3. 依照下列摘錄修改 XAML 程式碼:
<TextBox x:Name="CalleeTextBox" PlaceholderText="Who would you like to call?" />
<StackPanel>
    <Button x:Name="CallButton" Content="Start/Join call" Click="CallButton_Click" />
    <Button x:Name="HangupButton" Content="Hang up" Click="HangupButton_Click" />
</StackPanel>

使用通話 SDK API 設定應用程式

通話 SDK API 位於兩個不同的命名空間中。 下列步驟會向 C# 編譯器告知這兩個命名空間的資訊,讓 Visual Studio 的 Intellisense 能夠協助開發程式碼。

  1. Solution Explorer 面板中,按一下名為 MainPage.xaml 的檔案 (若為 UWP) 左側的箭號,或名為 MainWindows.xaml 的檔案 (若為 WinUI 3) 左側的箭號。
  2. 按兩下名為 MainPage.xaml.csMainWindows.xaml.cs 的檔案。
  3. 在目前的 using 陳述式底部新增下列命令。
using Azure.Communication.Calling.WindowsClient;

保持 MainPage.xaml.csMainWindows.xaml.cs 開啟。 後續步驟會在其中新增更多程式碼。

允許應用程式互動

先前新增的 UI 按鈕需要在放置的 CommunicationCall 頂端運作。 這表示您應該將 CommunicationCall 資料成員新增至 MainPageMainWindow 類別。 此外,為了讓建立 CallAgent 的非同步作業成功,也應該將 CallAgent 資料成員新增至相同的類別。

將下列資料成員新增至 MainPageMainWindow 類別:

CallAgent callAgent;
CommunicationCall call;

建立按鈕處理常式

先前已將兩個 UI 按鈕新增至 XAML 程式碼。 下列程式碼會新增要在使用者選取按鈕時執行的處理常式。 請在上一節的資料成員後面新增下列程式碼。

private async void CallButton_Click(object sender, RoutedEventArgs e)
{
    // Start call
}

private async void HangupButton_Click(object sender, RoutedEventArgs e)
{
    // End the current call
}

物件模型

下列類別和介面會處理 Azure 通訊服務通話用戶端程式庫 (適用於 UWP) 的一些主要功能。

名稱 描述
CallClient CallClient 是通話用戶端程式庫的主要進入點。
CallAgent CallAgent 可用來開始和加入通話。
CommunicationCall CommunicationCall 可用來管理已撥打或已加入的通話。
CommunicationTokenCredential CommunicationTokenCredential 是用來具現化 CallAgent 的權杖認證。
CallAgentOptions CallAgentOptions 包含用來識別來電者的資訊。
HangupOptions HangupOptions 可讓您知道是否應向所有參與者終止通話。

註冊影片架構處理程式

UI 元件,例如 XAML 的 MediaElement 或 MediaPlayerElement,您需要應用程式註冊轉譯本機和遠端視訊摘要的設定。 在 的Package.appxmanifest標記之間Package新增下列內容:

<Extensions>
    <Extension Category="windows.activatableClass.inProcessServer">
        <InProcessServer>
            <Path>RtmMvrUap.dll</Path>
            <ActivatableClass ActivatableClassId="VideoN.VideoSchemeHandler" ThreadingModel="both" />
        </InProcessServer>
    </Extension>
</Extensions>

初始化 CallAgent

若要從 CallClient 建立 CallAgent 執行個體,必須使用 CallClient.CreateCallAgentAsync 方法,在 CallAgent 物件初始化後以非同步方式傳回該物件。

若要建立 CallAgent,您必須傳遞 CallTokenCredential 物件和 CallAgentOptions 物件。 請記住,如果傳遞了格式錯誤的權杖,便會擲回 CallTokenCredential

請在要於應用程式初始化時呼叫的協助程式函式內新增下列程式碼。

var callClient = new CallClient();
this.deviceManager = await callClient.GetDeviceManagerAsync();

var tokenCredential = new CallTokenCredential("<AUTHENTICATION_TOKEN>");
var callAgentOptions = new CallAgentOptions()
{
    DisplayName = "<DISPLAY_NAME>"
};

this.callAgent = await callClient.CreateCallAgentAsync(tokenCredential, callAgentOptions);
this.callAgent.CallsUpdated += Agent_OnCallsUpdatedAsync;
this.callAgent.IncomingCallReceived += Agent_OnIncomingCallAsync;

以資源的有效認證權杖取代 <AUTHENTICATION_TOKEN>。 若要取得認證權杖,請參閱使用者存取權杖文件。

使用攝影機撥打 1:1 通話

建立 CallAgent 所需的物件現已準備就緒。 是時候以異步方式建立 CallAgent 和放置視訊通話了。

private async void CallButton_Click(object sender, RoutedEventArgs e)
{
    var callString = CalleeTextBox.Text.Trim();

    if (!string.IsNullOrEmpty(callString))
    {
        if (callString.StartsWith("8:")) // 1:1 Azure Communication Services call
        {
            this.call = await StartAcsCallAsync(callString);
        }
    }

    if (this.call != null)
    {
        this.call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
        this.call.StateChanged += OnStateChangedAsync;
    }
}

private async Task<CommunicationCall> StartAcsCallAsync(string acsCallee)
{
    var options = await GetStartCallOptionsAsynnc();
    var call = await this.callAgent.StartCallAsync( new [] { new UserCallIdentifier(acsCallee) }, options);
    return call;
}

var micStream = new LocalOutgoingAudioStream(); // Create a default local audio stream
var cameraStream = new LocalOutgoingVideoStreamde(this.viceManager.Cameras.FirstOrDefault() as VideoDeviceDetails); // Create a default video stream

private async Task<StartCallOptions> GetStartCallOptionsAsynnc()
{
    return new StartCallOptions() {
        OutgoingAudioOptions = new OutgoingAudioOptions() { IsMuted = true, Stream = micStream  },
        OutgoingVideoOptions = new OutgoingVideoOptions() { Streams = new OutgoingVideoStream[] { cameraStream } }
    };
}

本機相機預覽

您可以選擇性地設定本機相機預覽。 影片可以透過 MediaPlayerElement轉譯:

<Grid>
    <MediaPlayerElement x:Name="LocalVideo" AutoPlay="True" />
    <MediaPlayerElement x:Name="RemoteVideo" AutoPlay="True" />
</Grid>

若要初始化本機預覽 MediaPlayerElement

private async void CameraList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (cameraStream != null)
    {
        await cameraStream?.StopPreviewAsync();
        if (this.call != null)
        {
            await this.call?.StopVideoAsync(cameraStream);
        }
    }
    var selectedCamerea = CameraList.SelectedItem as VideoDeviceDetails;
    cameraStream = new LocalOutgoingVideoStream(selectedCamerea);

    var localUri = await cameraStream.StartPreviewAsync();
    LocalVideo.Source = MediaSource.CreateFromUri(localUri);

    if (this.call != null) {
        await this.call?.StartVideoAsync(cameraStream);
    }
}

轉譯遠端相機串流

設定偶數處理程式以回應 OnCallsUpdated 事件:

private async void OnCallsUpdatedAsync(object sender, CallsUpdatedEventArgs args)
{
    var removedParticipants = new List<RemoteParticipant>();
    var addedParticipants = new List<RemoteParticipant>();

    foreach(var call in args.RemovedCalls)
    {
        removedParticipants.AddRange(call.RemoteParticipants.ToList<RemoteParticipant>());
    }

    foreach (var call in args.AddedCalls)
    {
        addedParticipants.AddRange(call.RemoteParticipants.ToList<RemoteParticipant>());
    }

    await OnParticipantChangedAsync(removedParticipants, addedParticipants);
}

private async void OnRemoteParticipantsUpdatedAsync(object sender, ParticipantsUpdatedEventArgs args)
{
    await OnParticipantChangedAsync(
        args.RemovedParticipants.ToList<RemoteParticipant>(),
        args.AddedParticipants.ToList<RemoteParticipant>());
}

private async Task OnParticipantChangedAsync(IEnumerable<RemoteParticipant> removedParticipants, IEnumerable<RemoteParticipant> addedParticipants)
{
    foreach (var participant in removedParticipants)
    {
        foreach(var incomingVideoStream in  participant.IncomingVideoStreams)
        {
            var remoteVideoStream = incomingVideoStream as RemoteIncomingVideoStream;
            if (remoteVideoStream != null)
            {
                await remoteVideoStream.StopPreviewAsync();
            }
        }
        participant.VideoStreamStateChanged -= OnVideoStreamStateChanged;
    }

    foreach (var participant in addedParticipants)
    {
        participant.VideoStreamStateChanged += OnVideoStreamStateChanged;
    }
}

private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs e)
{
    CallVideoStream callVideoStream = e.CallVideoStream;

    switch (callVideoStream.StreamDirection)
    {
        case StreamDirection.Outgoing:
            OnOutgoingVideoStreamStateChanged(callVideoStream as OutgoingVideoStream);
            break;
        case StreamDirection.Incoming:
            OnIncomingVideoStreamStateChanged(callVideoStream as IncomingVideoStream);
            break;
    }
}

開始在 上 MediaPlayerElement轉譯遠端視訊串流:

private async void OnIncomingVideoStreamStateChanged(IncomingVideoStream incomingVideoStream)
{
    switch (incomingVideoStream.State)
    {
        case VideoStreamState.Available:
            {
                switch (incomingVideoStream.Kind)
                {
                    case VideoStreamKind.RemoteIncoming:
                        var remoteVideoStream = incomingVideoStream as RemoteIncomingVideoStream;
                        var uri = await remoteVideoStream.StartPreviewAsync();

                        await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                        {
                            RemoteVideo.Source = MediaSource.CreateFromUri(uri);
                        });

                        /* Or WinUI 3
                        this.DispatcherQueue.TryEnqueue(() => {
                            RemoteVideo.Source = MediaSource.CreateFromUri(uri);
                            RemoteVideo.MediaPlayer.Play();
                        });
                        */

                        break;

                    case VideoStreamKind.RawIncoming:
                        break;
                }

                break;
            }
        case VideoStreamState.Started:
            break;
        case VideoStreamState.Stopping:
            break;
        case VideoStreamState.Stopped:
            if (incomingVideoStream.Kind == VideoStreamKind.RemoteIncoming)
            {
                var remoteVideoStream = incomingVideoStream as RemoteIncomingVideoStream;
                await remoteVideoStream.StopPreviewAsync();
            }
            break;
        case VideoStreamState.NotAvailable:
            break;
    }
}

結束通話

撥打電話後,應使用 CommunicationCall 物件的 HangupAsync 方法來掛斷通話。

此外也應使用 HangupOptions 的執行個體,告知是否必須向所有參與者終止通話。

請在 HangupButton_Click 內新增下列程式碼。

var call = this.callAgent?.Calls?.FirstOrDefault();
if (call != null)
{
    var call = this.callAgent?.Calls?.FirstOrDefault();
    if (call != null)
    {
        foreach (var localVideoStream in call.OutgoingVideoStreams)
        {
            await call.StopVideoAsync(localVideoStream);
        }

        try
        {
            if (cameraStream != null)
            {
                await cameraStream.StopPreviewAsync();
            }

            await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
        }
        catch(Exception ex) 
        { 
            var errorCode = unchecked((int)(0x0000FFFFU & ex.HResult));
            if (errorCode != 98) // Sample error code, sam_status_failed_to_hangup_for_everyone (98)
            {
                throw;
            }
        }
    }
}

執行程式碼

請確定 Visual Studio 針對 x64x86ARM64 建置應用程式,然後按 F5 開始執行應用程式。 然後,按一下 CommunicationCall 按鈕,向已定義的受話方撥打電話。

請記住,應用程式首次執行時,系統提示使用者授與麥克風的存取權。

下一步