통화 중 비디오 관리

Azure Communication Services SDKS를 사용하여 화상 통화를 관리하는 방법을 알아봅니다. 통화 내에서 비디오 수신 및 보내기를 관리하는 방법을 알아봅니다.

필수 조건

SDK 설치

npm install 명령을 사용하여 다음과 같은 JavaScript용 Azure Communication Services Common 및 통화 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()

Microsoft 인프라에 대한 SDK 연결을 가장 잘 관리하는 방법

Call Agent 인스턴스는 통화를 관리하는 데 도움이 됩니다(통화 참여 또는 시작). 작동하려면 통화 SDK가 Microsoft 인프라에 연결하여 수신 전화에 대한 알림을 가져오고 기타 호출 세부 정보를 조정해야 합니다. 사용자의 Call Agent에는 두 가지 상태가 있을 수 있습니다.

연결됨 - Call Agent connectionStatue 값이 Connected이면 클라이언트 SDK가 연결되어 있고 Microsoft 인프라로부터 알림을 받을 수 있음을 의미합니다.

연결 끊김 - DisconnectedCall Agent connectionStatue 값은 SDK가 제대로 연결되지 못하게 하는 문제가 있음을 나타냅니다. Call Agent를 다시 만들어야 합니다.

  • invalidToken: 토큰이 만료되었거나 유효하지 않은 경우 Call Agent 인스턴스가 이 오류와 함께 연결 끊기됩니다.
  • connectionIssue: Microsoft 인프라에 연결하는 클라이언트에 문제가 있는 경우 여러 번 다시 시도한 후 Call Agent에서 connectionIssue 오류를 노출합니다.

connectionState 속성의 현재 값을 검사하여 로컬 Call Agent가 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를 사용하여 로컬 디바이스의 마이크 및 카메라에 대한 액세스 권한을 요청할 수 있습니다.

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...]

기본 디바이스 설정

사용할 수 있는 디바이스를 알고 나면 마이크, 스피커, 카메라에 대한 기본 디바이스를 설정할 수 있습니다. 클라이언트 기본값이 설정되지 않은 경우 Communication Services 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: Video, ScreenSharing 또는 RawMedia
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});

프라미스가 해결되면 메서드는 audiovideo 권한이 부여되었는지 여부를 나타내는 DeviceAccess 개체를 반환합니다.

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

주의

  • videoDevicesUpdated 이벤트는 비디오 디바이스가 연결/연결 해제되면 발생합니다.
  • audioDevicesUpdated 이벤트는 오디오 디바이스가 연결되면 발생합니다.
  • DeviceManager가 만들어질 때 권한이 아직 부여되지 않은 경우 처음에는 디바이스에 대해 알지 못하므로 처음에는 디바이스 이름이 비어 있고 자세한 디바이스 정보가 포함되어 있지 않습니다. 그런 다음 DeviceManager.askPermission() API를 호출하면 사용자에게 디바이스 액세스에 대한 메시지가 표시됩니다. 사용자가 '허용'을 선택하여 액세스 권한을 부여하면 디바이스 관리자가 시스템의 디바이스에 대해 알게 되고 디바이스 목록을 업데이트하고 'audioDevicesUpdated' 및 'videoDevicesUpdated' 이벤트를 내보냅니다. 사용자가 페이지를 새로 고치고 디바이스 관리자를 만들면 사용자가 이전에 액세스 권한을 부여했기 때문에 디바이스 관리자는 디바이스에 대해 학습할 수 있습니다. 처음에는 디바이스 목록이 채워져 있으며 'audioDevicesUpdated' 또는 'videoDevicesUpdated' 이벤트를 발생시키지 않습니다.
  • Android Chrome, iOS Safari 및 macOS Safari에서는 스피커 열거/선택이 지원되지 않습니다.

비디오 카메라로 전화 걸기

Important

현재 발신 로컬 비디오 스트림은 하나만 지원됩니다.

