Guida introduttiva: Aggiungere videochiamate 1:1 all'app
Per iniziare a usare Servizi di comunicazione di Azure, usare l'SDK per chiamate di Servizi di comunicazione per aggiungere 1 su 1 videochiamata all'app. Si apprenderà come avviare e rispondere a una videochiamata usando Servizi di comunicazione di Azure Calling SDK per JavaScript.
Codice di esempio
Se si vuole passare direttamente alla fine, è possibile scaricare questa guida introduttiva come esempio in GitHub.
Nota
È possibile accedere alle chiamate in uscita a un utente Servizi di comunicazione di Azure usando la libreria dell'interfaccia utente di Servizi di comunicazione di Azure. La libreria dell'interfaccia utente consente agli sviluppatori di aggiungere un client di chiamata abilitato all'applicazione con solo un paio di righe di codice.
Prerequisiti
Ottenere un account Azure con una sottoscrizione attiva. Creare un account gratuitamente.
È necessario avere Node.js 18. È possibile usare il programma di installazione msi per installarlo.
Creare una risorsa di Servizi di comunicazione attiva. Creare una risorsa di Servizi di comunicazione. È necessario registrare il stringa di connessione per questa guida introduttiva.
Creare un token di accesso utente per creare un'istanza del client di chiamata. Informazioni su come creare e gestire i token di accesso utente. È anche possibile usare l'interfaccia della riga di comando di Azure ed eseguire il comando con il stringa di connessione per creare un utente e un token di accesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Per informazioni dettagliate, vedere Usare l'interfaccia della riga di comando di Azure per creare e gestire i token di accesso.
Configurazione
Creare una nuova applicazione Node.js
Aprire il terminale o la finestra di comando per creare una nuova directory per l'app e passare a tale directory.
mkdir calling-quickstart && cd calling-quickstart
Eseguire npm init -y
per creare un file package.json con le impostazioni predefinite.
npm init -y
Installare il pacchetto
Usare il npm install
comando per installare Servizi di comunicazione di Azure Calling SDK per JavaScript.
npm install @azure/communication-common --save
npm install @azure/communication-calling --save
Configurare il framework dell'app
Questa guida introduttiva usa Webpack per aggregare gli asset dell'applicazione. Eseguire il comando seguente per installare i webpack
pacchetti , webpack-cli
e webpack-dev-server
npm ed elencarli come dipendenze di sviluppo in 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
Ecco il codice:
Creare un index.html
file nella directory radice del progetto. Questo file viene usato per configurare un layout di base che consente all'utente di inserire una videochiamata 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>
Le classi e le interfacce seguenti gestiscono alcune delle principali funzionalità di Servizi di comunicazione di Azure Calling SDK:
Nome | Descrizione |
---|---|
CallClient |
Punto di ingresso principale di Calling SDK. |
AzureCommunicationTokenCredential |
Implementa l'interfaccia CommunicationTokenCredential usata per creare callAgent un'istanza di . |
CallAgent |
Usato per avviare e gestire le chiamate. |
DeviceManager |
Usato per gestire i dispositivi multimediali. |
Call |
Utilizzato per rappresentare una chiamata |
LocalVideoStream |
Usato per creare un flusso video locale per un dispositivo fotocamera nel sistema locale. |
RemoteParticipant |
Utilizzato per rappresentare un partecipante remoto nella chiamata |
RemoteVideoStream |
Utilizzato per rappresentare un flusso video remoto da un partecipante remoto. |
Creare un file nella directory radice del progetto chiamato index.js
per contenere la logica dell'applicazione per questa guida introduttiva. Aggiungere il codice seguente a 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();
});
Creare un file nella directory radice del progetto chiamato styles.css
per contenere lo stile dell'applicazione per questa guida introduttiva. Aggiungere il codice seguente 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); }
}
Aggiungere il codice del server locale webpack
Creare un file nella directory radice del progetto denominato webpack.config.js per contenere la logica del server locale per questa guida introduttiva. Aggiungere il codice seguente a 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'
]
}),
]
};
Eseguire il codice
Usare webpack-dev-server
per compilare ed eseguire l'app. Eseguire il comando seguente per aggregare l'host dell'applicazione in un server Web locale:
npx webpack serve --config webpack.config.js
Aprire il browser e passare a due schede per http://localhost:8080/.You visualizzare la schermata seguente:
Nella prima scheda immettere un token di accesso utente valido e nell'altra scheda immettere un altro token di accesso utente valido diverso.
Fare riferimento alla documentazione del token di accesso utente, se non sono già disponibili token da usare.
In entrambe le schede fare clic sui pulsanti "Inizializza agente di chiamata". Verrà visualizzata la schermata seguente:
Nella prima scheda immettere l'identità utente Servizi di comunicazione di Azure della seconda scheda e fare clic sul pulsante "Avvia chiamata". La prima scheda avvia la chiamata in uscita alla seconda scheda e il pulsante "Accetta chiamata" della seconda scheda diventa abilitato:
Nella seconda scheda fare clic sul pulsante "Accetta chiamata" e la chiamata viene avviata e connetti. Verrà visualizzata la schermata seguente:
Entrambe le schede sono ora correttamente in una videochiamata da 1 a 1. Entrambe le schede possono ascoltare l'audio dell'altro e vedere tra loro il flusso video.
Per iniziare a usare Servizi di comunicazione di Azure, usare la libreria client per chiamate di Servizi di comunicazione per aggiungere videochiamate all'app. Informazioni su come includere videochiamate 1:1 e come creare o partecipare a chiamate di gruppo. Inoltre, è possibile avviare, rispondere e partecipare a una videochiamata usando il Servizi di comunicazione di Azure Calling SDK per Android.
Per iniziare a usare il codice di esempio, è possibile scaricare l'app di esempio.
Prerequisiti
Un account Azure con una sottoscrizione attiva. Creare un account gratuitamente.
Android Studio per creare l'applicazione Android.
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. È necessario registrare il stringa di connessione per questa guida introduttiva.
Un token di accesso utente per il servizio di comunicazione di Azure. È anche possibile usare l'interfaccia della riga di comando di Azure ed eseguire il comando con il stringa di connessione per creare un utente e un token di accesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Per informazioni dettagliate, vedere Usare l'interfaccia della riga di comando di Azure per creare e gestire i token di accesso.
Creare un'app Android con un'attività vuota
In Android Studio selezionare Avvia un nuovo progetto Di Android Studio.
In Telefono e Tablet selezionare il modello di progetto Attività vuota.
Per Minimum SDK (SDK minimo) selezionare API 26: Android 8.0 (Oreo) o versione successiva. Vedere Versioni del supporto sdk.
Installare il pacchetto
Individuare il livello build.gradle
del progetto e aggiungere mavenCentral()
all'elenco dei repository in buildscript
e allprojects
buildscript {
repositories {
...
mavenCentral()
...
}
}
allprojects {
repositories {
...
mavenCentral()
...
}
}
Quindi, a livello build.gradle
di modulo, aggiungere le righe seguenti alle dependencies
sezioni 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'
...
}
Aggiungere autorizzazioni al manifesto dell'applicazione
Per richiedere le autorizzazioni necessarie per effettuare una chiamata, è prima necessario dichiarare le autorizzazioni nel manifesto dell'applicazione (app/src/main/AndroidManifest.xml
). Sostituire il contenuto del file con il codice seguente:
<?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>
Configurare il layout per l'app
È necessario un input di testo per l'ID chiamato o l'ID chiamata di gruppo, un pulsante per posizionare la chiamata e un pulsante aggiuntivo per appendere la chiamata.
Sono necessari anche due pulsanti per attivare e disattivare il video locale. È necessario inserire due contenitori per flussi video locali e remoti. È possibile aggiungere questi pulsanti tramite la finestra di progettazione o modificando il codice XML di layout.
Passare a app/src/main/res/layout/activity_main.xml e sostituire il contenuto del file con il codice seguente:
<?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>
Creare lo scaffolding e i binding per l'attività principale
Dopo aver creato il layout, è possibile aggiungere le associazioni e lo scaffolding di base dell'attività. L'attività gestisce la richiesta di autorizzazioni di runtime, la creazione dell'agente di chiamata e l'inserimento della chiamata quando viene premuto il pulsante.
Il onCreate
metodo viene sottoposto a override per richiamare getAllPermissions
e createAgent
e aggiungere le associazioni per il pulsante di chiamata. Questo evento si verifica una sola volta quando viene creata l'attività. Per altre informazioni su onCreate
, vedere la guida Informazioni sul ciclo di vita delle attività.
Passare a MainActivity.java file e sostituire il contenuto con il codice seguente:
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
}
}
Richiedere autorizzazioni in fase di esecuzione
Per Android 6.0 e versioni successive (livello API 23) e targetSdkVersion
23 o versioni successive, le autorizzazioni vengono concesse in fase di esecuzione anziché quando l'app è installata. Per supportarlo, getAllPermissions
può essere implementato per chiamare ActivityCompat.checkSelfPermission
e ActivityCompat.requestPermissions
per ogni autorizzazione necessaria.
/**
* 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
Quando si progetta l'app, prendere in considerazione quando devono essere richieste queste autorizzazioni. È consigliabile che vengano richieste quando è necessario, non prima. Per altre informazioni, vedere la Guida alle autorizzazioni di Android.
Modello a oggetti
Le classi e le interfacce seguenti gestiscono alcune delle principali funzionalità di Servizi di comunicazione di Azure Calling SDK:
Nome | Descrizione |
---|---|
CallClient |
Punto di ingresso principale di Calling SDK. |
CallAgent |
Usato per avviare e gestire le chiamate. |
CommunicationTokenCredential |
Usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationIdentifier |
Utilizzato come tipo diverso di partecipante che potrebbe far parte di una chiamata. |
Creare un agente dal token di accesso utente
È necessario un token utente per creare un agente di chiamata autenticato. In genere, questo token viene generato da un servizio con autenticazione specifica per l'applicazione. Per altre informazioni sui token di accesso utente, vedere Token di accesso utente.
Per la guida introduttiva, sostituire <User_Access_Token>
con un token di accesso utente generato per la risorsa Servizi di comunicazione di 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();
}
}
Avviare una videochiamata usando l'agente di chiamata
È possibile effettuare la chiamata usando l'agente di chiamata. Tutto quello che devi fare è fornire un elenco di ID chiamato e le opzioni di chiamata.
Per effettuare una chiamata con video, è necessario enumerare le fotocamere locali usando l'API deviceManager
getCameras
. Dopo aver selezionato una fotocamera desiderata, usarla per costruire un'istanza LocalVideoStream
. Passarlo quindi come videoOptions
elemento nella localVideoStream
matrice a un metodo di chiamata. Quando la chiamata si connette, avvia automaticamente l'invio di un flusso video dalla fotocamera selezionata all'altro partecipante.
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);
}
In questa guida introduttiva si fa affidamento sulla funzione getNextAvailableCamera
per selezionare la fotocamera usata dalla chiamata. La funzione accetta l'enumerazione delle fotocamere come input e scorre l'elenco per ottenere la fotocamera successiva disponibile. Se l'argomento è null
, la funzione seleziona il primo dispositivo nell'elenco. Se non sono disponibili fotocamere quando si seleziona Avvia chiamata, viene avviata una chiamata audio. Tuttavia, se il partecipante remoto ha risposto con il video, è comunque possibile visualizzare il flusso video 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);
}
Dopo aver costruito un'istanza LocalVideoStream
, è possibile creare un renderer per visualizzarlo nell'interfaccia utente.
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);
});
}
Per consentire all'utente di attivare o disattivare l'origine video locale, usare switchSource
. Questo metodo seleziona la fotocamera disponibile successiva e la definisce come flusso locale.
public void switchSource() {
if (currentVideoStream != null) {
try {
currentCamera = getNextAvailableCamera(currentCamera);
currentVideoStream.switchSource(currentCamera).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Accettare una chiamata in ingresso
È possibile ottenere una chiamata in ingresso sottoscrivendo addOnIncomingCallListener
su callAgent
.
private void handleIncomingCall() {
callAgent.addOnIncomingCallListener((incomingCall) -> {
this.incomingCall = incomingCall;
Executors.newCachedThreadPool().submit(this::answerIncomingCall);
});
}
Per accettare una chiamata con la videocamera, enumerare le fotocamere locali usando l'API deviceManager
getCameras
. Selezionare una fotocamera e costruire un'istanza LocalVideoStream
. Passarlo a acceptCallOptions
prima di chiamare il accept
metodo su un call
oggetto .
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);
}
Partecipanti remoti e flussi video remoti
Quando si avvia una chiamata o si risponde a una chiamata in arrivo, è necessario sottoscrivere l'evento addOnRemoteParticipantsUpdatedListener
per gestire i partecipanti remoti.
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
Quando si usano listener di eventi definiti all'interno della stessa classe, associare il listener a una variabile. Passare la variabile in come argomento per aggiungere e rimuovere i metodi del listener.
Se si tenta di passare il listener direttamente come argomento, si perde il riferimento a tale listener. Java crea nuove istanze di questi listener, non facendo riferimento a quelli creati in precedenza. Non è possibile rimuovere le istanze precedenti, perché non si dispone di un riferimento a tali istanze.
Aggiornamenti del flusso video remoto
Per le chiamate 1:1, è necessario gestire i partecipanti aggiunti. Quando si rimuove il partecipante remoto, la chiamata termina. Per i partecipanti aggiunti, si sottoscrive per addOnVideoStreamsUpdatedListener
gestire gli aggiornamenti del flusso video.
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();
}
}
Eseguire il rendering di video remoti
Creare un renderer del flusso video remoto e collegarlo alla visualizzazione per avviare il rendering della visualizzazione remota. Eliminare la visualizzazione per interrompere il rendering.
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;
}
}
Aggiornamento dello stato della chiamata
Lo stato di una chiamata può cambiare da connesso a disconnesso. Quando la chiamata è connessa, si gestisce il partecipante remoto e quando la chiamata viene disconnessa, si elimina di previewRenderer
per arrestare il video locale.
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);
}
}
Terminare una chiamata
Terminare la chiamata chiamando la funzione nell'istanza hangUp()
di chiamata. Elimina per previewRenderer
arrestare il video locale.
private void hangUp() {
try {
call.hangUp().get();
switchSourceButton.setVisibility(View.INVISIBLE);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
if (previewRenderer != null) {
previewRenderer.dispose();
}
}
Nascondi e mostra video locale
Quando la chiamata è stata avviata, è possibile arrestare il rendering e lo streaming video locali con turnOffLocalVideo()
, questo metodo rimuove la visualizzazione che esegue il wrapping del rendering locale ed elimina il flusso corrente. Per riprendere il flusso ed eseguire di nuovo il rendering dell'anteprima locale, usare turnOnLocalVideo()
, questo metodo mostra l'anteprima video e avvia lo 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();
}
}
Eseguire il codice
È ora possibile avviare l'app usando il pulsante Esegui app sulla barra degli strumenti di Android Studio.
Applicazione completata | 1:1 Chiamata |
---|---|
Aggiungere la funzionalità di chiamata di gruppo
È ora possibile aggiornare l'app per consentire all'utente di scegliere tra chiamate 1:1 o chiamate di gruppo.
Aggiornare il layout
Usare i pulsanti di opzione per selezionare se l'SDK crea una chiamata 1:1 o partecipa a una chiamata di gruppo. I pulsanti di opzione si trovano nella parte superiore, quindi la prima sezione di app/src/main/res/layout/activity_main.xml fine come indicato di seguito.
<?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>
Aggiornare MainActivity.Java
È ora possibile aggiornare gli elementi e la logica per decidere quando creare una chiamata 1:1 e quando partecipare a una chiamata di gruppo. La prima parte del codice richiede aggiornamenti per aggiungere dipendenze, elementi e altre configurazioni.
Dipendenze:
import android.widget.RadioButton;
import com.azure.android.communication.calling.GroupCallLocator;
import com.azure.android.communication.calling.JoinCallOptions;
import java.util.UUID;
Elementi globali:
RadioButton oneToOneCall, groupCall;
AggiornamentoonCreate()
:
@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);
}
AggiornamentostartCall()
:
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);
}
Aggiungi 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;
}
}
Eseguire l'app aggiornata
A questo punto, è possibile avviare l'app usando il pulsante Esegui app sulla barra degli strumenti di Android Studio.
Aggiornamento dello schermo | Chiamata di gruppo |
---|---|
Per iniziare a usare Servizi di comunicazione di Azure, usare l'SDK per chiamate di Servizi di comunicazione per aggiungerlo a una sola videochiamata all'app. Si apprenderà come avviare e rispondere a una videochiamata usando Servizi di comunicazione di Azure Calling SDK per iOS.
Codice di esempio
Se si vuole passare direttamente alla fine, è possibile scaricare questa guida introduttiva come esempio in GitHub.
Prerequisiti
Ottenere un account Azure con una sottoscrizione attiva. Creare un account gratuitamente.
Un Mac che esegue Xcode, insieme a un certificato dello sviluppatore valido installato nel portachiavi.
Creare una risorsa di Servizi di comunicazione attiva. Creare una risorsa di Servizi di comunicazione. È necessario registrare il stringa di connessione per questa guida introduttiva.
Un token di accesso utente per il servizio di comunicazione di Azure. È anche possibile usare l'interfaccia della riga di comando di Azure ed eseguire il comando con il stringa di connessione per creare un utente e un token di accesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Per informazioni dettagliate, vedere Usare l'interfaccia della riga di comando di Azure per creare e gestire i token di accesso.
Configurazione
Creazione del progetto Xcode
In Xcode creare un nuovo progetto iOS e selezionare il modello Single View Application. Questa esercitazione usa il framework SwiftUI, quindi è necessario impostare Il linguaggio su Swift e l'interfaccia utente su SwiftUI. In questo argomento di avvio rapido non verranno creati test. È possibile deselezionare Includi test.
Installazione di CocoaPods
Usare questa guida per installare CocoaPods nel Mac.
Installare il pacchetto e le dipendenze con CocoaPods
Per creare un
Podfile
oggetto per l'applicazione, aprire il terminale e passare alla cartella del progetto ed eseguire pod init.Aggiungere il codice seguente a
Podfile
e salvare. Vedere Versioni del supporto sdk.
platform :ios, '13.0'
use_frameworks!
target 'VideoCallingQuickstart' do
pod 'AzureCommunicationCalling', '~> 1.0.0'
end
Eseguire l'installazione del pod.
Aprire
.xcworkspace
con Xcode.
Uso diretto di XCFramework
Se non si usa CocoaPods
come gestore delle dipendenze, è possibile scaricare direttamente il AzureCommunicationCalling.xcframework
file direttamente dalla pagina di rilascio.
È importante sapere che AzureCommunicationCalling
ha una dipendenza da AzureCommunicationCommon
, quindi è necessario installarlo anche nel progetto.
Nota
Anche se AzureCommunicationCommon
è un pacchetto swift puro, non è possibile installarlo usando Swift Package Manager
per usarlo con AzureCommunicationCalling
perché quest'ultimo è un framework Objective-C e Swift Package Manager
deliberatamente non supporta le intestazioni di interfaccia Swift ObjC per progettazione, il che significa che non è possibile collaborare con AzureCommunicationCalling
se installato usando Swift Package Manager
. È necessario eseguire l'installazione tramite un altro gestore delle dipendenze o generare un oggetto xcframework
da AzureCommunicationCommon
origini e importare nel progetto.
Richiedere l'accesso al microfono e alla fotocamera
Per accedere al microfono e alla fotocamera del dispositivo, è necessario aggiornare l'elenco delle proprietà delle informazioni dell'app con e NSMicrophoneUsageDescription
NSCameraUsageDescription
. Impostare il valore associato su una stringa che include la finestra di dialogo usata dal sistema per richiedere l'accesso all'utente.
Fare clic con il pulsante destro del mouse sulla Info.plist
voce dell'albero del progetto e scegliere Apri come > codice sorgente. Aggiungere le righe seguenti nella sezione <dict>
di primo livello e quindi salvare il file.
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>
<key>NSCameraUsageDescription</key>
<string>Need camera access for video calling</string>
Configurare il framework dell'app
Aprire il file del ContentView.swift
progetto e aggiungere una dichiarazione di importazione all'inizio del file per importare la AzureCommunicationCalling
libreria e AVFoundation
. AVFoundation viene usato per acquisire l'autorizzazione audio dal codice.
import AzureCommunicationCalling
import AVFoundation
Modello a oggetti
Le classi e le interfacce seguenti gestiscono alcune delle principali funzionalità di Servizi di comunicazione di Azure Calling SDK per iOS.
Nome | Descrizione |
---|---|
CallClient |
CallClient è il punto di ingresso principale di Calling SDK. |
CallAgent |
Viene CallAgent utilizzato per avviare e gestire le chiamate. |
CommunicationTokenCredential |
Viene CommunicationTokenCredential usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationIdentifier |
Viene CommunicationIdentifier usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: CommunicationUserIdentifier o PhoneNumberIdentifier CallingApplication . |
Creare l'agente di chiamata
Sostituire l'implementazione di ContentView struct
con alcuni semplici controlli dell'interfaccia utente che consentono a un utente di avviare e terminare una chiamata. In questa guida introduttiva si aggiunge la logica di business a questi controlli.
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()
}
}
Autenticare il client
Per inizializzare un'istanza CallAgent
, è necessario un token di accesso utente, che consente di effettuare e ricevere chiamate. Fare riferimento alla documentazione del token di accesso utente, se non si dispone di un token disponibile.
Dopo aver ottenuto un token, aggiungere il codice seguente al onAppear
callback in ContentView.swift
. È necessario sostituire <USER ACCESS TOKEN>
con un token di accesso utente valido per la risorsa:
var userCredential: CommunicationTokenCredential?
do {
userCredential = try CommunicationTokenCredential(token: "<USER ACCESS TOKEN>")
} catch {
print("ERROR: It was not possible to create user credential.")
return
}
Inizializzare CallAgent e accedere Gestione dispositivi
Per creare un'istanza CallAgent da un CallClient, usare il callClient.createCallAgent
metodo che restituisce in modo asincrono un oggetto CallAgent dopo l'inizializzazione. DeviceManager consente di enumerare i dispositivi locali che possono essere usati in una chiamata per trasmettere flussi audio/video. Consente inoltre di richiedere l'autorizzazione da un utente per accedere al microfono o alla fotocamera.
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")
}
}
}
}
Richiedere le autorizzazioni
È necessario aggiungere il codice seguente al onAppear
callback per richiedere le autorizzazioni per l'audio e il video.
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
if granted {
AVCaptureDevice.requestAccess(for: .video) { (videoGranted) in
/* NO OPERATION */
}
}
}
Configurare la sessione audio
Si usa un AVAudioSession
oggetto per configurare la sessione audio dell'app. Ecco un esempio di abilitazione del dispositivo audio Bluetooth per l'app:
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
}
Visualizzare il video locale
Prima di avviare una chiamata, è possibile gestire le impostazioni correlate al video. In questo argomento di avvio rapido viene introdotta l'implementazione di attivazione/disattivazione del video locale prima o durante una chiamata.
Prima di tutto è necessario accedere alle fotocamere locali con deviceManager
. Dopo aver selezionato la fotocamera desiderata, è possibile costruire LocalVideoStream
e creare un VideoStreamRenderer
oggetto e quindi collegarlo a previewView
. Durante la chiamata, è possibile usare startVideo
o stopVideo
per avviare o interrompere l'invio LocalVideoStream
ai partecipanti remoti. Questa funzione funziona anche con la gestione delle chiamate in ingresso.
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
}
}
}
}
}
Effettuare una chiamata in uscita
Il startCall
metodo viene impostato come azione eseguita quando viene toccato il pulsante Avvia chiamata. In questa guida introduttiva le chiamate in uscita sono solo audio per impostazione predefinita. Per avviare una chiamata con un video, è necessario impostare VideoOptions
LocalVideoStream
e passarlo con startCallOptions
per impostare le opzioni iniziali per la chiamata.
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
vengono usati per gestire gli eventi mid-call e i partecipanti remoti. Gli osservatori vengono impostati nella setCallAndObserver
funzione .
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")
}
}
Rispondere a una chiamata in arrivo
Per rispondere a una chiamata in arrivo, implementare un oggetto IncomingCallHandler
per visualizzare il banner di chiamata in ingresso per rispondere o rifiutare la chiamata. Inserire l'implementazione seguente in 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
}
}
}
È necessario creare un'istanza di IncomingCallHandler
aggiungendo il codice seguente al onAppear
callback in ContentView.swift
:
let incomingCallHandler = IncomingCallHandler.getOrCreateInstance()
incomingCallHandler.contentView = self
Impostare un delegato su CallAgent dopo che CallAgent è stato creato correttamente:
self.callAgent!.delegate = incomingCallHandler
Dopo aver eseguito una chiamata in arrivo, chiama IncomingCallHandler
la funzione showIncomingCallBanner
per visualizzare answer
e decline
il pulsante.
func showIncomingCallBanner(_ incomingCall: IncomingCall?) {
isIncomingCall = true
self.incomingCall = incomingCall
}
Le azioni associate a answer
e decline
vengono implementate come codice seguente. Per rispondere alla chiamata con il video, è necessario attivare il video locale e impostare le opzioni di AcceptCallOptions
con 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
}
Flussi video dei partecipanti remoti
È possibile creare una RemoteVideoStreamData
classe per gestire il rendering dei flussi video del partecipante remoto.
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)
}
}
Sottoscrivere eventi
È possibile implementare una CallObserver
classe per sottoscrivere una raccolta di eventi per ricevere una notifica quando i valori cambiano durante la chiamata.
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
}
}
Gestione partecipanti remoti
Tutti i partecipanti remoti sono rappresentati con il RemoteParticipant
tipo e sono disponibili tramite la raccolta in un'istanza remoteParticipants
di chiamata.
È possibile implementare una RemoteParticipantObserver
classe per sottoscrivere gli aggiornamenti sui flussi video remoti dei partecipanti remoti.
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()
}
}
}
Eseguire il codice
È possibile compilare ed eseguire l'app nel simulatore iOS selezionando Product Run (Esegui prodotto > ) o usando il tasto di scelta rapida (⌘-R).
Questo argomento di avvio rapido illustra come avviare una videochiamata 1:1 usando Servizi di comunicazione di Azure Calling SDK per Windows.
Codice di esempio UWP
Prerequisiti
Per completare questa esercitazione è necessario soddisfare i prerequisiti seguenti:
Un account Azure con una sottoscrizione attiva. Creare un account gratuitamente.
Installare Visual Studio 2022 con piattaforma UWP (Universal Windows Platform) carico di lavoro sviluppo.
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. È necessario registrare il stringa di connessione per questa guida introduttiva.
Un token di accesso utente per il servizio di comunicazione di Azure. È anche possibile usare l'interfaccia della riga di comando di Azure ed eseguire il comando con il stringa di connessione per creare un utente e un token di accesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Per informazioni dettagliate, vedere Usare l'interfaccia della riga di comando di Azure per creare e gestire i token di accesso.
Configurazione
Creazione del progetto
In Visual Studio creare un nuovo progetto con il modello App vuota (Windows universale) per configurare un'app UWP (Single Page Piattaforma UWP (Universal Windows Platform)).
Installare il pacchetto
Fare clic con il pulsante destro del mouse sul progetto e passare a Manage Nuget Packages
per installare Azure.Communication.Calling.WindowsClient
la versione 1.2.0-beta.1 o superiore. Verificare che l'opzione Includi Preleased sia selezionata.
Richiedere l'accesso
Passare a Package.appxmanifest
e fare clic su Capabilities
.
Controllare Internet (Client & Server)
di ottenere l'accesso in ingresso e in uscita a Internet.
Controllare Microphone
di accedere al feed audio del microfono.
Controllare WebCam
di accedere alla fotocamera del dispositivo.
Aggiungere il codice seguente al Package.appxmanifest
proprio facendo clic con il pulsante destro del mouse e scegliendo Visualizza codice.
<Extensions>
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>RtmMvrUap.dll</Path>
<ActivatableClass ActivatableClassId="VideoN.VideoSchemeHandler" ThreadingModel="both" />
</InProcessServer>
</Extension>
</Extensions>
Configurare il framework dell'app
È necessario configurare un layout di base per collegare la logica. Per effettuare una chiamata in uscita, è necessario specificare TextBox
l'ID utente del chiamato. Abbiamo anche bisogno di un Start Call
pulsante e un Hang Up
pulsante.
È anche necessario visualizzare in anteprima il video locale ed eseguire il rendering del video remoto dell'altro partecipante. Sono quindi necessari due elementi per visualizzare i flussi video.
Aprire il MainPage.xaml
del progetto e sostituire il contenuto con l'implementazione seguente.
<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>
Aprire su App.xaml.cs
(fare clic con il pulsante destro del mouse e scegliere Visualizza codice) e aggiungere questa riga all'inizio:
using CallingQuickstart;
Aprire (fare clic con il pulsante destro del MainPage.xaml.cs
mouse e scegliere Visualizza codice) e sostituire il contenuto con l'implementazione seguente:
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
}
}
}
Modello a oggetti
Le classi e le interfacce seguenti gestiscono alcune delle principali funzionalità di Servizi di comunicazione di Azure Calling SDK:
Nome | Descrizione |
---|---|
CallClient |
CallClient è il punto di ingresso principale della libreria client chiamante. |
CallAgent |
Viene CallAgent utilizzato per avviare e partecipare alle chiamate. |
CommunicationCall |
L'oggetto CommunicationCall viene utilizzato per gestire le chiamate inserite o unite in join. |
CallTokenCredential |
Viene CallTokenCredential usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationUserIdentifier |
Viene CommunicationUserIdentifier usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: CommunicationUserIdentifier o PhoneNumberIdentifier CallingApplication . |
Autenticare il client
Per inizializzare un , CallAgent
è necessario un token di accesso utente. In genere questo token viene generato da un servizio con autenticazione specifica per l'applicazione. Per altre informazioni sui token di accesso utente, vedere la Guida ai token di accesso utente.
Per la guida di avvio rapido, sostituire <AUTHENTICATION_TOKEN>
con un token di accesso utente generato per la risorsa del servizio di comunicazione di Azure.
Dopo aver ottenuto un token, inizializzare un'istanza CallAgent
con essa, che consente di effettuare e ricevere chiamate. Per accedere alle fotocamere nel dispositivo, è anche necessario ottenere Gestione dispositivi istanza.
Aggiungere il codice seguente alla InitCallAgentAndDeviceManagerAsync
funzione .
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
}
}
Avviare una chiamata con il video
Aggiungere l'implementazione a CallButton_Click
per avviare una chiamata con il video. È necessario enumerare le fotocamere con l'istanza di Gestione dispositivi e costruire LocalOutgoingVideoStream
. È necessario impostare VideoOptions
con LocalVideoStream
e passarlo con startCallOptions
per impostare le opzioni iniziali per la chiamata. LocalOutgoingVideoStream
Collegando a un MediaElement
oggetto , è possibile visualizzare l'anteprima del video locale.
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;
}
Aggiungere i metodi per avviare o partecipare ai diversi tipi di chiamata (1:1 Servizi di comunicazione di Azure chiamata, 1:1 telefonata, Servizi di comunicazione di Azure chiamata di gruppo, partecipazione alla riunione di Teams e così via).
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 } }
};
}
Aggiungere il codice per creare LocalVideoStream a seconda della fotocamera selezionata nel CameraList_SelectionChanged
metodo .
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);
}
Accettare una chiamata in ingresso
Aggiungere l'implementazione a per rispondere a OnIncomingCallAsync
una chiamata in ingresso con video, passare LocalVideoStream
a acceptCallOptions
.
var incomingCall = args.IncomingCall;
var acceptCallOptions = new AcceptCallOptions() {
IncomingVideoOptions = new IncomingVideoOptions()
{
IncomingVideoStreamKind = VideoStreamKind.RemoteIncoming
}
};
_ = await incomingCall.AcceptAsync(acceptCallOptions);
Partecipanti remoti e flussi video remoti
Tutti i partecipanti remoti sono disponibili tramite la raccolta in un'istanza RemoteParticipants
di chiamata. Una volta connessa la chiamata, è possibile accedere ai partecipanti remoti della chiamata e gestire i flussi video remoti.
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;
}
}
Eseguire il rendering di video remoti
Per ogni flusso video remoto, collegarlo all'oggetto MediaElement
.
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();
});
}
}
Aggiornamento dello stato della chiamata
È necessario pulire i renderer video una volta disconnessa la chiamata e gestire il caso quando i partecipanti remoti partecipano inizialmente alla chiamata.
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;
}
}
Terminare una chiamata
Terminare la chiamata corrente quando si fa clic sul Hang Up
pulsante. Aggiungere l'implementazione al HangupButton_Click per terminare una chiamata con callAgent creato e rimuovere i gestori eventi di chiamata e aggiornamento del partecipante.
var call = this.callAgent?.Calls?.FirstOrDefault();
if (call != null)
{
try
{
await call.HangUpAsync(new HangUpOptions() { ForEveryone = true });
}
catch(Exception ex)
{
}
}
Eseguire il codice
È possibile compilare ed eseguire il codice in Visual Studio. Per le piattaforme di soluzioni, è supportato ARM64
, x64
e x86
.
È possibile effettuare una videochiamata in uscita specificando un ID utente nel campo di testo e facendo clic sul Start Call
pulsante.
Nota: la chiamata arresta 8:echo123
il flusso video perché echo bot non supporta lo streaming video.
Per altre informazioni sugli ID utente (identità) vedere la guida ai token di accesso utente.
Codice di esempio winUI 3
Prerequisiti
Per completare questa esercitazione è necessario soddisfare i prerequisiti seguenti:
Un account Azure con una sottoscrizione attiva. Creare un account gratuitamente.
Installare Visual Studio 2022 e SDK per app di Windows versione 1.2 preview 2.
Conoscenza di base di come creare un'app WinUI 3. Creare il primo progetto WinUI 3 (SDK per app di Windows) è una buona risorsa da cui iniziare.
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. È necessario registrare il stringa di connessione per questa guida introduttiva.
Un token di accesso utente per il servizio di comunicazione di Azure. È anche possibile usare l'interfaccia della riga di comando di Azure ed eseguire il comando con il stringa di connessione per creare un utente e un token di accesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Per informazioni dettagliate, vedere Usare l'interfaccia della riga di comando di Azure per creare e gestire i token di accesso.
Configurazione
Creazione del progetto
In Visual Studio creare un nuovo progetto con il modello App vuota, In pacchetto (WinUI 3 in Desktop) per configurare un'app WinUI 3 a pagina singola.
Installare il pacchetto
Fare clic con il pulsante destro del mouse sul progetto e passare a Manage Nuget Packages
per installare Azure.Communication.Calling.WindowsClient
la versione 1.0.0 o superiore. Verificare che l'opzione Includi Preleased sia selezionata.
Richiedere l'accesso
Aggiungere il codice seguente a app.manifest
:
<file name="RtmMvrMf.dll">
<activatableClass name="VideoN.VideoSchemeHandler" threadingModel="both" xmlns="urn:schemas-microsoft-com:winrt.v1" />
</file>
Configurare il framework dell'app
È necessario configurare un layout di base per collegare la logica. Per effettuare una chiamata in uscita, è necessario specificare TextBox
l'ID utente del chiamato. Abbiamo anche bisogno di un Start Call
pulsante e un Hang Up
pulsante.
È anche necessario visualizzare in anteprima il video locale ed eseguire il rendering del video remoto dell'altro partecipante. Sono quindi necessari due elementi per visualizzare i flussi video.
Aprire il MainWindow.xaml
del progetto e sostituire il contenuto con l'implementazione seguente.
<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>
Aprire su App.xaml.cs
(fare clic con il pulsante destro del mouse e scegliere Visualizza codice) e aggiungere questa riga all'inizio:
using CallingQuickstart;
Aprire (fare clic con il pulsante destro del MainWindow.xaml.cs
mouse e scegliere Visualizza codice) e sostituire il contenuto con l'implementazione seguente:
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();
});
}
}
}
Modello a oggetti
Le classi e le interfacce seguenti gestiscono alcune delle principali funzionalità di Servizi di comunicazione di Azure Calling SDK:
Nome | Descrizione |
---|---|
CallClient |
CallClient è il punto di ingresso principale della libreria client chiamante. |
CallAgent |
Viene CallAgent utilizzato per avviare e partecipare alle chiamate. |
CommunicationCall |
L'oggetto CommunicationCall viene utilizzato per gestire le chiamate inserite o unite in join. |
CallTokenCredential |
Viene CallTokenCredential usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationUserIdentifier |
Viene CommunicationUserIdentifier usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: CommunicationUserIdentifier o PhoneNumberIdentifier CallingApplication . |
Autenticare il client
Per inizializzare un , CallAgent
è necessario un token di accesso utente. In genere questo token viene generato da un servizio con autenticazione specifica per l'applicazione. Per altre informazioni sui token di accesso utente, vedere la Guida ai token di accesso utente.
Per la guida di avvio rapido, sostituire <AUTHENTICATION_TOKEN>
con un token di accesso utente generato per la risorsa del servizio di comunicazione di Azure.
Dopo aver inizializzato un CallAgent
token con esso, che consente di effettuare e ricevere chiamate. Per accedere alle fotocamere nel dispositivo, è anche necessario ottenere Gestione dispositivi istanza.
Aggiungere il codice seguente alla InitCallAgentAndDeviceManagerAsync
funzione .
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;
Avviare una chiamata con il video
Aggiungere l'implementazione a CallButton_Click
per avviare una chiamata con il video. È necessario enumerare le fotocamere con l'istanza di Gestione dispositivi e costruire LocalVideoStream
. È necessario impostare VideoOptions
con LocalVideoStream
e passarlo con startCallOptions
per impostare le opzioni iniziali per la chiamata. LocalVideoStream
Collegando a un MediaPlayerElement
oggetto , è possibile visualizzare l'anteprima del video locale.
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;
Accettare una chiamata in ingresso
Aggiungere l'implementazione a per rispondere a Agent_OnIncomingCallAsync
una chiamata in ingresso con video, passare LocalVideoStream
a 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);
Partecipanti remoti e flussi video remoti
Tutti i partecipanti remoti sono disponibili tramite la raccolta in un'istanza RemoteParticipants
di chiamata. Una volta connessa la chiamata, è possibile accedere ai partecipanti remoti della chiamata e gestire i flussi video remoti.
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);
}
}
Eseguire il rendering di video remoti
Per ogni flusso video remoto, collegarlo all'oggetto MediaPlayerElement
.
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();
});
}
}
Aggiornamento dello stato della chiamata
È necessario pulire i renderer video una volta disconnessa la chiamata e gestire il caso quando i partecipanti remoti partecipano inizialmente alla chiamata.
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;
}
}
Terminare una chiamata
Terminare la chiamata corrente quando si fa clic sul Hang Up
pulsante. Aggiungere l'implementazione al HangupButton_Click per terminare una chiamata con callAgent creato e rimuovere i gestori eventi di chiamata e aggiornamento del partecipante.
this.call.OnRemoteParticipantsUpdated -= Call_OnRemoteParticipantsUpdatedAsync;
this.call.OnStateChanged -= Call_OnStateChangedAsync;
await this.call.HangUpAsync(new HangUpOptions());
Eseguire il codice
È possibile compilare ed eseguire il codice in Visual Studio. Per le piattaforme di soluzioni, è supportato ARM64
, x64
e x86
.
È possibile effettuare una videochiamata in uscita specificando un ID utente nel campo di testo e facendo clic sul Start Call
pulsante.
Nota: la chiamata arresta 8:echo123
il flusso video perché echo bot non supporta lo streaming video.
Per altre informazioni sugli ID utente (identità) vedere la guida ai token di accesso utente.
In questa guida introduttiva si apprenderà come avviare una chiamata usando Servizi di comunicazione di Azure Calling SDK per Unity. Per la ricezione e il rendering di fotogrammi video nella piattaforma Unity, vedere l'argomento di avvio rapido sull'accesso multimediale non elaborato.
È possibile scaricare l'app di esempio da GitHub.
Prerequisiti
Per completare questa esercitazione è necessario soddisfare i prerequisiti seguenti:
Un account Azure con una sottoscrizione attiva. Creare un account gratuitamente.
Installare Unity Hub e Unity Editor con piattaforma UWP (Universal Windows Platform) carico di lavoro sviluppo.
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. È necessario registrare il stringa di connessione per questa guida introduttiva.
Un token di accesso utente per il servizio di comunicazione di Azure. È anche possibile usare l'interfaccia della riga di comando di Azure ed eseguire il comando con il stringa di connessione per creare un utente e un token di accesso.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Per informazioni dettagliate, vedere Usare l'interfaccia della riga di comando di Azure per creare e gestire i token di accesso.
Configurazione
Creazione del progetto
In Unity Hub creare un nuovo progetto con il modello 2D Core per configurare il progetto unity.
Installare il pacchetto
Esistono due modi per installare Azure Communication Calling SDK per Unity.
Scaricare l'SDK dal feed npm pubblico e importarlo nella gestione pacchetti dell'editor di Unity, disponibile nella scheda Windows.
Scaricare Realtà mista Feature Tool da Microsoft e installarlo tramite lo strumento di gestione degli strumenti di realtà mista.
Configurare il framework dell'app
È necessario configurare un layout di base per collegare la logica. Per effettuare una chiamata in uscita, è necessario specificare TextBox
l'ID utente del chiamato. Abbiamo anche bisogno di un Start/Join call
pulsante e un Hang up
pulsante.
Creare una nuova scena denominata Main
nel progetto.
Aprire il Main.unity
file e sostituire il contenuto con l'implementazione seguente:
Codice 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
Creare uno script denominato AppManager.cs e collegarlo all'oggetto AppManager nell'editor di Unity. Sostituire il contenuto con l'implementazione seguente:
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;
}
}
}
}
In GameObject denominato AppManager trascinare lo script appena creato nel relativo componente script. Trascinare l'oggetto testo Stato nel campo testo Stato chiamata per abilitare gli aggiornamenti dell'interfaccia utente dello stato della chiamata. L'SDK fornisce video tramite URI, ma attualmente il lettore video di Unity potrebbe non supportare la riproduzione dell'URI.
Modello a oggetti
Nella tabella successiva sono elencate le classi e le interfacce che gestiscono alcune delle principali funzionalità di Servizi di comunicazione di Azure Calling SDK:
Nome | Descrizione |
---|---|
CallClient |
CallClient è il punto di ingresso principale di Calling SDK. |
CallAgent |
Viene CallAgent utilizzato per avviare e gestire le chiamate. |
Call |
Viene CommunicationCall utilizzato per gestire una chiamata in corso. |
CallTokenCredential |
Viene CallTokenCredential usato come credenziale del token per creare un'istanza di CallAgent . |
CallIdentifier |
Viene CallIdentifier usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: UserCallIdentifier , PhoneNumberCallIdentifier e così via. |
Autenticare il client
Inizializzare un'istanza CallAgent
con un token di accesso utente che consente di effettuare e ricevere chiamate e, facoltativamente, ottenere un'istanza di DeviceManager per eseguire query per le configurazioni del dispositivo client.
Nel codice sostituire <AUTHENTICATION_TOKEN>
con un token di accesso utente. Se non è disponibile un token, vedere la documentazione dei token di accesso utente.
Aggiungere InitCallAgentAndDeviceManagerAsync
la funzione , che esegue il bootstrap dell'SDK. Questo helper può essere personalizzato per soddisfare i requisiti dell'applicazione.
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;
}
Avviare una chiamata con il video
Una volta ottenuto un StartCallOptions
oggetto, CallAgent
è possibile usare per avviare la chiamata Servizi di comunicazione di 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;
}
Gestire il partecipante remoto e il video in ingresso remoto
Il video in ingresso è associato a partecipanti remoti specifici, pertanto RemoteParticipantsUpdated è l'evento chiave per rimanere informati e ottenere riferimenti ai partecipanti in modifica.
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;
}
}
Tutti i partecipanti remoti sono disponibili tramite la raccolta in un'istanza RemoteParticipants
di chiamata. Una volta connessa la chiamata, è possibile accedere ai partecipanti remoti della chiamata e gestire i flussi video remoti.
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;
}
}
Un flusso video transiterà attraverso una sequenza di stati interni. VideoStreamState.Available
è lo stato preferito per associare il flusso video all'elemento dell'interfaccia utente per il rendering del flusso video, ad esempio MediaPlayerElement
, ed VideoStreamState.Stopped
è in genere dove devono essere eseguite le attività di pulizia, ad esempio l'arresto dell'anteprima video.
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;
}
}
Terminare una chiamata
Terminare la chiamata corrente quando si fa clic sul Hang up
pulsante. Aggiungere l'implementazione al HangupButton_Click per terminare una chiamata e arrestare i flussi di anteprima e video.
public async void HangupButton_Click()
{
if (call != null)
{
try
{
await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
}
catch (Exception ex)
{
}
}
}
Accettare una chiamata in ingresso
IncomingCallReceived
il sink di eventi viene configurato nell'helper InitCallAgentAndDeviceManagerAsync
bootstrap dell'SDK.
callAgent.IncomingCallReceived += OnIncomingCallAsync;
L'applicazione ha la possibilità di configurare la modalità di accettazione della chiamata in ingresso, ad esempio i tipi di flusso video e audio.
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;
}
Monitorare e rispondere all'evento di modifica dello stato della chiamata
StateChanged
l'evento sull'oggetto Call
viene generato quando un oggetto in corso chiama transazioni da uno stato a un altro. L'applicazione offre le opportunità di riflettere le modifiche dello stato nell'interfaccia utente o inserire le logiche di business.
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;
}
}
}
Eseguire il codice
È possibile compilare ed eseguire il codice nell'editor di Unity o nei dispositivi che usano Unity.
È possibile effettuare una chiamata in uscita specificando un ID utente nel campo di testo e facendo clic sul Start Call/Join
pulsante. La chiamata 8:echo123
si connette con un bot echo, questa funzionalità è ideale per iniziare e verificare che i dispositivi audio funzionino.
Pulire le risorse
Se si vuole pulire e rimuovere una sottoscrizione a Servizi di comunicazione, è possibile eliminare la risorsa o il gruppo di risorse. L'eliminazione del gruppo di risorse comporta anche l'eliminazione di tutte le altre risorse associate. Altre informazioni sulla pulizia delle risorse.
Passaggi successivi
Per altre informazioni, vedere gli articoli seguenti:
- Consultare l'esempio hero per le chiamate
- Introduzione alla libreria dell'interfaccia utente
- Informazioni sulle funzionalità di Calling SDK
- Leggere altre informazioni sul funzionamento delle chiamate
Commenti e suggerimenti
https://aka.ms/ContentUserFeedback.
Presto disponibile: Nel corso del 2024 verranno gradualmente disattivati i problemi di GitHub come meccanismo di feedback per il contenuto e ciò verrà sostituito con un nuovo sistema di feedback. Per altre informazioni, vedereInvia e visualizza il feedback per