Guia de início rápido: adicionar chamadas de vídeo 1:1 ao seu aplicativo
Comece a usar os Serviços de Comunicação do Azure usando o SDK de chamada dos Serviços de Comunicação para adicionar chamadas de vídeo 1 em 1 ao seu aplicativo. Você aprende a iniciar e atender uma chamada de vídeo usando o SDK de Chamada dos Serviços de Comunicação do Azure para JavaScript.
Código de Exemplo
Se você quiser pular para o final, você pode baixar este início rápido como um exemplo no GitHub.
Nota
As chamadas de saída para um usuário dos Serviços de Comunicação do Azure podem ser acessadas usando a Biblioteca de Interface do Usuário dos Serviços de Comunicação do Azure. A biblioteca de interface do usuário permite que os desenvolvedores adicionem um cliente de chamada habilitado para VoIP em seu aplicativo com apenas algumas linhas de código.
Pré-requisitos
Obtenha uma conta do Azure com uma assinatura ativa. Crie uma conta gratuitamente.
Você precisa ter Node.js 18. Você pode usar o instalador msi para instalá-lo.
Crie um recurso ativo dos Serviços de Comunicação. Crie um recurso de Serviços de Comunicação. Você precisa gravar sua cadeia de conexão para este início rápido.
Crie um Token de Acesso de Usuário para instanciar o cliente de chamada. Saiba como criar e gerenciar tokens de acesso de usuário. Você também pode usar a CLI do Azure e executar o comando com sua cadeia de conexão para criar um usuário e um token de acesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para obter detalhes, consulte Usar a CLI do Azure para criar e gerenciar tokens de acesso.
Configuração
Criar uma nova aplicação Node.js
Abra o terminal ou a janela de comando, crie um novo diretório para seu aplicativo e navegue até ele.
mkdir calling-quickstart && cd calling-quickstart
Execute npm init -y
para criar um arquivo package.json com as configurações padrão.
npm init -y
Instalar o pacote
Use o npm install
comando para instalar o SDK de Chamada dos Serviços de Comunicação do Azure para JavaScript.
npm install @azure/communication-common --save
npm install @azure/communication-calling --save
Configurar a estrutura do aplicativo
Este guia de início rápido usa o Webpack para agrupar os ativos do aplicativo. Execute o seguinte comando para instalar os webpack
pacotes , webpack-cli
e webpack-dev-server
npm e listá-los como dependências de desenvolvimento em seu package.json
:
npm install copy-webpack-plugin@^11.0.0 webpack@^5.88.2 webpack-cli@^5.1.4 webpack-dev-server@^4.15.1 --save-dev
Aqui está o código:
Crie um index.html
arquivo no diretório raiz do seu projeto. Usamos este arquivo para configurar um layout básico que permite ao usuário fazer uma chamada de vídeo 1:1.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Azure Communication Services - Calling Web SDK</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
<h4>Azure Communication Services - Calling Web SDK</h4>
<input id="user-access-token"
type="text"
placeholder="User access token"
style="margin-bottom:1em; width: 500px;"/>
<button id="initialize-call-agent" type="button">Initialize Call Agent</button>
<br>
<br>
<input id="callee-acs-user-id"
type="text"
placeholder="Enter callee's Azure Communication Services user identity in format: '8:acs:resourceId_userId'"
style="margin-bottom:1em; width: 500px; display: block;"/>
<button id="start-call-button" type="button" disabled="true">Start Call</button>
<button id="hangup-call-button" type="button" disabled="true">Hang up Call</button>
<button id="accept-call-button" type="button" disabled="true">Accept Call</button>
<button id="start-video-button" type="button" disabled="true">Start Video</button>
<button id="stop-video-button" type="button" disabled="true">Stop Video</button>
<br>
<br>
<div id="connectedLabel" style="color: #13bb13;" hidden>Call is connected!</div>
<br>
<div id="remoteVideosGallery" style="width: 40%;" hidden>Remote participants' video streams:</div>
<br>
<div id="localVideoContainer" style="width: 30%;" hidden>Local video stream:</div>
<!-- points to the bundle generated from client.js -->
<script src="./main.js"></script>
</body>
</html>
As seguintes classes e interfaces lidam com alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure:
Nome | Descrição |
---|---|
CallClient |
O ponto de entrada principal para o SDK de chamada. |
AzureCommunicationTokenCredential |
Implementa a CommunicationTokenCredential interface, que é usada para instanciar callAgent o . |
CallAgent |
Usado para iniciar e gerenciar chamadas. |
DeviceManager |
Usado para gerenciar dispositivos de mídia. |
Call |
Usado para representar uma chamada |
LocalVideoStream |
Usado para criar um fluxo de vídeo local para um dispositivo de câmera no sistema local. |
RemoteParticipant |
Usado para representar um participante remoto na chamada |
RemoteVideoStream |
Usado para representar um fluxo de vídeo remoto de um participante remoto. |
Crie um arquivo no diretório raiz do seu projeto chamado index.js
para conter a lógica do aplicativo para este início rápido. Adicione o seguinte código ao index.js:
// Make sure to install the necessary dependencies
const { CallClient, VideoStreamRenderer, LocalVideoStream } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential } = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");
// Set the log level and output
setLogLevel('verbose');
AzureLogger.log = (...args) => {
console.log(...args);
};
// Calling web sdk objects
let callAgent;
let deviceManager;
let call;
let incomingCall;
let localVideoStream;
let localVideoStreamRenderer;
// UI widgets
let userAccessToken = document.getElementById('user-access-token');
let calleeAcsUserId = document.getElementById('callee-acs-user-id');
let initializeCallAgentButton = document.getElementById('initialize-call-agent');
let startCallButton = document.getElementById('start-call-button');
let hangUpCallButton = document.getElementById('hangup-call-button');
let acceptCallButton = document.getElementById('accept-call-button');
let startVideoButton = document.getElementById('start-video-button');
let stopVideoButton = document.getElementById('stop-video-button');
let connectedLabel = document.getElementById('connectedLabel');
let remoteVideosGallery = document.getElementById('remoteVideosGallery');
let localVideoContainer = document.getElementById('localVideoContainer');
/**
* Using the CallClient, initialize a CallAgent instance with a CommunicationUserCredential which will enable us to make outgoing calls and receive incoming calls.
* You can then use the CallClient.getDeviceManager() API instance to get the DeviceManager.
*/
initializeCallAgentButton.onclick = async () => {
try {
const callClient = new CallClient();
tokenCredential = new AzureCommunicationTokenCredential(userAccessToken.value.trim());
callAgent = await callClient.createCallAgent(tokenCredential)
// Set up a camera device to use.
deviceManager = await callClient.getDeviceManager();
await deviceManager.askDevicePermission({ video: true });
await deviceManager.askDevicePermission({ audio: true });
// Listen for an incoming call to accept.
callAgent.on('incomingCall', async (args) => {
try {
incomingCall = args.incomingCall;
acceptCallButton.disabled = false;
startCallButton.disabled = true;
} catch (error) {
console.error(error);
}
});
startCallButton.disabled = false;
initializeCallAgentButton.disabled = true;
} catch(error) {
console.error(error);
}
}
/**
* Place a 1:1 outgoing video call to a user
* Add an event listener to initiate a call when the `startCallButton` is clicked:
* First you have to enumerate local cameras using the deviceManager `getCameraList` API.
* In this quickstart we're using the first camera in the collection. Once the desired camera is selected, a
* LocalVideoStream instance will be constructed and passed within `videoOptions` as an item within the
* localVideoStream array to the call method. Once your call connects it will automatically start sending a video stream to the other participant.
*/
startCallButton.onclick = async () => {
try {
const localVideoStream = await createLocalVideoStream();
const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
call = callAgent.startCall([{ communicationUserId: calleeAcsUserId.value.trim() }], { videoOptions });
// Subscribe to the call's properties and events.
subscribeToCall(call);
} catch (error) {
console.error(error);
}
}
/**
* Accepting an incoming call with video
* Add an event listener to accept a call when the `acceptCallButton` is clicked:
* After subscribing to the `CallAgent.on('incomingCall')` event, you can accept the incoming call.
* You can pass the local video stream which you want to use to accept the call with.
*/
acceptCallButton.onclick = async () => {
try {
const localVideoStream = await createLocalVideoStream();
const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
call = await incomingCall.accept({ videoOptions });
// Subscribe to the call's properties and events.
subscribeToCall(call);
} catch (error) {
console.error(error);
}
}
/**
* Subscribe to a call obj.
* Listen for property changes and collection updates.
*/
subscribeToCall = (call) => {
try {
// Inspect the initial call.id value.
console.log(`Call Id: ${call.id}`);
//Subscribe to call's 'idChanged' event for value changes.
call.on('idChanged', () => {
console.log(`Call Id changed: ${call.id}`);
});
// Inspect the initial call.state value.
console.log(`Call state: ${call.state}`);
// Subscribe to call's 'stateChanged' event for value changes.
call.on('stateChanged', async () => {
console.log(`Call state changed: ${call.state}`);
if(call.state === 'Connected') {
connectedLabel.hidden = false;
acceptCallButton.disabled = true;
startCallButton.disabled = true;
hangUpCallButton.disabled = false;
startVideoButton.disabled = false;
stopVideoButton.disabled = false;
remoteVideosGallery.hidden = false;
} else if (call.state === 'Disconnected') {
connectedLabel.hidden = true;
startCallButton.disabled = false;
hangUpCallButton.disabled = true;
startVideoButton.disabled = true;
stopVideoButton.disabled = true;
console.log(`Call ended, call end reason={code=${call.callEndReason.code}, subCode=${call.callEndReason.subCode}}`);
}
});
call.on('isLocalVideoStartedChanged', () => {
console.log(`isLocalVideoStarted changed: ${call.isLocalVideoStarted}`);
});
console.log(`isLocalVideoStarted: ${call.isLocalVideoStarted}`);
call.localVideoStreams.forEach(async (lvs) => {
localVideoStream = lvs;
await displayLocalVideoStream();
});
call.on('localVideoStreamsUpdated', e => {
e.added.forEach(async (lvs) => {
localVideoStream = lvs;
await displayLocalVideoStream();
});
e.removed.forEach(lvs => {
removeLocalVideoStream();
});
});
// Inspect the call's current remote participants and subscribe to them.
call.remoteParticipants.forEach(remoteParticipant => {
subscribeToRemoteParticipant(remoteParticipant);
});
// Subscribe to the call's 'remoteParticipantsUpdated' event to be
// notified when new participants are added to the call or removed from the call.
call.on('remoteParticipantsUpdated', e => {
// Subscribe to new remote participants that are added to the call.
e.added.forEach(remoteParticipant => {
subscribeToRemoteParticipant(remoteParticipant)
});
// Unsubscribe from participants that are removed from the call
e.removed.forEach(remoteParticipant => {
console.log('Remote participant removed from the call.');
});
});
} catch (error) {
console.error(error);
}
}
/**
* Subscribe to a remote participant obj.
* Listen for property changes and collection udpates.
*/
subscribeToRemoteParticipant = (remoteParticipant) => {
try {
// Inspect the initial remoteParticipant.state value.
console.log(`Remote participant state: ${remoteParticipant.state}`);
// Subscribe to remoteParticipant's 'stateChanged' event for value changes.
remoteParticipant.on('stateChanged', () => {
console.log(`Remote participant state changed: ${remoteParticipant.state}`);
});
// Inspect the remoteParticipants's current videoStreams and subscribe to them.
remoteParticipant.videoStreams.forEach(remoteVideoStream => {
subscribeToRemoteVideoStream(remoteVideoStream)
});
// Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be
// notified when the remoteParticiapant adds new videoStreams and removes video streams.
remoteParticipant.on('videoStreamsUpdated', e => {
// Subscribe to new remote participant's video streams that were added.
e.added.forEach(remoteVideoStream => {
subscribeToRemoteVideoStream(remoteVideoStream)
});
// Unsubscribe from remote participant's video streams that were removed.
e.removed.forEach(remoteVideoStream => {
console.log('Remote participant video stream was removed.');
})
});
} catch (error) {
console.error(error);
}
}
/**
* Subscribe to a remote participant's remote video stream obj.
* You have to subscribe to the 'isAvailableChanged' event to render the remoteVideoStream. If the 'isAvailable' property
* changes to 'true', a remote participant is sending a stream. Whenever availability of a remote stream changes
* you can choose to destroy the whole 'Renderer', a specific 'RendererView' or keep them, but this will result in displaying blank video frame.
*/
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');
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);
}
}
}
/**
* Start your local video stream.
* This will send your local video stream to remote participants so they can view it.
*/
startVideoButton.onclick = async () => {
try {
const localVideoStream = await createLocalVideoStream();
await call.startVideo(localVideoStream);
} catch (error) {
console.error(error);
}
}
/**
* Stop your local video stream.
* This will stop your local video stream from being sent to remote participants.
*/
stopVideoButton.onclick = async () => {
try {
await call.stopVideo(localVideoStream);
} catch (error) {
console.error(error);
}
}
/**
* To render a LocalVideoStream, you need to create a new instance of VideoStreamRenderer, and then
* create a new VideoStreamRendererView instance using the asynchronous createView() method.
* You may then attach view.target to any UI element.
*/
createLocalVideoStream = async () => {
const camera = (await deviceManager.getCameras())[0];
if (camera) {
return new LocalVideoStream(camera);
} else {
console.error(`No camera device found on the system`);
}
}
/**
* Display your local video stream preview in your UI
*/
displayLocalVideoStream = async () => {
try {
localVideoStreamRenderer = new VideoStreamRenderer(localVideoStream);
const view = await localVideoStreamRenderer.createView();
localVideoContainer.hidden = false;
localVideoContainer.appendChild(view.target);
} catch (error) {
console.error(error);
}
}
/**
* Remove your local video stream preview from your UI
*/
removeLocalVideoStream = async() => {
try {
localVideoStreamRenderer.dispose();
localVideoContainer.hidden = true;
} catch (error) {
console.error(error);
}
}
/**
* End current call
*/
hangUpCallButton.addEventListener("click", async () => {
// end the current call
await call.hangUp();
});
Crie um arquivo no diretório raiz do seu projeto chamado styles.css
para conter o estilo do aplicativo para este início rápido. Adicione o seguinte código a styles.css:
/**
* CSS for styling the loading spinner over the remote video stream
*/
.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); }
}
Adicionar o código do servidor local do webpack
Crie um arquivo no diretório raiz do seu projeto chamado webpack.config.js para conter a lógica do servidor local para este início rápido. Adicione o seguinte código ao webpack.config.js:
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
static: {
directory: path.join(__dirname, './')
},
},
plugins: [
new CopyPlugin({
patterns: [
'./index.html'
]
}),
]
};
Executar o código
Use o webpack-dev-server
para criar e executar seu aplicativo. Execute o seguinte comando para agrupar o host do aplicativo em um servidor Web local:
npx webpack serve --config webpack.config.js
Abra o navegador e, em duas guias, navegue até a http://localhost:8080/.You seguinte tela:
Na primeira guia, insira um token de acesso de usuário válido e, na outra guia, insira outro token de acesso de usuário válido diferente.
Consulte a documentação do token de acesso do usuário, se você ainda não tiver tokens disponíveis para uso.
Em ambas as guias, clique nos botões "Inicializar Call Agent". Você deve ver a seguinte tela:
Na primeira guia, insira a identidade de usuário dos Serviços de Comunicação do Azure da segunda guia e clique no botão "Iniciar chamada". A primeira guia inicia a chamada de saída para a segunda guia e o botão "Aceitar chamada" da segunda guia fica habilitado:
Na segunda aba, clique no botão "Aceitar chamada" e a chamada começa e se conecta. Você deve ver a seguinte tela:
Ambas as abas agora são bem-sucedidas em uma chamada de vídeo de 1 para 1. Ambos os separadores podem ouvir o áudio um do outro e ver o fluxo de vídeo um do outro.
Comece a usar os Serviços de Comunicação do Azure usando a biblioteca de cliente de chamada dos Serviços de Comunicação para adicionar chamadas de vídeo ao seu aplicativo. Saiba como incluir chamadas de vídeo 1:1 e como criar ou participar de chamadas em grupo. Além disso, você pode iniciar, atender e participar de uma chamada de vídeo usando o SDK de Chamada dos Serviços de Comunicação do Azure para Android.
Se você quiser começar com o código de exemplo, você pode baixar o aplicativo de exemplo.
Pré-requisitos
Uma conta do Azure com uma subscrição ativa. Crie uma conta gratuitamente.
Android Studio, para criar a sua aplicação Android.
Um recurso de Serviços de Comunicação implantado. Crie um recurso de Serviços de Comunicação. Você precisa gravar sua cadeia de conexão para este início rápido.
Um Token de Acesso de Usuário para seu Serviço de Comunicação do Azure. Você também pode usar a CLI do Azure e executar o comando com sua cadeia de conexão para criar um usuário e um token de acesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para obter detalhes, consulte Usar a CLI do Azure para criar e gerenciar tokens de acesso.
Criar um aplicativo Android com uma atividade vazia
No Android Studio, selecione Iniciar um novo projeto do Android Studio.
Em Telefone e Tablet, selecione o modelo de projeto Atividade vazia.
Para o SDK mínimo, selecione API 26: Android 8.0 (Oreo) ou posterior. Consulte Versões de suporte do SDK.
Instalar o pacote
Localize o nível build.gradle
do projeto e adicione mavenCentral()
à lista de repositórios em buildscript
e allprojects
buildscript {
repositories {
...
mavenCentral()
...
}
}
allprojects {
repositories {
...
mavenCentral()
...
}
}
Em seguida, no nível build.gradle
do dependencies
módulo, adicione as seguintes linhas às seções e android
:
android {
...
packagingOptions {
pickFirst 'META-INF/*'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
...
implementation 'com.azure.android:azure-communication-calling:2.0.0'
...
}
Adicionar permissões ao manifesto do aplicativo
Para solicitar as permissões necessárias para fazer uma chamada, você deve primeiro declarar as permissões no manifesto do aplicativo (app/src/main/AndroidManifest.xml
). Substitua o conteúdo do arquivo pelo seguinte código:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.contoso.acsquickstart">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!--Our Calling SDK depends on the Apache HTTP SDK.
When targeting Android SDK 28+, this library needs to be explicitly referenced.
See https://developer.android.com/about/versions/pie/android-9.0-changes-28#apache-p-->
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Configurar o layout do aplicativo
Você precisa de uma entrada de texto para o ID do destinatário ou ID de chamada de grupo, um botão para fazer a chamada e um botão extra para desligar a chamada.
Também precisa de dois botões para ligar e desligar o vídeo local. Você precisa colocar dois contêineres para fluxos de vídeo locais e remotos. Você pode adicionar esses botões através do designer ou editando o XML do layout.
Vá para app/src/main/res/layout/activity_main.xml e substitua o conteúdo do arquivo pelo seguinte código:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/call_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:gravity="center"
android:hint="Callee ID"
android:inputType="textPersonName"
app:layout_constraintBottom_toTopOf="@+id/call_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.064" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/call_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Call"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/show_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Show Video"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/hide_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Hide Video"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/hang_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Hang Up"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<GridLayout
android:id="@+id/remotevideocontainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="2"
android:padding="10dp"></GridLayout>
</ScrollView>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/localvideocontainer"
android:layout_width="180dp"
android:layout_height="300dp"
android:layout_gravity="right|bottom"
android:orientation="vertical"
android:padding="10dp">
<Button
android:id="@+id/switch_source"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Switch Source"
android:visibility="invisible" />
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Criar os andaimes e encadernações da atividade principal
Com o layout criado, você pode adicionar as ligações e o andaime básico da atividade. A atividade lida com a solicitação de permissões de tempo de execução, a criação do agente de chamada e a realização da chamada quando o botão é pressionado.
O onCreate
método é substituído para invocar getAllPermissions
e createAgent
, e adicionar as ligações para o botão de chamada. Esse evento ocorre apenas uma vez quando a atividade é criada. Para obter mais informações sobre onCreate
o , consulte o guia Compreender o ciclo de vida da atividade.
Vá para MainActivity.java arquivo e substitua o conteúdo pelo seguinte código:
package com.example.videocallingquickstart;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridLayout;
import android.widget.Toast;
import android.widget.LinearLayout;
import android.content.Context;
import com.azure.android.communication.calling.CallState;
import com.azure.android.communication.calling.CallingCommunicationException;
import com.azure.android.communication.calling.ParticipantsUpdatedListener;
import com.azure.android.communication.calling.PropertyChangedEvent;
import com.azure.android.communication.calling.PropertyChangedListener;
import com.azure.android.communication.calling.StartCallOptions;
import com.azure.android.communication.calling.VideoDeviceInfo;
import com.azure.android.communication.common.CommunicationIdentifier;
import com.azure.android.communication.common.CommunicationTokenCredential;
import com.azure.android.communication.calling.CallAgent;
import com.azure.android.communication.calling.CallClient;
import com.azure.android.communication.calling.DeviceManager;
import com.azure.android.communication.calling.VideoOptions;
import com.azure.android.communication.calling.LocalVideoStream;
import com.azure.android.communication.calling.VideoStreamRenderer;
import com.azure.android.communication.calling.VideoStreamRendererView;
import com.azure.android.communication.calling.CreateViewOptions;
import com.azure.android.communication.calling.ScalingMode;
import com.azure.android.communication.calling.IncomingCall;
import com.azure.android.communication.calling.Call;
import com.azure.android.communication.calling.AcceptCallOptions;
import com.azure.android.communication.calling.ParticipantsUpdatedEvent;
import com.azure.android.communication.calling.RemoteParticipant;
import com.azure.android.communication.calling.RemoteVideoStream;
import com.azure.android.communication.calling.RemoteVideoStreamsEvent;
import com.azure.android.communication.calling.RendererListener;
import com.azure.android.communication.common.CommunicationUserIdentifier;
import com.azure.android.communication.common.MicrosoftTeamsUserIdentifier;
import com.azure.android.communication.common.PhoneNumberIdentifier;
import com.azure.android.communication.common.UnknownIdentifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
private CallAgent callAgent;
private VideoDeviceInfo currentCamera;
private LocalVideoStream currentVideoStream;
private DeviceManager deviceManager;
private IncomingCall incomingCall;
private Call call;
VideoStreamRenderer previewRenderer;
VideoStreamRendererView preview;
final Map<Integer, StreamData> streamData = new HashMap<>();
private boolean renderRemoteVideo = true;
private ParticipantsUpdatedListener remoteParticipantUpdatedListener;
private PropertyChangedListener onStateChangedListener;
final HashSet<String> joinedParticipants = new HashSet<>();
Button switchSourceButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getAllPermissions();
createAgent();
handleIncomingCall();
Button callButton = findViewById(R.id.call_button);
callButton.setOnClickListener(l -> startCall());
Button hangupButton = findViewById(R.id.hang_up);
hangupButton.setOnClickListener(l -> hangUp());
Button startVideo = findViewById(R.id.show_preview);
startVideo.setOnClickListener(l -> turnOnLocalVideo());
Button stopVideo = findViewById(R.id.hide_preview);
stopVideo.setOnClickListener(l -> turnOffLocalVideo());
switchSourceButton = findViewById(R.id.switch_source);
switchSourceButton.setOnClickListener(l -> switchSource());
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
}
/**
* Request each required permission if the app doesn't already have it.
*/
private void getAllPermissions() {
// See section on requesting permissions
}
/**
* Create the call agent for placing calls
*/
private void createAgent() {
// See section on creating the call agent
}
/**
* Handle incoming calls
*/
private void handleIncomingCall() {
// See section on answering incoming call
}
/**
* Place a call to the callee id provided in `callee_id` text input.
*/
private void startCall() {
// See section on starting the call
}
/**
* End calls
*/
private void hangUp() {
// See section on ending the call
}
/**
* Mid-call operations
*/
public void turnOnLocalVideo() {
// See section
}
public void turnOffLocalVideo() {
// See section
}
/**
* Change the active camera for the next available
*/
public void switchSource() {
// See section
}
}
Solicitar permissões em tempo de execução
Para Android 6.0 e posterior (nível de API 23) e targetSdkVersion
23 ou posterior, as permissões são concedidas em tempo de execução em vez de quando o aplicativo é instalado. A fim de apoiá-lo, getAllPermissions
pode ser implementado para chamar ActivityCompat.checkSelfPermission
e ActivityCompat.requestPermissions
para cada permissão necessária.
/**
* Request each required permission if the app doesn't already have it.
*/
private void getAllPermissions() {
String[] requiredPermissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_PHONE_STATE};
ArrayList<String> permissionsToAskFor = new ArrayList<>();
for (String permission : requiredPermissions) {
if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
permissionsToAskFor.add(permission);
}
}
if (!permissionsToAskFor.isEmpty()) {
ActivityCompat.requestPermissions(this, permissionsToAskFor.toArray(new String[0]), 1);
}
}
Nota
Ao projetar seu aplicativo, considere quando essas permissões devem ser solicitadas. As permissões devem ser solicitadas conforme necessário, não com antecedência. Para obter mais informações, consulte o Guia de permissões do Android.
Modelo de objeto
As seguintes classes e interfaces lidam com alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure:
Nome | Descrição |
---|---|
CallClient |
O ponto de entrada principal para o SDK de chamada. |
CallAgent |
Usado para iniciar e gerenciar chamadas. |
CommunicationTokenCredential |
Usado como a credencial de token para instanciar o CallAgent . |
CommunicationIdentifier |
Usado como um tipo diferente de participante que pode fazer parte de uma chamada. |
Criar um agente a partir do token de acesso do usuário
Você precisa de um token de usuário para criar um agente de chamada autenticado. Geralmente, esse token é gerado a partir de um serviço com autenticação específica para o aplicativo. Para obter mais informações sobre tokens de acesso de usuário, consulte Tokens de acesso de usuário.
Para o início rápido, substitua <User_Access_Token>
por um token de acesso de usuário gerado para seu recurso dos Serviços de Comunicação do Azure.
/**
* Create the call agent for placing calls
*/
private void createAgent() {
Context context = this.getApplicationContext();
String userToken = "<USER_ACCESS_TOKEN>";
try {
CommunicationTokenCredential credential = new CommunicationTokenCredential(userToken);
CallClient callClient = new CallClient();
deviceManager = callClient.getDeviceManager(context).get();
callAgent = callClient.createCallAgent(getApplicationContext(), credential).get();
} catch (Exception ex) {
Toast.makeText(context, "Failed to create call agent.", Toast.LENGTH_SHORT).show();
}
}
Iniciar uma chamada de vídeo usando o agente de chamada
Você pode fazer a chamada usando o agente de chamada. Tudo o que você precisa fazer é fornecer uma lista de IDs de destinatários e as opções de chamada.
Para fazer uma chamada com vídeo, você precisa enumerar câmeras locais usando a deviceManager
getCameras
API. Depois de selecionar uma câmera desejada, use-a para construir uma LocalVideoStream
instância. Em seguida, passe-o como videoOptions
um item na localVideoStream
matriz para um método de chamada. Quando a chamada se conecta, ela começa automaticamente a enviar um fluxo de vídeo da câmera selecionada para o outro participante.
private void startCall() {
Context context = this.getApplicationContext();
EditText callIdView = findViewById(R.id.call_id);
String callId = callIdView.getText().toString();
ArrayList<CommunicationIdentifier> participants = new ArrayList<CommunicationIdentifier>();
List<VideoDeviceInfo> cameras = deviceManager.getCameras();
StartCallOptions options = new StartCallOptions();
if(!cameras.isEmpty()) {
currentCamera = getNextAvailableCamera(null);
currentVideoStream = new LocalVideoStream(currentCamera, context);
LocalVideoStream[] videoStreams = new LocalVideoStream[1];
videoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(videoStreams);
options.setVideoOptions(videoOptions);
showPreview(currentVideoStream);
}
participants.add(new CommunicationUserIdentifier(callId));
call = callAgent.startCall(
context,
participants,
options);
//Subscribe to events on updates of call state and remote participants
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
onStateChangedListener = this::handleCallOnStateChanged;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
call.addOnStateChangedListener(onStateChangedListener);
}
Neste início rápido, você confia na função getNextAvailableCamera
para escolher a câmera que a chamada usa. A função toma a enumeração de câmeras como entrada, e itera através da lista para obter a próxima câmera disponível. Se o argumento for null
, a função seleciona o primeiro dispositivo da lista. Se não houver câmeras disponíveis quando você selecionar Iniciar chamada, uma chamada de áudio será iniciada. Mas se o participante remoto respondeu com vídeo, você ainda pode ver o fluxo de vídeo remoto.
private VideoDeviceInfo getNextAvailableCamera(VideoDeviceInfo camera) {
List<VideoDeviceInfo> cameras = deviceManager.getCameras();
int currentIndex = 0;
if (camera == null) {
return cameras.isEmpty() ? null : cameras.get(0);
}
for (int i = 0; i < cameras.size(); i++) {
if (camera.getId().equals(cameras.get(i).getId())) {
currentIndex = i;
break;
}
}
int newIndex = (currentIndex + 1) % cameras.size();
return cameras.get(newIndex);
}
Depois de construir uma LocalVideoStream
instância, você pode criar um renderizador para exibi-la na interface do usuário.
private void showPreview(LocalVideoStream stream) {
previewRenderer = new VideoStreamRenderer(stream, this);
LinearLayout layout = findViewById(R.id.localvideocontainer);
preview = previewRenderer.createView(new CreateViewOptions(ScalingMode.FIT));
preview.setTag(0);
runOnUiThread(() -> {
layout.addView(preview);
switchSourceButton.setVisibility(View.VISIBLE);
});
}
Para permitir que o usuário alterne a fonte de vídeo local, use switchSource
. Este método escolhe a próxima câmara disponível e define-a como o fluxo local.
public void switchSource() {
if (currentVideoStream != null) {
try {
currentCamera = getNextAvailableCamera(currentCamera);
currentVideoStream.switchSource(currentCamera).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Aceitar uma chamada recebida
Você pode obter uma chamada de entrada assinando addOnIncomingCallListener
em callAgent
.
private void handleIncomingCall() {
callAgent.addOnIncomingCallListener((incomingCall) -> {
this.incomingCall = incomingCall;
Executors.newCachedThreadPool().submit(this::answerIncomingCall);
});
}
Para aceitar uma chamada com a câmera de vídeo ativada, enumere as câmeras locais usando a deviceManager
getCameras
API. Escolha uma câmera e construa uma LocalVideoStream
instância. Passe para dentro acceptCallOptions
antes de chamar o accept
método em um call
objeto.
private void answerIncomingCall() {
Context context = this.getApplicationContext();
if (incomingCall == null){
return;
}
AcceptCallOptions acceptCallOptions = new AcceptCallOptions();
List<VideoDeviceInfo> cameras = deviceManager.getCameras();
if(!cameras.isEmpty()) {
currentCamera = getNextAvailableCamera(null);
currentVideoStream = new LocalVideoStream(currentCamera, context);
LocalVideoStream[] videoStreams = new LocalVideoStream[1];
videoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(videoStreams);
acceptCallOptions.setVideoOptions(videoOptions);
showPreview(currentVideoStream);
}
try {
call = incomingCall.accept(context, acceptCallOptions).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//Subscribe to events on updates of call state and remote participants
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
onStateChangedListener = this::handleCallOnStateChanged;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
call.addOnStateChangedListener(onStateChangedListener);
}
Participantes remotos e fluxos de vídeo remotos
Todos os participantes remotos estão disponíveis através do getRemoteParticipants()
método em uma instância de chamada. Uma vez que a chamada se conecta (CallState.CONNECTED
), podemos acessar os participantes remotos da chamada e lidar com os fluxos de vídeo remotos.
Ao iniciar uma chamada ou atender uma chamada recebida, você precisa se inscrever no addOnRemoteParticipantsUpdatedListener
evento para lidar com participantes remotos.
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
Quando você usa ouvintes de eventos definidos dentro da mesma classe, vincule o ouvinte a uma variável. Passe a variável como um argumento para adicionar e remover métodos de ouvinte.
Se você tentar passar o ouvinte diretamente como um argumento, você perde a referência a esse ouvinte. O Java cria novas instâncias desses ouvintes, não fazendo referência a instâncias criadas anteriormente. Não é possível remover instâncias anteriores, porque você não tem uma referência a elas.
Nota
Quando um usuário ingressa em uma chamada, ele pode acessar os participantes remotos atuais por meio do getRemoteParticipants()
método. O addOnRemoteParticipantsUpdatedListener
evento não será acionado para esses participantes existentes. Esse evento só será acionado quando um participante remoto entrar ou sair da chamada enquanto o usuário já estiver na chamada.
Atualizações de fluxo de vídeo remoto
Para chamadas 1:1, você precisa lidar com participantes adicionados. Quando você remove o participante remoto, a chamada termina. Para participantes adicionados, você se inscreve para addOnVideoStreamsUpdatedListener
lidar com atualizações de fluxo de vídeo.
public void handleRemoteParticipantsUpdate(ParticipantsUpdatedEvent args) {
handleAddedParticipants(args.getAddedParticipants());
}
private void handleAddedParticipants(List<RemoteParticipant> participants) {
for (RemoteParticipant remoteParticipant : participants) {
if(!joinedParticipants.contains(getId(remoteParticipant))) {
joinedParticipants.add(getId(remoteParticipant));
if (renderRemoteVideo) {
for (RemoteVideoStream stream : remoteParticipant.getVideoStreams()) {
StreamData data = new StreamData(stream, null, null);
streamData.put(stream.getId(), data);
startRenderingVideo(data);
}
}
remoteParticipant.addOnVideoStreamsUpdatedListener(videoStreamsEventArgs -> videoStreamsUpdated(videoStreamsEventArgs));
}
}
}
private void videoStreamsUpdated(RemoteVideoStreamsEvent videoStreamsEventArgs) {
for(RemoteVideoStream stream : videoStreamsEventArgs.getAddedRemoteVideoStreams()) {
StreamData data = new StreamData(stream, null, null);
streamData.put(stream.getId(), data);
if (renderRemoteVideo) {
startRenderingVideo(data);
}
}
for(RemoteVideoStream stream : videoStreamsEventArgs.getRemovedRemoteVideoStreams()) {
stopRenderingVideo(stream);
}
}
public String getId(final RemoteParticipant remoteParticipant) {
final CommunicationIdentifier identifier = remoteParticipant.getIdentifier();
if (identifier instanceof PhoneNumberIdentifier) {
return ((PhoneNumberIdentifier) identifier).getPhoneNumber();
} else if (identifier instanceof MicrosoftTeamsUserIdentifier) {
return ((MicrosoftTeamsUserIdentifier) identifier).getUserId();
} else if (identifier instanceof CommunicationUserIdentifier) {
return ((CommunicationUserIdentifier) identifier).getId();
} else {
return ((UnknownIdentifier) identifier).getId();
}
}
Renderizar vídeos remotos
Crie um renderizador do fluxo de vídeo remoto e anexe-o à visualização para começar a renderizar a exibição remota. Descarte o modo de exibição para parar de renderizá-lo.
void startRenderingVideo(StreamData data){
if (data.renderer != null) {
return;
}
GridLayout layout = ((GridLayout)findViewById(R.id.remotevideocontainer));
data.renderer = new VideoStreamRenderer(data.stream, this);
data.renderer.addRendererListener(new RendererListener() {
@Override
public void onFirstFrameRendered() {
String text = data.renderer.getSize().toString();
Log.i("MainActivity", "Video rendering at: " + text);
}
@Override
public void onRendererFailedToStart() {
String text = "Video failed to render";
Log.i("MainActivity", text);
}
});
data.rendererView = data.renderer.createView(new CreateViewOptions(ScalingMode.FIT));
data.rendererView.setTag(data.stream.getId());
runOnUiThread(() -> {
GridLayout.LayoutParams params = new GridLayout.LayoutParams(layout.getLayoutParams());
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
params.height = (int)(displayMetrics.heightPixels / 2.5);
params.width = displayMetrics.widthPixels / 2;
layout.addView(data.rendererView, params);
});
}
void stopRenderingVideo(RemoteVideoStream stream) {
StreamData data = streamData.get(stream.getId());
if (data == null || data.renderer == null) {
return;
}
runOnUiThread(() -> {
GridLayout layout = findViewById(R.id.remotevideocontainer);
for(int i = 0; i < layout.getChildCount(); ++ i) {
View childView = layout.getChildAt(i);
if ((int)childView.getTag() == data.stream.getId()) {
layout.removeViewAt(i);
}
}
});
data.rendererView = null;
// Dispose renderer
data.renderer.dispose();
data.renderer = null;
}
static class StreamData {
RemoteVideoStream stream;
VideoStreamRenderer renderer;
VideoStreamRendererView rendererView;
StreamData(RemoteVideoStream stream, VideoStreamRenderer renderer, VideoStreamRendererView rendererView) {
this.stream = stream;
this.renderer = renderer;
this.rendererView = rendererView;
}
}
Atualização do estado da chamada
O estado de uma chamada pode mudar de conectado para desconectado. Quando a chamada está conectada, você lida com o participante remoto e, quando a chamada é desconectada, você descarta previewRenderer
para parar o vídeo local.
private void handleCallOnStateChanged(PropertyChangedEvent args) {
if (call.getState() == CallState.CONNECTED) {
runOnUiThread(() -> Toast.makeText(this, "Call is CONNECTED", Toast.LENGTH_SHORT).show());
handleCallState();
}
if (call.getState() == CallState.DISCONNECTED) {
runOnUiThread(() -> Toast.makeText(this, "Call is DISCONNECTED", Toast.LENGTH_SHORT).show());
if (previewRenderer != null) {
previewRenderer.dispose();
}
switchSourceButton.setVisibility(View.INVISIBLE);
}
}
Terminar uma chamada
Termine a chamada chamando a hangUp()
função na instância de chamada. Descarte previewRenderer
para parar o vídeo local.
private void hangUp() {
try {
call.hangUp().get();
switchSourceButton.setVisibility(View.INVISIBLE);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
if (previewRenderer != null) {
previewRenderer.dispose();
}
}
Ocultar e mostrar vídeo local
Quando a chamada for iniciada, você poderá interromper a renderização e o streaming de vídeo local com turnOffLocalVideo()
o , esse método remove a exibição que encapsula a renderização local e descarta o fluxo atual. Para retomar o fluxo e renderizar a visualização local novamente, use turnOnLocalVideo()
, este método mostra a visualização do vídeo e inicia o streaming.
public void turnOnLocalVideo() {
List<VideoDeviceInfo> cameras = deviceManager.getCameras();
if(!cameras.isEmpty()) {
try {
currentVideoStream = new LocalVideoStream(currentCamera, this);
showPreview(currentVideoStream);
call.startVideo(this, currentVideoStream).get();
switchSourceButton.setVisibility(View.VISIBLE);
} catch (CallingCommunicationException acsException) {
acsException.printStackTrace();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}
public void turnOffLocalVideo() {
try {
LinearLayout container = findViewById(R.id.localvideocontainer);
for (int i = 0; i < container.getChildCount(); ++i) {
Object tag = container.getChildAt(i).getTag();
if (tag != null && (int)tag == 0) {
container.removeViewAt(i);
}
}
switchSourceButton.setVisibility(View.INVISIBLE);
previewRenderer.dispose();
previewRenderer = null;
call.stopVideo(this, currentVideoStream).get();
} catch (CallingCommunicationException acsException) {
acsException.printStackTrace();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
Executar o código
Agora você pode iniciar o aplicativo usando o botão Executar aplicativo na barra de ferramentas do Android Studio.
Candidatura concluída | Chamada 1:1 |
---|---|
Adicionar capacidade de chamada de grupo
Agora você pode atualizar seu aplicativo para permitir que o usuário escolha entre chamadas 1:1 ou chamadas em grupo.
Atualizar layout
Use botões de opção para selecionar se o SDK cria uma chamada 1:1 ou ingressa em uma chamada de grupo. Os botões de opção estão na parte superior, de modo que a primeira seção de app/src/main/res/layout/activity_main.xml termina da seguinte forma.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/one_to_one_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="One to one call" />
<RadioButton
android:id="@+id/group_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Group call" />
</RadioGroup>
<EditText
android:id="@+id/call_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:gravity="center"
android:hint="Callee ID"
android:inputType="textPersonName"
app:layout_constraintBottom_toTopOf="@+id/call_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.064" />
.
.
.
</androidx.constraintlayout.widget.ConstraintLayout>
Atualizar MainActivity.Java
Agora você pode atualizar os elementos e a lógica para decidir quando criar uma chamada 1:1 e quando ingressar em uma chamada de grupo. A primeira parte do código requer atualizações para adicionar dependências, itens e outras configurações.
Dependências:
import android.widget.RadioButton;
import com.azure.android.communication.calling.GroupCallLocator;
import com.azure.android.communication.calling.JoinCallOptions;
import java.util.UUID;
Elementos globais:
RadioButton oneToOneCall, groupCall;
Atualização onCreate()
:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getAllPermissions();
createAgent();
handleIncomingCall();
Button callButton = findViewById(R.id.call_button);
callButton.setOnClickListener(l -> startCall());
Button hangupButton = findViewById(R.id.hang_up);
hangupButton.setOnClickListener(l -> hangUp());
Button startVideo = findViewById(R.id.show_preview);
startVideo.setOnClickListener(l -> turnOnLocalVideo());
Button stopVideo = findViewById(R.id.hide_preview);
stopVideo.setOnClickListener(l -> turnOffLocalVideo());
switchSourceButton = findViewById(R.id.switch_source);
switchSourceButton.setOnClickListener(l -> switchSource());
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
oneToOneCall = findViewById(R.id.one_to_one_call);
oneToOneCall.setOnClickListener(this::onCallTypeSelected);
oneToOneCall.setChecked(true);
groupCall = findViewById(R.id.group_call);
groupCall.setOnClickListener(this::onCallTypeSelected);
}
Atualização startCall()
:
private void startCall() {
Context context = this.getApplicationContext();
EditText callIdView = findViewById(R.id.call_id);
String callId = callIdView.getText().toString();
ArrayList<CommunicationIdentifier> participants = new ArrayList<CommunicationIdentifier>();
List<VideoDeviceInfo> cameras = deviceManager.getCameras();
if(oneToOneCall.isChecked()){
StartCallOptions options = new StartCallOptions();
if(!cameras.isEmpty()) {
currentCamera = getNextAvailableCamera(null);
currentVideoStream = new LocalVideoStream(currentCamera, context);
LocalVideoStream[] videoStreams = new LocalVideoStream[1];
videoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(videoStreams);
options.setVideoOptions(videoOptions);
showPreview(currentVideoStream);
}
participants.add(new CommunicationUserIdentifier(callId));
call = callAgent.startCall(
context,
participants,
options);
}
else{
JoinCallOptions options = new JoinCallOptions();
if(!cameras.isEmpty()) {
currentCamera = getNextAvailableCamera(null);
currentVideoStream = new LocalVideoStream(currentCamera, context);
LocalVideoStream[] videoStreams = new LocalVideoStream[1];
videoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(videoStreams);
options.setVideoOptions(videoOptions);
showPreview(currentVideoStream);
}
GroupCallLocator groupCallLocator = new GroupCallLocator(UUID.fromString(callId));
call = callAgent.join(
context,
groupCallLocator,
options);
}
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
onStateChangedListener = this::handleCallOnStateChanged;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
call.addOnStateChangedListener(onStateChangedListener);
}
Adicionar onCallTypeSelected()
:
public void onCallTypeSelected(View view) {
boolean checked = ((RadioButton) view).isChecked();
EditText callIdView = findViewById(R.id.call_id);
switch(view.getId()) {
case R.id.one_to_one_call:
if (checked){
callIdView.setHint("Callee id");
}
break;
case R.id.group_call:
if (checked){
callIdView.setHint("Group Call GUID");
}
break;
}
}
Executar o aplicativo atualizado
Neste ponto, você pode iniciar o aplicativo usando o botão Executar aplicativo na barra de ferramentas do Android Studio.
Atualização do ecrã | Chamada em grupo |
---|---|
Comece a usar os Serviços de Comunicação do Azure usando o SDK de chamada dos Serviços de Comunicação para adicionar uma chamada de vídeo individual ao seu aplicativo. Você aprende a iniciar e atender uma chamada de vídeo usando o SDK de Chamada dos Serviços de Comunicação do Azure para iOS.
Código de Exemplo
Se você quiser pular para o final, você pode baixar este início rápido como um exemplo no GitHub.
Pré-requisitos
Obtenha uma conta do Azure com uma assinatura ativa. Crie uma conta gratuitamente.
Um Mac com Xcode, juntamente com um certificado de programador válido instalado no seu Porta-chaves.
Crie um recurso ativo dos Serviços de Comunicação. Crie um recurso de Serviços de Comunicação. Você precisa gravar sua cadeia de conexão para este início rápido.
Um Token de Acesso de Usuário para seu Serviço de Comunicação do Azure. Você também pode usar a CLI do Azure e executar o comando com sua cadeia de conexão para criar um usuário e um token de acesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para obter detalhes, consulte Usar a CLI do Azure para criar e gerenciar tokens de acesso.
Configuração
Criando o projeto Xcode
No Xcode, crie um novo projeto iOS e selecione o modelo Single View App. Este tutorial usa a estrutura SwiftUI, portanto, você deve definir o idioma como Swift e a interface do usuário como SwiftUI. Você não vai criar testes durante esse início rápido. Sinta-se à vontade para desmarcar Incluir testes.
Instalação do CocoaPods
Use este guia para instalar o CocoaPods no seu Mac.
Instale o pacote e as dependências com o CocoaPods
Para criar um
Podfile
para seu aplicativo, abra o terminal e navegue até a pasta do projeto e execute pod init.Adicione o seguinte código ao
Podfile
e salve. Consulte Versões de suporte do SDK.
platform :ios, '13.0'
use_frameworks!
target 'VideoCallingQuickstart' do
pod 'AzureCommunicationCalling', '~> 1.0.0'
end
Execute a instalação do pod.
Abra o com Xcode
.xcworkspace
.
Usando XCFramework diretamente
Se você não estiver usando CocoaPods
como um gerenciador de dependência, você pode baixá-lo AzureCommunicationCalling.xcframework
diretamente de nossa página de lançamento.
É importante saber que AzureCommunicationCalling
tem uma dependência de AzureCommunicationCommon
então você precisa instalá-lo também em seu projeto.
Nota
Embora AzureCommunicationCommon
seja um pacote swift puro, você não pode instalá-lo usando Swift Package Manager
para usá-lo porque AzureCommunicationCalling
o último é uma estrutura Objective-C e Swift Package Manager
deliberadamente não suporta cabeçalhos de interface Swift ObjC por design, o que significa que não é possível trabalhar em conjunto se AzureCommunicationCalling
instalado usando Swift Package Manager
. Você teria que instalar por meio de outro gerenciador de dependência ou gerar um xcframework
de AzureCommunicationCommon
fontes e importar para seu projeto.
Solicitar acesso ao microfone e à câmara
Para acessar o microfone e a câmera do dispositivo, você precisa atualizar a Lista de Propriedades de Informações do seu aplicativo com um NSMicrophoneUsageDescription
e NSCameraUsageDescription
. Você define o valor associado como uma cadeia de caracteres que inclui a caixa de diálogo que o sistema usa para solicitar acesso do usuário.
Clique com o botão direito do mouse na Info.plist
entrada da árvore do projeto e selecione Abrir como > código-fonte. Adicione as seguintes linhas à secção de nível <dict>
superior e, em seguida, guarde o ficheiro.
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>
<key>NSCameraUsageDescription</key>
<string>Need camera access for video calling</string>
Configurar a estrutura do aplicativo
Abra o arquivo do ContentView.swift
projeto e adicione uma declaração de importação à parte superior do arquivo para importar a biblioteca e AVFoundation
o AzureCommunicationCalling
. AVFoundation é usado para capturar permissão de áudio do código.
import AzureCommunicationCalling
import AVFoundation
Modelo de objeto
As classes e interfaces a seguir lidam com alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure para iOS.
Nome | Descrição |
---|---|
CallClient |
O CallClient é o principal ponto de entrada para o SDK de chamada. |
CallAgent |
O CallAgent é usado para iniciar e gerenciar chamadas. |
CommunicationTokenCredential |
O CommunicationTokenCredential é usado como a credencial de token para instanciar o CallAgent . |
CommunicationIdentifier |
O CommunicationIdentifier é usado para representar a identidade do usuário, que pode ser uma das seguintes opções: CommunicationUserIdentifier , PhoneNumberIdentifier ou CallingApplication . |
Criar o Call Agent
Substitua a implementação do ContentView struct
por alguns controles de interface do usuário simples que permitem que um usuário inicie e termine uma chamada. Adicionamos lógica de negócios a esses controles neste início rápido.
struct ContentView: View {
@State var callee: String = ""
@State var callClient: CallClient?
@State var callAgent: CallAgent?
@State var call: Call?
@State var deviceManager: DeviceManager?
@State var localVideoStream:[LocalVideoStream]?
@State var incomingCall: IncomingCall?
@State var sendingVideo:Bool = false
@State var errorMessage:String = "Unknown"
@State var remoteVideoStreamData:[Int32:RemoteVideoStreamData] = [:]
@State var previewRenderer:VideoStreamRenderer? = nil
@State var previewView:RendererView? = nil
@State var remoteRenderer:VideoStreamRenderer? = nil
@State var remoteViews:[RendererView] = []
@State var remoteParticipant: RemoteParticipant?
@State var remoteVideoSize:String = "Unknown"
@State var isIncomingCall:Bool = false
@State var callObserver:CallObserver?
@State var remoteParticipantObserver:RemoteParticipantObserver?
var body: some View {
NavigationView {
ZStack{
Form {
Section {
TextField("Who would you like to call?", text: $callee)
Button(action: startCall) {
Text("Start Call")
}.disabled(callAgent == nil)
Button(action: endCall) {
Text("End Call")
}.disabled(call == nil)
Button(action: toggleLocalVideo) {
HStack {
Text(sendingVideo ? "Turn Off Video" : "Turn On Video")
}
}
}
}
// Show incoming call banner
if (isIncomingCall) {
HStack() {
VStack {
Text("Incoming call")
.padding(10)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
Button(action: answerIncomingCall) {
HStack {
Text("Answer")
}
.frame(width:80)
.padding(.vertical, 10)
.background(Color(.green))
}
Button(action: declineIncomingCall) {
HStack {
Text("Decline")
}
.frame(width:80)
.padding(.vertical, 10)
.background(Color(.red))
}
}
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(10)
.background(Color.gray)
}
ZStack{
VStack{
ForEach(remoteViews, id:\.self) { renderer in
ZStack{
VStack{
RemoteVideoView(view: renderer)
.frame(width: .infinity, height: .infinity)
.background(Color(.lightGray))
}
}
Button(action: endCall) {
Text("End Call")
}.disabled(call == nil)
Button(action: toggleLocalVideo) {
HStack {
Text(sendingVideo ? "Turn Off Video" : "Turn On Video")
}
}
}
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack{
if(sendingVideo)
{
VStack{
PreviewVideoStream(view: previewView!)
.frame(width: 135, height: 240)
.background(Color(.lightGray))
}
}
}.frame(maxWidth:.infinity, maxHeight:.infinity,alignment: .bottomTrailing)
}
}
.navigationBarTitle("Video Calling Quickstart")
}.onAppear{
// Authenticate the client
// Initialize the CallAgent and access Device Manager
// Ask for permissions
}
}
}
//Functions and Observers
struct PreviewVideoStream: UIViewRepresentable {
let view:RendererView
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
struct RemoteVideoView: UIViewRepresentable {
let view:RendererView
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Autenticar o cliente
Para inicializar uma CallAgent
instância, você precisa de um Token de Acesso de Usuário, que permite que ele faça e receba chamadas. Consulte a documentação do token de acesso do usuário, se você não tiver um token disponível.
Depois de ter um token, adicione o seguinte código ao retorno de onAppear
chamada em ContentView.swift
. Você precisa substituir <USER ACCESS TOKEN>
por um token de acesso de usuário válido para seu recurso:
var userCredential: CommunicationTokenCredential?
do {
userCredential = try CommunicationTokenCredential(token: "<USER ACCESS TOKEN>")
} catch {
print("ERROR: It was not possible to create user credential.")
return
}
Inicializar o CallAgent e acessar o Gerenciador de Dispositivos
Para criar uma instância CallAgent a partir de um CallClient, use o método que retorna de forma assíncrona callClient.createCallAgent
um objeto CallAgent depois de inicializado. O DeviceManager permite enumerar dispositivos locais que podem ser usados em uma chamada para transmitir fluxos de áudio/vídeo. Ele também permite que você solicite permissão de um usuário para acessar microfone / câmera.
self.callClient = CallClient()
self.callClient?.createCallAgent(userCredential: userCredential!) { (agent, error) in
if error != nil {
print("ERROR: It was not possible to create a call agent.")
return
}
else {
self.callAgent = agent
print("Call agent successfully created.")
self.callAgent!.delegate = incomingCallHandler
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")
}
}
}
}
Pedir permissões
Precisamos adicionar o seguinte código ao retorno de onAppear
chamada para pedir permissões para áudio e vídeo.
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
if granted {
AVCaptureDevice.requestAccess(for: .video) { (videoGranted) in
/* NO OPERATION */
}
}
}
Configurar sessão de áudio
Você usa um AVAudioSession
objeto para configurar a sessão de áudio do seu aplicativo. Aqui está um exemplo de como ativar o dispositivo de áudio bluetooth para seu aplicativo:
func configureAudioSession() -> Error? {
// Retrieve the audio session.
let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
// set options to allow bluetooth device
let options: AVAudioSession.CategoryOptions = .allowBluetooth
var configError: Error?
do {
// Set the audio session category.
try audioSession.setCategory(.playAndRecord, options: options)
print("configureAudioSession successfully")
} catch {
print("configureAudioSession failed")
configError = error
}
return configError
}
Exibir vídeo local
Antes de iniciar uma chamada, você pode gerenciar as configurações relacionadas ao vídeo. Neste início rápido, apresentamos a implementação de alternar vídeo local antes ou durante uma chamada.
Primeiro precisamos acessar câmeras locais com deviceManager
. Uma vez que a câmera desejada é selecionada, podemos construir LocalVideoStream
e criar um VideoStreamRenderer
e, em seguida, anexá-lo ao previewView
. Durante a chamada, podemos usar startVideo
ou stopVideo
iniciar ou parar o envio LocalVideoStream
para participantes remotos. Esta função também funciona com o tratamento de chamadas recebidas.
func toggleLocalVideo() {
// toggling video before call starts
if (call == nil)
{
if(!sendingVideo)
{
self.callClient = 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")
}
}
guard let deviceManager = deviceManager else {
return
}
let camera = deviceManager.cameras.first
let scalingMode = ScalingMode.fit
if (self.localVideoStream == nil) {
self.localVideoStream = [LocalVideoStream]()
}
localVideoStream!.append(LocalVideoStream(camera: camera!))
previewRenderer = try! VideoStreamRenderer(localVideoStream: localVideoStream!.first!)
previewView = try! previewRenderer!.createView(withOptions: CreateViewOptions(scalingMode:scalingMode))
self.sendingVideo = true
}
else{
self.sendingVideo = false
self.previewView = nil
self.previewRenderer!.dispose()
self.previewRenderer = nil
}
}
// toggle local video during the call
else{
if (sendingVideo) {
call!.stopVideo(stream: localVideoStream!.first!) { (error) in
if (error != nil) {
print("cannot stop video")
}
else {
self.sendingVideo = false
self.previewView = nil
self.previewRenderer!.dispose()
self.previewRenderer = nil
}
}
}
else {
guard let deviceManager = deviceManager else {
return
}
let camera = deviceManager.cameras.first
let scalingMode = ScalingMode.fit
if (self.localVideoStream == nil) {
self.localVideoStream = [LocalVideoStream]()
}
localVideoStream!.append(LocalVideoStream(camera: camera!))
previewRenderer = try! VideoStreamRenderer(localVideoStream: localVideoStream!.first!)
previewView = try! previewRenderer!.createView(withOptions: CreateViewOptions(scalingMode:scalingMode))
call!.startVideo(stream:(localVideoStream?.first)!) { (error) in
if (error != nil) {
print("cannot start video")
}
else {
self.sendingVideo = true
}
}
}
}
}
Fazer uma chamada de saída
O startCall
método é definido como a ação que é executada quando o botão Iniciar chamada é tocado. Neste início rápido, as chamadas de saída são apenas áudio por padrão. Para iniciar uma chamada com vídeo, precisamos definir VideoOptions
com LocalVideoStream
e passá-lo com startCallOptions
para definir as opções iniciais para a chamada.
func startCall() {
let startCallOptions = StartCallOptions()
if(sendingVideo)
{
if (self.localVideoStream == nil) {
self.localVideoStream = [LocalVideoStream]()
}
let videoOptions = VideoOptions(localVideoStreams: localVideoStream!)
startCallOptions.videoOptions = videoOptions
}
let callees:[CommunicationIdentifier] = [CommunicationUserIdentifier(self.callee)]
self.callAgent?.startCall(participants: callees, options: startCallOptions) { (call, error) in
setCallAndObserver(call: call, error: error)
}
}
CallObserver
e RemoteParticipantObserver
são usados para gerenciar eventos de chamada intermediária e participantes remotos. Colocamos os observadores na setCallAndObserver
função.
func setCallAndObserver(call: Call!, error: Error?) {
if error == nil {
self.call = call
self.callObserver = CallObserver(self)
self.call!.delegate = self.callObserver
self.remoteParticipantObserver = RemoteParticipantObserver(self)
} else {
print("Failed to get call object")
}
}
Atender uma chamada recebida
Para atender uma chamada de entrada, implemente um IncomingCallHandler
para exibir o banner de chamada de entrada para atender ou recusar a chamada. Coloque a seguinte implementação em IncomingCallHandler.swift
.
final class IncomingCallHandler: NSObject, CallAgentDelegate, IncomingCallDelegate {
public var contentView: ContentView?
private var incomingCall: IncomingCall?
private static var instance: IncomingCallHandler?
static func getOrCreateInstance() -> IncomingCallHandler {
if let c = instance {
return c
}
instance = IncomingCallHandler()
return instance!
}
private override init() {}
public func callAgent(_ callAgent: CallAgent, didRecieveIncomingCall incomingCall: IncomingCall) {
self.incomingCall = incomingCall
self.incomingCall?.delegate = self
contentView?.showIncomingCallBanner(self.incomingCall!)
}
public func callAgent(_ callAgent: CallAgent, didUpdateCalls args: CallsUpdatedEventArgs) {
if let removedCall = args.removedCalls.first {
contentView?.callRemoved(removedCall)
self.incomingCall = nil
}
}
}
Precisamos criar uma instância de adicionando o seguinte código ao retorno de IncomingCallHandler
chamada em ContentView.swift
:onAppear
let incomingCallHandler = IncomingCallHandler.getOrCreateInstance()
incomingCallHandler.contentView = self
Defina um delegado para o CallAgent após a criação bem-sucedida do CallAgent:
self.callAgent!.delegate = incomingCallHandler
Uma vez que há uma chamada recebida, as IncomingCallHandler
chamadas a função showIncomingCallBanner
para exibir answer
e decline
botão.
func showIncomingCallBanner(_ incomingCall: IncomingCall?) {
isIncomingCall = true
self.incomingCall = incomingCall
}
As ações anexadas e answer
decline
são implementadas como o código a seguir. A fim de atender a chamada com vídeo, precisamos ativar o vídeo local e definir as opções de AcceptCallOptions
com localVideoStream
.
func answerIncomingCall() {
isIncomingCall = false
let options = AcceptCallOptions()
if (self.incomingCall != nil) {
guard let deviceManager = deviceManager else {
return
}
if (self.localVideoStream == nil) {
self.localVideoStream = [LocalVideoStream]()
}
if(sendingVideo)
{
let camera = deviceManager.cameras.first
localVideoStream!.append(LocalVideoStream(camera: camera!))
let videoOptions = VideoOptions(localVideoStreams: localVideoStream!)
options.videoOptions = videoOptions
}
self.incomingCall!.accept(options: options) { (call, error) in
setCallAndObserver(call: call, error: error)
}
}
}
func declineIncomingCall() {
self.incomingCall!.reject { (error) in }
isIncomingCall = false
}
Fluxos de vídeo de participantes remotos
Podemos criar uma RemoteVideoStreamData
classe para lidar com fluxos de vídeo de renderização de participantes remotos.
public class RemoteVideoStreamData : NSObject, RendererDelegate {
public func videoStreamRenderer(didFailToStart renderer: VideoStreamRenderer) {
owner.errorMessage = "Renderer failed to start"
}
private var owner:ContentView
let stream:RemoteVideoStream
var renderer:VideoStreamRenderer? {
didSet {
if renderer != nil {
renderer!.delegate = self
}
}
}
var views:[RendererView] = []
init(view:ContentView, stream:RemoteVideoStream) {
owner = view
self.stream = stream
}
public func videoStreamRenderer(didRenderFirstFrame renderer: VideoStreamRenderer) {
let size:StreamSize = renderer.size
owner.remoteVideoSize = String(size.width) + " X " + String(size.height)
}
}
Subscrever eventos
Podemos implementar uma CallObserver
classe para se inscrever em uma coleção de eventos a serem notificados quando os valores mudarem durante a chamada.
public class CallObserver: NSObject, CallDelegate, IncomingCallDelegate {
private var owner: ContentView
init(_ view:ContentView) {
owner = view
}
public func call(_ call: Call, didChangeState args: PropertyChangedEventArgs) {
if(call.state == CallState.connected) {
initialCallParticipant()
}
}
// render remote video streams when remote participant changes
public func call(_ call: Call, didUpdateRemoteParticipant args: ParticipantsUpdatedEventArgs) {
for participant in args.addedParticipants {
participant.delegate = owner.remoteParticipantObserver
for stream in participant.videoStreams {
if !owner.remoteVideoStreamData.isEmpty {
return
}
let data:RemoteVideoStreamData = RemoteVideoStreamData(view: owner, stream: stream)
let scalingMode = ScalingMode.fit
data.renderer = try! VideoStreamRenderer(remoteVideoStream: stream)
let view:RendererView = try! data.renderer!.createView(withOptions: CreateViewOptions(scalingMode:scalingMode))
data.views.append(view)
self.owner.remoteViews.append(view)
owner.remoteVideoStreamData[stream.id] = data
}
owner.remoteParticipant = participant
}
}
// Handle remote video streams when the call is connected
public func initialCallParticipant() {
for participant in owner.call!.remoteParticipants {
participant.delegate = owner.remoteParticipantObserver
for stream in participant.videoStreams {
renderRemoteStream(stream)
}
owner.remoteParticipant = participant
}
}
//create render for RemoteVideoStream and attach it to view
public func renderRemoteStream(_ stream: RemoteVideoStream!) {
if !owner.remoteVideoStreamData.isEmpty {
return
}
let data:RemoteVideoStreamData = RemoteVideoStreamData(view: owner, stream: stream)
let scalingMode = ScalingMode.fit
data.renderer = try! VideoStreamRenderer(remoteVideoStream: stream)
let view:RendererView = try! data.renderer!.createView(withOptions: CreateViewOptions(scalingMode:scalingMode))
self.owner.remoteViews.append(view)
owner.remoteVideoStreamData[stream.id] = data
}
}
Gestão remota de participantes
Todos os participantes remotos são representados com o RemoteParticipant
tipo e ficam disponíveis através da remoteParticipants
coleção em uma instância de chamada assim que a chamada se conecta (CallState.connected
).
Nota
Quando um usuário ingressa em uma chamada, ele pode acessar os participantes remotos atuais por meio da RemoteParticipants
coleção. O didUpdateRemoteParticipant
evento não será acionado para esses participantes existentes. Esse evento só será acionado quando um participante remoto entrar ou sair da chamada enquanto o usuário já estiver na chamada.
Podemos implementar uma RemoteParticipantObserver
aula para se inscrever para as atualizações em fluxos de vídeo remotos de participantes remotos.
public class RemoteParticipantObserver : NSObject, RemoteParticipantDelegate {
private var owner:ContentView
init(_ view:ContentView) {
owner = view
}
public func renderRemoteStream(_ stream: RemoteVideoStream!) {
let data:RemoteVideoStreamData = RemoteVideoStreamData(view: owner, stream: stream)
let scalingMode = ScalingMode.fit
data.renderer = try! VideoStreamRenderer(remoteVideoStream: stream)
let view:RendererView = try! data.renderer!.createView(withOptions: CreateViewOptions(scalingMode:scalingMode))
self.owner.remoteViews.append(view)
owner.remoteVideoStreamData[stream.id] = data
}
// render RemoteVideoStream when remote participant turns on the video, dispose the renderer when remote video is off
public func remoteParticipant(_ remoteParticipant: RemoteParticipant, didUpdateVideoStreams args: RemoteVideoStreamsEventArgs) {
for stream in args.addedRemoteVideoStreams {
renderRemoteStream(stream)
}
for stream in args.removedRemoteVideoStreams {
for data in owner.remoteVideoStreamData.values {
data.renderer?.dispose()
}
owner.remoteViews.removeAll()
}
}
}
Executar o código
Você pode criar e executar seu aplicativo no simulador do iOS selecionando Execução de produto > ou usando o atalho de teclado (⌘-R).
Neste guia de início rápido, você aprenderá a iniciar uma chamada de vídeo 1:1 usando o SDK de Chamada dos Serviços de Comunicação do Azure para Windows.
Código de exemplo UWP
Pré-requisitos
Para concluir este tutorial, precisa dos seguintes pré-requisitos:
Uma conta do Azure com uma subscrição ativa. Crie uma conta gratuitamente.
Instale o Visual Studio 2022 com a carga de trabalho de desenvolvimento da Plataforma Universal do Windows.
Um recurso de Serviços de Comunicação implantado. Crie um recurso de Serviços de Comunicação. Você precisa gravar sua cadeia de conexão para este início rápido.
Um Token de Acesso de Usuário para seu Serviço de Comunicação do Azure. Você também pode usar a CLI do Azure e executar o comando com sua cadeia de conexão para criar um usuário e um token de acesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para obter detalhes, consulte Usar a CLI do Azure para criar e gerenciar tokens de acesso.
Configuração
Criação do projeto
No Visual Studio, crie um novo projeto com o modelo Aplicativo em Branco (Universal Windows) para configurar um aplicativo da Plataforma Universal do Windows (UWP) de página única.
Instalar o pacote
Clique com o botão direito do mouse em seu projeto e vá para Manage Nuget Packages
instalar Azure.Communication.Calling.WindowsClient
a versão 1.2.0-beta.1 ou superior. Certifique-se de que a opção Incluir pré-liberado está marcada.
Pedir acesso
Vá para Package.appxmanifest
e clique em Capabilities
.
Verifique Internet (Client & Server)
para obter acesso de entrada e saída à Internet.
Verifique Microphone
para acessar o feed de áudio do microfone.
Verifique WebCam
para acessar a câmera do dispositivo.
Adicione o seguinte código ao seu Package.appxmanifest
clicando com o botão direito do mouse e escolhendo View Code.
<Extensions>
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>RtmMvrUap.dll</Path>
<ActivatableClass ActivatableClassId="VideoN.VideoSchemeHandler" ThreadingModel="both" />
</InProcessServer>
</Extension>
</Extensions>
Configurar a estrutura do aplicativo
Precisamos configurar um layout básico para anexar nossa lógica. Para fazer uma chamada de saída, precisamos de um TextBox
para fornecer o ID de usuário do destinatário. Precisamos também de um Start Call
botão e de um Hang Up
botão.
Também precisamos visualizar o vídeo local e renderizar o vídeo remoto do outro participante. Portanto, precisamos de dois elementos para exibir os fluxos de vídeo.
Abra o MainPage.xaml
do seu projeto e substitua o conteúdo pela seguinte implementação.
<Page
x:Class="CallingQuickstart.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CallingQuickstart"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid x:Name="MainGrid" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200*"/>
<RowDefinition Height="60*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="AppTitleBar" Background="LightSeaGreen">
<!-- Width of the padding columns is set in LayoutMetricsChanged handler. -->
<!-- Using padding columns instead of Margin ensures that the background paints the area under the caption control buttons (for transparent buttons). -->
<TextBlock x:Name="QuickstartTitle" Text="Calling Quickstart sample title bar" Style="{StaticResource CaptionTextBlockStyle}" Padding="4,4,0,0"/>
</Grid>
<TextBox Grid.Row="1" x:Name="CalleeTextBox" PlaceholderText="Who would you like to call?" TextWrapping="Wrap" VerticalAlignment="Center" />
<Grid Grid.Row="2" Background="LightGray">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<MediaPlayerElement x:Name="LocalVideo" HorizontalAlignment="Center" Stretch="UniformToFill" Grid.Column="0" VerticalAlignment="Center" AutoPlay="True" />
<MediaPlayerElement x:Name="RemoteVideo" HorizontalAlignment="Center" Stretch="UniformToFill" Grid.Column="1" VerticalAlignment="Center" AutoPlay="True" />
</Grid>
<StackPanel Grid.Row="3" Orientation="Vertical" Grid.RowSpan="2">
<StackPanel Orientation="Horizontal" Margin="10">
<TextBlock VerticalAlignment="Center">Cameras:</TextBlock>
<ComboBox x:Name="CameraList" HorizontalAlignment="Left" Grid.Column="0" DisplayMemberPath="Name" SelectionChanged="CameraList_SelectionChanged" Margin="10"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button x:Name="CallButton" Content="Start/Join call" Click="CallButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="123"/>
<Button x:Name="HangupButton" Content="Hang up" Click="HangupButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="123"/>
<CheckBox x:Name="MuteLocal" Content="Mute" Margin="10,0,0,0" Click="MuteLocal_Click" Width="74"/>
<CheckBox x:Name="BackgroundBlur" Content="Background blur" Width="142" Margin="10,0,0,0" Click="BackgroundBlur_Click"/>
</StackPanel>
</StackPanel>
<TextBox Grid.Row="4" x:Name="Stats" Text="" TextWrapping="Wrap" VerticalAlignment="Center" Height="30" Margin="0,2,0,0" BorderThickness="2" IsReadOnly="True" Foreground="LightSlateGray" />
</Grid>
</Page>
Abra para App.xaml.cs
(clique com o botão direito do mouse e escolha View Code) e adicione esta linha ao topo:
using CallingQuickstart;
Abra o (clique com o botão direito do MainPage.xaml.cs
mouse e escolha View Code) e substitua o conteúdo pela seguinte implementação:
using Azure.Communication.Calling.WindowsClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using Windows.Media.Core;
using Windows.Networking.PushNotifications;
using Windows.UI;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace CallingQuickstart
{
public sealed partial class MainPage : Page
{
private const string authToken = "<Azure Communication Services auth token>";
private CallClient callClient;
private CallTokenRefreshOptions callTokenRefreshOptions;
private CallAgent callAgent;
private CommunicationCall call = null;
private LocalOutgoingAudioStream micStream;
private LocalOutgoingVideoStream cameraStream;
#region Page initialization
public MainPage()
{
this.InitializeComponent();
// Hide default title bar.
var coreTitleBar = CoreApplication.GetCurrentView().TitleBar;
coreTitleBar.ExtendViewIntoTitleBar = true;
QuickstartTitle.Text = $"{Package.Current.DisplayName} - Ready";
Window.Current.SetTitleBar(AppTitleBar);
CallButton.IsEnabled = true;
HangupButton.IsEnabled = !CallButton.IsEnabled;
MuteLocal.IsChecked = MuteLocal.IsEnabled = !CallButton.IsEnabled;
ApplicationView.PreferredLaunchViewSize = new Windows.Foundation.Size(800, 600);
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
await InitCallAgentAndDeviceManagerAsync();
base.OnNavigatedTo(e);
}
#endregion
private async Task InitCallAgentAndDeviceManagerAsync()
{
// Initialize call agent and Device Manager
}
private async void Agent_OnIncomingCallAsync(object sender, IncomingCall incomingCall)
{
// Accept an incoming call
}
private async void CallButton_Click(object sender, RoutedEventArgs e)
{
// Start a call with video
}
private async void HangupButton_Click(object sender, RoutedEventArgs e)
{
// End the current call
}
private async void Call_OnStateChangedAsync(object sender, PropertyChangedEventArgs args)
{
var call = sender as CommunicationCall;
if (call != null)
{
var state = call.State;
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
QuickstartTitle.Text = $"{Package.Current.DisplayName} - {state.ToString()}";
Window.Current.SetTitleBar(AppTitleBar);
HangupButton.IsEnabled = state == CallState.Connected || state == CallState.Ringing;
CallButton.IsEnabled = !HangupButton.IsEnabled;
MuteLocal.IsEnabled = !CallButton.IsEnabled;
});
switch (state)
{
case CallState.Connected:
{
break;
}
case CallState.Disconnected:
{
break;
}
default: break;
}
}
}
private async void CameraList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Handle camera selection
}
}
}
Modelo de objeto
As seguintes classes e interfaces lidam com alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure:
Nome | Descrição |
---|---|
CallClient |
O CallClient é o principal ponto de entrada para a biblioteca de cliente de chamada. |
CallAgent |
O CallAgent é usado para iniciar e participar de chamadas. |
CommunicationCall |
O CommunicationCall é usado para gerenciar chamadas feitas ou associadas. |
CallTokenCredential |
O CallTokenCredential é usado como a credencial de token para instanciar o CallAgent . |
CommunicationUserIdentifier |
O CommunicationUserIdentifier é usado para representar a identidade do usuário, que pode ser uma das seguintes opções: CommunicationUserIdentifier , PhoneNumberIdentifier ou CallingApplication . |
Autenticar o cliente
Para inicializar um CallAgent
, você precisa de um Token de Acesso de Usuário. Geralmente esse token é gerado a partir de um serviço com autenticação específica para o aplicativo. Para obter mais informações sobre tokens de acesso de usuário, consulte o guia Tokens de acesso de usuário.
Para o início rápido, substitua <AUTHENTICATION_TOKEN>
por um token de acesso de usuário gerado para seu recurso do Serviço de Comunicação do Azure.
Depois de ter um token, inicialize uma CallAgent
instância com ele, o que nos permite fazer e receber chamadas. Para acessar as câmeras no dispositivo, também precisamos obter a instância do Gerenciador de Dispositivos.
Adicione o seguinte código à InitCallAgentAndDeviceManagerAsync
função.
this.callClient = new CallClient(new CallClientOptions() {
Diagnostics = new CallDiagnosticsOptions() {
AppName = "CallingQuickstart",
AppVersion="1.0",
Tags = new[] { "Calling", "ACS", "Windows" }
}
});
// Set up local video stream using the first camera enumerated
var deviceManager = await this.callClient.GetDeviceManagerAsync();
var camera = deviceManager?.Cameras?.FirstOrDefault();
var mic = deviceManager?.Microphones?.FirstOrDefault();
micStream = new LocalOutgoingAudioStream();
CameraList.ItemsSource = deviceManager.Cameras.ToList();
if (camera != null)
{
CameraList.SelectedIndex = 0;
}
callTokenRefreshOptions = new CallTokenRefreshOptions(false);
callTokenRefreshOptions.TokenRefreshRequested += OnTokenRefreshRequestedAsync;
var tokenCredential = new CallTokenCredential(authToken, callTokenRefreshOptions);
var callAgentOptions = new CallAgentOptions()
{
DisplayName = "Contoso",
//https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes/blob/master/all/all.csv
EmergencyCallOptions = new EmergencyCallOptions() { CountryCode = "840" }
};
try
{
this.callAgent = await this.callClient.CreateCallAgentAsync(tokenCredential, callAgentOptions);
//await this.callAgent.RegisterForPushNotificationAsync(await this.RegisterWNS());
this.callAgent.CallsUpdated += OnCallsUpdatedAsync;
this.callAgent.IncomingCallReceived += OnIncomingCallAsync;
}
catch(Exception ex)
{
if (ex.HResult == -2147024809)
{
// E_INVALIDARG
// Handle possible invalid token
}
}
Iniciar uma chamada com vídeo
Adicione a implementação ao CallButton_Click
para iniciar uma chamada com vídeo. Precisamos enumerar as câmeras com a instância do gerenciador de dispositivos e construir LocalOutgoingVideoStream
. Precisamos definir o VideoOptions
com LocalVideoStream
e passá-lo com startCallOptions
para definir as opções iniciais para a chamada. Ao anexar LocalOutgoingVideoStream
a um MediaElement
, podemos ver a visualização do vídeo local.
var callString = CalleeTextBox.Text.Trim();
if (!string.IsNullOrEmpty(callString))
{
if (callString.StartsWith("8:")) // 1:1 Azure Communication Services call
{
call = await StartAcsCallAsync(callString);
}
else if (callString.StartsWith("+")) // 1:1 phone call
{
call = await StartPhoneCallAsync(callString, "+12133947338");
}
else if (Guid.TryParse(callString, out Guid groupId))// Join group call by group guid
{
call = await JoinGroupCallByIdAsync(groupId);
}
else if (Uri.TryCreate(callString, UriKind.Absolute, out Uri teamsMeetinglink)) //Teams meeting link
{
call = await JoinTeamsMeetingByLinkAsync(teamsMeetinglink);
}
}
if (call != null)
{
call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
call.StateChanged += OnStateChangedAsync;
}
Adicione os métodos para iniciar ou ingressar nos diferentes tipos de chamada (chamada 1:1 dos Serviços de Comunicação do Azure, chamada telefônica 1:1, chamada do Grupo dos Serviços de Comunicação do Azure, participação na reunião do Teams, etc.).
private async Task<CommunicationCall> StartAcsCallAsync(string acsCallee)
{
var options = await GetStartCallOptionsAsynnc();
var call = await this.callAgent.StartCallAsync( new [] { new UserCallIdentifier(acsCallee) }, options);
return call;
}
private async Task<CommunicationCall> StartPhoneCallAsync(string acsCallee, string alternateCallerId)
{
var options = await GetStartCallOptionsAsynnc();
options.AlternateCallerId = new PhoneNumberCallIdentifier(alternateCallerId);
var call = await this.callAgent.StartCallAsync( new [] { new PhoneNumberCallIdentifier(acsCallee) }, options);
return call;
}
private async Task<CommunicationCall> JoinGroupCallByIdAsync(Guid groupId)
{
var joinCallOptions = await GetJoinCallOptionsAsync();
var groupCallLocator = new GroupCallLocator(groupId);
var call = await this.callAgent.JoinAsync(groupCallLocator, joinCallOptions);
return call;
}
private async Task<CommunicationCall> JoinTeamsMeetingByLinkAsync(Uri teamsCallLink)
{
var joinCallOptions = await GetJoinCallOptionsAsync();
var teamsMeetingLinkLocator = new TeamsMeetingLinkLocator(teamsCallLink.AbsoluteUri);
var call = await callAgent.JoinAsync(teamsMeetingLinkLocator, joinCallOptions);
return call;
}
private async Task<StartCallOptions> GetStartCallOptionsAsynnc()
{
return new StartCallOptions() {
OutgoingAudioOptions = new OutgoingAudioOptions() { IsOutgoingAudioMuted = true, OutgoingAudioStream = micStream },
OutgoingVideoOptions = new OutgoingVideoOptions() { OutgoingVideoStreams = new OutgoingVideoStream[] { cameraStream } }
};
}
private async Task<JoinCallOptions> GetJoinCallOptionsAsync()
{
return new JoinCallOptions() {
OutgoingAudioOptions = new OutgoingAudioOptions() { IsOutgoingAudioMuted = true },
OutgoingVideoOptions = new OutgoingVideoOptions() { OutgoingVideoStreams = new OutgoingVideoStream[] { cameraStream } }
};
}
Adicione o código para criar o LocalVideoStream dependendo da câmera selecionada no CameraList_SelectionChanged
método.
var selectedCamerea = CameraList.SelectedItem as VideoDeviceDetails;
cameraStream = new LocalOutgoingVideoStream(selectedCamerea);
var localUri = await cameraStream.StartPreviewAsync();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
LocalVideo.Source = MediaSource.CreateFromUri(localUri);
});
if (call != null)
{
await call?.StartVideoAsync(cameraStream);
}
Aceitar uma chamada recebida
Adicione a implementação ao OnIncomingCallAsync
para atender uma chamada recebida com vídeo, passe o LocalVideoStream
para acceptCallOptions
.
var incomingCall = args.IncomingCall;
var acceptCallOptions = new AcceptCallOptions() {
IncomingVideoOptions = new IncomingVideoOptions()
{
IncomingVideoStreamKind = VideoStreamKind.RemoteIncoming
}
};
_ = await incomingCall.AcceptAsync(acceptCallOptions);
Participantes remotos e fluxos de vídeo remotos
Todos os participantes remotos estão disponíveis através da RemoteParticipants
coleta em uma instância de chamada. Uma vez que a chamada se conecta (CallState.Connected
), podemos acessar os participantes remotos da chamada e lidar com os fluxos de vídeo remotos.
Nota
Quando um usuário ingressa em uma chamada, ele pode acessar os participantes remotos atuais por meio da RemoteParticipants
coleção. O RemoteParticipantsUpdated
evento não será acionado para esses participantes existentes. Esse evento só será acionado quando um participante remoto entrar ou sair da chamada enquanto o usuário já estiver na chamada.
private async void Call_OnVideoStreamsUpdatedAsync(object sender, RemoteVideoStreamsEventArgs args)
{
foreach (var remoteVideoStream in args.AddedRemoteVideoStreams)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
{
RemoteVideo.Source = await remoteVideoStream.Start();
});
}
foreach (var remoteVideoStream in args.RemovedRemoteVideoStreams)
{
remoteVideoStream.Stop();
}
}
private async void Agent_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 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;
}
}
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);
});
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;
}
}
Renderizar vídeos remotos
Para cada fluxo de vídeo remoto, anexe-o MediaElement
ao .
private async Task AddVideoStreamsAsync(IReadOnlyList<RemoteVideoStream> remoteVideoStreams)
{
foreach (var remoteVideoStream in remoteVideoStreams)
{
var remoteUri = await remoteVideoStream.Start();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
RemoteVideo.Source = remoteUri;
RemoteVideo.Play();
});
}
}
Atualização do estado da chamada
Precisamos limpar os renderizadores de vídeo assim que a chamada for desconectada e lidar com o caso quando os participantes remotos entrarem inicialmente na chamada.
private async void Call_OnStateChanged(object sender, PropertyChangedEventArgs args)
{
switch (((Call)sender).State)
{
case CallState.Disconnected:
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
LocalVideo.Source = null;
RemoteVideo.Source = null;
});
break;
case CallState.Connected:
foreach (var remoteParticipant in call.RemoteParticipants)
{
String remoteParticipantMRI = remoteParticipant.Identifier.ToString();
remoteParticipantDictionary.TryAdd(remoteParticipantMRI, remoteParticipant);
await AddVideoStreams(remoteParticipant.VideoStreams);
remoteParticipant.OnVideoStreamsUpdated += Call_OnVideoStreamsUpdated;
}
break;
default:
break;
}
}
Terminar uma chamada
Termine a chamada atual quando o Hang Up
botão for clicado. Adicione a implementação ao HangupButton_Click para encerrar uma chamada com o callAgent que criamos e destrua os manipuladores de eventos de atualização e estado de chamada do participante.
var call = this.callAgent?.Calls?.FirstOrDefault();
if (call != null)
{
try
{
await call.HangUpAsync(new HangUpOptions() { ForEveryone = true });
}
catch(Exception ex)
{
}
}
Executar o código
Você pode criar e executar o código no Visual Studio. Para plataformas de solução, suportamos ARM64
e x86
x64
.
Você pode fazer uma chamada de vídeo de saída fornecendo um ID de usuário no campo de texto e clicando no Start Call
botão.
Nota: A chamada 8:echo123
interrompe o fluxo de vídeo porque o echo bot não suporta streaming de vídeo.
Para obter mais informações sobre IDs de usuário (identidade), consulte o guia de Tokens de Acesso de Usuário.
Código de exemplo do WinUI 3
Pré-requisitos
Para concluir este tutorial, precisa dos seguintes pré-requisitos:
Uma conta do Azure com uma subscrição ativa. Crie uma conta gratuitamente.
Instale o Visual Studio 2022 e o Windows App SDK versão 1.2 preview 2.
Compreensão básica de como criar um aplicativo WinUI 3. Criar seu primeiro projeto WinUI 3 (Windows App SDK) é um bom recurso para começar.
Um recurso de Serviços de Comunicação implantado. Crie um recurso de Serviços de Comunicação. Você precisa gravar sua cadeia de conexão para este início rápido.
Um Token de Acesso de Usuário para seu Serviço de Comunicação do Azure. Você também pode usar a CLI do Azure e executar o comando com sua cadeia de conexão para criar um usuário e um token de acesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para obter detalhes, consulte Usar a CLI do Azure para criar e gerenciar tokens de acesso.
Configuração
Criação do projeto
No Visual Studio, crie um novo projeto com o modelo Aplicativo em branco, empacotado (WinUI 3 na área de trabalho) para configurar um aplicativo WinUI 3 de página única.
Instalar o pacote
Clique com o botão direito do mouse em seu projeto e vá para instalar Azure.Communication.Calling.WindowsClient
a Manage Nuget Packages
versão 1.0.0 ou superior. Certifique-se de que a opção Incluir pré-liberado está marcada.
Pedir acesso
Adicione o seguinte código ao seu app.manifest
:
<file name="RtmMvrMf.dll">
<activatableClass name="VideoN.VideoSchemeHandler" threadingModel="both" xmlns="urn:schemas-microsoft-com:winrt.v1" />
</file>
Configurar a estrutura do aplicativo
Precisamos configurar um layout básico para anexar nossa lógica. Para fazer uma chamada de saída, precisamos de um TextBox
para fornecer o ID de usuário do destinatário. Precisamos também de um Start Call
botão e de um Hang Up
botão.
Também precisamos visualizar o vídeo local e renderizar o vídeo remoto do outro participante. Portanto, precisamos de dois elementos para exibir os fluxos de vídeo.
Abra o MainWindow.xaml
do seu projeto e substitua o conteúdo pela seguinte implementação.
<Page
x:Class="CallingQuickstart.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CallingQuickstart"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="MainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200*"/>
<RowDefinition Height="60*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="AppTitleBar" Background="LightSeaGreen">
<!-- Width of the padding columns is set in LayoutMetricsChanged handler. -->
<!-- Using padding columns instead of Margin ensures that the background paints the area under the caption control buttons (for transparent buttons). -->
<TextBlock x:Name="QuickstartTitle" Text="Calling Quickstart sample title bar" Style="{StaticResource CaptionTextBlockStyle}" Padding="4,4,0,0"/>
</Grid>
<TextBox Grid.Row="1" x:Name="CalleeTextBox" PlaceholderText="Who would you like to call?" TextWrapping="Wrap" VerticalAlignment="Center" />
<Grid Grid.Row="2" Background="LightGray">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<MediaPlayerElement x:Name="LocalVideo" HorizontalAlignment="Center" Stretch="UniformToFill" Grid.Column="0" VerticalAlignment="Center" AutoPlay="True" />
<MediaPlayerElement x:Name="RemoteVideo" HorizontalAlignment="Center" Stretch="UniformToFill" Grid.Column="1" VerticalAlignment="Center" AutoPlay="True" />
</Grid>
<StackPanel Grid.Row="3" Orientation="Vertical" Grid.RowSpan="2">
<StackPanel Orientation="Horizontal" Margin="10">
<TextBlock VerticalAlignment="Center">Cameras:</TextBlock>
<ComboBox x:Name="CameraList" HorizontalAlignment="Left" Grid.Column="0" DisplayMemberPath="Name" SelectionChanged="CameraList_SelectionChanged" Margin="10"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button x:Name="CallButton" Content="Start/Join call" Click="CallButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="123"/>
<Button x:Name="HangupButton" Content="Hang up" Click="HangupButton_Click" VerticalAlignment="Center" Margin="10,0,0,0" Height="40" Width="123"/>
<CheckBox x:Name="MuteLocal" Content="Mute" Margin="10,0,0,0" Click="MuteLocal_Click" Width="74"/>
<CheckBox x:Name="BackgroundBlur" Content="Background blur" Width="142" Margin="10,0,0,0" Click="BackgroundBlur_Click"/>
</StackPanel>
</StackPanel>
<TextBox Grid.Row="4" x:Name="Stats" Text="" TextWrapping="Wrap" VerticalAlignment="Center" Height="30" Margin="0,2,0,0" BorderThickness="2" IsReadOnly="True" Foreground="LightSlateGray" />
</Grid>
</Page>
Abra para App.xaml.cs
(clique com o botão direito do mouse e escolha View Code) e adicione esta linha ao topo:
using CallingQuickstart;
Abra o (clique com o botão direito do MainWindow.xaml.cs
mouse e escolha View Code) e substitua o conteúdo pela seguinte implementação:
using Azure.Communication.Calling.WindowsClient;
using Azure.WinRT.Communication;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Media.Core;
namespace CallingQuickstart
{
public sealed partial class MainWindow : Window
{
CallAgent callAgent;
Call call;
DeviceManager deviceManager;
Dictionary<string, RemoteParticipant> remoteParticipantDictionary = new Dictionary<string, RemoteParticipant>();
public MainWindow()
{
this.InitializeComponent();
Task.Run(() => this.InitCallAgentAndDeviceManagerAsync()).Wait();
}
private async Task InitCallAgentAndDeviceManagerAsync()
{
// Initialize call agent and Device Manager
}
private async void Agent_OnIncomingCallAsync(object sender, IncomingCall incomingCall)
{
// Accept an incoming call
}
private async void CallButton_Click(object sender, RoutedEventArgs e)
{
// Start a call with video
}
private async void HangupButton_Click(object sender, RoutedEventArgs e)
{
// End the current call
}
private async void Call_OnStateChangedAsync(object sender, PropertyChangedEventArgs args)
{
var state = (sender as Call)?.State;
this.DispatcherQueue.TryEnqueue(() => {
State.Text = state.ToString();
});
}
}
}
Modelo de objeto
As seguintes classes e interfaces lidam com alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure:
Nome | Descrição |
---|---|
CallClient |
O CallClient é o principal ponto de entrada para a biblioteca de cliente de chamada. |
CallAgent |
O CallAgent é usado para iniciar e participar de chamadas. |
CommunicationCall |
O CommunicationCall é usado para gerenciar chamadas feitas ou associadas. |
CallTokenCredential |
O CallTokenCredential é usado como a credencial de token para instanciar o CallAgent . |
CommunicationUserIdentifier |
O CommunicationUserIdentifier é usado para representar a identidade do usuário, que pode ser uma das seguintes opções: CommunicationUserIdentifier , PhoneNumberIdentifier ou CallingApplication . |
Autenticar o cliente
Para inicializar um CallAgent
, você precisa de um Token de Acesso de Usuário. Geralmente esse token é gerado a partir de um serviço com autenticação específica para o aplicativo. Para obter mais informações sobre tokens de acesso de usuário, consulte o guia Tokens de acesso de usuário.
Para o início rápido, substitua <AUTHENTICATION_TOKEN>
por um token de acesso de usuário gerado para seu recurso do Serviço de Comunicação do Azure.
Depois de ter um token, inicialize uma CallAgent
instância com ele, o que nos permite fazer e receber chamadas. Para acessar as câmeras no dispositivo, também precisamos obter a instância do Gerenciador de Dispositivos.
Adicione o seguinte código à InitCallAgentAndDeviceManagerAsync
função.
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.OnCallsUpdated += Agent_OnCallsUpdatedAsync;
this.callAgent.OnIncomingCall += Agent_OnIncomingCallAsync;
Iniciar uma chamada com vídeo
Adicione a implementação ao CallButton_Click
para iniciar uma chamada com vídeo. Precisamos enumerar as câmeras com a instância do gerenciador de dispositivos e construir LocalVideoStream
. Precisamos definir o VideoOptions
com LocalVideoStream
e passá-lo com startCallOptions
para definir as opções iniciais para a chamada. Ao anexar LocalVideoStream
a um MediaPlayerElement
, podemos ver a visualização do vídeo local.
var startCallOptions = new StartCallOptions();
if (this.deviceManager.Cameras?.Count > 0)
{
var videoDeviceInfo = this.deviceManager.Cameras?.FirstOrDefault();
if (videoDeviceInfo != null)
{
var selectedCamerea = CameraList.SelectedItem as VideoDeviceDetails;
cameraStream = new LocalOutgoingVideoStream(selectedCamerea);
var localUri = await cameraStream.StartPreviewAsync();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
LocalVideo.Source = MediaSource.CreateFromUri(localUri);
});
startCallOptions.VideoOptions = new OutgoingVideoOptions(new[] { cameraStream });
}
}
var callees = new ICommunicationIdentifier[1]
{
new CommunicationUserIdentifier(CalleeTextBox.Text.Trim())
};
this.call = await this.callAgent.StartCallAsync(callees, startCallOptions);
this.call.OnRemoteParticipantsUpdated += Call_OnRemoteParticipantsUpdatedAsync;
this.call.OnStateChanged += Call_OnStateChangedAsync;
Aceitar uma chamada recebida
Adicione a implementação ao Agent_OnIncomingCallAsync
para atender uma chamada recebida com vídeo, passe o LocalVideoStream
para acceptCallOptions
.
var acceptCallOptions = new AcceptCallOptions();
if (this.deviceManager.Cameras?.Count > 0)
{
var videoDeviceInfo = this.deviceManager.Cameras?.FirstOrDefault();
if (videoDeviceInfo != null)
{
var selectedCamerea = CameraList.SelectedItem as VideoDeviceDetails;
cameraStream = new LocalOutgoingVideoStream(selectedCamerea);
var localUri = await cameraStream.StartPreviewAsync();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
LocalVideo.Source = MediaSource.CreateFromUri(localUri);
});
acceptCallOptions.VideoOptions = new OutgoingVideoOptions(new[] { localVideoStream });
}
}
call = await incomingCall.AcceptAsync(acceptCallOptions);
Participantes remotos e fluxos de vídeo remotos
Todos os participantes remotos estão disponíveis através da RemoteParticipants
coleta em uma instância de chamada. Uma vez que a chamada está conectada, podemos acessar os participantes remotos da chamada e lidar com os fluxos de vídeo remotos.
Nota
Quando um usuário ingressa em uma chamada, ele pode acessar os participantes remotos atuais por meio da RemoteParticipants
coleção. O OnRemoteParticipantsUpdated
evento não será acionado para esses participantes existentes. Esse evento só será acionado quando um participante remoto entrar ou sair da chamada enquanto o usuário já estiver na chamada.
private async void Call_OnVideoStreamsUpdatedAsync(object sender, RemoteVideoStreamsEventArgs args)
{
foreach (var remoteVideoStream in args.AddedRemoteVideoStreams)
{
this.DispatcherQueue.TryEnqueue(async () => {
RemoteVideo.Source = MediaSource.CreateFromUri(await remoteVideoStream.Start());
RemoteVideo.MediaPlayer.Play();
});
}
foreach (var remoteVideoStream in args.RemovedRemoteVideoStreams)
{
remoteVideoStream.Stop();
}
}
private async void Agent_OnCallsUpdatedAsync(object sender, CallsUpdatedEventArgs args)
{
foreach (var call in args.AddedCalls)
{
foreach (var remoteParticipant in call.RemoteParticipants)
{
var remoteParticipantMRI = remoteParticipant.Identifier.ToString();
this.remoteParticipantDictionary.TryAdd(remoteParticipantMRI, remoteParticipant);
await AddVideoStreamsAsync(remoteParticipant.VideoStreams);
remoteParticipant.OnVideoStreamsUpdated += Call_OnVideoStreamsUpdatedAsync;
}
}
}
private async void Call_OnRemoteParticipantsUpdatedAsync(object sender, ParticipantsUpdatedEventArgs args)
{
foreach (var remoteParticipant in args.AddedParticipants)
{
String remoteParticipantMRI = remoteParticipant.Identifier.ToString();
this.remoteParticipantDictionary.TryAdd(remoteParticipantMRI, remoteParticipant);
await AddVideoStreamsAsync(remoteParticipant.VideoStreams);
remoteParticipant.OnVideoStreamsUpdated += Call_OnVideoStreamsUpdatedAsync;
}
foreach (var remoteParticipant in args.RemovedParticipants)
{
String remoteParticipantMRI = remoteParticipant.Identifier.ToString();
this.remoteParticipantDictionary.Remove(remoteParticipantMRI);
}
}
Renderizar vídeos remotos
Para cada fluxo de vídeo remoto, anexe-o MediaPlayerElement
ao .
private async Task AddVideoStreamsAsync(IReadOnlyList<RemoteVideoStream> remoteVideoStreams)
{
foreach (var remoteVideoStream in remoteVideoStreams)
{
var remoteUri = await remoteVideoStream.Start();
this.DispatcherQueue.TryEnqueue(() => {
RemoteVideo.Source = MediaSource.CreateFromUri(remoteUri);
RemoteVideo.MediaPlayer.Play();
});
}
}
Atualização do estado da chamada
Precisamos limpar os renderizadores de vídeo assim que a chamada for desconectada e lidar com o caso quando os participantes remotos entrarem inicialmente na chamada.
private async void Call_OnStateChanged(object sender, PropertyChangedEventArgs args)
{
switch (((Call)sender).State)
{
case CallState.Disconnected:
this.DispatcherQueue.TryEnqueue(() => { =>
{
LocalVideo.Source = null;
RemoteVideo.Source = null;
});
break;
case CallState.Connected:
foreach (var remoteParticipant in call.RemoteParticipants)
{
String remoteParticipantMRI = remoteParticipant.Identifier.ToString();
remoteParticipantDictionary.TryAdd(remoteParticipantMRI, remoteParticipant);
await AddVideoStreams(remoteParticipant.VideoStreams);
remoteParticipant.OnVideoStreamsUpdated += Call_OnVideoStreamsUpdated;
}
break;
default:
break;
}
}
Terminar uma chamada
Termine a chamada atual quando o Hang Up
botão for clicado. Adicione a implementação ao HangupButton_Click para encerrar uma chamada com o callAgent que criamos e destrua os manipuladores de eventos de atualização e estado de chamada do participante.
this.call.OnRemoteParticipantsUpdated -= Call_OnRemoteParticipantsUpdatedAsync;
this.call.OnStateChanged -= Call_OnStateChangedAsync;
await this.call.HangUpAsync(new HangUpOptions());
Executar o código
Você pode criar e executar o código no Visual Studio. Para plataformas de solução, suportamos ARM64
e x86
x64
.
Você pode fazer uma chamada de vídeo de saída fornecendo um ID de usuário no campo de texto e clicando no Start Call
botão.
Nota: A chamada 8:echo123
interrompe o fluxo de vídeo porque o echo bot não suporta streaming de vídeo.
Para obter mais informações sobre IDs de usuário (identidade), consulte o guia de Tokens de Acesso de Usuário.
Neste guia de início rápido, você aprenderá a iniciar uma chamada usando o SDK de Chamada dos Serviços de Comunicação do Azure para Unity. Para receber e renderizar quadros de vídeo na plataforma Unity, consulte o Guia de início rápido do Raw Media Access.
Pode transferir a aplicação de exemplo do GitHub.
Pré-requisitos
Para concluir este tutorial, precisa dos seguintes pré-requisitos:
Uma conta do Azure com uma subscrição ativa. Crie uma conta gratuitamente.
Instale o Unity Hub e o Unity Editor com a carga de trabalho de desenvolvimento da Plataforma Universal do Windows.
Um recurso de Serviços de Comunicação implantado. Crie um recurso de Serviços de Comunicação. Você precisa gravar sua cadeia de conexão para este início rápido.
Um Token de Acesso de Usuário para seu Serviço de Comunicação do Azure. Você também pode usar a CLI do Azure e executar o comando com sua cadeia de conexão para criar um usuário e um token de acesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para obter detalhes, consulte Usar a CLI do Azure para criar e gerenciar tokens de acesso.
Configuração
Criação do projeto
No Unity Hub, crie um novo projeto com o modelo 2D Core para configurar o projeto unity.
Instalar o pacote
Há duas maneiras de instalar o SDK de Chamada de Comunicação do Azure para Unity.
Baixe o SDK do feed npm público e importe-o no gerenciador de pacotes do Unity Editor, encontrado na guia Windows.
Transfira a Mixed Reality Feature Tool da Microsoft e instale-a através do gestor de ferramentas de realidade mista.
Configurar a estrutura do aplicativo
Precisamos configurar um layout básico para anexar nossa lógica. Para fazer uma chamada de saída, precisamos de um TextBox
para fornecer o ID de usuário do destinatário. Precisamos também de um Start/Join call
botão e de um Hang up
botão.
Crie uma nova cena chamada Main
em seu projeto.
Abra o Main.unity
arquivo e substitua o conteúdo pela seguinte implementação:
Código Main.Unity
Main.unity
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 0
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 500
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &247756367
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 247756370}
- component: {fileID: 247756369}
- component: {fileID: 247756368}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &247756368
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 247756367}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &247756369
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 247756367}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &247756370
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 247756367}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &293984669
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 293984671}
- component: {fileID: 293984670}
m_Layer: 0
m_Name: AppManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &293984670
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 293984669}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c7d18b32fdb6b14e857ebb6d9627958, type: 3}
m_Name:
m_EditorClassIdentifier:
callStatus: {fileID: 1529611528}
videoPlayer: {fileID: 0}
--- !u!4 &293984671
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 293984669}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &438770860
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 438770861}
- component: {fileID: 438770863}
- component: {fileID: 438770862}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &438770861
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 438770860}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1732033234}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &438770862
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 438770860}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Start Call
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &438770863
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 438770860}
m_CullTransparentMesh: 1
--- !u!1 &519420028
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 519420032}
- component: {fileID: 519420031}
- component: {fileID: 519420029}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &519420029
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 1
--- !u!20 &519420031
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 0
m_HDR: 1
m_AllowMSAA: 0
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 0
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &519420032
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &857336305
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 857336306}
- component: {fileID: 857336309}
- component: {fileID: 857336308}
- component: {fileID: 857336307}
m_Layer: 5
m_Name: Placeholder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &857336306
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1787936407}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &857336307
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 1
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &857336308
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Who Would you like to call?
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 2150773298
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 2
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 1
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &857336309
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_CullTransparentMesh: 1
--- !u!1 &963546686
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 963546687}
- component: {fileID: 963546690}
- component: {fileID: 963546689}
- component: {fileID: 963546688}
m_Layer: 5
m_Name: InputField (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &963546687
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1787936407}
m_Father: {fileID: 1843906927}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.00002861, y: 327}
m_SizeDelta: {x: 1337.7578, y: 71.4853}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &963546688
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2da0c512f12947e489f739169773d7ca, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 963546689}
m_TextViewport: {fileID: 1787936407}
m_TextComponent: {fileID: 1676708954}
m_Placeholder: {fileID: 857336308}
m_VerticalScrollbar: {fileID: 0}
m_VerticalScrollbarEventHandler: {fileID: 0}
m_LayoutGroup: {fileID: 0}
m_ScrollSensitivity: 1
m_ContentType: 0
m_InputType: 0
m_AsteriskChar: 42
m_KeyboardType: 0
m_LineType: 0
m_HideMobileInput: 0
m_HideSoftKeyboard: 0
m_CharacterValidation: 0
m_RegexValue:
m_GlobalPointSize: 14
m_CharacterLimit: 0
m_OnEndEdit:
m_PersistentCalls:
m_Calls: []
m_OnSubmit:
m_PersistentCalls:
m_Calls: []
m_OnSelect:
m_PersistentCalls:
m_Calls: []
m_OnDeselect:
m_PersistentCalls:
m_Calls: []
m_OnTextSelection:
m_PersistentCalls:
m_Calls: []
m_OnEndTextSelection:
m_PersistentCalls:
m_Calls: []
m_OnValueChanged:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 293984670}
m_TargetAssemblyTypeName: CallClientHost, Assembly-CSharp
m_MethodName: set_CalleeIdentity
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
m_OnTouchScreenKeyboardStatusChanged:
m_PersistentCalls:
m_Calls: []
m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_CustomCaretColor: 0
m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412}
m_Text:
m_CaretBlinkRate: 0.85
m_CaretWidth: 1
m_ReadOnly: 0
m_RichText: 1
m_GlobalFontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_OnFocusSelectAll: 1
m_ResetOnDeActivation: 1
m_RestoreOriginalTextOnEscape: 1
m_isRichTextEditingAllowed: 0
m_LineLimit: 0
m_InputValidator: {fileID: 0}
--- !u!114 &963546689
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &963546690
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_CullTransparentMesh: 1
--- !u!1 &1184525248
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1184525249}
- component: {fileID: 1184525251}
- component: {fileID: 1184525250}
m_Layer: 5
m_Name: Status Header
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1184525249
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1184525248}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1843906927}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -23, y: -303}
m_SizeDelta: {x: 159.05, y: 33.5037}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1184525250
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1184525248}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Status
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 2.5243988, z: 10.097656, w: -2.5243645}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1184525251
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1184525248}
m_CullTransparentMesh: 1
--- !u!1 &1332239153
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1332239154}
- component: {fileID: 1332239157}
- component: {fileID: 1332239156}
- component: {fileID: 1332239155}
m_Layer: 5
m_Name: Hang Up Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1332239154
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1917486034}
m_Father: {fileID: 1843906927}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -277, y: -329}
m_SizeDelta: {x: 212.1357, y: 53.698}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1332239155
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1332239156}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 293984670}
m_TargetAssemblyTypeName: AppManager, Assembly-CSharp
m_MethodName: HangupButton_Click
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &1332239156
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1332239157
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_CullTransparentMesh: 1
--- !u!1 &1529611526
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1529611527}
- component: {fileID: 1529611529}
- component: {fileID: 1529611528}
m_Layer: 5
m_Name: Status
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1529611527
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1529611526}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1843906927}
m_RootOrder: 4
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -2.525, y: -344.75}
m_SizeDelta: {x: 200, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1529611528
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1529611526}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Disconnected
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 30
m_fontSizeBase: 30
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: -25.861023, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1529611529
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1529611526}
m_CullTransparentMesh: 1
--- !u!1 &1676708952
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1676708953}
- component: {fileID: 1676708955}
- component: {fileID: 1676708954}
m_Layer: 5
m_Name: Text
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1676708953
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1676708952}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1787936407}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1676708954
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1676708952}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u200B"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 1
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1676708955
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1676708952}
m_CullTransparentMesh: 1
--- !u!1 &1732033233
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1732033234}
- component: {fileID: 1732033237}
- component: {fileID: 1732033236}
- component: {fileID: 1732033235}
m_Layer: 5
m_Name: Start Call Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1732033234
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 438770861}
m_Father: {fileID: 1843906927}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -525.52, y: -329}
m_SizeDelta: {x: 212.1357, y: 53.698}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1732033235
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1732033236}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 293984670}
m_TargetAssemblyTypeName: CallClientHost, Assembly-CSharp
m_MethodName: CallButton_Click
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &1732033236
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1732033237
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_CullTransparentMesh: 1
--- !u!1 &1787936406
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1787936407}
- component: {fileID: 1787936408}
m_Layer: 5
m_Name: Text Area
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1787936407
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1787936406}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 857336306}
- {fileID: 1676708953}
m_Father: {fileID: 963546687}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -0.4999962}
m_SizeDelta: {x: -20, y: -13}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1787936408
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1787936406}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding: {x: -8, y: -5, z: -8, w: -5}
m_Softness: {x: 0, y: 0}
--- !u!1 &1843906923
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1843906927}
- component: {fileID: 1843906926}
- component: {fileID: 1843906925}
- component: {fileID: 1843906924}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1843906924
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &1843906925
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &1843906926
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_AdditionalShaderChannelsFlag: 25
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!224 &1843906927
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 963546687}
- {fileID: 1732033234}
- {fileID: 1332239154}
- {fileID: 1184525249}
- {fileID: 1529611527}
m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!1 &1917486033
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1917486034}
- component: {fileID: 1917486036}
- component: {fileID: 1917486035}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1917486034
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917486033}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1332239154}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1917486035
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917486033}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Hang Up
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1917486036
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917486033}
m_CullTransparentMesh: 1
Crie um script chamado AppManager.cs e vincule-o ao objeto AppManager no Unity Editor. Substitua o conteúdo pela seguinte implementação:
using Azure.Communication.Calling.UnityClient;
using System.Runtime.InteropServices.ComTypes;
using System;
using UnityEngine;
using System.Linq;
using UnityEngine.UI;
using TMPro;
/// <summary>
/// A singleton which hosts an Azure Communication calling client. This calling client
/// is then shared across the application.
/// </summary>
public class AppManager : MonoBehaviour
{
private CallClient callClient;
private CallAgent callAgent;
private DeviceManager deviceManager;
private CommunicationCall call;
private LocalOutgoingAudioStream micStream;
private LocalVideoStream cameraStream;
public string CalleeIdentity { get; set; }
public TMP_Text callStatus;
public static AppManager Instance;
private void Awake()
{
// start of new code
if (Instance != null)
{
Destroy(gameObject);
return;
}
// end of new code
callClient = new CallClient();
Instance = this;
DontDestroyOnLoad(gameObject);
InitCallAgentAndDeviceManagerAsync();
}
public async void CallButton_Click()
{
// Start a call
}
public async void HangupButton_Click()
{
// Hang up a call
}
#region API event handlers
private async void OnIncomingCallAsync(object sender, IncomingCallReceivedEventArgs args)
{
// Handle incoming call event
}
private async void OnStateChangedAsync(object sender, Azure.Communication.Calling.UnityClient.PropertyChangedEventArgs args)
{
// Handle connected and disconnected state change of a call
}
private async void OnRemoteParticipantsUpdatedAsync(object sender, ParticipantsUpdatedEventArgs args)
{
// Handle remote participant arrival or departure events and subscribe to individual participant's VideoStreamStateChanged event
}
private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs e)
{
// Handle incoming or outgoing video stream change events
}
private async void OnIncomingVideoStreamStateChangedAsync(IncomingVideoStream incomingVideoStream)
{
// Handle incoming IncomingVideoStreamStateChanged event and process individual VideoStreamState
}
#endregion
//Used For Updating the UI
private void Update()
{
if (call != null)
{
switch (call.State)
{
case CallState.Connected:
if (callStatus.text != "Connected")
callStatus.text = "Connected";
break;
case CallState.Disconnected:
if (callStatus.text != "Disconnected")
callStatus.text = "Disconnected";
break;
}
}
}
}
No GameObject chamado AppManager, arraste o script recém-criado para seu componente de script. Arraste o objeto de texto Status para o campo de texto Status da Chamada para habilitar as atualizações da interface do usuário do estado da Chamada. O SDK fornece vídeo por meio de URI, mas, atualmente, o player de vídeo do Unity pode não suportar a reprodução de URI.
Modelo de objeto
A tabela a seguir listou as classes e interfaces que manipulam alguns dos principais recursos do SDK de Chamada dos Serviços de Comunicação do Azure:
Nome | Descrição |
---|---|
CallClient |
O CallClient é o principal ponto de entrada para o SDK de chamada. |
CallAgent |
O CallAgent é usado para iniciar e gerenciar chamadas. |
Call |
O CommunicationCall é usado para gerenciar uma chamada em andamento. |
CallTokenCredential |
O CallTokenCredential é usado como a credencial de token para instanciar o CallAgent . |
CallIdentifier |
O CallIdentifier é usado para representar a identidade do usuário, que pode ser uma das seguintes opções: UserCallIdentifier , PhoneNumberCallIdentifier etc. |
Autenticar o cliente
Inicialize uma CallAgent
instância com um Token de Acesso de Usuário que nos permite fazer e receber chamadas e, opcionalmente, obter uma instância do DeviceManager para consultar configurações de dispositivo cliente.
No código, substitua <AUTHENTICATION_TOKEN>
por um Token de Acesso de Usuário. Consulte a documentação do token de acesso do usuário se ainda não tiver um token disponível.
Adicionar InitCallAgentAndDeviceManagerAsync
função, que inicializa o SDK. Este auxiliar pode ser personalizado para atender aos requisitos do seu aplicativo.
private async void InitCallAgentAndDeviceManagerAsync()
{
deviceManager = await callClient.GetDeviceManager();
var tokenCredential = new CallTokenCredential(<AUTHENTICATION_TOKEN>);
var callAgentOptions = new CallAgentOptions()
{
DisplayName = $"{Environment.MachineName}/{Environment.UserName}",
};
callAgent = await callClient.CreateCallAgent(tokenCredential, callAgentOptions);
callAgent.IncomingCallReceived += OnIncomingCallAsync;
}
Iniciar uma chamada com vídeo
Depois que um StartCallOptions
objeto é obtido, CallAgent
pode ser usado para iniciar a chamada dos Serviços de Comunicação do Azure:
public async void CallButton_Click()
{
var startCallOptions = new StartCallOptions();
startCallOptions = new StartCallOptions()
{
OutgoingVideoOptions = new OutgoingVideoOptions() { Streams = new OutgoingVideoStream[] { cameraStream } }
};
var callee = new UserCallIdentifier(CalleeIdentity);
call = await callAgent.StartCallAsync(new CallIdentifier[] { callee }, startCallOptions);
// Set up handler for remote participant updated events, such as VideoStreamStateChanged event
call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
// Set up handler for call StateChanged event
call.StateChanged += OnStateChangedAsync;
}
Manipule o participante remoto e o vídeo de entrada remoto
O vídeo recebido está associado a participantes remotos específicos, portanto, RemoteParticipantsUpdated é o evento-chave para permanecer notificado e obter referências aos participantes que mudam.
private void OnRemoteParticipantsUpdatedAsync(object sender, ParticipantsUpdatedEventArgs args)
{
foreach (var participant in args.RemovedParticipants)
{
foreach (var incomingVideoStream in participant.IncomingVideoStreams)
{
var remoteVideoStream = incomingVideoStream as RemoteVideoStream;
if (remoteVideoStream != null)
{
remoteVideoStream.Stop();
}
}
// Tear down the event handler on the departing participant
participant.VideoStreamStateChanged -= OnVideoStreamStateChanged;
}
foreach (var participant in args.AddedParticipants)
{
// Set up handler for VideoStreamStateChanged of the participant who just joined the call
participant.VideoStreamStateChanged += OnVideoStreamStateChanged;
}
}
Todos os participantes remotos estão disponíveis através da RemoteParticipants
coleta em uma instância de chamada. Uma vez que a chamada está conectada, podemos acessar os participantes remotos da chamada e lidar com os fluxos de vídeo remotos.
private void OnVideoStreamStateChanged(object sender, VideoStreamStateChangedEventArgs e)
{
CallVideoStream callVideoStream = e.Stream;
switch (callVideoStream.Direction)
{
case StreamDirection.Outgoing:
//OnOutgoingVideoStreamStateChanged(callVideoStream as OutgoingVideoStream);
break;
case StreamDirection.Incoming:
OnIncomingVideoStreamStateChangedAsync(callVideoStream as IncomingVideoStream);
break;
}
}
Um fluxo de vídeo transitará por uma sequência de estados internos. VideoStreamState.Available
é o estado preferencial para vincular o fluxo de vídeo ao elemento da interface do usuário para renderizar o fluxo de vídeo, como MediaPlayerElement
, e VideoStreamState.Stopped
normalmente é onde as tarefas de limpeza, como interromper a visualização de vídeo, devem ser feitas.
private async void OnIncomingVideoStreamStateChangedAsync(IncomingVideoStream incomingVideoStream)
{
switch (incomingVideoStream.State)
{
case VideoStreamState.Available:
switch (incomingVideoStream.Kind)
{
case VideoStreamKind.RemoteIncoming:
var remoteVideoStream = incomingVideoStream as RemoteVideoStream;
var uri = await remoteVideoStream.StartAsync();
break;
case VideoStreamKind.RawIncoming:
break;
}
break;
case VideoStreamState.Started:
break;
case VideoStreamState.Stopping:
case VideoStreamState.Stopped:
if (incomingVideoStream.Kind == VideoStreamKind.RemoteIncoming)
{
var remoteVideoStream = incomingVideoStream as RemoteVideoStream;
remoteVideoStream.Stop();
}
break;
case VideoStreamState.NotAvailable:
break;
}
}
Terminar uma chamada
Termine a chamada atual quando o Hang up
botão for clicado. Adicione a implementação ao HangupButton_Click para encerrar uma chamada e interrompa a visualização e os fluxos de vídeo.
public async void HangupButton_Click()
{
if (call != null)
{
try
{
await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
}
catch (Exception ex)
{
}
}
}
Aceitar uma chamada recebida
IncomingCallReceived
o coletor de eventos é configurado no auxiliar InitCallAgentAndDeviceManagerAsync
de bootstrap do SDK.
callAgent.IncomingCallReceived += OnIncomingCallAsync;
O aplicativo tem a oportunidade de configurar como a chamada recebida deve ser aceita, como tipos de fluxo de vídeo e áudio.
private async void OnIncomingCallAsync(object sender, IncomingCallReceivedEventArgs args)
{
var incomingCall = args.IncomingCall;
var acceptCallOptions = new AcceptCallOptions()
{
IncomingVideoOptions = new IncomingVideoOptions()
{
StreamKind = VideoStreamKind.RemoteIncoming
}
};
call = await incomingCall.AcceptAsync(acceptCallOptions);
// Set up handler for remote participant updated events, such as VideoStreamStateChanged event
call.RemoteParticipantsUpdated += OnRemoteParticipantsUpdatedAsync;
// Set up handler for incoming call StateChanged event
call.StateChanged += OnStateChangedAsync;
}
Monitorar e responder ao evento de alteração de estado da chamada
StateChanged
evento no Call
objeto é acionado quando um em andamento chama transações de um estado para outro. O aplicativo oferece a oportunidade de refletir as alterações de estado na interface do usuário ou inserir lógicas de negócios.
private async void OnStateChangedAsync(object sender, Azure.Communication.Calling.UnityClient.PropertyChangedEventArgs args)
{
var call = sender as CommunicationCall;
if (call != null)
{
var state = call.State;
switch (state)
{
case CallState.Connected:
{
await call.StartAudioAsync(micStream);
break;
}
case CallState.Disconnected:
{
call.RemoteParticipantsUpdated -= OnRemoteParticipantsUpdatedAsync;
call.StateChanged -= OnStateChangedAsync;
call.Dispose();
break;
}
default: break;
}
}
}
Executar o código
Você pode criar e executar o código no Unity Editor ou em dispositivos que usam Unity.
Você pode fazer uma chamada de saída fornecendo um ID de usuário no campo de texto e clicando no Start Call/Join
botão. Ligar 8:echo123
conecta você a um bot de eco, esse recurso é ótimo para começar e verificar se seus dispositivos de áudio estão funcionando.
Clean up resources (Limpar recursos)
Se quiser limpar e remover uma assinatura dos Serviços de Comunicação, você pode excluir o recurso ou grupo de recursos. A exclusão do grupo de recursos também exclui quaisquer outros recursos associados a ele. Saiba mais sobre a limpeza de recursos.
Próximos passos
Para obter mais informações, consulte os seguintes artigos:
- Confira nossa amostra de herói chamador
- Introdução à biblioteca da interface do usuário
- Saiba mais sobre os recursos do SDK de chamada
- Saiba mais sobre como as chamadas funcionam