화상 통화를 하려면 deviceManager에서 getCameras() 메서드를 사용하여 로컬 카메라를 열거해야 합니다.

카메라를 선택한 후에는 이를 사용하여 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를 사용하여 비디오로 수락하고 통화할 수도 있습니다.
  • 전화가 연결되면 선택한 카메라에서 다른 참가자로 비디오 스트림을 자동으로 보내기 시작합니다.

통화 중 로컬 비디오 보내기 시작 및 중지

비디오 시작

통화 중에 비디오를 시작하려면 deviceManager 개체의 getCameras 메서드를 사용하여 카메라를 열거해야 합니다. 그런 다음, 원하는 카메라로 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);

비디오 중지

비디오 전송이 성공적으로 시작되면 Video 형식의 LocalVideoStream 인스턴스가 통화 인스턴스의 localVideoStreams 컬렉션에 추가됩니다.

통화 개체에서 동영상 스트림 찾기

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

로컬 동영상 중지 통화 중에 로컬 동영상을 중지하려면 동영상에 사용되는 localVideoStream 인스턴스를 Call의 stopVideo 메서드에 전달합니다.

await call.stopVideo(localVideoStream);

해당 LocalVideoStream 인스턴스에서 switchSource를 호출하여 활성 LocalVideoStream이 있는 동안 다른 카메라 디바이스로 전환할 수 있습니다.

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

지정된 동영상 디바이스를 사용할 수 없는 경우:

  • 통화 중에 동영상이 꺼져 있고 call.startVideo()를 사용하여 비디오를 시작하면 이 메서드에서 SourceUnavailableError가 발생하고 cameraStartFailed 사용자 연결 진단은 true로 설정됩니다.
  • localVideoStream.switchSource() 메서드를 호출하면 cameraStartFailed가 true로 설정됩니다. 통화 진단 가이드에서는 통화 관련 문제를 진단하는 방법에 대한 추가 정보를 제공합니다.

로컬 동영상이 켜져 있는지 꺼져 있는지 확인하려면 true 또는 false를 반환하는 Call 메서드 isLocalVideoStarted를 사용할 수 있습니다.

// 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 컬렉션에서 화면 공유를 찾습니다.

화면 공유 전송이 성공적으로 시작되면 통화 ScreenSharing 형식의 LocalVideoStream 인스턴스가 통화 인스턴스의 localVideoStreams 컬렉션에 추가됩니다.

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

화면 공유 중지

통화하는 동안 화면 공유를 중지하려면 비동기 API stoptScreenSharing을 사용하면 됩니다.

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

화면 공유 상태 확인

화면 공유가 켜져 있는지 꺼져 있는지 확인하려면 true 또는 false를 반환하는 isScreenSharingOn API를 사용할 수 있습니다.

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

Important

Azure Communication Services의 이 기능은 현재 미리 보기 상태입니다.

미리 보기 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에 대한 참조를 가져오는 것입니다. 이는 RemoteParticipant의 배열 또는 동영상 스트림(videoStreams)을 통해 수행될 수 있습니다. 원격 참가자 컬렉션은 Call 개체를 통해 액세스됩니다.

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

RemoteVideoStream을 렌더링하려면 isAvailableChanged 이벤트를 구독해야 합니다. isAvailable 속성이 true로 변경되면 원격 참가자가 동영상 스트림을 보내는 것입니다. 이 경우 VideoStreamRenderer의 새 인스턴스를 만든 다음, 비동기 createView 메서드를 사용하여 새 VideoStreamRendererView 인스턴스를 만듭니다.
그러면 모든 UI 요소에 view.target을 연결할 수 있습니다.

원격 스트림의 가용성이 변경될 때마다 전체 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); }
}

원격 동영상 품질

