你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

在通话期间管理视频

了解如何使用 Azure 通信服务 SDK 管理视频通话。 我们将了解如何在通话中管理接收和发送的视频。

先决条件

安装 SDK

使用 npm install 命令安装适用于 JavaScript 的 Azure 通信服务通用 SDK 和通话 SDK:

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

初始化所需的对象

大多数通话操作需要 CallClient 实例。 创建新的 CallClient 实例时,可以使用自定义选项(如 Logger 实例)对其进行配置。

有了 CallClient 实例后,可以通过调用 CallAgent 创建 createCallAgent 实例。 此方法将异步返回 CallAgent 实例对象。

createCallAgent 方法使用 CommunicationTokenCredential 作为参数。 它接受用户访问令牌

可在 getDeviceManager 实例上使用 CallClient 方法来访问 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()

管理与Microsoft基础结构的 SDK 连接

Call Agent 实例可帮助你管理通话(以加入或启动通话)。 通话 SDK 需要连接到 Microsoft 基础结构以获取传入通话通知并协调其他通话详细信息,否则无法工作。 你的 Call Agent 有两种可能的状态:

已连接 - Call Agent connectionStatue 值为 Connected 表示客户端 SDK 已连接,能够接收来自 Microsoft 基础结构的通知。

已断开连接 - Call Agent connectionStatue 值为 Disconnected 表示存在阻止 SDK 正确连接的问题。 应重新创建 Call Agent

  • invalidToken:如果令牌已过期或无效,Call Agent 实例会断开连接并出现此错误。
  • connectionIssue:如果客户端在连接到 Microsoft 基础结构时出现问题,则在多次重试后,Call Agent 会显示 connectionIssue 错误。

可以通过检查 Call Agent 属性的当前值来检查本地 connectionState 是否已连接到 Microsoft 基础结构。 在通话过程中,可以侦听 connectionStateChanged 事件,以确定 Call Agent 是否从“已连接”状态更改为“已断开连接”状态。

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);

设备管理

若要开始通过通话 SDK 使用视频通话,你需要能够管理设备。 设备使你能够控制哪些设备将音频和视频传输到通话中。

deviceManager用于枚举可在通话中传输音频和视频流的本地设备。 还可使用 deviceManager 来请求访问本地设备的麦克风和相机的权限。

可以调用 deviceManager 方法来访问 callClient.getDeviceManager()

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 使用不同的麦克风和扬声器。 它们不应共享相同的麦克风和扬声器。 如果发生共享,则可能会触发面向用户的麦克风诊断 (UFD),导致麦克风停止工作(具体取决于所用的浏览器/操作系统)。

本地视频流

若要让用户在通话中发送视频,必须创建一个 LocalVideoStream 对象。

const localVideoStream = new LocalVideoStream(camera);

作为参数传递的相机是 VideoDeviceInfo 方法返回的 deviceManager.getCameras() 对象。

LocalVideoStream 具有以下属性:

  • source 是设备信息。

    const source = localVideoStream.source;
    
  • mediaStreamType 可以是 VideoScreenSharingRawMedia

    const type: MediaStreamType = localVideoStream.mediaStreamType;
    

本地相机预览

可使用 deviceManagerVideoStreamRenderer 开始呈现来自本地相机的流。

创建 LocalVideoStream 后,使用它进行设置VideoStreamRenderer。 创建 VideoStreamRenderer 后,请调用其 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 对象,来指示是否授予了 audiovideo 权限:

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

说明

  • 插入/拔出视频设备时触发 videoDevicesUpdated 事件。
  • 插入音频设备时触发 audioDevicesUpdated 事件。
  • 首次创建DeviceManager时,如果尚未授予权限,则无法识别任何设备。 最初其设备名称为空,并且不包含详细的设备信息。 需要调用 DeviceManager.askPermission(),这会提示用户进行设备访问。 当用户允许访问时,设备管理器将了解系统上的设备、更新设备列表以及发送 audioDevicesUpdatedvideoDevicesUpdated 事件。 如果用户刷新页面并创建设备管理器,设备管理器将了解设备,因为用户以前授予了访问权限。 它的设备列表初始已填充,且不发出 audioDevicesUpdatedvideoDevicesUpdated 事件。
  • 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);
  • 还可以使用 CallAgent.join() API 加入视频通话,并使用 Call.Accept() API 接受和进行视频通话。
  • 在通话接通后,它就会自动将来自所选相机的视频流发送给其他参与者。

通话时开始和停止发送本地视频

开始视频

若要在通话时开始视频,必须在 getCameras 对象上使用 deviceManager 方法来枚举相机。 接下来使用所需相机创建一个新的 LocalVideoStream 实例,然后将 LocalVideoStream 对象传递给现有通话对象的 startVideo 方法:

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 实例传递给 Call 的 stopVideo 方法:

