AZure

Primeros pasos con Azure Communication Service en Flutter para hacer videollamadas

En este post vamos a ver cómo comenzar a integrar Azure Communication Service en nuestras aplicaciones. Crearemos el servicio en Azure y lo utilizaremos desde una aplicación Android/iOS creada con flutter.

¡¡¡Comenzamos!!!

¿Qué vamos a crear?

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.

Creando el servicio en Azure

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.

Precios del 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/

Obteniendo token de acceso

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

Creando la aplicación móvil con Flutter

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).

Desarrollando parte común en Dart

El primer paso que vamos a dar es el de añadir los paquetes necesarios para hacer la app:

    • uuid: Nos servirá para generar un UUID de manera sencilla. Lo vamos a utilizar para crear el identificador de una reunión.

    • jwt_decoder: Con este paquete decodificaremos nuestro token y obtendremos el identificador único. Aunque no es el objetivo de este post, este identificador es el que se utilizaría para hacer llamadas punto a punto.

    • http: Lo utilizaremos para hacer la llamada a la función de Azure que nos proporciona el token de acceso a communication service.

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.

Desarrollando parte nativa en Android

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:

    • Asegurarnos de que mavenCentral se utiliza como repositorio en buildscript y allporjects de nuestro .gradle a nivel de proyecto.

buildscript { 
    repositories { 
        ... 
        mavenCentral() 
        ...
    }
} 
... 
allprojects { 
    repositories { 
        ... 
        mavenCentral() 
        ...
    }
}

    • En el .gradle, a nivel de módulo, añadir la siguiente dependencia y opciones de compilación.

android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    ..
}
...
dependencies {
    ...
    implementation 'com.azure.android:azure-communication-ui-calling:+'
    ...
}

    • Añadir los permisos necesarios en el AndroidManifest.xml.

<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>

    • Añadir un replace en el label de application, para evitar problemas al ejecutar la aplicación.

<manifest 
    ...
    xmlns:tools="http://schemas.android.com/tools">
   <application
        ...
        tools:replace="android:label">
        ...
    </application>
</manifest>

    • Crear canal de comunicación con flutter. Este canal tiene que tener el mismo nombre que el utilizado en Dart para que la comunicación se materialice correctamente. Además, se añade el método al que se va a llamar para inicia la llamada.

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.

Desarrollando parte nativa en iOS

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:

    • El primer paso es hacer un pod init desde el terminal, ya que la instalación del SDK se hace vía pod.

    • Una vez iniciados los pod, abrimos el archivo Podfile y lo modificamos para dejarlo de la siguiente manera.

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.

    • Ejecutar la instrucción pod install. Ésta generará un .xcworkspace, que será el proyecto que tenemos que abrir en XCode.

    • Añadir las siguientes líneas en el info.plist que existe en la ruta ios/Runner

<key>NSCameraUsageDescription</key>
<string>Need camera access for video calling</string>
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for video calling</string>

    • Crear canal de comunicación con flutter. Al igual que para Android, este canal tiene que tener el mismo nombre que el utilizado en Dart para que la comunicación se materialice correctamente. Además, se añade el método al que se va a llamar para inicia la llamada.

@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

Compartir
Publicado por
Jorge Diego

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 😊)