버전 1.15.1부터 Azure Communication Services WebJS SDK에서는 OVC(최적 비디오 수)라는 기능을 제공합니다. 이 기능은 그룹 통화(참가자 2명 이상)에서 지정된 순간에 최적으로 렌더링할 수 있는 서로 다른 참가자로부터 수신되는 비디오 수를 런타임 시에 애플리케이션에 알리는 데 사용될 수 있습니다. 이 기능은 로컬 엔드포인트의 네트워크 및 하드웨어 기능을 기반으로 호출 중에 동적으로 변경되는 optimalVideoCount 속성을 노출합니다. optimalVideoCount의 값은 지정된 순간에 다른 참여자 애플리케이션에서 렌더링해야 하는 동영상 수를 자세히 설명합니다. 애플리케이션은 이러한 변경 내용을 처리하고 권장 사항에 따라 렌더링된 비디오 수를 업데이트해야 합니다. 각 업데이트 사이에는 디바운스 기간(약 10초)이 있습니다.

사용optimalVideoCount 기능은 통화 기능입니다. Call 개체의 feature 메서드를 통해 OptimalVideoCount 기능을 참조해야 합니다. 그런 다음 최적의 VideoCount가 변경될 때 알림을 받도록 OptimalVideoCountCallFeatureon 메서드를 통해 수신기를 설정할 수 있습니다. 변경 내용을 구독 취소하려면 off 메서드를 호출하면 됩니다.

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: Video 또는 ScreenSharing일 수 있습니다.
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();

애플리케이션 UI에 연결하여 원격 비디오 스트림을 렌더링할 수 있는 VideoStreamRendererView 인스턴스를 만들고, 비동기 createView() 메서드를 사용합니다. 이 메서드는 스트림을 렌더링할 준비가 되었을 때를 확인하고 DOM 트리의 아무 곳에나 삽입될 수 있는 video 요소를 나타내는 target 속성이 있는 개체를 반환합니다.

videoStreamRenderer.dispose();

videoStreamRenderer 및 모든 연결된 VideoStreamRendererView 인스턴스를 삭제합니다.

VideoStreamRendererView 메서드 및 속성

VideoStreamRendererView를 만들 때 scalingModeisMirrored 속성을 지정할 수 있습니다. scalingModeStretch, Crop 또는 Fit일 수 있습니다. isMirrored가 지정된 경우 렌더링된 스트림은 세로로 대칭 이동됩니다.

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

모든 VideoStreamRendererView 인스턴스에는 렌더링 화면을 나타내는 target 속성이 있습니다. 애플리케이션 UI에서 이 속성을 연결합니다.

htmlElement.appendChild(view.target);

나중에 updateScalingMode 메서드를 호출하여 scalingMode를 업데이트할 수 있습니다.

view.updateScalingMode('Crop');

같은 데스크톱 디바이스의 동일한 통화에서 서로 다른 카메라 2개의 비디오 스트림을 보냅니다.

Important

Azure Communication Services의 이 기능은 현재 미리 보기 상태입니다.

미리 보기 API 및 SDK는 서비스 수준 계약 없이 제공됩니다. 프로덕션 워크로드에는 사용하지 않는 것이 좋습니다. 일부 기능은 지원되지 않거나 기능이 제한될 수 있습니다.

자세한 내용은 Microsoft Azure 미리 보기에 대한 보충 사용 약관을 검토하세요.

동일한 통화에서 서로 다른 두 카메라의 동영상 스트림 전송은 데스크톱 지원 브라우저에서 버전 1.17.1-beta.1+의 일부로 지원됩니다.

  • 다음 코드 조각을 사용하여 같은 통화의 단일 데스크톱 브라우저 탭/앱에서 서로 다른 카메라 2개의 비디오 스트림을 보낼 수 있습니다.
// 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();