await call.stopVideo(localVideoStream);

通过在该 switchSource 实例上调用 LocalVideoStream,可以在具有活动 LocalVideoStream 的同时切换到其他相机设备:

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();
});

通话时开始和停止屏幕共享

若要在通话中开始屏幕共享,可以在 startScreenSharing() 对象上使用异步方法 Call

开始屏幕共享

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

注意

仅桌面浏览器支持发送 screenshare。

在 LocalVideoStream 集合中查找屏幕共享

成功开始发送屏幕共享后,将一个类型为 LocalVideoStreamScreenSharing 实例添加到通话实例的 localVideoStreams 集合中。

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

停止屏幕共享

若要在通话中停止屏幕共享,可以使用异步 API stoptScreenSharing:

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

检查屏幕共享状态

要验证屏幕共享是打开还是关闭,可以使用 isScreenSharingOn API,此 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 通信服务功能目前以预览版提供。 预览版中的功能已公开发布,可供所有新客户和现有Microsoft客户使用。

提供的预览版 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 stopped 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 的引用。

只能通过浏览 videoStreams 的数组或视频流 (RemoteParticipant) 来呈现远程参与者。 通过 Call 对象,可访问远程参与者集合。

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

若要呈现 RemoteVideoStream,必须订阅其 isAvailableChanged 事件。 如果 isAvailable 属性更改为 true,则远程参与者正在发送视频流。 发生这种情况后,请创建一个新实例VideoStreamRenderer,然后使用异步VideoStreamRendererView方法创建新createView实例。 然后,可以将 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) 名以上参与者的群组通话中以最佳方式呈现多少个来自不同参与者的传入视频。

此功能公开一个属性 optimalVideoCount,该属性会根据本地终结点的网络和硬件功能在调用期间动态更改。 optimalVideoCount 的值详细说明了应用程序应在给定时间呈现的来自不同参与者的视频数。 应用程序应根据建议处理这些更改并更新呈现的视频数。 每次更新之间有一个解脱期(大约十 (10) 秒)。

用法

optimalVideoCount特性是呼叫功能。 你需要通过 OptimalVideoCount 对象的 feature 方法引用功能 Call

然后,可以通过 onOptimalVideoCountCallFeature 方法设置侦听器,以在 optimalVideoCount 发生变化时收到通知。 若要取消订阅此类变化,可以调用 off 方法。

当前可呈现的传入视频的最大数量为 16。 为了正确支持 16 个传入视频,计算机至少需要 16 GB RAM 和四 (4) 核或更大且使用时间不超过三 (3) 年的 CPU。

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

示例用法:你的应用程序订阅群组通话中最佳视频计数的变化。 可以通过创建新的呈现器 createView 方法,或者处置视图 dispose 并相应地更新应用程序布局来处理最佳视频计数的变化。

远程视频流属性

远程视频流具有以下属性:

const id: number = remoteVideoStream.id;
  • 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() 方法,该方法会在流准备好呈现时进行解析,并返回一个具有 target 属性的对象(该对象表示可以插入到 DOM 树中的任何位置的 video 元素)。

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);

可以调用 scalingMode 方法来更新 updateScalingMode

view.updateScalingMode('Crop');

从两个不同的相机发送视频流,在同一次来自同一桌面设备的通话中。

重要说明

此项 Azure 通信服务功能目前以预览版提供。 预览版中的功能已公开发布,可供所有新客户和现有Microsoft客户使用。

提供的预览版 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 对象。
  • 在代码示例中,两个 CallAgent 都联接同一个通话(同一个通话 ID)。 还可以与每个代理加入不同的呼叫,并在一次呼叫上发送一个视频,另一个呼叫上的另一个视频。
  • 不支持在两个 CallAgents 中发送同一相机。 它们必须是两个不同的相机。
  • 目前不支持使用一个 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 实例,必须对 createCallAgent 实例调用 CallClient 方法。 此调用将异步返回 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 访问其麦克风和相机的权限。

若要访问 deviceManager,请调用 callClient.getDeviceManager() 该方法。

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);

发起启用相机的一对一通话

警告

目前仅支持一个传出本地视频流。 若要通过视频进行呼叫,必须使用 API 枚举本地相机 deviceManagergetCameras 。 选择相机后,使用它构造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对象上枚举相机。 然后,创建 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();

switchSource 实例调用 LocalVideoStream 来发送视频时,可切换到不同的相机设备:

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 表示远程参与者当前正在发送流。 发生此情况后,请创建 Renderer 的新实例,然后使用异步 RendererView API 创建新的 createView,并在应用程序 UI 中的任意位置附加 view.target

