La mejor manera de usar Zoom en tu app de Angular
Déjame contarte una pequeña historia. Recientemente participe en un proyecto en donde el cliente necesitaba integrar Zoom a su plataforma. Según los requisitos del cliente, la plataforma se haría en Angular y se necesitaban al menos 4 rutas y 5 pantallas modal que abrieran una reunión de Zoom diferente cada una. Yo, siendo el encargado de hacer el frontend, hice dicha integración revisando la documentación del Web SDK de Zoom así como el ejemplo de integración con Angular.Llega el día de presentarle la plataforma al cliente... y estaba completamente furioso con mi equipo y, sobre todo, conmigo. Y con justa razón. Digo, la plataforma "funcionaba", pero andaba a paso de caracol y, lo peor de todo, estaba súper buggeada (más adelante especifico los detalles).Por lo tanto, mi equipo y yo tuvimos que idear una solución para la plataforma.
Esta solución ahora la considero la mejor manera de trabajar con el Web SDK de Zoom, por lo menos hasta el momento en el que escribo esto. Mi intención con este artículo no es despotricar en contra del Web SDK de Zoom, sino ayudar a todos mis colegas que quizá se topen con unos requisitos de proyecto parecidos y evitar que sus clientes se queden con un mal sabor de boca.
¿Qué problemas hay al trabajar con el Web SDK de Zoom?
No dudo que el Web SDK de Zoom funcione muy bien para integraciones sencillas como la que muestran en su ejemplo de integración, pero si requieres algo más complejo, te vas a topar con algunas dificultades. Empecemos detallando los principales problemas con los que me topé al trabajar con el Web SDK de Zoom.
1. Es difícil de personalizar
Cuando inicias el Web SDK de Zoom, se añaden múltiples scripts y elementos al DOM. Principalmente se añade un div con el ID "zmmtg-root" en donde se encuentra todo el UI de Zoom. Al iniciar una reunión este contenedor cubre toda la pantalla. Pero ¿y si quieres que la reunión se vea como un video incrustado dentro de tu sitio? Debería ser tan sencillo como especificar el height y width de zmmtg-root con CSS ¿cierto?
Desafortunadamente te encontrarás con que algo tan sencillo como cambiar el tamaño u ocultar un elemento de la UI de Zoom se convierte en una inspección completa del DOM que te va a tomar toda la tarde. Y ojalá no estés utilizando Bootstrap en tu sitio, porque al cargar el Web SDK de Zoom se va a llevar por delante cualquier personalización ya sea de color o directamente a alguna clase de Bootstrap (😨). Ah y si personalizaste la fuente del sitio también se la va a llevar por delante, independientemente si usas Bootstrap o no (😱).
2. Empeora el rendimiento de tu sitio
Como ya lo mencioné, un requisito particular del cliente para su plataforma era que algunas de las rutas del sitio abrieran una reunión de Zoom diferente. Este requisito no tiene nada de especial y es muy probable que te topes con algo parecido. Súper sencillo de hacer con Angular. Se puede usar el mismo componente para todas las reuniones de Zoom e indicar con un parámetro de ruta cuál reunión es la que se debe de abrir.
Por supuesto, la plataforma no solo tiene rutas para abrir reuniones de Zoom. Así que se puede crear un módulo que contenga ese componente y usar lazy loading del módulo para que solo se cargue al entrar a una ruta de reunión. Y aquí es donde se presenta el problema. Con el simple hecho de importar el Web SDK de Zoom en el componente de reunión hace que la navegación a dicho componente tarde 2 o 3 segundos. No hace falta que lo diga, pero 3 segundos para cambiar de ruta en Angular es una eternidad 🐌. Este simple hecho hizo que toda la plataforma se sintiese muy lenta, e incluso, que se detenía por completo.
3. Problemas para manejar múltiples reuniones
Voy a ser muy franco ahorita. Tratar de manejar diferentes rutas y pantallas modal con una reunión de Zoom distinta cada una, dan ganas de darte un tiro. Y tiene que ver con la manera en que funciona el SDK de Zoom. Antes de abrir una reunión se tienen que cargar los scripts de JS y WebAssembly del SDK, pero una vez que ya los cargaste no los puedes remover a menos que recargues el sitio. Y si los intentas cargar nuevamente tira un error y deja de funcionar por completo.
Además, para iniciar otra reunión tienes que salirte de la anterior. Pero para probarlo necesitas tener las reuniones abiertas, porque si no te marca un error y por alguna razón ya no te deja unirte a otras reuniones. Entonces nada más para entrar a una reunión se tendría que checar si ya se cargaron los scripts del SDK o no, si hay una reunión abierta o no, si el usuario pudo unirse a la reunión o no. Y hay algunos errores del SDK que son imposibles de depurar, de esos que nada más brota el error, pero no te dice ni que paso.
Simplemente, tratar de utilizar el Web SDK de Zoom en una aplicación compleja se vuelve una pesadilla. Pero como dije al principio, encontramos una solución que te quita de todos los problemas que acabo de describir, y es usar el SDK dentro de un iframe.
Usando el Web SDK de Zoom dentro de un iframe
Antes de cualquier cosa, no vamos a incrustar Zoom en un iframe como si fuera un simple video. Tampoco necesitas crear una aplicación aparte con un subdominio o algo así. Lo que vamos a hacer es crear un componente especial de reunión de Zoom en la misma aplicación y vamos a poner la ruta de este componente como la fuente del iframe. Si, también yo estaba confundido cuando mi equipo me lo propuso.
Configuración básica del proyecto
Recuerda que puedes checar el resultado de este tutorial en este repositorio en GitHub. Lo primero que vamos a hacer es crear un nuevo proyecto de Angular. Seguido vamos a instalar Bootstrap por medio de su CDN. Este paso, aunque opcional, demostrará que esta solución solventa cualquier problema de personalización. También vamos a instalar el Web SDK de Zoom, usando el comando npm install @zoomus/websdk
.
Para usar el SDK necesitamos una pequeña configuración en nuestro archivo angular.json. Los apartados de assets y styles quedarían así:
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/@zoomus/websdk/dist/lib/",
"output": "./node_modules/@zoomus/websdk/dist/lib/"
}
],
"styles": [
"node_modules/@zoomus/websdk/dist/css/react-select.css",
"src/styles.scss"
],
Si has visto el ejemplo de integración con Angular de Zoom, notarás que ese ejemplo tiene una línea adicional en el apartado styles con esto: node_modules/@zoomus/websdk/dist/css/bootstrap.css
. Esta línea no es necesaria en nuestro proyecto porque ya estamos usando Bootstrap. Si no estás usando Bootstrap en tu proyecto debes incluirla.
En este punto es buena idea que crees una aplicación en el Marketplace de Zoom para que obtengas tu API Key y Secret. Solo incluye tu API Key en tus archivos de environment. Por otro lado, necesitarás generar una firma para unirte a una reunión. Esto va más allá del alcance de este artículo, pero solo es crear un endpoint muy sencillo en tu backend y Zoom ofrece ejemplos de esto en varios lenguajes en su documentación.
Componentes y rutas
En nuestro proyecto vamos a crear un componente de navbar, uno de home y uno de meeting. El navbar siempre se muestra claro. El home no va a hacer nada solo es para demostrar que se pueden tener rutas sin reuniones. Va a ser el componente de meeting el que va a tener el iframe para mostrar las reuniones, cambiando de reunión dependiendo de un parámetro de ruta. No vamos a usar lazy loading para las rutas de home y meeting pero esto es algo que puedes hacer sin problema. Hasta el momento nuestras rutas en el app-routing.module quedarían así:
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: ':meeting', component: MeetingComponent },
{ path: '**', pathMatch: 'full', redirectTo: 'home' }
];
También vamos a crear un sanitizer pipe que nos va a ayudar a sanitizar la URL que vamos a insertar en el iframe. Este pipe quedaría así:
@Pipe({
name: 'sanitizer'
})
export class SanitizerPipe implements PipeTransform {
constructor(
private sanitizer: DomSanitizer,
) {}
transform(url: string): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}
Dentro del componente de meeting vamos, primero, a obtener el parámetro de ruta que nos va a señalar la reunión que se debe de abrir. Para esto debemos suscribirnos a la propiedad params de ActivatedRoute, porque si no, no vamos a poder detectar el cambio de ruta a otra reunión.
ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy)).subscribe(
({ meeting }) => this.validateMeeting(meeting)
);
}
Aquí llamamos un método para validar el identificador de la reunión y obtener la reunión activa. En este caso la validación la hago con un diccionario de reuniones local, pero puedes crear un archivo con este diccionario o tener un endpoint para obtener la información de la reunión. En este ejemplo, quedaría de esta forma:
private validateMeeting(meetingName: string) {
const meeting = this.meetings.find(meeting => meeting.name === meetingName);
return meeting
? this.setMeetingUrl(meeting)
: this.router.navigateByUrl('/home');
}
Por último, mandamos llamar al método que crea la URL que vamos a usar en el iframe, de esta manera:
private setMeetingUrl(meeting: Meeting) {
this.activeMeeting = meeting;
this.url = `/zoom/${ meeting.zoomId }/${ meeting.zoomPasscode }`;
}
Nota que aún no definimos una ruta que corresponda con esa URL, pronto lo haremos. Para mostrar el iframe solo debemos agregar esto en el HTML de nuestro componente, es muy importante que uses el pipe sanitizer en este punto.
<iframe [src]="url | sanitizer" frameborder="0" class="w-100 h-100"></iframe>
Si todo sale bien, deberías poder observar el mismo sitio web dentro del iframe. Ahora podemos continuar definiendo el módulo de Zoom, donde se encontrará toda la lógica para usar el Web SDK.
Módulo de Zoom
Vamos a crear un módulo zoom con la bandera --routing
. Esto va a crear un archivo de rutas para este módulo. También vamos a crear un componente zoom y un servicio zoom. El componente zoom lo puedes crear con las banderas --inline-style --inline-template
ya que no va a tener nada en el template. Ahora vamos a añadir una ruta en nuestro app-routing.module y esta si va a ser lazy loaded, quedando de esta manera:
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'zoom', loadChildren: () => import('./zoom/zoom.module').then(m => m.ZoomModule) },
{ path: ':meeting', component: MeetingComponent },
{ path: '**', pathMatch: 'full', redirectTo: 'home' }
];
Mientras que, dentro del archivo zoom-routing.module.ts, vamos a declarar una solo ruta que va a tener dos parámetros, uno para el ID de reunión y uno para su contraseña.
const routes: Routes = [
{ path: ':meeting/:passcode', component: ZoomComponent }
];
Entonces la ruta completa para el componente zoom sería zoom/:meeting/:passcode
que corresponde con la que ya habíamos definido en el componente meeting. Ahora podemos continuar con el zoom.service.
Para este servicio, primero que nada, hay que quitar la opción providedIn: 'root'
del decorador y proveerlo solo en el zoom.module. También necesitamos inyectar el HttpClient en el constructor del servicio (no olvides importar el HttpClientModule en el app.module) para poder realizar una llamada a nuestro endpoint que genera la firma requerida por Zoom.
El zoom.service va a contener los siguientes métodos:
- loadZoom: Carga los scripts propios de Zoom (encapsulados dentro del iframe)
- startMeeting: Inicia el proceso para ingresar a una reunión, manda a llamar getSignature y initZoomMeeting
- getSignature: Obtiene la firma requerida por Zoom haciendo una petición HTTP al endpoint que especifiquemos
- initZoomMeeting: Manda a llamar el método init del SDK. Si es exitoso, manda a llamar joinMeeting. Este método envuelve el llamado al SDK en una promesa para mejorar el control del flujo
- joinMeeting: Manda a llamar el método join del SDK con la firma, API Key, ID y contraseña de la reunión, e información del usuario. Este método envuelve el llamado al SDK en una promesa para mejorar el control del flujo
Entonces, el zoom.service queda de la siguiente manera:
@Injectable()
export class ZoomService {
private signatureEndpoint = '';
private leaveUrl = '';
constructor(
private http: HttpClient,
) { }
loadZoom() {
ZoomMtg.preLoadWasm();
ZoomMtg.prepareJssdk();
}
async startMeeting(meeting: Meeting, user: UserData) {
const { signature } = await this.getSignature(meeting.id);
return this.initZoomMeeting(signature, meeting, user);
}
private getSignature(meetingNumber: string, role = 0) {
return this.http.post<{ signature: string }>(this.signatureEndpoint, { meetingNumber, role }).toPromise();
}
private initZoomMeeting(signature: string, meeting: Meeting, user: UserData) {
return new Promise<ZoomResponse>((resolve, reject) => {
ZoomMtg.init({
leaveUrl: this.leaveUrl,
isSupportAV: true,
success: (_: ZoomResponse) => {
this.joinMeeting(signature, meeting, user)
.then((response) => resolve(response))
.catch((error) => reject(error));
},
error: (error: ZoomResponse) => reject(error)
});
});
}
private joinMeeting(signature: string, meeting: Meeting, user: UserData) {
return new Promise<ZoomResponse>((resolve, reject) => {
ZoomMtg.join({
signature,
meetingNumber: meeting.id,
apiKey: zoomApiKey,
userName: user.name,
userEmail: user.email,
passWord: meeting.passcode,
success: (success: ZoomResponse) => {
resolve(success);
},
error: (error: ZoomResponse) => reject(error)
});
});
}
}
Por último, el zoom.component solo obtiene el ID y passcode de la reunión de los parámetros de la URL y manda a llamar los métodos loadZoom y startMeeting del zoom.service.
Con esto ya tendríamos todo lo necesario para probar el funcionamiento de nuestra aplicación y comprobar que efectivamente, no tiene ninguno de los problemas que describí al inicio de este artículo. Recuerda que tenemos disponible este proyecto de ejemplo en este repositorio, por si necesitas ayuda extra.
Si necesitas ayuda para crear tu sitio web, en DevAces somos expertos en la creación de sitios web profesionales, tiendas en línea y marketing digital. ¡Acércate a nosotros y crearemos la solución digital que más se acomode a tu negocio y presupuesto!
Da clic aquí para conocer nuestros servicios y contactarnos.
No olvides darle like a nuestra página en Facebook para mantenerte al tanto de nuestras publicaciones.