¡¡¡Comenzamos!!!
Para poneos en contexto, aquí vemos el resultado final de la app que vamos a crear.
Nótese que las opciones que nos ofrece Communication Service son tan amplias que es imposible abordarlas todas en un artículo, por lo que, como he indicado en el título, aquí vamos a dar nuestros primeros pasos. Nos permite el envío de email, SMS, llamadas de voz, chat y videollamadas. Concretamente, vamos a tratar la llamada grupal, aunque utilizando este servicio también se podría implementar llamadas punto a punto.
Crear este servicio es bastante sencillo. Lo primero que tenemos que hacer es ir a nuestro portal de Azure y crear un nuevo recurso.
El recurso que nos interesa es Communication Service. Una vez localizado, lo creamos e informamos toda la información que se nos solicita.
Una vez creado el servicio, lo que vamos a necesitar para empezar a utilizarlo es la cadena de conexión, que podemos encontrarla dentro de la sección Keys, que la utilizaremos para obtener los tokens utilizados por las aplicaciones cliente que utilizan el servicio.
Antes de continuar, creo que es importante conocer el pricing de este servicio, por lo que aquí dejo una tabla con los datos para el oeste de Europa.
Asimismo, aquí os dejo el link con todos los detalles: https://azure.microsoft.com/es-es/pricing/details/communication-services/
Dado que el objetivo de este post es hacer una introducción a Azure Communication Service y empezar a usarlo en flutter, no nos hemos preocupado por la implementación de la seguridad. Por ello, hemos creado un Azure Function que, utilizando la cadena de conexión del servicio, siempre devuelve un token nuevo, sin hacer ninguna comprobación de seguridad.
using Azure;
using Azure.Communication;
using Azure.Communication.Identity;
using Azure.Core;
using Microsoft.Azure.Functions.Worker;
using System.Text.Json;
namespace AuthFunction
{
public class AuthFunction
{
[Function("function")]
public static async Task<object> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestMessage req)
{
string connectionString = "{YOUR_CONNECTION_STRING}}";
CommunicationIdentityClient identityClient = new CommunicationIdentityClient(connectionString);
Response<CommunicationUserIdentifier> user = await identityClient.CreateUserAsync();
Response<AccessToken> userToken = await identityClient.GetTokenAsync(user, new[] { CommunicationTokenScope.VoIP });
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
return JsonSerializer.Serialize(userToken.Value, options);
}
}
}
Por otro lado, si no queréis implementar está función, también se puede obtener un token para hacer pruebas desde el propio servicio en el portal de Azure, en el apartado Identities & User Access Tokens
Una vez que tenemos el servicio y la función de Azure para servirnos tokens, es el momento de crear los diferentes clientes que se conectarán a este servicio.
Dado que, en el momento de escribir este post, no hay ningún paquete que nos permita operar con Communication Service directamente en flutter, vamos a utilizar el SDK nativo. Por lo que vamos a dividir esta sección en tres puntos. En el primero de ellos, veremos todo el código escrito en Dart, en la parte propia de flutter, la cual será común a la aplicación Android y iOS. Y en las otras dos escribiremos todo lo relacionado con la parte de iOS y Android (escrito en swift y kotlin, respectivamente).
El primer paso que vamos a dar es el de añadir los paquetes necesarios para hacer la app:
Una vez que tenemos los paquetes instalados, procedemos a la creación del widget que será la pantalla inicial de nuestra app y desde la que se lanzará la funcionalidad de vídeo llamadas, encapsulada en el SDK.
Comenzaremos escribiendo la parte de diseño, que al ser una app de demo, es bastante sencilla.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late TextEditingController _groupCallIdController;
late TextEditingController _userIdController;
late TextEditingController _userNameController;
@override
void initState() {
super.initState();
_groupCallIdController = TextEditingController();
_userIdController = TextEditingController();
_userNameController = TextEditingController();
}
@override
void dispose() {
_groupCallIdController.dispose();
_userIdController.dispose();
_userNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 10),
TextButton(onPressed: _getToken, child: const Text('Get token')),
const SizedBox(height: 10),
TextField(
controller: _userIdController,
decoration: const InputDecoration(labelText: 'User ID'),
readOnly: true,
),
const SizedBox(height: 10),
TextField(
controller: _userNameController,
decoration: const InputDecoration(labelText: 'User name'),
),
Row(
children: [
Expanded(
child: TextField(
controller: _groupCallIdController,
decoration: const InputDecoration(labelText: 'Group call ID'),
),
),
const SizedBox(width: 5),
IconButton(
icon: const Icon(Icons.refresh_rounded),
onPressed: () {
var uuid = const Uuid();
setState(() {
_groupCallIdController.text = uuid.v1();
});
},
),
],
),
const SizedBox(height: 10),
TextButton(onPressed: _joinGroup, child: const Text('Join group call')),
],
),
),
),
);
}
}
La parte importante está en los métodos utilizados para la obtención del token realizando una llamada a la función de Azure creada anteriormente:
late String _userToken;
void _getToken() async {
var response = await http.get(Uri.parse('{YOUR AZURE FUNCTION ENDPOINT}'));
if (response.statusCode != 200) {
return;
}
var jsonResponse = convert.jsonDecode(response.body) as Map<String, dynamic>;
var token = jsonResponse['token'];
_userToken = token;
Map<String, dynamic> decodedToken = JwtDecoder.decode(token);
_userIdController.text = decodedToken["skypeid"];
}
Y en el método para unirnos a una llamada. Dado que el SDK que utilizamos es nativo, tenemos que utilizar un channel para ejecutar el método de la parte nativa, pasándole todos los parámetros que necesita. Estos son el token, el identificador del grupo y el nombre de usuario.
static const platform = MethodChannel('com.example.flutter_azure_calling_ui/calling');
void _joinGroup() async {
final bool result = await platform.invokeMethod('startCall', {"groupCallId": _groupCallIdController.text, "displayName": _userNameController.text, "userToken": _userToken});
if (kDebugMode) {
print("The call result is $result");
}
}
Cabe destacar que, si estamos creando la reunión, podemos utilizar el botón a la derecha de Group call ID para generar un identificador de grupo nuevo. Sin embargo, si queremos conectarnos a una reunión ya creada, escribiremos el ID del grupo. Obviamente, en una aplicación real esta gestión debe quedar encapsulada en el backend y ser totalmente transparente para la app cliente.
Tal y como hemos dicho, tenemos que irnos a la parte nativa para interactuar con el SDK de Communication Service. Por lo que, para conseguir que nuestra app funcione en Android, abriremos el proyecto de Android en Android Studio y seguiremos los siguientes pasos:
buildscript {
repositories {
...
mavenCentral()
...
}
}
...
allprojects {
repositories {
...
mavenCentral()
...
}
}
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
..
}
...
dependencies {
...
implementation 'com.azure.android:azure-communication-ui-calling:+'
...
}
<manifest ...>
<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" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>
<manifest
...
xmlns:tools="http://schemas.android.com/tools">
<application
...
tools:replace="android:label">
...
</application>
</manifest>
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.flutter_azure_calling_ui/calling"
private lateinit var methodChannel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
methodChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
if (call.method.equals("startCall")) {
val groupCallId = call.argument<String>("groupCallId")
val displayName = call.argument<String>("displayName")
val userToken = call.argument<String>("userToken")
startCallComposite(groupCallId!!, displayName!!, userToken!!)
result.success(true)
} else {
result.notImplemented()
}
}
}
private fun startCallComposite(groupCallId: String, displayName: String, userToken: String) {
val communicationTokenCredential =
CommunicationTokenCredential(userToken)
val locator: CallCompositeJoinLocator =
CallCompositeGroupCallLocator(UUID.fromString(groupCallId))
val remoteOptions =
CallCompositeRemoteOptions(locator, communicationTokenCredential, displayName)
val callComposite = CallCompositeBuilder().build()
callComposite.addOnErrorEventHandler { event: CallCompositeErrorEvent ->
// Process error event
println(event.cause)
println(event.errorCode)
}
callComposite.launch(this, remoteOptions)
}
}
Con todo lo anterior, nuestra aplicación estaría lista para ejecutarse y poder hacer video llamadas en varios dispositivos Android.
Como te podrás imaginas, la implementación en iOS es totalmente análoga a la de Android. Al igual que en ésta, tenemos que añadir el SDK de Communication Service (lo haremos vía pod) e implementar el canal para poder llamar desde flutter a las funcionalidades que nos ofrece el SDK.
A continuación, se detallan todos los puntos a seguir para completar la implementación:
platform :ios, '14.0'
target 'Runner' do
use_frameworks!
pod 'AzureCommunicationUICalling', '1.3.0'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.build_settings['WRAPPER_EXTENSION'] == 'bundle'
config.build_settings['DEVELOPMENT_TEAM'] = 'com.example.flutterAzureCallingUi'
end
end
end
end
Dependiendo del momento en el que estés leyendo este post, es posible que tengas que modificar el target o la versión del SDK. Asimismo, ten en cuenta que en lugar de usar com.example.flutterAzureCallingUI, debes usar el nombre de paquete de tu app.
<key>NSCameraUsageDescription</key>
<string>Need camera access for video calling</string>
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for video calling</string>
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private var callComposite: CallComposite?
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: "com.example.flutter_azure_calling_ui/calling",
binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "startCall":
guard let args = call.arguments as? [String: String] else {return}
let groupCallId = args["groupCallId"]!
let displayName = args["displayName"]!
let token = args["userToken"]!
self.startCallComposite(groupCallId: groupCallId, displayName: displayName, userToken: token)
result(true)
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func startCallComposite(groupCallId: String, displayName: String, userToken: String) {
let callCompositeOptions = CallCompositeOptions();
callComposite = CallComposite(withOptions: callCompositeOptions);
let communicationTokenCredential = try! CommunicationTokenCredential(token: userToken);
let remoteOptions = RemoteOptions(for: .groupCall(groupId: UUID(uuidString: groupCallId)!),
credential: communicationTokenCredential,
displayName: displayName)
callComposite?.launch(remoteOptions: remoteOptions)
}
}
Con todo lo anterior, ahora podemos ejecutar la app también en iOS y hacer vídeo llamadas a través del móvil, independientemente de la plataforma.
Aquí dejo el enlace al repo, para que podáis revisar todo el código de este ejemplo: https://github.com/Encamina/Blogs/tree/master/Por%20una%20nube%20sostenible/CommunicationService
Este sitio web utiliza cookies para que tengas la mejor experiencia de usuario. Si continuas navegando, estás dando tu consentimiento para aceptar las cookies y también nuestra política de cookies (esperemos que no te empaches con tanta cookie 😊)