Szybki start: dodawanie wywołania wideo 1:1 do aplikacji
Rozpocznij pracę z usługami Azure Communication Services przy użyciu zestawu SDK wywołującego usługi Communication Services, aby dodać 1 na 1 wideo wywołujące aplikację. Dowiesz się, jak rozpocząć i odpowiedzieć na połączenie wideo przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla języka JavaScript.
Przykładowy kod
Jeśli chcesz przejść do końca, możesz pobrać ten przewodnik Szybki start jako przykład w usłudze GitHub.
Uwaga
Dostęp do połączeń wychodzących do użytkownika usług Azure Communication Services można uzyskać przy użyciu biblioteki interfejsu użytkownika usług Azure Communication Services. Biblioteka interfejsu użytkownika umożliwia deweloperom dodawanie klienta wywołania, który jest włączony w aplikacji voIP z tylko kilkoma wierszami kodu.
Wymagania wstępne
Uzyskaj konto platformy Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
Musisz mieć Node.js 18. Aby go zainstalować, możesz użyć instalatora msi.
Utwórz aktywny zasób usług komunikacyjnych. Utwórz zasób usług komunikacyjnych. Musisz zarejestrować parametry połączenia na potrzeby tego przewodnika Szybki start.
Utwórz token dostępu użytkownika, aby utworzyć wystąpienie klienta wywołania. Dowiedz się, jak tworzyć tokeny dostępu użytkowników i zarządzać nimi. Możesz również użyć interfejsu wiersza polecenia platformy Azure i uruchomić polecenie za pomocą parametry połączenia, aby utworzyć użytkownika i token dostępu.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Aby uzyskać szczegółowe informacje, zobacz Tworzenie tokenów dostępu za pomocą interfejsu wiersza polecenia platformy Azure i zarządzanie nimi.
Konfigurowanie
Tworzenie nowej aplikacji Node.js
Otwórz terminal lub okno polecenia utwórz nowy katalog dla aplikacji i przejdź do niego.
mkdir calling-quickstart && cd calling-quickstart
Uruchom polecenie npm init -y
, aby utworzyć plik package.json z ustawieniami domyślnymi.
npm init -y
Instalowanie pakietu
npm install
Użyj polecenia , aby zainstalować zestaw SDK wywołujący usługi Azure Communication Services dla języka JavaScript.
npm install @azure/communication-common --save
npm install @azure/communication-calling --save
Konfigurowanie struktury aplikacji
Ten przewodnik Szybki start używa pakietu WebPack do tworzenia pakietów zasobów aplikacji. Uruchom następujące polecenie, aby zainstalować webpack
pakiety i webpack-cli
webpack-dev-server
npm i wyświetlić je jako zależności programistyczne w pliku 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
Oto kod:
index.html
Utwórz plik w katalogu głównym projektu. Ten plik służy do konfigurowania podstawowego układu, który umożliwia użytkownikowi umieszczenie połączenia wideo 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>
Następujące klasy i interfejsy obsługują niektóre główne funkcje zestawu AZURE Communication Services Calling SDK:
Nazwa/nazwisko | opis |
---|---|
CallClient |
Główny punkt wejścia do zestawu SDK wywołującego. |
AzureCommunicationTokenCredential |
Implementuje CommunicationTokenCredential interfejs używany do tworzenia wystąpienia callAgent elementu . |
CallAgent |
Służy do uruchamiania wywołań i zarządzania nimi. |
DeviceManager |
Służy do zarządzania urządzeniami multimedialnymi. |
Call |
Służy do reprezentowania wywołania |
LocalVideoStream |
Służy do tworzenia lokalnego strumienia wideo dla urządzenia aparatu w systemie lokalnym. |
RemoteParticipant |
Służy do reprezentowania uczestnika zdalnego w wywołaniu |
RemoteVideoStream |
Służy do reprezentowania zdalnego strumienia wideo z uczestnika zdalnego. |
Utwórz plik w katalogu głównym projektu o nazwie index.js
, aby zawierał logikę aplikacji na potrzeby tego przewodnika Szybki start. Dodaj następujący kod do 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();
});
Utwórz plik w katalogu głównym projektu o nazwie styles.css
, aby zawierał styl aplikacji na potrzeby tego przewodnika Szybki start. Dodaj następujący kod, aby 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); }
}
Dodawanie kodu serwera lokalnego webpack
Utwórz plik w katalogu głównym projektu o nazwie webpack.config.js zawierający logikę serwera lokalnego dla tego przewodnika Szybki start. Dodaj następujący kod, aby 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'
]
}),
]
};
Uruchamianie kodu
Użyj polecenia , webpack-dev-server
aby skompilować i uruchomić aplikację. Uruchom następujące polecenie, aby utworzyć pakiet hosta aplikacji na lokalnym serwerze internetowym:
npx webpack serve --config webpack.config.js
Otwórz przeglądarkę i na dwóch kartach przejdź, aby http://localhost:8080/.You wyświetlić następujący ekran:
Na pierwszej karcie wprowadź prawidłowy token dostępu użytkownika, a na drugiej karcie wprowadź inny prawidłowy token dostępu użytkownika.
Zapoznaj się z dokumentacją tokenu dostępu użytkownika, jeśli nie masz jeszcze dostępnych tokenów do użycia.
Na obu kartach kliknij przyciski "Inicjowanie agenta połączeń". Powinien zostać wyświetlony następujący ekran:
Na pierwszej karcie wprowadź tożsamość użytkownika usług Azure Communication Services na drugiej karcie, a następnie kliknij przycisk "Rozpocznij połączenie". Pierwsza karta uruchamia połączenie wychodzące na drugą kartę, a drugi przycisk "Akceptuj połączenie" zostanie włączony:
Na drugiej karcie kliknij przycisk "Akceptuj połączenie", a wywołanie rozpoczyna się i nawiąż połączenie. Powinien zostać wyświetlony następujący ekran:
Obie karty są teraz pomyślnie w wywołaniu wideo od 1 do 1. Obie karty mogą usłyszeć dźwięk i zobaczyć się nawzajem strumieniowo wideo.
Rozpocznij pracę z usługami Azure Communication Services przy użyciu usług komunikacyjnych wywołujących bibliotekę klienta, aby dodać wywołanie wideo do aplikacji. Dowiedz się, jak dołączać połączenia wideo 1:1 oraz jak tworzyć lub dołączać wywołania grupowe. Ponadto możesz rozpocząć, odpowiedzieć i dołączyć do połączenia wideo przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla systemu Android.
Jeśli chcesz rozpocząć pracę z przykładowym kodem, możesz pobrać przykładową aplikację.
Wymagania wstępne
Konto platformy Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
Program Android Studio do tworzenia aplikacji systemu Android.
Wdrożony zasób usług komunikacyjnych. Utwórz zasób usług komunikacyjnych. Musisz zarejestrować parametry połączenia na potrzeby tego przewodnika Szybki start.
Token dostępu użytkownika dla usługi Azure Communication Service. Możesz również użyć interfejsu wiersza polecenia platformy Azure i uruchomić polecenie za pomocą parametry połączenia, aby utworzyć użytkownika i token dostępu.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Aby uzyskać szczegółowe informacje, zobacz Tworzenie tokenów dostępu za pomocą interfejsu wiersza polecenia platformy Azure i zarządzanie nimi.
Tworzenie aplikacji systemu Android z pustym działaniem
W programie Android Studio wybierz pozycję Uruchom nowy projekt programu Android Studio.
W obszarze Telefon i tablet wybierz szablon projektu Empty Activity (Puste działanie ).
W polu Minimalny zestaw SDK wybierz pozycję INTERFEJS API 26: Android 8.0 (Oreo) lub nowszy. Zobacz Wersje obsługi zestawu SDK.
Instalowanie pakietu
Znajdź poziom build.gradle
projektu i dodaj mavenCentral()
go do listy repozytoriów w obszarze buildscript
i allprojects
buildscript {
repositories {
...
mavenCentral()
...
}
}
allprojects {
repositories {
...
mavenCentral()
...
}
}
Następnie na poziomie build.gradle
modułu dodaj następujące wiersze do dependencies
sekcji i 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'
...
}
Dodawanie uprawnień do manifestu aplikacji
Aby zażądać uprawnień wymaganych do wykonania wywołania, musisz najpierw zadeklarować uprawnienia w manifeście aplikacji (app/src/main/AndroidManifest.xml
). Zastąp zawartość pliku następującym kodem:
<?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>
Konfigurowanie układu aplikacji
Potrzebujesz wprowadzania tekstu dla identyfikatora wywoływanego lub identyfikatora wywołania grupy, przycisku do umieszczenia wywołania i dodatkowego przycisku w celu zawieszenia wywołania.
Potrzebujesz również dwóch przycisków, aby włączyć i wyłączyć lokalne wideo. Należy umieścić dwa kontenery dla lokalnych i zdalnych strumieni wideo. Możesz dodać te przyciski za pośrednictwem projektanta lub edytując kod XML układu.
Przejdź do pozycji app/src/main/res/layout/activity_main.xml i zastąp zawartość pliku następującym kodem:
<?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>
Tworzenie szkieletu i powiązań głównego działania
Po utworzeniu układu można dodać powiązania i podstawowe szkielety działania. Działanie obsługuje żądanie uprawnień środowiska uruchomieniowego, tworzenie agenta wywołania i umieszczanie wywołania po naciśnięciu przycisku.
Metoda onCreate
jest zastępowana w celu wywołania getAllPermissions
metody i createAgent
i dodania powiązań dla przycisku wywołania. To zdarzenie występuje tylko raz po utworzeniu działania. Aby uzyskać więcej informacji na temat onCreate
programu , zobacz przewodnik Omówienie cyklu życia działania.
Przejdź do pliku MainActivity.java i zastąp zawartość następującym kodem:
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
}
}
Żądanie uprawnień w czasie wykonywania
W przypadku systemu Android w wersji 6.0 lub nowszej (poziom 23 interfejsu API) i targetSdkVersion
23 lub nowszych uprawnienia są przyznawane w czasie wykonywania zamiast podczas instalowania aplikacji. Aby można było go obsługiwać, getAllPermissions
można zaimplementować wywołanie ActivityCompat.checkSelfPermission
i ActivityCompat.requestPermissions
dla każdego wymaganego uprawnienia.
/**
* 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);
}
}
Uwaga
Podczas projektowania aplikacji należy wziąć pod uwagę, kiedy należy zażądać tych uprawnień. Uprawnienia powinny być wymagane, ponieważ są potrzebne, a nie przed upływem czasu. Aby uzyskać więcej informacji, zobacz Przewodnik po uprawnieniach systemu Android.
Model obiektów
Następujące klasy i interfejsy obsługują niektóre główne funkcje zestawu AZURE Communication Services Calling SDK:
Nazwa/nazwisko | opis |
---|---|
CallClient |
Główny punkt wejścia do zestawu SDK wywołującego. |
CallAgent |
Służy do uruchamiania wywołań i zarządzania nimi. |
CommunicationTokenCredential |
Używany jako poświadczenie tokenu w celu utworzenia wystąpienia elementu CallAgent . |
CommunicationIdentifier |
Używany jako inny typ uczestnika, który może być częścią połączenia. |
Tworzenie agenta na podstawie tokenu dostępu użytkownika
Aby utworzyć uwierzytelnionego agenta wywołania, potrzebny jest token użytkownika. Zazwyczaj ten token jest generowany na podstawie usługi z uwierzytelnianiem specyficznym dla aplikacji. Aby uzyskać więcej informacji na temat tokenów dostępu użytkowników, zobacz Tokeny dostępu użytkowników.
W tym przewodniku Szybki start zastąp <User_Access_Token>
ciąg tokenem dostępu użytkownika wygenerowanym dla zasobu usług Azure Communication Services.
/**
* 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();
}
}
Uruchamianie połączenia wideo przy użyciu agenta połączeń
Połączenie można umieścić przy użyciu agenta połączeń. Wystarczy podać listę identyfikatorów wywoływanych i opcji wywołania.
Aby umieścić połączenie za pomocą wideo, należy wyliczyć lokalne kamery przy użyciu interfejsu deviceManager
getCameras
API. Po wybraniu żądanego aparatu użyj go do konstruowania LocalVideoStream
wystąpienia. Następnie przekaż go videoOptions
jako element w tablicy localVideoStream
do metody wywołania. Po nawiązaniu połączenia następuje automatyczne wysłanie strumienia wideo z wybranego aparatu do innego uczestnika.
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);
}
W tym przewodniku Szybki start użyjesz funkcji getNextAvailableCamera
, aby wybrać aparat używany przez wywołanie. Funkcja pobiera wyliczenie aparatów jako dane wejściowe i iteruje przez listę, aby uzyskać dostęp do następnego aparatu. Jeśli argument to null
, funkcja wybiera pierwsze urządzenie na liście. Jeśli podczas wybierania opcji Rozpocznij połączenie nie ma dostępnych kamer, zostanie uruchomione wywołanie audio. Ale jeśli zdalny uczestnik odpowiedział wideo, nadal możesz zobaczyć zdalny strumień wideo.
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);
}
Po utworzeniu LocalVideoStream
wystąpienia można utworzyć moduł renderowania w celu wyświetlenia go w interfejsie użytkownika.
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);
});
}
Aby umożliwić użytkownikowi przełączenie lokalnego źródła wideo, użyj polecenia switchSource
. Ta metoda wybiera następny dostępny aparat i definiuje go jako strumień lokalny.
public void switchSource() {
if (currentVideoStream != null) {
try {
currentCamera = getNextAvailableCamera(currentCamera);
currentVideoStream.switchSource(currentCamera).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Akceptowanie połączenia przychodzącego
Możesz uzyskać połączenie przychodzące, subskrybując polecenie addOnIncomingCallListener
w dniu callAgent
.
private void handleIncomingCall() {
callAgent.addOnIncomingCallListener((incomingCall) -> {
this.incomingCall = incomingCall;
Executors.newCachedThreadPool().submit(this::answerIncomingCall);
});
}
Aby zaakceptować połączenie z kamerą wideo, należy wyliczyć lokalne kamery przy użyciu interfejsu deviceManager
getCameras
API. Wybierz kamerę i skonstruuj LocalVideoStream
wystąpienie. Przekaż go acceptCallOptions
przed wywołaniem accept
metody w call
obiekcie .
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);
}
Zdalny uczestnik i zdalne strumienie wideo
Wszyscy uczestnicy zdalni są dostępni za pośrednictwem metody w wystąpieniu getRemoteParticipants()
wywołania. Po nawiązaniu połączenia (CallState.CONNECTED
) możemy uzyskać dostęp do zdalnych uczestników połączenia i obsługiwać zdalne strumienie wideo.
Po rozpoczęciu połączenia lub odebraniu połączenia przychodzącego należy zasubskrybować addOnRemoteParticipantsUpdatedListener
wydarzenie, aby obsługiwać uczestników zdalnych.
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
Jeśli używasz odbiorników zdarzeń zdefiniowanych w tej samej klasie, powiąż odbiornik ze zmienną. Przekaż zmienną jako argument, aby dodać i usunąć metody odbiornika.
Jeśli próbujesz przekazać odbiornik bezpośrednio jako argument, utracisz odwołanie do tego odbiornika. Język Java tworzy nowe wystąpienia tych odbiorników, nie odwołując się do utworzonych wcześniej. Nie można usunąć wcześniejszych wystąpień, ponieważ nie masz do nich odwołania.
Uwaga
Gdy użytkownik dołącza do wywołania, może uzyskać dostęp do bieżących uczestników zdalnych za pośrednictwem getRemoteParticipants()
metody . Wydarzenie addOnRemoteParticipantsUpdatedListener
nie zostanie wyzwolne dla tych istniejących uczestników. To zdarzenie zostanie wyzwolone tylko wtedy, gdy uczestnik zdalny dołączy lub opuści połączenie, gdy użytkownik jest już w wywołaniu.
Aktualizacje zdalnego strumienia wideo
W przypadku wywołania 1:1 należy obsługiwać dodanych uczestników. Po usunięciu uczestnika zdalnego wywołanie kończy się. Dla dodanych uczestników subskrybujesz addOnVideoStreamsUpdatedListener
obsługę aktualizacji strumienia wideo.
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();
}
}
Renderowanie zdalnych filmów wideo
Utwórz program renderujący zdalnego strumienia wideo i dołącz go do widoku, aby rozpocząć renderowanie widoku zdalnego. Usuwanie widoku w celu zatrzymania renderowania go.
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;
}
}
Aktualizacja stanu wywołania
Stan wywołania może ulec zmianie z połączonego na odłączony. Po nawiązaniu połączenia obsłużysz uczestnika zdalnego, a gdy połączenie zostanie rozłączone, możesz previewRenderer
zatrzymać lokalne wideo.
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);
}
}
Kończ połączenie
Zakończ wywołanie, wywołując hangUp()
funkcję w wystąpieniu wywołania. Usuwanie, previewRenderer
aby zatrzymać lokalne wideo.
private void hangUp() {
try {
call.hangUp().get();
switchSourceButton.setVisibility(View.INVISIBLE);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
if (previewRenderer != null) {
previewRenderer.dispose();
}
}
Ukrywanie i pokazywanie lokalnego wideo
Po uruchomieniu wywołania można zatrzymać lokalne renderowanie wideo i przesyłanie strumieniowe za pomocą turnOffLocalVideo()
polecenia , ta metoda usuwa widok, który opakowuje renderowanie lokalne i usuwa bieżący strumień. Aby wznowić strumień i ponownie wyrenderować lokalną wersję zapoznawcza, użyj turnOnLocalVideo()
metody , aby wyświetlić podgląd wideo i uruchomić przesyłanie strumieniowe.
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();
}
}
Uruchamianie kodu
Teraz możesz uruchomić aplikację przy użyciu przycisku Uruchom aplikację na pasku narzędzi programu Android Studio.
Ukończona aplikacja | Połączenie 1:1 |
---|---|
Dodawanie możliwości wywołania grupy
Teraz możesz zaktualizować aplikację, aby umożliwić użytkownikowi wybór między wywołaniami 1:1 lub wywołaniami grup.
Aktualizowanie układu
Użyj przycisków radiowych, aby wybrać, czy zestaw SDK utworzy wywołanie 1:1 lub dołączy do wywołania grupy. Przyciski radiowe znajdują się u góry, więc pierwsza sekcja pliku app/src/main/res/layout/activity_main.xml kończy się w następujący sposób.
<?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>
Aktualizowanie pliku MainActivity.Java
Teraz możesz zaktualizować elementy i logikę, aby zdecydować, kiedy utworzyć wywołanie 1:1 i kiedy dołączyć do wywołania grupy. Pierwsza część kodu wymaga aktualizacji w celu dodania zależności, elementów i innych konfiguracji.
Zależności:
import android.widget.RadioButton;
import com.azure.android.communication.calling.GroupCallLocator;
import com.azure.android.communication.calling.JoinCallOptions;
import java.util.UUID;
Elementy globalne:
RadioButton oneToOneCall, groupCall;
Aktualizacja onCreate()
:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getAllPermissions();
createAgent();
handleIncomingCall();
Button callButton = findViewById(R.id.call_button);
callButton.setOnClickListener(l -> startCall());
Button hangupButton = findViewById(R.id.hang_up);
hangupButton.setOnClickListener(l -> hangUp());
Button startVideo = findViewById(R.id.show_preview);
startVideo.setOnClickListener(l -> turnOnLocalVideo());
Button stopVideo = findViewById(R.id.hide_preview);
stopVideo.setOnClickListener(l -> turnOffLocalVideo());
switchSourceButton = findViewById(R.id.switch_source);
switchSourceButton.setOnClickListener(l -> switchSource());
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
oneToOneCall = findViewById(R.id.one_to_one_call);
oneToOneCall.setOnClickListener(this::onCallTypeSelected);
oneToOneCall.setChecked(true);
groupCall = findViewById(R.id.group_call);
groupCall.setOnClickListener(this::onCallTypeSelected);
}
Aktualizacja startCall()
:
private void startCall() {
Context context = this.getApplicationContext();
EditText callIdView = findViewById(R.id.call_id);
String callId = callIdView.getText().toString();
ArrayList<CommunicationIdentifier> participants = new ArrayList<CommunicationIdentifier>();
List<VideoDeviceInfo> cameras = deviceManager.getCameras();
if(oneToOneCall.isChecked()){
StartCallOptions options = new StartCallOptions();
if(!cameras.isEmpty()) {
currentCamera = getNextAvailableCamera(null);
currentVideoStream = new LocalVideoStream(currentCamera, context);
LocalVideoStream[] videoStreams = new LocalVideoStream[1];
videoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(videoStreams);
options.setVideoOptions(videoOptions);
showPreview(currentVideoStream);
}
participants.add(new CommunicationUserIdentifier(callId));
call = callAgent.startCall(
context,
participants,
options);
}
else{
JoinCallOptions options = new JoinCallOptions();
if(!cameras.isEmpty()) {
currentCamera = getNextAvailableCamera(null);
currentVideoStream = new LocalVideoStream(currentCamera, context);
LocalVideoStream[] videoStreams = new LocalVideoStream[1];
videoStreams[0] = currentVideoStream;
VideoOptions videoOptions = new VideoOptions(videoStreams);
options.setVideoOptions(videoOptions);
showPreview(currentVideoStream);
}
GroupCallLocator groupCallLocator = new GroupCallLocator(UUID.fromString(callId));
call = callAgent.join(
context,
groupCallLocator,
options);
}
remoteParticipantUpdatedListener = this::handleRemoteParticipantsUpdate;
onStateChangedListener = this::handleCallOnStateChanged;
call.addOnRemoteParticipantsUpdatedListener(remoteParticipantUpdatedListener);
call.addOnStateChangedListener(onStateChangedListener);
}
Dodaj onCallTypeSelected()
polecenie :
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;
}
}
Uruchamianie uaktualnionej aplikacji
Na tym etapie możesz uruchomić aplikację przy użyciu przycisku Uruchom aplikację na pasku narzędzi programu Android Studio.
Aktualizacja ekranu | Rozmowa grupowa |
---|---|
Rozpocznij pracę z usługami Azure Communication Services przy użyciu zestawu SDK wywołującego usługi Communication Services, aby dodać go do jednej usługi wideo wywołującej aplikację. Dowiesz się, jak uruchomić i odpowiedzieć na połączenie wideo przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla systemu iOS.
Przykładowy kod
Jeśli chcesz przejść do końca, możesz pobrać ten przewodnik Szybki start jako przykład w usłudze GitHub.
Wymagania wstępne
Uzyskaj konto platformy Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
Komputer Mac z uruchomionym programem Xcode wraz z prawidłowym certyfikatem dewelopera zainstalowanym w pęku kluczy.
Utwórz aktywny zasób usług komunikacyjnych. Utwórz zasób usług komunikacyjnych. Musisz zarejestrować parametry połączenia na potrzeby tego przewodnika Szybki start.
Token dostępu użytkownika dla usługi Azure Communication Service. Możesz również użyć interfejsu wiersza polecenia platformy Azure i uruchomić polecenie za pomocą parametry połączenia, aby utworzyć użytkownika i token dostępu.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Aby uzyskać szczegółowe informacje, zobacz Tworzenie tokenów dostępu za pomocą interfejsu wiersza polecenia platformy Azure i zarządzanie nimi.
Konfigurowanie
Tworzenie projektu Xcode
W programie Xcode utwórz nowy projekt systemu iOS i wybierz szablon Aplikacja z jednym widokiem. W tym samouczku jest używana struktura SwiftUI, dlatego należy ustawić język na swift i interfejs użytkownika na swiftUI. Podczas tego przewodnika Szybki start nie utworzysz testów. Możesz usunąć zaznaczenie pola wyboru Uwzględnij testy.
Instalowanie platformy CocoaPods
Skorzystaj z tego przewodnika, aby zainstalować narzędzie CocoaPods na komputerze Mac.
Instalowanie pakietu i zależności za pomocą narzędzia CocoaPods
Aby utworzyć aplikację
Podfile
dla aplikacji, otwórz terminal i przejdź do folderu projektu i uruchom init zasobnika.Dodaj następujący kod do pliku
Podfile
i zapisz. Zobacz Wersje obsługi zestawu SDK.
platform :ios, '13.0'
use_frameworks!
target 'VideoCallingQuickstart' do
pod 'AzureCommunicationCalling', '~> 1.0.0'
end
Uruchom instalację zasobnika.
Otwórz plik za
.xcworkspace
pomocą programu Xcode.
Bezpośrednie używanie elementu XCFramework
Jeśli nie używasz CocoaPods
go jako menedżera zależności, możesz pobrać go AzureCommunicationCalling.xcframework
bezpośrednio ze strony wydania.
Ważne jest, aby wiedzieć, że AzureCommunicationCalling
ma zależność, AzureCommunicationCommon
więc musisz go zainstalować, jak również w projekcie.
Uwaga
Chociaż AzureCommunicationCommon
jest to czysty pakiet swift, nie można zainstalować go za pomocą polecenia Swift Package Manager
AzureCommunicationCalling
, ponieważ ten ostatni jest strukturą Objective-C i Swift Package Manager
celowo nie obsługują nagłówków interfejsu Swift ObjC zgodnie z projektem, co oznacza, że nie można współpracować ze AzureCommunicationCalling
sobą, jeśli jest zainstalowany przy użyciu programu Swift Package Manager
. Konieczne byłoby zainstalowanie za pośrednictwem innego menedżera zależności lub wygenerowanie elementu ze AzureCommunicationCommon
źródeł i zaimportowanie xcframework
do projektu.
Żądanie dostępu do mikrofonu i kamery
Aby uzyskać dostęp do mikrofonu i aparatu fotograficznego urządzenia, musisz zaktualizować listę właściwości informacji aplikacji za pomocą elementu NSMicrophoneUsageDescription
i NSCameraUsageDescription
. Skojarzona wartość jest ustawiana na ciąg zawierający okno dialogowe używane przez system do żądania dostępu od użytkownika.
Kliknij prawym przyciskiem myszy Info.plist
wpis drzewa projektu i wybierz pozycję Otwórz jako > kod źródłowy. Dodaj następujące wiersze sekcji najwyższego poziomu <dict>
, a następnie zapisz plik.
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>
<key>NSCameraUsageDescription</key>
<string>Need camera access for video calling</string>
Konfigurowanie struktury aplikacji
Otwórz plik projektu ContentView.swift
i dodaj deklarację importu na początku pliku, aby zaimportować bibliotekę AzureCommunicationCalling
i AVFoundation
. Funkcja AVFoundation służy do przechwytywania uprawnień dźwięku z kodu.
import AzureCommunicationCalling
import AVFoundation
Model obiektów
Następujące klasy i interfejsy obsługują niektóre główne funkcje zestawu SDK wywołującego usługi Azure Communication Services dla systemu iOS.
Nazwa/nazwisko | opis |
---|---|
CallClient |
Jest CallClient to główny punkt wejścia do zestawu SDK wywołującego. |
CallAgent |
Służy do uruchamiania CallAgent wywołań i zarządzania nimi. |
CommunicationTokenCredential |
Element CommunicationTokenCredential jest używany jako poświadczenie tokenu w celu utworzenia wystąpienia elementu CallAgent . |
CommunicationIdentifier |
Element CommunicationIdentifier służy do reprezentowania tożsamości użytkownika, która może być jedną z następujących opcji: CommunicationUserIdentifier , PhoneNumberIdentifier lub CallingApplication . |
Tworzenie agenta połączeń
Zastąp implementację elementu ContentView struct
kilkoma prostymi kontrolkami interfejsu użytkownika, które umożliwiają użytkownikowi zainicjowanie i zakończenie wywołania. W tym przewodniku Szybki start dodamy logikę biznesową do tych kontrolek.
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()
}
}
Uwierzytelnianie użytkownika
Aby zainicjować CallAgent
wystąpienie, potrzebny jest token dostępu użytkownika, który umożliwia wykonywanie i odbieranie wywołań. Jeśli nie masz dostępnego tokenu dostępu, zapoznaj się z dokumentacją tokenu dostępu użytkownika.
Po utworzeniu tokenu dodaj następujący kod do wywołania zwrotnego onAppear
w pliku ContentView.swift
. Musisz zastąpić <USER ACCESS TOKEN>
prawidłowym tokenem dostępu użytkownika dla zasobu:
var userCredential: CommunicationTokenCredential?
do {
userCredential = try CommunicationTokenCredential(token: "<USER ACCESS TOKEN>")
} catch {
print("ERROR: It was not possible to create user credential.")
return
}
Inicjowanie klasy CallAgent i uzyskiwanie dostępu Menedżer urządzeń
Aby utworzyć wystąpienie callAgent z obiektu CallClient, użyj callClient.createCallAgent
metody, która asynchronicznie zwraca obiekt CallAgent po zainicjowaniu. DeviceManager umożliwia wyliczanie urządzeń lokalnych, które mogą być używane w wywołaniu do przesyłania strumieni audio/wideo. Umożliwia również zażądanie uprawnień od użytkownika w celu uzyskania dostępu do mikrofonu/kamery.
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")
}
}
}
}
Poproś o uprawnienia
Musimy dodać następujący kod do wywołania zwrotnego onAppear
, aby poprosić o uprawnienia do audio i wideo.
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
if granted {
AVCaptureDevice.requestAccess(for: .video) { (videoGranted) in
/* NO OPERATION */
}
}
}
Konfigurowanie sesji audio
Obiekt służy AVAudioSession
do konfigurowania sesji audio aplikacji. Oto przykład włączania urządzenia audio Bluetooth dla aplikacji:
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
}
Wyświetlanie lokalnego wideo
Przed rozpoczęciem połączenia można zarządzać ustawieniami związanymi z wideo. W tym przewodniku Szybki start przedstawimy implementację przełączania lokalnego wideo przed lub podczas wywołania.
Najpierw musimy uzyskać dostęp do lokalnych kamer za pomocą polecenia deviceManager
. Po wybraniu żądanego aparatu możemy skonstruować LocalVideoStream
i utworzyć element VideoStreamRenderer
, a następnie dołączyć go do previewView
elementu . Podczas rozmowy możemy użyć startVideo
polecenia lub stopVideo
rozpocząć lub zatrzymać wysyłanie LocalVideoStream
do uczestników zdalnych. Ta funkcja działa również z obsługą wywołań przychodzących.
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
}
}
}
}
}
Umieszczanie połączenia wychodzącego
Metoda startCall
jest ustawiana jako akcja wykonywana po naciśnięciu przycisku Uruchom wywołanie. W tym przewodniku Szybki start połączenia wychodzące są domyślnie tylko audio. Aby rozpocząć połączenie za pomocą wideo, musimy ustawić VideoOptions
element i LocalVideoStream
przekazać go za pomocą startCallOptions
, aby ustawić początkowe opcje dla wywołania.
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
i RemoteParticipantObserver
są używane do zarządzania zdarzeniami w połowie połączenia i uczestnikami zdalnymi. Ustawiamy obserwatorów w setCallAndObserver
funkcji .
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")
}
}
Odpowiedz na połączenie przychodzące
Aby odpowiedzieć na połączenie przychodzące, zaimplementuj element , IncomingCallHandler
aby wyświetlić baner połączenia przychodzącego, aby odpowiedzieć lub odrzucić połączenie. Umieść następującą implementację w pliku 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
}
}
}
Musimy utworzyć wystąpienie klasy IncomingCallHandler
, dodając następujący kod do wywołania zwrotnego onAppear
w pliku ContentView.swift
:
let incomingCallHandler = IncomingCallHandler.getOrCreateInstance()
incomingCallHandler.contentView = self
Ustaw delegata na callAgent po pomyślnym utworzeniu callAgent:
self.callAgent!.delegate = incomingCallHandler
Po wywołaniu przychodzącym funkcja wywołuje funkcję showIncomingCallBanner
w IncomingCallHandler
celu wyświetlenia answer
i decline
przycisku.
func showIncomingCallBanner(_ incomingCall: IncomingCall?) {
isIncomingCall = true
self.incomingCall = incomingCall
}
Akcje dołączone do answer
i decline
są implementowane jako następujący kod. Aby odpowiedzieć na połączenie wideo, musimy włączyć lokalne wideo i ustawić opcje za AcceptCallOptions
localVideoStream
pomocą polecenia .
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
}
Strumienie wideo uczestnika zdalnego
Możemy utworzyć klasę RemoteVideoStreamData
do obsługi renderowania strumieni wideo uczestnika zdalnego.
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)
}
}
Subskrybowanie zdarzeń
Możemy zaimplementować klasę CallObserver
, aby subskrybować kolekcję zdarzeń, aby otrzymywać powiadomienia, gdy wartości zmieniają się podczas wywołania.
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
}
}
Zdalne zarządzanie uczestnikami
Wszyscy uczestnicy zdalni są reprezentowani z RemoteParticipant
typem i są dostępni za pośrednictwem remoteParticipants
kolekcji w wystąpieniu wywołania po nawiązaniu połączenia (CallState.connected
).
Uwaga
Gdy użytkownik dołącza do wywołania, może uzyskać dostęp do bieżących uczestników zdalnych za pośrednictwem kolekcji RemoteParticipants
. Wydarzenie didUpdateRemoteParticipant
nie zostanie wyzwolne dla tych istniejących uczestników. To zdarzenie zostanie wyzwolone tylko wtedy, gdy uczestnik zdalny dołączy lub opuści połączenie, gdy użytkownik jest już w wywołaniu.
Możemy zaimplementować klasę RemoteParticipantObserver
, aby zasubskrybować aktualizacje zdalnych strumieni wideo uczestników zdalnych.
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()
}
}
}
Uruchamianie kodu
Aplikację można skompilować i uruchomić w symulatorze systemu iOS, wybierając pozycję Uruchom produkt > lub za pomocą skrótu klawiaturowego (⌘-R).
Z tego przewodnika Szybki start dowiesz się, jak uruchomić połączenie wideo 1:1 przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla systemu Windows.
Przykładowy kod platformy UWP
Wymagania wstępne
Do wykonania kroków tego samouczka niezbędne jest spełnienie następujących wymagań wstępnych:
Konto platformy Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
Zainstaluj program Visual Studio 2022 z pakietem roboczym programowania platforma uniwersalna systemu Windows.
Wdrożony zasób usług komunikacyjnych. Utwórz zasób usług komunikacyjnych. Musisz zarejestrować parametry połączenia na potrzeby tego przewodnika Szybki start.
Token dostępu użytkownika dla usługi Azure Communication Service. Możesz również użyć interfejsu wiersza polecenia platformy Azure i uruchomić polecenie za pomocą parametry połączenia, aby utworzyć użytkownika i token dostępu.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Aby uzyskać szczegółowe informacje, zobacz Tworzenie tokenów dostępu za pomocą interfejsu wiersza polecenia platformy Azure i zarządzanie nimi.
Konfigurowanie
Tworzenie projektu
W programie Visual Studio utwórz nowy projekt przy użyciu szablonu Pusta aplikacja (uniwersalny system Windows), aby skonfigurować jednostronicową aplikację platforma uniwersalna systemu Windows (UWP).
Instalowanie pakietu
Kliknij prawym przyciskiem myszy projekt i przejdź do Manage Nuget Packages
witryny , aby zainstalować Azure.Communication.Calling.WindowsClient
wersję 1.2.0-beta.1 lub nowszą. Upewnij się, że zaznaczono opcję Uwzględnij wersję Preleased.
Żądanie dostępu
Przejdź do Package.appxmanifest
strony i kliknij pozycję Capabilities
.
Sprawdź Internet (Client & Server)
, czy uzyskać dostęp przychodzący i wychodzący do Internetu.
Sprawdź Microphone
, czy uzyskać dostęp do kanału audio mikrofonu.
Sprawdź WebCam
, czy chcesz uzyskać dostęp do aparatu urządzenia.
Dodaj następujący kod do pliku Package.appxmanifest
, klikając prawym przyciskiem myszy i wybierając polecenie Wyświetl kod.
<Extensions>
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>RtmMvrUap.dll</Path>
<ActivatableClass ActivatableClassId="VideoN.VideoSchemeHandler" ThreadingModel="both" />
</InProcessServer>
</Extension>
</Extensions>
Konfigurowanie struktury aplikacji
Musimy skonfigurować podstawowy układ, aby dołączyć naszą logikę. Aby umieścić wywołanie wychodzące, musimy TextBox
podać identyfikator użytkownika wywoływanego. Potrzebujemy Start Call
również przycisku i Hang Up
przycisku.
Musimy również wyświetlić podgląd lokalnego wideo i renderować zdalne wideo innego uczestnika. Potrzebujemy więc dwóch elementów do wyświetlania strumieni wideo.
MainPage.xaml
Otwórz projekt i zastąp zawartość następującą implementacją.
<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>
Otwórz do App.xaml.cs
(kliknij prawym przyciskiem myszy i wybierz polecenie Wyświetl kod) i dodaj ten wiersz do góry:
using CallingQuickstart;
Otwórz (kliknij prawym przyciskiem MainPage.xaml.cs
myszy i wybierz pozycję Wyświetl kod) i zastąp zawartość następującą implementacją:
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
}
}
}
Model obiektów
Następujące klasy i interfejsy obsługują niektóre główne funkcje zestawu AZURE Communication Services Calling SDK:
Nazwa/nazwisko | opis |
---|---|
CallClient |
Jest CallClient to główny punkt wejścia do biblioteki wywołującej klienta. |
CallAgent |
Element służy do uruchamiania CallAgent i dołączania wywołań. |
CommunicationCall |
Służy CommunicationCall do zarządzania umieszczonymi lub sprzężonym wywołaniami. |
CallTokenCredential |
Element CallTokenCredential jest używany jako poświadczenie tokenu w celu utworzenia wystąpienia elementu CallAgent . |
CommunicationUserIdentifier |
Element CommunicationUserIdentifier służy do reprezentowania tożsamości użytkownika, która może być jedną z następujących opcji: CommunicationUserIdentifier , PhoneNumberIdentifier lub CallingApplication . |
Uwierzytelnianie użytkownika
Aby zainicjować element CallAgent
, potrzebujesz tokenu dostępu użytkownika. Zazwyczaj ten token jest generowany na podstawie usługi z uwierzytelnianiem specyficznym dla aplikacji. Aby uzyskać więcej informacji na temat tokenów dostępu użytkowników, zapoznaj się z przewodnikiem Tokeny dostępu użytkowników.
W tym przewodniku Szybki start zastąp <AUTHENTICATION_TOKEN>
ciąg tokenem dostępu użytkownika wygenerowanym dla zasobu usługi Azure Communication Service.
Po utworzeniu tokenu zainicjuj CallAgent
wystąpienie, co umożliwi nam wykonywanie i odbieranie wywołań. Aby uzyskać dostęp do kamer na urządzeniu, musimy również uzyskać Menedżer urządzeń wystąpienie.
Dodaj następujący kod do InitCallAgentAndDeviceManagerAsync
funkcji .
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
}
}
Rozpoczynanie połączenia za pomocą wideo
Dodaj implementację do CallButton_Click
elementu , aby rozpocząć wywołanie za pomocą wideo. Musimy wyliczyć aparaty za pomocą wystąpienia menedżera urządzeń i skonstruować LocalOutgoingVideoStream
element . Musimy ustawić VideoOptions
element i LocalVideoStream
przekazać go za startCallOptions
pomocą , aby ustawić początkowe opcje dla wywołania. Dołączając LocalOutgoingVideoStream
do pliku MediaElement
, możemy zobaczyć podgląd lokalnego wideo.
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;
}
Dodaj metody uruchamiania lub dołączania do różnych typów połączeń (połączenie z usługami Azure Communication Services 1:1, połączenie telefoniczne 1:1, połączenie grupy usług Azure Communication Services, dołączanie do spotkania w usłudze Teams itp.).
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 } }
};
}
Dodaj kod, aby utworzyć element LocalVideoStream w zależności od wybranej kamery w metodzie 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);
}
Akceptowanie połączenia przychodzącego
Dodaj implementację do OnIncomingCallAsync
elementu , aby odpowiedzieć na połączenie przychodzące za pomocą wideo, przekaż element do acceptCallOptions
.LocalVideoStream
var incomingCall = args.IncomingCall;
var acceptCallOptions = new AcceptCallOptions() {
IncomingVideoOptions = new IncomingVideoOptions()
{
IncomingVideoStreamKind = VideoStreamKind.RemoteIncoming
}
};
_ = await incomingCall.AcceptAsync(acceptCallOptions);
Zdalny uczestnik i zdalne strumienie wideo
Wszyscy uczestnicy zdalni są dostępni za pośrednictwem kolekcji w wystąpieniu RemoteParticipants
wywołania. Po nawiązaniu połączenia (CallState.Connected
) możemy uzyskać dostęp do zdalnych uczestników połączenia i obsługiwać zdalne strumienie wideo.
Uwaga
Gdy użytkownik dołącza do wywołania, może uzyskać dostęp do bieżących uczestników zdalnych za pośrednictwem kolekcji RemoteParticipants
. Wydarzenie RemoteParticipantsUpdated
nie zostanie wyzwolne dla tych istniejących uczestników. To zdarzenie zostanie wyzwolone tylko wtedy, gdy uczestnik zdalny dołączy lub opuści połączenie, gdy użytkownik jest już w wywołaniu.
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;
}
}
Renderowanie zdalnych filmów wideo
Dla każdego zdalnego strumienia wideo dołącz go do .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();
});
}
}
Aktualizacja stanu wywołania
Musimy wyczyścić programy renderujących wideo po rozłączeniu wywołania i obsłużyć przypadek, gdy uczestnicy zdalni początkowo dołączą do wywołania.
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;
}
}
Kończ połączenie
Zakończ bieżące wywołanie po kliknięciu Hang Up
przycisku. Dodaj implementację do HangupButton_Click, aby zakończyć wywołanie utworzonego przez nas agenta callAgent i usunąć aktualizację uczestnika i wywołać programy obsługi zdarzeń stanu.
var call = this.callAgent?.Calls?.FirstOrDefault();
if (call != null)
{
try
{
await call.HangUpAsync(new HangUpOptions() { ForEveryone = true });
}
catch(Exception ex)
{
}
}
Uruchamianie kodu
Możesz skompilować i uruchomić kod w programie Visual Studio. W przypadku platform rozwiązań obsługujemy ARM64
platformy , x64
i x86
.
Możesz wykonać wychodzące połączenie wideo, podając identyfikator użytkownika w polu tekstowym i klikając Start Call
przycisk.
Uwaga: wywołanie 8:echo123
zatrzymuje strumień wideo, ponieważ bot echo nie obsługuje przesyłania strumieniowego wideo.
Aby uzyskać więcej informacji na temat identyfikatorów użytkowników (tożsamości), zapoznaj się z przewodnikiem Tokeny dostępu użytkowników.
Przykładowy kod WinUI 3
Wymagania wstępne
Do wykonania kroków tego samouczka niezbędne jest spełnienie następujących wymagań wstępnych:
Konto platformy Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
Zainstaluj programy Visual Studio 2022 i Windows App SDK w wersji 1.2 (wersja zapoznawcza 2).
Podstawowa wiedza na temat tworzenia aplikacji WinUI 3. Utwórz swój pierwszy projekt WinUI 3 (Windows App SDK) to dobry zasób na początek.
Wdrożony zasób usług komunikacyjnych. Utwórz zasób usług komunikacyjnych. Musisz zarejestrować parametry połączenia na potrzeby tego przewodnika Szybki start.
Token dostępu użytkownika dla usługi Azure Communication Service. Możesz również użyć interfejsu wiersza polecenia platformy Azure i uruchomić polecenie za pomocą parametry połączenia, aby utworzyć użytkownika i token dostępu.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Aby uzyskać szczegółowe informacje, zobacz Tworzenie tokenów dostępu za pomocą interfejsu wiersza polecenia platformy Azure i zarządzanie nimi.
Konfigurowanie
Tworzenie projektu
W programie Visual Studio utwórz nowy projekt przy użyciu szablonu Blank App, Packaged (WinUI 3 in Desktop), aby skonfigurować jednostronicową aplikację WinUI 3.
Instalowanie pakietu
Kliknij prawym przyciskiem myszy projekt i przejdź do Manage Nuget Packages
witryny , aby zainstalować Azure.Communication.Calling.WindowsClient
wersję 1.0.0 lub wyższą. Upewnij się, że zaznaczono opcję Uwzględnij wersję Preleased.
Żądanie dostępu
Dodaj następujący kod do pliku app.manifest
:
<file name="RtmMvrMf.dll">
<activatableClass name="VideoN.VideoSchemeHandler" threadingModel="both" xmlns="urn:schemas-microsoft-com:winrt.v1" />
</file>
Konfigurowanie struktury aplikacji
Musimy skonfigurować podstawowy układ, aby dołączyć naszą logikę. Aby umieścić wywołanie wychodzące, musimy TextBox
podać identyfikator użytkownika wywoływanego. Potrzebujemy Start Call
również przycisku i Hang Up
przycisku.
Musimy również wyświetlić podgląd lokalnego wideo i renderować zdalne wideo innego uczestnika. Potrzebujemy więc dwóch elementów do wyświetlania strumieni wideo.
MainWindow.xaml
Otwórz projekt i zastąp zawartość następującą implementacją.
<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>
Otwórz do App.xaml.cs
(kliknij prawym przyciskiem myszy i wybierz polecenie Wyświetl kod) i dodaj ten wiersz do góry:
using CallingQuickstart;
Otwórz (kliknij prawym przyciskiem MainWindow.xaml.cs
myszy i wybierz pozycję Wyświetl kod) i zastąp zawartość następującą implementacją:
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();
});
}
}
}
Model obiektów
Następujące klasy i interfejsy obsługują niektóre główne funkcje zestawu AZURE Communication Services Calling SDK:
Nazwa/nazwisko | opis |
---|---|
CallClient |
Jest CallClient to główny punkt wejścia do biblioteki wywołującej klienta. |
CallAgent |
Element służy do uruchamiania CallAgent i dołączania wywołań. |
CommunicationCall |
Służy CommunicationCall do zarządzania umieszczonymi lub sprzężonym wywołaniami. |
CallTokenCredential |
Element CallTokenCredential jest używany jako poświadczenie tokenu w celu utworzenia wystąpienia elementu CallAgent . |
CommunicationUserIdentifier |
Element CommunicationUserIdentifier służy do reprezentowania tożsamości użytkownika, która może być jedną z następujących opcji: CommunicationUserIdentifier , PhoneNumberIdentifier lub CallingApplication . |
Uwierzytelnianie użytkownika
Aby zainicjować element CallAgent
, potrzebujesz tokenu dostępu użytkownika. Zazwyczaj ten token jest generowany na podstawie usługi z uwierzytelnianiem specyficznym dla aplikacji. Aby uzyskać więcej informacji na temat tokenów dostępu użytkowników, zapoznaj się z przewodnikiem Tokeny dostępu użytkowników.
W tym przewodniku Szybki start zastąp <AUTHENTICATION_TOKEN>
ciąg tokenem dostępu użytkownika wygenerowanym dla zasobu usługi Azure Communication Service.
Po zainicjowaniu CallAgent
wystąpienia tokenu, co umożliwi nam wykonywanie i odbieranie wywołań. Aby uzyskać dostęp do kamer na urządzeniu, musimy również uzyskać Menedżer urządzeń wystąpienie.
Dodaj następujący kod do InitCallAgentAndDeviceManagerAsync
funkcji .
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;
Rozpoczynanie połączenia za pomocą wideo
Dodaj implementację do CallButton_Click
elementu , aby rozpocząć wywołanie za pomocą wideo. Musimy wyliczyć aparaty za pomocą wystąpienia menedżera urządzeń i skonstruować LocalVideoStream
element . Musimy ustawić VideoOptions
element i LocalVideoStream
przekazać go za startCallOptions
pomocą , aby ustawić początkowe opcje dla wywołania. Dołączając LocalVideoStream
do pliku MediaPlayerElement
, możemy zobaczyć podgląd lokalnego wideo.
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;
Akceptowanie połączenia przychodzącego
Dodaj implementację do Agent_OnIncomingCallAsync
elementu , aby odpowiedzieć na połączenie przychodzące za pomocą wideo, przekaż element do acceptCallOptions
.LocalVideoStream
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);
Zdalny uczestnik i zdalne strumienie wideo
Wszyscy uczestnicy zdalni są dostępni za pośrednictwem kolekcji w wystąpieniu RemoteParticipants
wywołania. Po nawiązaniu połączenia możemy uzyskać dostęp do zdalnych uczestników połączenia i obsłużyć zdalne strumienie wideo.
Uwaga
Gdy użytkownik dołącza do wywołania, może uzyskać dostęp do bieżących uczestników zdalnych za pośrednictwem kolekcji RemoteParticipants
. Wydarzenie OnRemoteParticipantsUpdated
nie zostanie wyzwolne dla tych istniejących uczestników. To zdarzenie zostanie wyzwolone tylko wtedy, gdy uczestnik zdalny dołączy lub opuści połączenie, gdy użytkownik jest już w wywołaniu.
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);
}
}
Renderowanie zdalnych filmów wideo
Dla każdego zdalnego strumienia wideo dołącz go do .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();
});
}
}
Aktualizacja stanu wywołania
Musimy wyczyścić programy renderujących wideo po rozłączeniu wywołania i obsłużyć przypadek, gdy uczestnicy zdalni początkowo dołączą do wywołania.
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;
}
}
Kończ połączenie
Zakończ bieżące wywołanie po kliknięciu Hang Up
przycisku. Dodaj implementację do HangupButton_Click, aby zakończyć wywołanie utworzonego przez nas agenta callAgent i usunąć aktualizację uczestnika i wywołać programy obsługi zdarzeń stanu.
this.call.OnRemoteParticipantsUpdated -= Call_OnRemoteParticipantsUpdatedAsync;
this.call.OnStateChanged -= Call_OnStateChangedAsync;
await this.call.HangUpAsync(new HangUpOptions());
Uruchamianie kodu
Możesz skompilować i uruchomić kod w programie Visual Studio. W przypadku platform rozwiązań obsługujemy ARM64
platformy , x64
i x86
.
Możesz wykonać wychodzące połączenie wideo, podając identyfikator użytkownika w polu tekstowym i klikając Start Call
przycisk.
Uwaga: wywołanie 8:echo123
zatrzymuje strumień wideo, ponieważ bot echo nie obsługuje przesyłania strumieniowego wideo.
Aby uzyskać więcej informacji na temat identyfikatorów użytkowników (tożsamości), zapoznaj się z przewodnikiem Tokeny dostępu użytkowników.
Z tego przewodnika Szybki start dowiesz się, jak uruchomić wywołanie przy użyciu zestawu SDK wywołującego usługi Azure Communication Services dla aparatu Unity. Aby odbierać i renderować ramki wideo na platformie Unity, zapoznaj się z przewodnikiem Szybki start dotyczący dostępu do multimediów pierwotnych.
Przykładową aplikację można pobrać z witryny GitHub.
Wymagania wstępne
Do wykonania kroków tego samouczka niezbędne jest spełnienie następujących wymagań wstępnych:
Konto platformy Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
Zainstaluj centrum aparatu Unity i edytor aparatu Unity przy użyciu obciążenia programistycznego platforma uniwersalna systemu Windows.
Wdrożony zasób usług komunikacyjnych. Utwórz zasób usług komunikacyjnych. Musisz zarejestrować parametry połączenia na potrzeby tego przewodnika Szybki start.
Token dostępu użytkownika dla usługi Azure Communication Service. Możesz również użyć interfejsu wiersza polecenia platformy Azure i uruchomić polecenie za pomocą parametry połączenia, aby utworzyć użytkownika i token dostępu.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Aby uzyskać szczegółowe informacje, zobacz Tworzenie tokenów dostępu za pomocą interfejsu wiersza polecenia platformy Azure i zarządzanie nimi.
Konfigurowanie
Tworzenie projektu
W usłudze Unity Hub utwórz nowy projekt z szablonem 2D Core , aby skonfigurować projekt aparatu Unity.
Instalowanie pakietu
Istnieją dwa sposoby instalowania zestawu AZURE Communication Calling SDK dla aparatu Unity.
Pobierz zestaw SDK z publicznego kanału informacyjnego npm i zaimportuj go w menedżerze pakietów edytora Aparatu Unity, który znajduje się na karcie Systemu Windows.
Pobierz narzędzie funkcji rzeczywistości mieszanej od firmy Microsoft i zainstaluj je za pośrednictwem menedżera narzędzi rzeczywistości mieszanej.
Konfigurowanie struktury aplikacji
Musimy skonfigurować podstawowy układ, aby dołączyć naszą logikę. Aby umieścić wywołanie wychodzące, musimy TextBox
podać identyfikator użytkownika wywoływanego. Potrzebujemy Start/Join call
również przycisku i Hang up
przycisku.
Utwórz nową scenę o nazwie Main
w projekcie.
Main.unity
Otwórz plik i zastąp zawartość następującą implementacją:
Kod Main.Unity
Main.unity
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 0
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 500
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &247756367
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 247756370}
- component: {fileID: 247756369}
- component: {fileID: 247756368}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &247756368
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 247756367}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &247756369
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 247756367}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &247756370
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 247756367}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &293984669
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 293984671}
- component: {fileID: 293984670}
m_Layer: 0
m_Name: AppManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &293984670
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 293984669}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c7d18b32fdb6b14e857ebb6d9627958, type: 3}
m_Name:
m_EditorClassIdentifier:
callStatus: {fileID: 1529611528}
videoPlayer: {fileID: 0}
--- !u!4 &293984671
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 293984669}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &438770860
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 438770861}
- component: {fileID: 438770863}
- component: {fileID: 438770862}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &438770861
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 438770860}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1732033234}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &438770862
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 438770860}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Start Call
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &438770863
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 438770860}
m_CullTransparentMesh: 1
--- !u!1 &519420028
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 519420032}
- component: {fileID: 519420031}
- component: {fileID: 519420029}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &519420029
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 1
--- !u!20 &519420031
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 0
m_HDR: 1
m_AllowMSAA: 0
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 0
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &519420032
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 519420028}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &857336305
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 857336306}
- component: {fileID: 857336309}
- component: {fileID: 857336308}
- component: {fileID: 857336307}
m_Layer: 5
m_Name: Placeholder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &857336306
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1787936407}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &857336307
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 1
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &857336308
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Who Would you like to call?
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 2150773298
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 2
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 1
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &857336309
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 857336305}
m_CullTransparentMesh: 1
--- !u!1 &963546686
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 963546687}
- component: {fileID: 963546690}
- component: {fileID: 963546689}
- component: {fileID: 963546688}
m_Layer: 5
m_Name: InputField (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &963546687
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1787936407}
m_Father: {fileID: 1843906927}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0.00002861, y: 327}
m_SizeDelta: {x: 1337.7578, y: 71.4853}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &963546688
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2da0c512f12947e489f739169773d7ca, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 963546689}
m_TextViewport: {fileID: 1787936407}
m_TextComponent: {fileID: 1676708954}
m_Placeholder: {fileID: 857336308}
m_VerticalScrollbar: {fileID: 0}
m_VerticalScrollbarEventHandler: {fileID: 0}
m_LayoutGroup: {fileID: 0}
m_ScrollSensitivity: 1
m_ContentType: 0
m_InputType: 0
m_AsteriskChar: 42
m_KeyboardType: 0
m_LineType: 0
m_HideMobileInput: 0
m_HideSoftKeyboard: 0
m_CharacterValidation: 0
m_RegexValue:
m_GlobalPointSize: 14
m_CharacterLimit: 0
m_OnEndEdit:
m_PersistentCalls:
m_Calls: []
m_OnSubmit:
m_PersistentCalls:
m_Calls: []
m_OnSelect:
m_PersistentCalls:
m_Calls: []
m_OnDeselect:
m_PersistentCalls:
m_Calls: []
m_OnTextSelection:
m_PersistentCalls:
m_Calls: []
m_OnEndTextSelection:
m_PersistentCalls:
m_Calls: []
m_OnValueChanged:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 293984670}
m_TargetAssemblyTypeName: CallClientHost, Assembly-CSharp
m_MethodName: set_CalleeIdentity
m_Mode: 0
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
m_OnTouchScreenKeyboardStatusChanged:
m_PersistentCalls:
m_Calls: []
m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_CustomCaretColor: 0
m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412}
m_Text:
m_CaretBlinkRate: 0.85
m_CaretWidth: 1
m_ReadOnly: 0
m_RichText: 1
m_GlobalFontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_OnFocusSelectAll: 1
m_ResetOnDeActivation: 1
m_RestoreOriginalTextOnEscape: 1
m_isRichTextEditingAllowed: 0
m_LineLimit: 0
m_InputValidator: {fileID: 0}
--- !u!114 &963546689
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &963546690
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963546686}
m_CullTransparentMesh: 1
--- !u!1 &1184525248
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1184525249}
- component: {fileID: 1184525251}
- component: {fileID: 1184525250}
m_Layer: 5
m_Name: Status Header
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1184525249
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1184525248}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1843906927}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -23, y: -303}
m_SizeDelta: {x: 159.05, y: 33.5037}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1184525250
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1184525248}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Status
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 2.5243988, z: 10.097656, w: -2.5243645}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1184525251
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1184525248}
m_CullTransparentMesh: 1
--- !u!1 &1332239153
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1332239154}
- component: {fileID: 1332239157}
- component: {fileID: 1332239156}
- component: {fileID: 1332239155}
m_Layer: 5
m_Name: Hang Up Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1332239154
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1917486034}
m_Father: {fileID: 1843906927}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -277, y: -329}
m_SizeDelta: {x: 212.1357, y: 53.698}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1332239155
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1332239156}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 293984670}
m_TargetAssemblyTypeName: AppManager, Assembly-CSharp
m_MethodName: HangupButton_Click
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &1332239156
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1332239157
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1332239153}
m_CullTransparentMesh: 1
--- !u!1 &1529611526
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1529611527}
- component: {fileID: 1529611529}
- component: {fileID: 1529611528}
m_Layer: 5
m_Name: Status
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1529611527
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1529611526}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1843906927}
m_RootOrder: 4
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -2.525, y: -344.75}
m_SizeDelta: {x: 200, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1529611528
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1529611526}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Disconnected
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 30
m_fontSizeBase: 30
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: -25.861023, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1529611529
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1529611526}
m_CullTransparentMesh: 1
--- !u!1 &1676708952
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1676708953}
- component: {fileID: 1676708955}
- component: {fileID: 1676708954}
m_Layer: 5
m_Name: Text
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1676708953
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1676708952}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1787936407}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1676708954
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1676708952}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u200B"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 14
m_fontSizeBase: 14
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 1
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1676708955
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1676708952}
m_CullTransparentMesh: 1
--- !u!1 &1732033233
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1732033234}
- component: {fileID: 1732033237}
- component: {fileID: 1732033236}
- component: {fileID: 1732033235}
m_Layer: 5
m_Name: Start Call Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1732033234
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 438770861}
m_Father: {fileID: 1843906927}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -525.52, y: -329}
m_SizeDelta: {x: 212.1357, y: 53.698}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1732033235
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1732033236}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 293984670}
m_TargetAssemblyTypeName: CallClientHost, Assembly-CSharp
m_MethodName: CallButton_Click
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &1732033236
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1732033237
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732033233}
m_CullTransparentMesh: 1
--- !u!1 &1787936406
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1787936407}
- component: {fileID: 1787936408}
m_Layer: 5
m_Name: Text Area
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1787936407
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1787936406}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 857336306}
- {fileID: 1676708953}
m_Father: {fileID: 963546687}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -0.4999962}
m_SizeDelta: {x: -20, y: -13}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1787936408
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1787936406}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding: {x: -8, y: -5, z: -8, w: -5}
m_Softness: {x: 0, y: 0}
--- !u!1 &1843906923
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1843906927}
- component: {fileID: 1843906926}
- component: {fileID: 1843906925}
- component: {fileID: 1843906924}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1843906924
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &1843906925
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &1843906926
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_AdditionalShaderChannelsFlag: 25
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!224 &1843906927
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1843906923}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 963546687}
- {fileID: 1732033234}
- {fileID: 1332239154}
- {fileID: 1184525249}
- {fileID: 1529611527}
m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!1 &1917486033
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1917486034}
- component: {fileID: 1917486036}
- component: {fileID: 1917486035}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1917486034
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917486033}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1332239154}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1917486035
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917486033}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Hang Up
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &1917486036
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1917486033}
m_CullTransparentMesh: 1
Utwórz skrypt o nazwie AppManager.cs i połącz go z obiektem AppManager w edytorze aparatu Unity. Zastąp zawartość następującą implementacją:
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;
}
}
}
}
W obiekcie GameObject o nazwie AppManager przeciągnij nowo utworzony skrypt do składnika skryptu. Przeciągnij obiekt tekstowy Stan do pola Tekst stanu wywołania, aby włączyć aktualizacje interfejsu użytkownika stanu wywołania. Zestaw SDK udostępnia wideo za pośrednictwem identyfikatora URI, ale obecnie odtwarzacz wideo aparatu Unity może nie obsługiwać odtwarzania identyfikatora URI.
Model obiektów
W następnej tabeli wymieniono klasy i interfejsy obsługujące niektóre główne funkcje zestawu SDK wywołującego usługi Azure Communication Services:
Nazwa/nazwisko | opis |
---|---|
CallClient |
Jest CallClient to główny punkt wejścia do zestawu SDK wywołującego. |
CallAgent |
Służy do uruchamiania CallAgent wywołań i zarządzania nimi. |
Call |
Służy CommunicationCall do zarządzania trwającym połączeniem. |
CallTokenCredential |
Element CallTokenCredential jest używany jako poświadczenie tokenu w celu utworzenia wystąpienia elementu CallAgent . |
CallIdentifier |
Element CallIdentifier służy do reprezentowania tożsamości użytkownika, która może być jedną z następujących opcji: UserCallIdentifier , PhoneNumberCallIdentifier itp. |
Uwierzytelnianie użytkownika
Zainicjuj CallAgent
wystąpienie przy użyciu tokenu dostępu użytkownika, które umożliwia nam wykonywanie i odbieranie wywołań, a opcjonalnie uzyskiwanie wystąpienia deviceManager w celu wykonywania zapytań dotyczących konfiguracji urządzeń klienckich.
W kodzie zastąp <AUTHENTICATION_TOKEN>
element tokenem dostępu użytkownika. Jeśli nie masz jeszcze dostępnego tokenu dostępu, zapoznaj się z dokumentacją tokenu dostępu użytkownika.
Dodaj InitCallAgentAndDeviceManagerAsync
funkcję , która uruchamia zestaw SDK. Ten pomocnik można dostosować w celu spełnienia wymagań aplikacji.
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;
}
Rozpoczynanie połączenia za pomocą wideo
StartCallOptions
Po uzyskaniu CallAgent
obiektu można go użyć do zainicjowania wywołania usług Azure Communication Services:
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;
}
Obsługa zdalnego uczestnika i zdalnego przychodzącego wideo
Przychodzące wideo jest skojarzone z określonymi uczestnikami zdalnymi, dlatego RemoteParticipantsUpdated jest kluczowym wydarzeniem, które ma zostać powiadomione i uzyskać odwołania do zmieniających się uczestników.
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;
}
}
Wszyscy uczestnicy zdalni są dostępni za pośrednictwem kolekcji w wystąpieniu RemoteParticipants
wywołania. Po nawiązaniu połączenia możemy uzyskać dostęp do zdalnych uczestników połączenia i obsłużyć zdalne strumienie wideo.
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;
}
}
Strumień wideo będzie przesyłany przez sekwencję stanów wewnętrznych. VideoStreamState.Available
jest preferowanym stanem powiązania strumienia wideo z elementem interfejsu użytkownika na potrzeby renderowania strumienia wideo, takiego jak , i VideoStreamState.Stopped
jest zwykle miejscem, w którym należy wykonać zadania oczyszczania, takie jak MediaPlayerElement
zatrzymywanie podglądu wideo.
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;
}
}
Kończ połączenie
Zakończ bieżące wywołanie po kliknięciu Hang up
przycisku. Dodaj implementację do HangupButton_Click, aby zakończyć wywołanie, i zatrzymać podgląd i strumienie wideo.
public async void HangupButton_Click()
{
if (call != null)
{
try
{
await call.HangUpAsync(new HangUpOptions() { ForEveryone = false });
}
catch (Exception ex)
{
}
}
}
Akceptowanie połączenia przychodzącego
IncomingCallReceived
Ujście zdarzeń jest konfigurowane w pomocniku InitCallAgentAndDeviceManagerAsync
bootstrap zestawu SDK.
callAgent.IncomingCallReceived += OnIncomingCallAsync;
Aplikacja ma możliwość skonfigurowania sposobu akceptowania połączenia przychodzącego, takiego jak rodzaje strumieni wideo i 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;
}
Monitorowanie zdarzenia zmiany stanu i reagowanie na nie
StateChanged
zdarzenie na Call
obiekcie jest wyzwalane, gdy transakcje wywołania w toku z jednego stanu do innego. Aplikacja oferuje możliwości odzwierciedlenia zmian stanu w interfejsie użytkownika lub wstawiania logiki biznesowej.
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;
}
}
}
Uruchamianie kodu
Możesz skompilować i uruchomić kod w edytorze aparatu Unity lub na urządzeniach korzystających z aparatu Unity.
Możesz wykonać wywołanie wychodzące, podając identyfikator użytkownika w polu tekstowym i klikając Start Call/Join
przycisk. Wywoływanie 8:echo123
łączy Cię z botem echo. Ta funkcja doskonale nadaje się do rozpoczęcia pracy i sprawdzania, czy urządzenia audio działają.
Czyszczenie zasobów
Jeśli chcesz wyczyścić i usunąć subskrypcję usług Komunikacyjnych, możesz usunąć zasób lub grupę zasobów. Usunięcie grupy zasobów powoduje również usunięcie wszelkich innych skojarzonych z nią zasobów. Dowiedz się więcej o czyszczeniu zasobów.
Następne kroki
Aby uzyskać więcej informacji, zobacz następujące artykuły:
- Zapoznaj się z przykładem elementu hero wywołującego
- Wprowadzenie do biblioteki interfejsu użytkownika
- Dowiedz się więcej o możliwościach wywoływania zestawu SDK
- Dowiedz się więcej o tym, jak działa wywołanie