제한 사항:

  • 이는 서로 다른 ID를 사용하는 두 개의 서로 다른 CallAgent 인스턴스로 수행되어야 합니다. 코드 조각은 각각 고유한 통화 개체가 있는 두 개의 통화 에이전트가 사용되는 것을 보여 줍니다.
  • 코드 예제에서는 두 CallAgent가 같은 호출(같은 호출 ID)에 조인합니다. 각 에이전트와 다른 통화에 조인하고 한 통화의 비디오 하나와 다른 통화의 다른 비디오를 보낼 수도 있습니다.
  • 두 CallAgent 모두에서 같은 카메라를 보낼 수 없습니다. 서로 다른 카메라 2개여야 합니다.
  • 현재 CallAgent 하나를 사용하여 서로 다른 카메라 2개를 보낼 수 없습니다.
  • 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 전화 걸기

Warning

현재는 발신 로컬 비디오 스트림 하나만 지원됩니다. 비디오를 통해 전화를 걸려면 deviceManagergetCameras API를 사용하여 로컬 카메라를 열거해야 합니다. 원하는 카메라를 선택한 후에는 해당 카메라를 사용하여 LocalVideoStream 인스턴스를 생성하고 call 메서드에 대한 localVideoStream 배열의 항목으로 videoOptions에 전달합니다. 전화가 연결되면 선택한 카메라에서 다른 참가자로 비디오 스트림을 자동으로 보내기 시작합니다.

참고 항목

개인 정보 보호 문제 때문에 비디오를 로컬로 미리 보지 않는 경우에는 비디오가 통화에서 공유되지 않습니다. 자세한 내용은 로컬 카메라 미리 보기를 참조하세요.

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

로컬 비디오 보내기 시작 및 중지

비디오를 시작하려면 deviceManager 개체의 getCameraList 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();

비디오 전송을 성공적으로 시작하면 호출 인스턴스의 localVideoStreams 컬렉션에 LocalVideoStream 인스턴스가 추가됩니다.

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로 변경되면 원격 참가자가 현재 스트림을 보내고 있다는 뜻입니다. 이 경우 새 Renderer 인스턴스를 만든 다음, 비동기 createView API를 사용하여 새 RendererView를 만들고 애플리케이션의 UI 내 아무 위치에나 view.target을 연결합니다.

원격 스트림의 가용성이 변경될 때마다 전체 렌더러, 즉, 특정 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 - 'Video' 또는 'ScreenSharing' 중 하나
MediaStreamType type = remoteVideoStream.getMediaStreamType();
  • isAvailable - 원격 참가자 엔드포인트가 스트림을 능동적으로 전송하고 있는지 여부를 표시
boolean availability = remoteVideoStream.isAvailable();

렌더러 메서드 및 속성

API를 따르는 렌더러 개체

  • 나중에 애플리케이션 UI에서 연결하여 원격 비디오 스트림을 렌더링할 수 있는 VideoStreamRendererView 인스턴스를 만듭니다.
// 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 속성을 지정할 수 있습니다. 크기 조정 모드는 ‘CROP’ | ‘FIT’ 중 하나로 설정할 수 있습니다.

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

이렇게 만든 RendererView는 다음 코드 조각을 사용하여 애플리케이션 UI에 연결할 수 있습니다.

layout.addView(rendererView);

나중에 RendererView 개체에서 ScalingMode.CROP | ScalingMode.FIT 중 하나를 인수로 사용하고 updateScalingMode API를 호출하여 스케일링 모드를 업데이트할 수 있습니다.

// 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 항목을 마우스 오른쪽 단추로 클릭한 다음 Open As>Source Code를 선택합니다. 최상위 <dict> 섹션에 다음 줄을 추가한 다음, 파일을 저장합니다.

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

앱 프레임워크 설정

프로젝트의 ContentView.swift 파일을 엽니다. AzureCommunicationCalling 라이브러리를 가져오려면 파일 상단에 import 선언을 추가합니다. 추가로 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를 사용하여 오디오 또는 비디오 스트림을 전송하는 호출에 사용 가능한 로컬 디바이스를 열거할 수 있습니다. 또한 사용자에게 마이크 또는 카메라 액세스 권한을 요청할 수 있습니다. 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 앱 SDK 버전 1.3 이상이 필요합니다.

NuGet 패키지 관리자를 사용하여 패키지와 종속성 설치

