Inicio rápido: Unirse a una llamada de sala
Requisitos previos
- Una cuenta de Azure con una suscripción activa. Cree una cuenta gratuita.
- Un recurso activo de Communication Services y una cadena de conexión. Cree un recurso de Communication Services.
- Dos o más identidades de usuario de comunicación. Creación y administración de tokens de acceso o Creación rápida de identidades para pruebas.
- Una sala creada y un participante añadido. Creación y administración de salas
Obtenga un token de acceso de usuario.
Si ya ha creado usuarios y los ha agregado como participantes en la sala después de la sección "Configurar participantes de la sala" de esta página, puede usar directamente esos usuarios para unirse a la sala.
De lo contrario, deberá crear un token de acceso de usuario para cada participante de la llamada. Creación y administración de tokens de acceso. También puede usar la CLI de Azure y ejecutar el comando siguiente con la cadena de conexión para crear un usuario y un token de acceso. Una vez creados los usuarios, deberá agregarlos a la sala como participantes antes de unirse a la sala.
az communication identity token issue --scope voip --connection-string "yourConnectionString"
Para más información, consulte Uso de la CLI de Azure para crear y administrar tokens de acceso.
Nota:
Se puede acceder a las salas mediante la Biblioteca de interfaz de usuario de Azure Communication Services. Con la biblioteca de interfaz de usuario, los desarrolladores pueden agregar un cliente de llamada que está habilitado para salas a su aplicación con solo un par de líneas de código.
Unión a una llamada de una sala
Para seguir este inicio rápido, puede descargar el inicio rápido de llamada de sala en GitHub.
Requisitos previos
- Necesita tener Node.js 18. Puede usar el instalador msi para instalarlo.
Instalación
Creación de una aplicación Node.js
Abra la ventana de comandos o de terminal, cree un nuevo directorio para la aplicación y navegue hasta este.
mkdir calling-rooms-quickstart && cd calling-rooms-quickstart
Ejecute npm init -y
para crear un archivo package.json con la configuración predeterminada.
npm init -y
Instalar el paquete
Use el comando npm install
para instalar el SDK de llamadas de Azure Communication Services para JavaScript.
Importante
En este inicio rápido se usa la versión 1.14.1
del SDK de llamadas de Azure Communication Services. La capacidad de unirse a una llamada de sala y mostrar los roles de los participantes de la llamada está disponible en el SDK de JavaScript de Llamadas para exploradores web versión 1.13.1 y posteriores.
npm install @azure/communication-common --save
npm install @azure/communication-calling@1.14.1 --save
Instalación del marco de la aplicación
Esta guía de inicio rápido usa webpack para agrupar los recursos de la aplicación. Ejecute el siguiente comando para instalar los paquetes npm webpack
, webpack-cli
y webpack-dev-server
, y los enumera como dependencias de desarrollo en el archivo 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
Este es el código:
Cree un archivo index.html
en el directorio raíz del proyecto. Este archivo lo usamos para configurar un diseño básico que permita al usuario unirse a una llamada de salas.
<!-- index.html-->
<!DOCTYPE html>
<html>
<head>
<title>Azure Communication Services - Rooms Call Sample</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
<h4>Azure Communication Services - Rooms Call Sample</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="acs-room-id"
type="text"
placeholder="Enter Room Id"
style="margin-bottom:1em; width: 500px; display: block;"/>
<button id="join-room-call-button" type="button" disabled="true">Join Room Call</button>
<button id="hangup-call-button" type="button" disabled="true">Hang up 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>Room 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>
Cree un archivo en el directorio raíz del proyecto denominado index.js
que contendrá la lógica de la aplicación para esta guía de inicio rápido. Agregue el código siguiente a index.js:
// Make sure to install the necessary dependencies
const { CallClient, VideoStreamRenderer, LocalVideoStream } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential } = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");
// Set the log level and output
setLogLevel('verbose');
AzureLogger.log = (...args) => {
console.log(...args);
};
// Calling web sdk objects
let callAgent;
let deviceManager;
let call;
let localVideoStream;
let localVideoStreamRenderer;
// UI widgets
let userAccessToken = document.getElementById('user-access-token');
let acsRoomId = document.getElementById('acs-room-id');
let initializeCallAgentButton = document.getElementById('initialize-call-agent');
let startCallButton = document.getElementById('join-room-call-button');
let hangUpCallButton = document.getElementById('hangup-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 enable us to join a rooms call.
*/
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 });
startCallButton.disabled = false;
initializeCallAgentButton.disabled = true;
} catch(error) {
console.error(error);
}
}
startCallButton.onclick = async () => {
try {
const localVideoStream = await createLocalVideoStream();
const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
const roomCallLocator = { roomId: acsRoomId.value.trim() };
call = callAgent.join(roomCallLocator, { 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;
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;
remoteVideosGallery.hidden = 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';
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 room call
*/
hangUpCallButton.addEventListener("click", async () => {
await call.hangUp();
});
Adición del código del servidor local de webpack
Cree un archivo en el directorio raíz del proyecto denominado webpack.config.js que contendrá la lógica de la aplicación para esta guía de inicio rápido. Agregue el siguiente código a webpack.config.js:
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
static: {
directory: path.join(__dirname, './')
},
},
plugins: [
new CopyPlugin({
patterns: [
'./index.html'
]
}),
]
};
Ejecución del código
Utilice webpack-dev-server
para compilar y ejecutar la aplicación. Ejecute el siguiente comando para agrupar el host de aplicación en un servidor web local:
`npx webpack serve --config webpack.config.js`
- Abra el explorador y vaya a http://localhost:8080/.
- En el primer campo de entrada, escriba un token de acceso de usuario válido.
- Haga clic en "Inicializar agente de llamada" y escriba su id. de sala.
- Haga clic en "Unirse a la llamada de sala"
Ahora se ha unido correctamente a una llamada de sala
Descripción de la unión a una llamada de sala
Todo el código que ha agregado en la aplicación de inicio rápido le permitió iniciar y unirse correctamente a una llamada de sala. Aquí encontrará más información sobre los métodos y controladores a los que puede acceder para que Salas amplíe la funcionalidad en la aplicación.
Para mostrar el rol de los participantes en la llamada local o remota, suscríbase al controlador siguiente.
// Subscribe to changes for your role in a call
const callRoleChangedHandler = () => {
console.log(call.role);
};
call.on('roleChanged', callRoleChangedHandler);
// Subscribe to role changes for remote participants
const subscribeToRemoteParticipant = (remoteParticipant) => {
remoteParticipant.on('roleChanged', () => {
console.log(remoteParticipant.role);
});
}
Más información sobre los roles de los participantes en la llamada a la sala en la documentación del concepto de salas.
Unirse a una llamada de una sala
Para seguir este inicio rápido, puede descargar el inicio rápido de llamada de sala en GitHub.
Instalación
Creación del proyecto de Xcode
En Xcode, cree un nuevo proyecto de iOS y seleccione la plantilla Aplicación de una vista. En este tutorial se usa el marco SwiftUI, por lo que debe establecer el lenguaje en Swift y la interfaz de usuario en SwiftUI.
Instalación de CocoaPods
Utilice esta guía para instalar CocoaPods en su Mac.
Instalación del paquete y las dependencias con CocoaPods
Si desea crear un Podfile para la aplicación, abra el terminal, vaya a la carpeta del proyecto y ejecute el archivo init del pod.
Agregue el siguiente código al podfile y guárdelo:
platform :ios, '13.0'
use_frameworks!
target 'roomsquickstart' do
pod 'AzureCommunicationCalling', '~> 2.5.0'
end
Ejecute la instalación del pod.
Abra el archivo
.xcworkspace
con Xcode.
Solicite acceso al micrófono y a la cámara.
Para acceder al micrófono y a la cámara del dispositivo, actualice la lista de propiedades de información de la aplicación con NSMicrophoneUsageDescription
y NSCameraUsageDescription
. Establezca el valor asociado a una cadena que se vaya a incluir en el cuadro de diálogo empleado por el sistema para solicitar acceso al usuario.
Haga clic con el botón derecho en la entrada Info.plist
del árbol del proyecto y seleccione Open As > Source Code (Abrir como > Código fuente). Agregue las líneas siguientes a la sección <dict>
de nivel superior y guarde el archivo.
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>
<key>NSCameraUsageDescription</key>
<string>Need camera access for video calling</string>
Instalación del marco de la aplicación
Abra el archivo ContentView.swift
del proyecto y agregue una declaración de importación en la parte superior del archivo para importar la biblioteca AzureCommunicationCalling
y AVFoundation
. AVFoundation se usa para capturar el permiso de audio del código.
import AzureCommunicationCalling
import AVFoundation
Modelo de objetos
Las siguientes clases e interfaces controlan algunas de las características principales del SDK de llamadas de Azure Communication Services para iOS.
Nombre | Descripción |
---|---|
CallClient | CallClient es el punto de entrada principal al SDK de llamadas. |
CallAgent | CallAgent se usa para iniciar y administrar llamadas. |
CommunicationTokenCredential | CommunicationTokenCredential se usa como la credencial de token para crear una instancia de CallAgent. |
CommunicationIdentifier | CommunicationIdentifier se usa para representar la identidad del usuario y puede tener uno de los valores siguientes: CommunicationUserIdentifier/PhoneNumberIdentifier/CallingApplication. |
RoomCallLocator | CallAgent usa RoomCallLocator para unirse a una llamada de sala |
Creación del agente de llamadas
Reemplace la implementación del struct ContentView por algunos sencillos controles de interfaz de usuario que permitan a los usuarios iniciar y finalizar una llamada. En este inicio rápido, asociaremos la lógica de negocios a estos controles.
struct ContentView: View {
@State var roomId: String = ""
@State var callObserver:CallObserver?
@State var previewRenderer: VideoStreamRenderer? = nil
@State var previewView: RendererView? = nil
@State var sendingLocalVideo: Bool = false
@State var speakerEnabled: Bool = false
@State var muted: Bool = false
@State var callClient: CallClient?
@State var call: Call?
@State var callHandler: CallHandler?
@State var callAgent: CallAgent?
@State var deviceManager: DeviceManager?
@State var localVideoStreams: [LocalVideoStream]?
@State var callState: String = "Unknown"
@State var showAlert: Bool = false
@State var alertMessage: String = ""
@State var participants: [[Participant]] = [[]]
var body: some View {
NavigationView {
ZStack {
if (call == nil) {
Form {
Section {
TextField("Room ID", text: $roomId)
Button(action: joinRoomCall) {
Text("Join Room Call")
}
}
}
.navigationBarTitle("Rooms Quickstart")
} else {
ZStack {
VStack {
ForEach(participants, id:\.self) { array in
HStack {
ForEach(array, id:\.self) { participant in
ParticipantView(self, participant)
}
}
.frame(maxWidth: .infinity, maxHeight: 200, alignment: .topLeading)
}
}
.background(Color.black)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack {
if (sendingLocalVideo) {
HStack {
RenderInboundVideoView(view: $previewView)
.frame(width:90, height:160)
.padding(10)
.background(Color.green)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
HStack {
Button(action: toggleMute) {
HStack {
Text(muted ? "Unmute" : "Mute")
}
.frame(width:80)
.padding(.vertical, 10)
.background(Color(.lightGray))
}
Button(action: toggleLocalVideo) {
HStack {
Text(sendingLocalVideo ? "Video-Off" : "Video-On")
}
.frame(width:80)
.padding(.vertical, 10)
.background(Color(.lightGray))
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.padding(.vertical, 5)
HStack {
Button(action: leaveRoomCall) {
HStack {
Text("Leave Room Call")
}
.frame(width:80)
.padding(.vertical, 10)
.background(Color(.red))
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.padding(.vertical, 5)
HStack {
Text("Status:")
Text(callState)
}
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
}
}
}
}
.onAppear{
// Authenticate the client
// Initialize the CallAgent and access Device Manager
// Ask for permissions
}
}
}
//Functions and Observers
struct HomePageView_Previews: PreviewProvider {
static var previews: some View {
HomePageView()
}
}
Autenticar el cliente
Para poder inicializar instancias de CallAgent, es necesario un token de acceso de usuario que nos permitirá unirnos a las llamadas de sala.
Una vez que tenga un token, agregue el código siguiente a la devolución de llamada onAppear
en ContentView.swift
. Es necesario reemplazar <USER ACCESS TOKEN>
por un token de acceso de usuario válido para el recurso:
var userCredential: CommunicationTokenCredential?
do {
userCredential = try CommunicationTokenCredential(token: "<USER ACCESS TOKEN>")
} catch {
print("ERROR: It was not possible to create user credential.")
return
}
Inicialice CallAgent y acceda al Administrador de dispositivos
Para crear una instancia de CallAgent desde una instancia de CallClient, utilice el método callClient.createCallAgent
que devuelva asincrónicamente un objeto CallAgent cuando se inicialice. El Administrador de dispositivos permite enumerar los dispositivos locales que se pueden usar en una llamada para transmitir secuencias de audio o vídeo. También permite solicitar permiso a un usuario para acceder al micrófono o a la cámara.
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 = callHandler
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")
}
}
}
}
Solicitud de permisos
Para solicitar permisos de audio y vídeo, es necesario agregar el código siguiente a la devolución de llamada onAppear
.
AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
if granted {
AVCaptureDevice.requestAccess(for: .video) { (videoGranted) in
/* NO OPERATION */
}
}
}
Unirse a una llamada de sala
El método joinRoomCall
se establece como la acción que se llevará a cabo cuando se toque el botón Unirse a la llamada de sala. En este inicio rápido, las llamadas son de audio solo de forma predeterminada, pero pueden tener el vídeo habilitado una vez que se ha unido una sala.
func joinRoomCall() {
if self.callAgent == nil {
print("CallAgent not initialized")
return
}
if (self.roomId.isEmpty) {
print("Room ID not set")
return
}
// Join a call with a Room ID
let options = JoinCallOptions()
let audioOptions = AudioOptions()
audioOptions.muted = self.muted
options.audioOptions = audioOptions
let roomCallLocator = RoomCallLocator(roomId: roomId)
self.callAgent!.join(with: roomCallLocator, joinCallOptions: options) { (call, error) in
self.setCallAndObserver(call: call, error: error)
}
}
CallObserver
se usa para administrar participantes remotos y eventos que tienen lugar en mitad de la llamada. Estableceremos los observadores en la función setCallAndObserver
.
func setCallAndObserver(call:Call!, error:Error?) {
if (error == nil) {
self.call = call
self.callObserver = CallObserver(view:self)
self.call!.delegate = self.callObserver
if (self.call!.state == CallState.connected) {
self.callObserver!.handleInitialCallState(call: call)
}
} else {
print("Failed to get call object")
}
}
Salir de una llamada de sala
El método leaveRoomCall
se establece como la acción que se llevará a cabo cuando se toque el botón Abandonar la llamada de sala. Controla la salida de una llamada y limpia los recursos que se crearon.
private func leaveRoomCall() {
if (self.sendingLocalVideo) {
self.call!.stopVideo(stream: self.localVideoStreams!.first!) { (error) in
if (error != nil) {
print("Failed to stop video")
} else {
self.sendingLocalVideo = false
self.previewView = nil
self.previewRenderer?.dispose()
self.previewRenderer = nil
}
}
}
self.call?.hangUp(options: nil) { (error) in }
self.participants.removeAll()
self.call?.delegate = nil
self.call = nil
}
Vídeo de difusión
Durante una llamada de sala, se puede usar startVideo
o stopVideo
para iniciar o detener el envío de LocalVideoStream
a participantes remotos.
func toggleLocalVideo() {
if (self.sendingLocalVideo) {
self.call!.stopVideo(stream: self.localVideoStreams!.first!) { (error) in
if (error != nil) {
print("Cannot stop video")
} else {
self.sendingLocalVideo = false
self.previewView = nil
self.previewRenderer!.dispose()
self.previewRenderer = nil
}
}
} else {
let availableCameras = self.deviceManager!.cameras
let scalingMode:ScalingMode = .crop
if (self.localVideoStreams == nil) {
self.localVideoStreams = [LocalVideoStream]()
}
self.localVideoStreams!.append(LocalVideoStream(camera: availableCameras.first!))
self.previewRenderer = try! VideoStreamRenderer(localVideoStream: self.localVideoStreams!.first!)
self.previewView = try! previewRenderer!.createView(withOptions: CreateViewOptions(scalingMode:scalingMode))
self.call!.startVideo(stream: self.localVideoStreams!.first!) { (error) in
if (error != nil) {
print("Cannot start video")
}
else {
self.sendingLocalVideo = true
}
}
}
}
Silenciar audio local
Durante una llamada de sala podemos usar mute
o unMute
para silenciar o desactivar nuestro micrófono.
func toggleMute() {
if (self.muted) {
call!.unmuteOutgoingAudio(completionHandler: { (error) in
if error == nil {
self.muted = false
}
})
} else {
call!.muteOutgoingAudio(completionHandler: { (error) in
if error == nil {
self.muted = true
}
})
}
}
Control de las actualizaciones de llamadas
Para tratar las actualizaciones de llamadas, implemente un CallHandler
para controlar los eventos de actualización. Inserte la siguiente implementación en CallHandler.swift
.
final class CallHandler: NSObject, CallAgentDelegate {
public var owner: ContentView?
private static var instance: CallHandler?
static func getOrCreateInstance() -> CallHandler {
if let c = instance {
return c
}
instance = CallHandler()
return instance!
}
private override init() {}
public func callAgent(_ callAgent: CallAgent, didUpdateCalls args: CallsUpdatedEventArgs) {
if let removedCall = args.removedCalls.first {
owner?.call = nil
}
}
}
Es necesario crear una instancia de CallHandler
. Para ello, agregue el código siguiente a la devolución de llamada onAppear
en ContentView.swift
:
self.callHandler = CallHandler.getOrCreateInstance()
self.callHandler.owner = self
Una vez que CallAgent se haya creado correctamente, establezca un delegado:
self.callAgent!.delegate = callHandler
Administración remota de participantes
El tipo RemoteParticipant
representa a todos los participantes remotos y estos están disponibles mediante la colección remoteParticipants
en una instancia de llamada. Es posible implementar una clase Participant
para administrar las actualizaciones de secuencias de vídeo remotas de participantes remotos, entre otras cosas.
class Participant: NSObject, RemoteParticipantDelegate, ObservableObject {
private var videoStreamCount = 0
private let innerParticipant:RemoteParticipant
private let call:Call
private var renderedRemoteVideoStream:RemoteVideoStream?
@Published var state:ParticipantState = ParticipantState.disconnected
@Published var isMuted:Bool = false
@Published var isSpeaking:Bool = false
@Published var hasVideo:Bool = false
@Published var displayName:String = ""
@Published var videoOn:Bool = true
@Published var renderer:VideoStreamRenderer? = nil
@Published var rendererView:RendererView? = nil
@Published var scalingMode: ScalingMode = .fit
init(_ call: Call, _ innerParticipant: RemoteParticipant) {
self.call = call
self.innerParticipant = innerParticipant
self.displayName = innerParticipant.displayName
super.init()
self.innerParticipant.delegate = self
self.state = innerParticipant.state
self.isMuted = innerParticipant.isMuted
self.isSpeaking = innerParticipant.isSpeaking
self.hasVideo = innerParticipant.videoStreams.count > 0
if(self.hasVideo) {
handleInitialRemoteVideo()
}
}
deinit {
self.innerParticipant.delegate = nil
}
func getMri() -> String {
Utilities.toMri(innerParticipant.identifier)
}
func set(scalingMode: ScalingMode) {
if self.rendererView != nil {
self.rendererView!.update(scalingMode: scalingMode)
}
self.scalingMode = scalingMode
}
func handleInitialRemoteVideo() {
renderedRemoteVideoStream = innerParticipant.videoStreams[0]
renderer = try! VideoStreamRenderer(remoteVideoStream: renderedRemoteVideoStream!)
rendererView = try! renderer!.createView()
}
func toggleVideo() {
if videoOn {
rendererView = nil
renderer?.dispose()
videoOn = false
}
else {
renderer = try! VideoStreamRenderer(remoteVideoStream: innerParticipant.videoStreams[0])
rendererView = try! renderer!.createView()
videoOn = true
}
}
func remoteParticipant(_ remoteParticipant: RemoteParticipant, didUpdateVideoStreams args: RemoteVideoStreamsEventArgs) {
let hadVideo = hasVideo
hasVideo = innerParticipant.videoStreams.count > 0
if videoOn {
if hadVideo && !hasVideo {
// Remote user stopped sharing
rendererView = nil
renderer?.dispose()
} else if hasVideo && !hadVideo {
// remote user started sharing
renderedRemoteVideoStream = innerParticipant.videoStreams[0]
renderer = try! VideoStreamRenderer(remoteVideoStream: renderedRemoteVideoStream!)
rendererView = try! renderer!.createView()
} else if hadVideo && hasVideo {
if args.addedRemoteVideoStreams.count > 0 {
if renderedRemoteVideoStream?.id == args.addedRemoteVideoStreams[0].id {
return
}
// remote user added a second video, so switch to the latest one
guard let rendererTemp = renderer else {
return
}
rendererTemp.dispose()
renderedRemoteVideoStream = args.addedRemoteVideoStreams[0]
renderer = try! VideoStreamRenderer(remoteVideoStream: renderedRemoteVideoStream!)
rendererView = try! renderer!.createView()
} else if args.removedRemoteVideoStreams.count > 0 {
if args.removedRemoteVideoStreams[0].id == renderedRemoteVideoStream!.id {
// remote user stopped sharing video that we were rendering but is sharing
// another video that we can render
renderer!.dispose()
renderedRemoteVideoStream = innerParticipant.videoStreams[0]
renderer = try! VideoStreamRenderer(remoteVideoStream: renderedRemoteVideoStream!)
rendererView = try! renderer!.createView()
}
}
}
}
}
func remoteParticipant(_ remoteParticipant: RemoteParticipant, didChangeDisplayName args: PropertyChangedEventArgs) {
self.displayName = innerParticipant.displayName
}
}
class Utilities {
@available(*, unavailable) private init() {}
public static func toMri(_ id: CommunicationIdentifier?) -> String {
if id is CommunicationUserIdentifier {
let communicationUserIdentifier = id as! CommunicationUserIdentifier
return communicationUserIdentifier.identifier
} else {
return "<nil>"
}
}
}
Secuencias de vídeo de participantes remotos
Podemos crear un ParticipantView
para controlar las secuencias de vídeo de representación de participantes remotos. Colocar la implementación en ParticipantView.swift
struct ParticipantView : View, Hashable {
static func == (lhs: ParticipantView, rhs: ParticipantView) -> Bool {
return lhs.participant.getMri() == rhs.participant.getMri()
}
private let owner: HomePageView
@State var showPopUp: Bool = false
@State var videoHeight = CGFloat(200)
@ObservedObject private var participant:Participant
var body: some View {
ZStack {
if (participant.rendererView != nil) {
HStack {
RenderInboundVideoView(view: $participant.rendererView)
}
.background(Color(.black))
.frame(height: videoHeight)
.animation(Animation.default)
} else {
HStack {
Text("No incoming video")
}
.background(Color(.red))
.frame(height: videoHeight)
}
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(participant.getMri())
}
init(_ owner: HomePageView, _ participant: Participant) {
self.owner = owner
self.participant = participant
}
func resizeVideo() {
videoHeight = videoHeight == 200 ? 150 : 200
}
func showAlert(_ title: String, _ message: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.owner.alertMessage = message
self.owner.showAlert = true
}
}
}
struct RenderInboundVideoView: UIViewRepresentable {
@Binding var view:RendererView!
func makeUIView(context: Context) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
for view in uiView.subviews {
view.removeFromSuperview()
}
if (view != nil) {
uiView.addSubview(view)
}
}
}
Suscripción a los eventos
Podemos implementar una clase CallObserver
para suscribirnos a una colección de eventos a los que se enviarán notificaciones si los valores, como remoteParticipants
, cambian durante la llamada.
public class CallObserver : NSObject, CallDelegate
{
private var owner: ContentView
private var firstTimeCallConnected: Bool = true
init(view: ContentView) {
owner = view
super.init()
}
public func call(_ call: Call, didChangeState args: PropertyChangedEventArgs) {
let state = CallObserver.callStateToString(state:call.state)
owner.callState = state
if (call.state == CallState.disconnected) {
owner.leaveRoomCall()
}
else if (call.state == CallState.connected) {
if(self.firstTimeCallConnected) {
self.handleInitialCallState(call: call);
}
self.firstTimeCallConnected = false;
}
}
public func handleInitialCallState(call: Call) {
// We want to build a matrix with max 2 columns
owner.callState = CallObserver.callStateToString(state:call.state)
var participants = [Participant]()
// Add older/existing participants
owner.participants.forEach { (existingParticipants: [Participant]) in
participants.append(contentsOf: existingParticipants)
}
owner.participants.removeAll()
// Add new participants to the collection
for remoteParticipant in call.remoteParticipants {
let mri = Utilities.toMri(remoteParticipant.identifier)
let found = participants.contains { (participant) -> Bool in
participant.getMri() == mri
}
if !found {
let participant = Participant(call, remoteParticipant)
participants.append(participant)
}
}
// Convert 1-D array into a 2-D array with 2 columns
var indexOfParticipant = 0
while indexOfParticipant < participants.count {
var newParticipants = [Participant]()
newParticipants.append(participants[indexOfParticipant])
indexOfParticipant += 1
if (indexOfParticipant < participants.count) {
newParticipants.append(participants[indexOfParticipant])
indexOfParticipant += 1
}
owner.participants.append(newParticipants)
}
}
public func call(_ call: Call, didUpdateRemoteParticipant args: ParticipantsUpdatedEventArgs) {
var participants = [Participant]()
// Add older/existing participants
owner.participants.forEach { (existingParticipants: [Participant]) in
participants.append(contentsOf: existingParticipants)
}
owner.participants.removeAll()
// Remove deleted participants from the collection
args.removedParticipants.forEach { p in
let mri = Utilities.toMri(p.identifier)
participants.removeAll { (participant) -> Bool in
participant.getMri() == mri
}
}
// Add new participants to the collection
for remoteParticipant in args.addedParticipants {
let mri = Utilities.toMri(remoteParticipant.identifier)
let found = participants.contains { (view) -> Bool in
view.getMri() == mri
}
if !found {
let participant = Participant(call, remoteParticipant)
participants.append(participant)
}
}
// Convert 1-D array into a 2-D array with 2 columns
var indexOfParticipant = 0
while indexOfParticipant < participants.count {
var array = [Participant]()
array.append(participants[indexOfParticipant])
indexOfParticipant += 1
if (indexOfParticipant < participants.count) {
array.append(participants[indexOfParticipant])
indexOfParticipant += 1
}
owner.participants.append(array)
}
}
private static func callStateToString(state:CallState) -> String {
switch state {
case .connected: return "Connected"
case .connecting: return "Connecting"
case .disconnected: return "Disconnected"
case .disconnecting: return "Disconnecting"
case .none: return "None"
default: return "Unknown"
}
}
}
Ejecución del código
Para compilar y ejecutar la aplicación en el simulador de iOS, seleccione Product > Run (Producto > Ejecutar) o use el método abreviado de teclado (⌘-R).
La capacidad de unirse a una llamada de sala y mostrar los roles de los participantes de la llamada está disponible en el SDK de Llamadas móviles para iOS versión 2.5.0 y posteriores.
Más información sobre los roles de los participantes en la llamada a la sala en la documentación del concepto de salas.
Aplicación de ejemplo
Para seguir este inicio rápido, puede descargar el inicio rápido de llamada de sala en GitHub.
Configurar un proyecto
Creación de una aplicación de Android con una actividad vacía
En Android Studio, cree un proyecto nuevo:
Asigne al proyecto el nombre Inicio rápido de llamada de sala y seleccione Kotlin.
Instalar el paquete
En el nivel de módulo build.gradle
, agregue la siguiente línea a la sección dependencies
.
dependencies {
...
//Ability to join a Rooms calls is available in 2.4.0 or above.
implementation 'com.azure.android:azure-communication-calling:2.4.0'
...
}
Adición de permisos al manifiesto de aplicación
Para solicitar los permisos necesarios para realizar una llamada, primero se deben declarar los permisos en el manifiesto de aplicación (app/src/main/AndroidManifest.xml
). Copie lo siguiente en el archivo de manifiesto:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<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/Theme.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"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Configuración del diseño de la aplicación
Necesita una entrada de texto para el identificador de la llamada, un botón para realizar la llamada y otro botón para colgarla.
Vaya a app/src/main/res/layout/activity_main.xml
y reemplace el contenido del archivo con el código siguiente:
<?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">
<TextView
android:id="@+id/text_role"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Role:"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp" />
<TextView
android:id="@+id/text_call_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Call Status"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="48dp" />
<EditText
android:id="@+id/room_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Room ID"
android:inputType="textPersonName"
android:layout_marginTop="100dp"
android:layout_marginHorizontal="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="260dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/call_button"
android:layout_width="wrap_content"
android:layout_marginEnd="32dp"
android:layout_height="wrap_content"
android:text="Start Call" />
<Button
android:id="@+id/hangup_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hangup" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Crear la actividad principal
Con el diseño creado, puede agregar la lógica para iniciar una llamada de sala. La actividad administra la solicitud de los permisos en tiempo de ejecución, la creación del agente de llamadas y la realización de llamadas cuando se presione el botón.
El método onCreate
invoca getAllPermissions
y createAgent
, y agrega los enlaces para el botón de llamada.
Este evento solo ocurre una vez cuando se crea la actividad. Para más información sobre onCreate
, consulte la guía Descripción del ciclo de vida de la actividad.
Vaya al archivo MainActivity.kt y reemplace el contenido por el siguiente código:
package com.contoso.roomscallquickstart
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.media.AudioManager
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import com.azure.android.communication.calling.Call
import com.azure.android.communication.calling.CallAgent
import com.azure.android.communication.calling.CallClient
import com.azure.android.communication.calling.HangUpOptions
import com.azure.android.communication.calling.JoinCallOptions
import com.azure.android.communication.calling.RoomCallLocator
import com.azure.android.communication.common.CommunicationTokenCredential
import java.util.concurrent.ExecutionException
class MainActivity : AppCompatActivity() {
private val allPermissions = arrayOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA,
Manifest.permission.READ_PHONE_STATE
)
private val userToken = "<ACS_USER_TOKEN>"
private lateinit var callAgent: CallAgent
private var call: Call? = null
private lateinit var roleTextView: TextView
private lateinit var statusView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getAllPermissions()
createCallAgent()
val callButton: Button = findViewById(R.id.call_button)
callButton.setOnClickListener { startCall() }
val hangupButton: Button = findViewById(R.id.hangup_button)
hangupButton.setOnClickListener { endCall() }
roleTextView = findViewById(R.id.text_role)
statusView = findViewById(R.id.text_call_status)
volumeControlStream = AudioManager.STREAM_VOICE_CALL
}
/**
* Start a call
*/
private fun startCall() {
if (userToken.startsWith("<")) {
Toast.makeText(this, "Please enter token in source code", Toast.LENGTH_SHORT).show()
return
}
val roomIdView: EditText = findViewById(R.id.room_id)
val roomId = roomIdView.text.toString()
if (roomId.isEmpty()) {
Toast.makeText(this, "Please enter room ID", Toast.LENGTH_SHORT).show()
return
}
val joinCallOptions = JoinCallOptions()
val roomCallLocator = RoomCallLocator(roomId)
call = callAgent.join(applicationContext, roomCallLocator, joinCallOptions)
call?.addOnStateChangedListener { setCallStatus(call?.state.toString()) }
call?.addOnRoleChangedListener { setRoleText(call?.callParticipantRole.toString()) }
}
/**
* Ends the call previously started
*/
private fun endCall() {
try {
call?.hangUp(HangUpOptions())?.get()
} catch (e: ExecutionException) {
Toast.makeText(this, "Unable to hang up call", Toast.LENGTH_SHORT).show()
}
}
/**
* Create the call callAgent
*/
private fun createCallAgent() {
try {
val credential = CommunicationTokenCredential(userToken)
callAgent = CallClient().createCallAgent(applicationContext, credential).get()
} catch (ex: Exception) {
Toast.makeText(
applicationContext,
"Failed to create call callAgent.",
Toast.LENGTH_SHORT
).show()
}
}
/**
* Request each required permission if the app doesn't already have it.
*/
private fun getAllPermissions() {
val permissionsToAskFor = mutableListOf<String>()
for (permission in allPermissions) {
if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
permissionsToAskFor.add(permission)
}
}
if (permissionsToAskFor.isNotEmpty()) {
ActivityCompat.requestPermissions(this, permissionsToAskFor.toTypedArray(), 1)
}
}
/**
* Ensure all permissions were granted, otherwise inform the user permissions are missing.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
var allPermissionsGranted = true
for (result in grantResults) {
allPermissionsGranted = allPermissionsGranted && (result == PackageManager.PERMISSION_GRANTED)
}
if (!allPermissionsGranted) {
Toast.makeText(this, "All permissions are needed to make the call.", Toast.LENGTH_LONG).show()
finish()
}
}
@SuppressLint("SetTextI18n")
private fun setCallStatus(status: String?) {
runOnUiThread {
statusView.text = "Call Status: $status"
}
}
@SuppressLint("SetTextI18n")
private fun setRoleText(role: String?) {
runOnUiThread {
roleTextView.text = "Role: $role"
}
}
}
Nota:
Al diseñar la aplicación, tenga en cuenta cuándo se deben solicitar estos permisos. Se deben solicitar a medida que sean necesarios, pero no con anterioridad. Para más información, consulte la Guía de permisos de Android.
Ejecución del proyecto
Antes de ejecutar el proyecto, reemplace <ACS_USER_TOKEN>
en MainActivity.kt
por el token de acceso de usuario de Azure Communication Services.
private val userToken = "<ACS_USER_TOKEN>"
Ejecute el proyecto en un emulador o en un dispositivo físico.
Debería ver un campo para escribir el id. de sala y un botón para iniciar la llamada de sala. Escriba el id. de sala y compruebe que el estado de la llamada ha cambiado junto con el rol.
Descripción de la unión a una llamada de sala
Todo el código que ha agregado en la aplicación de inicio rápido le permitió iniciar y unirse correctamente a una llamada de sala. Necesitamos profundizar en cómo funciona todo y a qué más métodos/controladores puede acceder para Salas.
Las llamadas de sala se unen a través de CallAgent
, que se crea con un token de usuario válido:
private fun createCallAgent() {
try {
val credential = CommunicationTokenCredential(userToken)
callAgent = CallClient().createCallAgent(applicationContext, credential).get()
} catch (ex: Exception) {
Toast.makeText(
applicationContext,
"Failed to create call callAgent.",
Toast.LENGTH_SHORT
).show()
}
}
Mediante CallAgent
y RoomCallLocator
, podemos unir una llamada de sala mediante el método CallAgent.join
que devuelve un objeto Call
:
val joinCallOptions = JoinCallOptions()
val roomCallLocator = RoomCallLocator(roomId)
call = callAgent.join(applicationContext, roomCallLocator, joinCallOptions)
Una personalización más allá del MainActivity.kt
archivo incluye la suscripción a los eventos Call
para obtener actualizaciones:
call.addOnRemoteParticipantsUpdatedListener { args: ParticipantsUpdatedEvent? ->
handleRemoteParticipantsUpdate(
args!!
)
}
call.addOnStateChangedListener { args: PropertyChangedEvent? ->
this.handleCallOnStateChanged(
args!!
)
}
Puede ampliar MainActivity.kt
aún más para mostrar el rol de los participantes de la llamada local o remota utilizando estos métodos y controladores que se indican a continuación.
// Get your role in the call
call.getCallParticipantRole();
// Subscribe to changes for your role in a call
private void isCallRoleChanged(PropertyChangedEvent propertyChangedEvent) {
// handle self-role change
}
call.addOnRoleChangedListener(isCallRoleChanged);
// Subscribe to role changes for remote participants
private void isRoleChanged(PropertyChangedEvent propertyChangedEvent) {
// handle remote participant role change
}
remoteParticipant.addOnRoleChangedListener(isRoleChanged);
// Get role of the remote participant
remoteParticipant.getCallParticipantRole();
La capacidad de unirse a una llamada de sala y mostrar los roles de los participantes de la llamada está disponible en el SDK de Llamadas móviles para Android versión 2.4.0 y posteriores.
Más información sobre los roles de los participantes en la llamada a la sala en la documentación del concepto de salas.
Unión a una llamada de una sala
Para unirse a una llamada de una sala, configure la aplicación Windows mediante la guía Adición de llamadas de vídeo a la aplicación cliente. Como alternativa, puede descargar el inicio rápido de videollamadas en GitHub.
Cree un callAgent
con un token de usuario válido:
var creds = new CallTokenCredential("<user-token>");
CallAgentOptions callAgentOptions = new CallAgentOptions();
callAgentOptions.DisplayName = "<display-name>";
callAgent = await callClient.CreateCallAgentAsync(creds, callAgentOptions);
Use callAgent
y RoomCallLocator
para unirse a una llamada a sala. El método CallAgent.JoinAsync
devolverá un objeto CommunicationCall
:
RoomCallLocator roomCallLocator = new RoomCallLocator('<RoomId>');
CommunicationCall communicationCall = await callAgent.JoinAsync(roomCallLocator, joinCallOptions);
Suscríbase a eventos CommunicationCall
para obtener actualizaciones:
private async void CommunicationCall_OnStateChanged(object sender, PropertyChangedEventArgs args) {
var call = sender as CommunicationCall;
if (sender != null)
{
switch (call.State){
// Handle changes in call state
}
}
}
Para visualizar el rol de los participantes en la llamada, suscríbase a los cambios de rol:
private void RemoteParticipant_OnRoleChanged(object sender, Azure.Communication.Calling.WindowsClient.PropertyChangedEventArgs args)
{
_ = Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
System.Diagnostics.Trace.WriteLine("Raising Role change, new Role: " + remoteParticipant_.Role);
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs("RemoteParticipantRole"));
});
}
La capacidad de unirse a una llamada de sala y mostrar los roles de los participantes de la llamada está disponible en la versión Windows NuGet 1.1.0 y superiores.
Más información sobre los roles de los participantes en la llamada a la sala en la documentación del concepto de salas.
Pasos siguientes
En esta sección ha aprendido a hacer lo siguiente:
- Incorporar las videollamadas a la aplicación
- Pasar el identificador de sala al SDK de llamada
- Unirse a una llamada de sala desde la aplicación
Puede que también le interese:
- Más información sobre el concepto de salas
- Más información sobre los conceptos de llamadas de voz y vídeo
- Obtenga más información sobre conceptos de autenticación