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 videochiamate all'app. Si apprenderà come avviare e rispondere a una videochiamata usando Azure Communication Services 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 di 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 per VoIP 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 attiva di Servizi di comunicazione. Creare una risorsa di Servizi di comunicazione. Per questa guida introduttiva è necessario registrare la stringa di connessione.
Creare un token di accesso utente per creare un'istanza del client di chiamata. Vedere 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 la 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 comando npm install
per installare SDK di chiamata di Servizi di comunicazione di Azure per JavaScript.
npm install @azure/communication-common --save
npm install @azure/communication-calling --save
Configurare il framework dell'app
Questa guida di avvio rapido usa webpack per aggregare gli asset dell'applicazione. Eseguire il comando seguente per installare i pacchetti npm webpack
, webpack-cli
e webpack-dev-server
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 file index.html
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à del SDK Chiamate di Servizi di comunicazione di Azure:
Nome | Descrizione |
---|---|
CallClient |
Punto di ingresso principale di Calling SDK. |
AzureCommunicationTokenCredential |
Implementa l'interfaccia CommunicationTokenCredential usata per creare un'istanza di callAgent . |
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. |
Nella directory radice del progetto creare un file denominato index.js
in cui contenere la logica dell'applicazione per questa guida di avvio rapido. 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 updates.
*/
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 remoteParticipant 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();
});
Nella directory radice del progetto creare un file denominato styles.css
in cui contenere lo styling dell'applicazione per questa guida di avvio rapido. 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 in due schede passare a http://localhost:8080/.You dovrebbe 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.
Se non sono già disponibili token da usare, fare riferimento alla documentazione del token di accesso utente.
In entrambe le schede fare clic sui pulsanti "Inizializza agente di chiamata". Verrà visualizzata la schermata seguente:
Nella prima scheda immettere l'identità utente di 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.
Introduzione a Servizi di comunicazione di Azure, con l'aggiunta di videochiamate all'app tramite la libreria client Chiamate di Servizi di comunicazione. Informazioni su come includere videochiamate 1:1 e come creare o partecipare a chiamate di gruppo. È anche possibile avviare, rispondere e partecipare a una videochiamata usando Azure Communication Services 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. Per questa guida introduttiva è necessario registrare la stringa di connessione.
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 la 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 Avviare un nuovo progetto di Android Studio.
In Telefono e Tabletselezionare il modello di progetto Attività vuota.
Per l’SDK minimo, selezionare API 26: Android 8.0 (Oreo) o versione successiva. Vedere Le versioni del supporto sdk.
Installare il pacchetto
Individuare il livello di progetto build.gradle
, e aggiungere mavenCentral()
all'elenco dei repository in buildscript
e allprojects
buildscript {
repositories {
...
mavenCentral()
...
}
}
allprojects {
repositories {
...
mavenCentral()
...
}
}
Quindi, nel livello del modulo build.gradle
aggiungere le righe seguenti alle sezioni dependencies
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 del destinatario della chiamata o l'ID del gruppo di chiamata, un pulsante per effettuare la chiamata e un ulteriore pulsante per riagganciare 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 delle autorizzazioni in fase di esecuzione, la creazione dell'agente di chiamata e l'effettuazione della chiamata quando viene premuto il pulsante.
Il metodo onCreate
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 sul ciclo di vita dell'attività.
Passare al file MainActivity.java 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 versione successiva, le autorizzazioni vengono concesse in fase di esecuzione invece che durante l'installazione dell'app. Per supportare questo requisito, è possibile implementare getAllPermissions
per chiamare ActivityCompat.checkSelfPermission
e ActivityCompat.requestPermissions
per ogni autorizzazione richiesta.
/**
* 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, valutare quando dovranno 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à del SDK Chiamate di Servizi di comunicazione di Azure:
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 l'autenticazione specifica per l'applicazione. Per altre informazioni sui token di accesso utente, vedere Token di accesso utente.
Per la guida di avvio rapido, sostituire <User_Access_Token>
con un token di accesso utente generato per la risorsa dei 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 bisogna fare è fornire un elenco di ID del destinatario della chiamata 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 in videoOptions
come elemento nella matrice localVideoStream
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 arrivo
È possibile ottenere una chiamata in arrivo 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 di LocalVideoStream
. Passarlo in acceptCallOptions
prima di chiamare il metodo accept
su un oggetto call
.
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
Tutti i partecipanti remoti sono disponibili tramite il metodo getRemoteParticipants()
in un'istanza di chiamata. Quando la chiamata diventa connessa (CallState.CONNECTED
), è possibile accedere ai partecipanti da remoto della chiamata e gestire i 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 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.
Nota
Quando un utente partecipa a una chiamata, può accedere ai partecipanti remoti correnti tramite il metodo getRemoteParticipants()
. L'evento addOnRemoteParticipantsUpdatedListener
non verrà attivato per questi partecipanti esistenti. Questo evento viene attivato solo quando un partecipante remoto partecipa o lascia la chiamata mentre l'utente è già nella chiamata.
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 addOnVideoStreamsUpdatedListener
per 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 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 hangUp()
nell'istanza di chiamata. Eliminare previewRenderer
per 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();
}
}
Nascondere e mostrare 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 termina 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>
Update 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);
}
Aggiungere 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 Azure Communication Services 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 attiva di Servizi di comunicazione. Creare una risorsa di Servizi di comunicazione. Per questa guida introduttiva è necessario registrare la stringa di connessione.
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 la 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 Linguaggio su Swift e 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 oggetto
Podfile
per l'applicazione, aprire il terminale e passare alla cartella del progetto ed eseguire il pod init.Aggiungere il codice seguente a
Podfile
e salvare. Vedere Le 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 AzureCommunicationCalling.xcframework
direttamente dalla pagina di versione.
È 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
non supportano deliberatamente 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 xcframework
da origini AzureCommunicationCommon
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 un NSMicrophoneUsageDescription
e NSCameraUsageDescription
. Impostare il valore associato su una stringa inclusa nella finestra di dialogo usata dal sistema per richiedere l'accesso all'utente.
Fare clic con il pulsante destro del mouse sulla voce Info.plist
dell'albero del progetto e selezionare 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 ContentView.swift
del progetto e aggiungere una dichiarazione di importazione all'inizio del file per importare la libreria AzureCommunicationCalling
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à del SDK Chiamate di Servizi di comunicazione di Azure per iOS.
Nome | Descrizione |
---|---|
CallClient |
CallClient è il punto di ingresso principale di Calling SDK. |
CallAgent |
CallAgent viene usato per avviare e gestire le chiamate. |
CommunicationTokenCredential |
CommunicationTokenCredential viene usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationIdentifier |
CommunicationIdentifier viene usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: CommunicationUserIdentifier , PhoneNumberIdentifier o CallingApplication . |
Creare l'agente di chiamata
Sostituire l'implementazione del ContentView struct
con alcuni semplici controlli dell'interfaccia utente che consentano a un utente di avviare e terminare una chiamata. In questa guida di avvio rapido viene aggiunta 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. Se non è disponibile un token, vedere la documentazione dei token di accesso utente.
Dopo aver ottenuto un token, aggiungere il codice seguente al callback onAppear
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 a Gestione dispositivi
Per creare un'istanza CallAgent da callClient, utilizzare il metodo callClient.createCallAgent
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 callback onAppear
per richiedere autorizzazioni per audio e video.
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
if granted {
AVCaptureDevice.requestAccess(for: .video) { (videoGranted) in
/* NO OPERATION */
}
}
}
Configurare la sessione audio
Usare un oggetto AVAudioSession
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
, e quindi collegarlo a previewView
. Durante la chiamata, è possibile usare startVideo
o stopVideo
per avviare o interrompere l'invio di 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 metodo startCall
viene impostato come azione che viene eseguita quando viene toccato il pulsante Start Call (Avvia chiamata). In questa guida introduttiva le chiamate in uscita sono solo audio per impostazione predefinita. Per avviare una chiamata con video, è necessario impostare VideoOptions
con 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 di mid-call e i partecipanti remoti. Gli osservatori vengono impostati nella funzione setCallAndObserver
.
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, didReceiveIncomingCall 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 callback onAppear
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
Una volta eseguita una chiamata in arrivo, IncomingCallHandler
chiama la funzione showIncomingCallBanner
per visualizzare il pulsante answer
e decline
.
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 classe RemoteVideoStreamData
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 classe CallObserver
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 tipo di RemoteParticipant
e sono disponibili tramite la raccolta remoteParticipants
in un'istanza di chiamata dopo che la chiamata diventa connessa (CallState.connected
).
Nota
Quando un utente partecipa a una chiamata, può accedere ai partecipanti remoti correnti tramite la raccolta RemoteParticipants
. L'evento didUpdateRemoteParticipant
non verrà attivato per questi partecipanti esistenti. Questo evento viene attivato solo quando un partecipante remoto partecipa o lascia la chiamata mentre l'utente è già nella chiamata.
È possibile implementare una classe RemoteParticipantObserver
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 creare ed eseguire un'app nel simulatore iOS selezionando Prodotto > Esegui, o premendo i tasti di scelta rapida (⌘-R).
In questa guida introduttiva si apprenderà come avviare una videochiamata 1:1 usando Azure Communication Services 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 il carico di lavoro Sviluppo di app per la piattaforma UWP (Universal Windows Platform).
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. Per questa guida introduttiva è necessario registrare la stringa di connessione.
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 la 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 (Universal Windows Platform) 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
1.2.0-beta.1 o versione 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)
per ottenere l'accesso in ingresso e in uscita a Internet.
Controllare Microphone
per accedere al feed audio del microfono.
Controllare WebCam
per accedere alla fotocamera del dispositivo.
Aggiungere il codice seguente al Package.appxmanifest
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 un TextBox
per fornire l'ID utente del chiamato. È anche necessario un pulsante Start Call
e un pulsante Hang Up
.
È 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 il MainPage.xaml.cs
(fare clic con il pulsante destro del 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à del SDK Chiamate di Servizi di comunicazione di Azure:
Nome | Descrizione |
---|---|
CallClient |
CallClient è il principale punto di ingresso alla libreria client Chiamate. |
CallAgent |
Il CallAgent viene usato per avviare e unire chiamate. |
CommunicationCall |
L'oggetto CommunicationCall viene utilizzato per gestire le chiamate inserite o unite in join. |
CallTokenCredential |
CallTokenCredential viene usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationUserIdentifier |
CommunicationUserIdentifier viene usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: CommunicationUserIdentifier , PhoneNumberIdentifier o CallingApplication . |
Autenticare il client
Per inizializzare un CallAgent
, è necessario un token di accesso utente. In genere, questo token viene generato da un servizio con l'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 con essa un'istanza di CallAgent
, che consente di effettuare e ricevere chiamate. Per accedere alle fotocamere nel dispositivo, è anche necessario ottenere l'istanza di Gestione dispositivi.
Aggiungere il codice seguente alla funzione InitCallAgentAndDeviceManagerAsync
.
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. Collegando LocalOutgoingVideoStream
a un MediaElement
, è 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 chiamata di Servizi di comunicazione di Azure, chiamata 1:1, chiamata al gruppo di Servizi di comunicazione di Azure, partecipazione alle riunioni di Teams e così via).
private async Task<CommunicationCall> StartAcsCallAsync(string acsCallee)
{
var options = await GetStartCallOptionsAsync();
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 GetStartCallOptionsAsync();
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> GetStartCallOptionsAsync()
{
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 metodo CameraList_SelectionChanged
.
var selectedCamera = CameraList.SelectedItem as VideoDeviceDetails;
cameraStream = new LocalOutgoingVideoStream(selectedCamera);
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 arrivo
Aggiungere l'implementazione a OnIncomingCallAsync
per rispondere a 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 RemoteParticipants
in un'istanza di chiamata. Quando la chiamata diventa connessa (CallState.Connected
), è possibile accedere ai partecipanti remoti alla chiamata e gestire i flussi video remoti.
Nota
Quando un utente partecipa a una chiamata, può accedere ai partecipanti remoti correnti tramite la raccolta RemoteParticipants
. L'evento RemoteParticipantsUpdated
non verrà attivato per questi partecipanti esistenti. Questo evento viene attivato solo quando un partecipante remoto partecipa o lascia la chiamata mentre l'utente è già nella chiamata.
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 pulsante Hang Up
. Aggiungere l'implementazione all’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 fornendo un ID utente nel campo di testo e facendo clic sul pulsante Start Call
.
Nota: la chiamata 8:echo123
arresta il flusso video perché echo bot non supporta lo streaming video.
Per altre informazioni sugli ID utente (identità) vedere la guida 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 Windows App SDK versione 1.2 preview 2.
Conoscenza di base di come creare un'app WinUI 3. Creare il primo progetto WinUI 3 (Windows App SDK) è una buona risorsa da cui iniziare.
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. Per questa guida introduttiva è necessario registrare la stringa di connessione.
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 la 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 la versione Azure.Communication.Calling.WindowsClient
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 un TextBox
per fornire l'ID utente del chiamato. È anche necessario un pulsante Start Call
e un pulsante Hang Up
.
È 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 il MainWindow.xaml.cs
(fare clic con il pulsante destro del 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à del SDK Chiamate di Servizi di comunicazione di Azure:
Nome | Descrizione |
---|---|
CallClient |
CallClient è il principale punto di ingresso alla libreria client Chiamate. |
CallAgent |
Il CallAgent viene usato per avviare e unire chiamate. |
CommunicationCall |
L'oggetto CommunicationCall viene utilizzato per gestire le chiamate inserite o unite in join. |
CallTokenCredential |
CallTokenCredential viene usato come credenziale del token per creare un'istanza di CallAgent . |
CommunicationUserIdentifier |
CommunicationUserIdentifier viene usato per rappresentare l'identità dell'utente, che può essere una delle opzioni seguenti: CommunicationUserIdentifier , PhoneNumberIdentifier o CallingApplication . |
Autenticare il client
Per inizializzare un CallAgent
, è necessario un token di accesso utente. In genere, questo token viene generato da un servizio con l'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 con essa un'istanza di CallAgent
, che consente di effettuare e ricevere chiamate. Per accedere alle fotocamere nel dispositivo, è anche necessario ottenere l'istanza di Gestione dispositivi.
Aggiungere il codice seguente alla funzione InitCallAgentAndDeviceManagerAsync
.
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. Collegando LocalVideoStream
a un MediaPlayerElement
, è 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 selectedCamera = CameraList.SelectedItem as VideoDeviceDetails;
cameraStream = new LocalOutgoingVideoStream(selectedCamera);
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 arrivo
Aggiungere l'implementazione a Agent_OnIncomingCallAsync
per rispondere a 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 selectedCamera = CameraList.SelectedItem as VideoDeviceDetails;
cameraStream = new LocalOutgoingVideoStream(selectedCamera);
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 RemoteParticipants
in un'istanza di chiamata. Una volta connessa la chiamata, è possibile accedere ai partecipanti remoti della chiamata e gestire i flussi video remoti.
Nota
Quando un utente partecipa a una chiamata, può accedere ai partecipanti remoti correnti tramite la raccolta RemoteParticipants
. L'evento OnRemoteParticipantsUpdated
non verrà attivato per questi partecipanti esistenti. Questo evento viene attivato solo quando un partecipante remoto partecipa o lascia la chiamata mentre l'utente è già nella chiamata.
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 pulsante Hang Up
. Aggiungere l'implementazione all’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 fornendo un ID utente nel campo di testo e facendo clic sul pulsante Start Call
.
Nota: la chiamata 8:echo123
arresta il flusso video perché echo bot non supporta lo streaming video.
Per altre informazioni sugli ID utente (identità) vedere la guida token di accesso utente.
In questo argomento di avvio rapido si apprenderà come avviare una chiamata usando la libreria client Chiamate di Servizi di comunicazione di Azure per Unity. Per la ricezione e il rendering di fotogrammi video nella piattaforma Unity, vedere la guida introduttiva 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 carico di lavoro sviluppo della piattaforma UWP (Universal Windows Platform).
Una risorsa di Servizi di comunicazione distribuita. Creare una risorsa di Servizi di comunicazione. Per questa guida introduttiva è necessario registrare la stringa di connessione.
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 la 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 lo strumento di funzionalità di realtà mista 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 un TextBox
per fornire l'ID utente del chiamato. È anche necessario un pulsante Start/Join call
e un pulsante Hang up
.
Creare una nuova scena denominata Main
nel progetto.
Aprire il file Main.unity
e sostituire il contenuto con l'implementazione seguente:
Main.Unity Code
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
La tabella successiva elenca le classi e le interfacce che gestiscono alcune delle principali funzionalità di Azure Communication Services Calling SDK:
Nome | Descrizione |
---|---|
CallClient |
CallClient è il punto di ingresso principale di Calling SDK. |
CallAgent |
CallAgent viene usato per avviare e gestire le chiamate. |
Call |
CommunicationCall viene utilizzato per gestire una chiamata in corso. |
CallTokenCredential |
CallTokenCredential viene usato come credenziale del token per creare un'istanza di CallAgent . |
CallIdentifier |
CallIdentifier viene 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 la funzione InitCallAgentAndDeviceManagerAsync
, 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
Dopo aver ottenuto un oggetto StartCallOptions
, è possibile usare CallAgent
per avviare la chiamata di 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 RemoteParticipants
in un'istanza 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
, e 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 pulsante Hang up
. Aggiungere l'implementazione all’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 arrivo.
Il sink di eventi IncomingCallReceived
viene configurato nell'helper bootstrap dell'SDK InitCallAgentAndDeviceManagerAsync
.
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
L’evento StateChanged
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 fornendo un ID utente nel campo di testo e facendo clic sul pulsante Start Call/Join
. Con la chiamata a 8:echo123
ci si connette a un Echo Bot, questa funzionalità è la scelta ideale per iniziare e per 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