Calling SDK API와 라이브러리는 NuGet 패키지를 통해 공개적으로 사용할 수 있습니다.

다음 단계는 Calling 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. 오른쪽 탭에서 Communication Services 프로젝트에 해당하는 확인란을 선택합니다.
  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>

Calling SDK API를 사용하여 앱 설정

Calling SDK API는 두 개의 다른 네임스페이스에 있습니다. 다음 단계에서는 Visual Studio의 IntelliSense에서 코드 개발을 지원할 수 있도록 하는 이러한 네임스페이스에 대해 C# 컴파일러에 알립니다.

  1. Solution Explorer 패널에서 MainPage.xaml(UWP의 경우) 또는 MainWindows.xaml(WinUI 3의 경우)이라는 파일의 왼쪽에 있는 화살표를 클릭합니다.
  2. MainPage.xaml.cs 또는 MainWindows.xaml.cs라는 파일을 두 번 클릭합니다.
  3. 현재의 using문 아래에 다음 명령을 추가합니다.
using Azure.Communication.Calling.WindowsClient;

MainPage.xaml.cs 또는 MainWindows.xaml.cs를 열어 두세요. 다음 단계에서는 더 많은 코드를 추가합니다.

앱 상호 작용 허용

수행된 CommunicationCall에 대해 이전에 추가한 UI 단추가 작동해야 합니다. 즉, CommunicationCall 데이터 멤버가 MainPage 또는 MainWindow 클래스에 추가되어야 합니다. 또한 CallAgent를 생성하는 비동기 작업이 성공하려면 동일한 클래스에 CallAgent 데이터 멤버도 추가되어야 합니다.

MainPage 또는 MainWindow 클래스에 다음 데이터 멤버를 추가합니다.

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 Communication Services 통화 클라이언트 라이브러리의 주요 기능 중 일부를 처리하는 클래스와 인터페이스는 다음과 같습니다.

이름 설명
CallClient CallClient는 통화 클라이언트 라이브러리의 기본 진입점입니다.
CallAgent CallAgent는 통화를 시작하고 참여하는 데 사용됩니다.
CommunicationCall CommunicationCall은 발신 또는 참여한 통화를 관리하는 데 사용됩니다.
CommunicationTokenCredential CommunicationTokenCredentialCallAgent를 인스턴스화하기 위한 토큰 자격 증명으로 사용됩니다.
CallAgentOptions CallAgentOptions에는 호출자를 식별하는 정보가 포함되어 있습니다.
HangupOptions HangupOptions는 모든 참여자에게 통화를 종료해야 하는지 알려 줍니다.

비디오 스키마 처리기 등록

XAML의 MediaElement 또는 MediaPlayerElement와 같은 UI 구성 요소에는 로컬 및 원격 동영상 피드를 렌더링하기 위한 구성을 등록하는 앱이 필요합니다. Package.appxmanifestPackage 태그 사이에 다음 콘텐츠를 추가합니다.

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

CallAgent 초기화

CallClient에서 CallAgent 인스턴스를 만들려면 초기화된 후 비동기적으로 CallAgent 개체를 반환하는 CallClient.CreateCallAgentAsync 메서드를 사용해야 합니다.

CallAgent를 만들려면 CallTokenCredential 개체와 CallAgentOptions 개체를 전달해야 합니다. 잘못된 형식의 토큰이 전달되면 CallTokenCredential이 throw됩니다.

앱 초기화 시 호출할 도우미 함수 및 내부에 다음 코드를 추가해야 합니다.

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가 x64, x86 또는 ARM64용 앱을 빌드하는지 확인한 다음 F5를 눌러 앱 실행을 시작합니다. 그런 다음, CommunicationCall 단추를 클릭하여 정의된 통화 수신자에게 통화를 겁니다.

앱을 처음 실행하면 시스템에서 사용자에게 마이크에 대한 액세스 권한을 부여하라는 메시지를 표시합니다.

다음 단계