只要远程流的可用性发生更改,您可以选择销毁整个 Renderer,销毁特定的 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 - 远程视频流的 ID。

    int id = remoteVideoStream.getId();
    
  • MediaStreamType - 可以是 VideoScreenSharing

    MediaStreamType type = remoteVideoStream.getMediaStreamType();
    
  • isAvailable - 指示远程参与者终结点是否正在主动发送流。

    boolean availability = remoteVideoStream.isAvailable();
    

呈现器方法和属性

Renderer 对象使用以下方法。

  • 若要呈现远程视频流,请创建 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时,可以指定应用于此视图的ScalingModemirrored属性。

缩放模式可以是 CROPFIT 之一。

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

然后,可使用以下代码片段将创建的 RendererView 附加到应用程序 UI:

layout.addView(rendererView);

以后,可以使用updateScalingMode操作在RendererView对象上更新缩放模式,并使用ScalingMode.CROPScalingMode.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

若要从 CallAgent 创建 CallClient 实例,必须使用 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,可枚举通话中可用于传输音频或视频流的本地设备。 它还允许你向用户请求允许访问麦克风或相机。 可访问 deviceManager 对象上的 callClient

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()

发起一对一视频通话

若要获取设备管理器实例,请查看有关管理设备的部分。

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 项目

对于通用 Windows 平台应用,请在 Visual Studio 2022 中创建新的“空白应用(通用 Windows)”项目。 输入项目名称后,可随意选择任何版本高于 10.0.17763.0 的 Windows SDK。

对于 WinUI 3 应用,请使用“已打包空白应用(桌面中的 WinUI 3)”模板创建新项目,以设置单页 WinUI 3 应用。 需要 Windows App SDK 版本 1.3 或更高版本。

使用 NuGet 包管理器安装包和依赖项

可通过 NuGet 包公开提供通话 SDK API 和库。

要查找、下载和安装通话 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. 在右侧窗格中选中与 Azure 通信服务项目对应的复选框。
  6. 选择“安装”

请求访问麦克风

应用需要访问相机。 在通用 Windows 平台(UWP)应用中,需要在应用清单文件中声明相机功能。

  1. 在 Visual Studio 中打开项目。
  2. “解决方案资源管理器” 面板中,双击扩展名为 .appxmanifest “的文件”。
  3. 单击“ 功能 ”选项卡。
  4. 从功能列表中选中 Camera 复选框。

创建用于发起和挂起通话的 UI 按钮

此示例应用包含两个按钮。 一个用于发起通话,另一个用于挂起已发起的通话。

  1. “解决方案资源管理器”面板中,双击 UWP 对应的MainPage.xaml文件或 WinUI 3 对应的MainWindows.xaml文件。
  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. “解决方案资源管理器” 面板中,单击名为 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;

创建按钮处理程序

以前,我们向 XAML 代码添加了两个 UI 按钮。 以下代码将添加在用户选择按钮时要运行的处理程序。

在上一部分的数据成员后面添加以下代码。

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

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

对象模型

以下类和接口处理适用于 UWP 的 Azure 通信服务通话客户端库的某些主要功能。

名称 说明
CallClient CallClient 是通话客户端库的主入口点。
CallAgent CallAgent 用于启动和加入通话。
CommunicationCall CommunicationCall 用于管理已发起或已加入的通话。
CommunicationTokenCredential CommunicationTokenCredential 用作实例化 CallAgent 的令牌凭据。
CallAgentOptions CallAgentOptions 包含用于标识呼叫方的信息。
HangupOptions HangupOptions 告知是否应终止其所有参与者的通话。

注册视频架构处理程序

UI 组件(如 XAML) MediaElementMediaPlayerElement要求应用注册用于呈现本地和远程视频源的配置。

PackagePackage.appxmanifest 标记之间添加以下内容:

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

初始化 CallAgent

若要从 CallAgent 创建 CallClient 实例,必须使用 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> 更改为有效凭据令牌。 有关获取凭据令牌的详细信息,请参阅 用户访问令牌

发起启用相机的一对一通话

创建 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 GetStartCallOptionsAsync();
    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 LocalOutgoingVideoStream(this.viceManager.Cameras.FirstOrDefault() as VideoDeviceDetails); // Create a default video stream

private async Task<StartCallOptions> GetStartCallOptionsAsync()
{
    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 selectedCamera = CameraList.SelectedItem as VideoDeviceDetails;
    cameraStream = new LocalOutgoingVideoStream(selectedCamera);

    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;
    }
}

结束呼叫

发起通话后,使用 HangupAsync 对象的 CommunicationCall 方法来挂断通话。

使用实例 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;
            }
        }
    }
}

运行代码

  1. 请确保 Visual Studio 为 x64x86ARM64 生成应用。
  2. F5 开始运行应用。
  3. 单击 CommunicationCall 按钮向定义的收件人发出呼叫。

首次运行应用时,系统会提示用户授予对麦克风的访问权限。

后续步骤