
Compartir:
Javier studied Industrial Engineering back in Madrid where he's from. He is now one of our Solution Engineers, so if you get into trouble using our APIs he may be the one that gives you a hand. Out of work he loves playing football and travelling as much as he can.
Añade funciones de Video a Zendesk con la API de Video de Vonage
Tiempo de lectura: 13 minutos
En este tutorial, vamos a agregar funcionalidad de video, pantalla compartida y grabación a Zendesk mediante el uso de Video API de Vonage para que pueda ofrecer una experiencia más rica al cliente.
Puede que estés pensando que esto no es para ti porque no usas Zendesk, pero, de hecho, hay muchos otros sistemas de tickets en los que podrías aplicar estos consejos. Si eso no te convenció, déjanos mostrarte cómo manejar programáticamente grabaciones y subirlas a un ticket de Zendesk para que ambas partes puedan descargarlo.
El escenario
El cliente desea discutir un ticket pendiente con el ingeniero de soporte. Solicita una videollamada con el ingeniero de soporte pulsando el botón
Discuss Live with Javiery espera a que se una.

- El ticket se actualiza con un comentario interno, de modo que se notifica al ingeniero de soporte que el solicitante del ticket desea tener una sesión de vídeo.

El ingeniero de soporte se une a la sesión, revisan el ticket (no hay mucho que discutir en este caso concreto 😂). Deciden grabar la llamada y, una vez que se detiene la grabación, se carga en forma de comentario del ticket para que ambos participantes puedan descargarla.

Si esto le ha llamado la atención, sígalo.
Arquitectura
Para ofrecer una visión general de la arquitectura de esta integración, nos gustaría compartir con usted el siguiente diagrama:

Por un lado, el cliente final solicita una videollamada con el ingeniero de soporte a través de la página de solicitud de Zendesk. El servidor gestionará la solicitud y actualizará el ticket para llamar la atención del agente. Por el otro lado, el Agente que usa Zendesk se unirá a la misma sesión para hablar en vivo.
Requisitos previos
Antes de empezar, necesitarás lo siguiente:
Node.js instalado y algunos conocimientos básicos de JavaScript
Una Account de Zendesk con derechos de administrador
La aplicación Zendesk App Tools (ZAT) instalado
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
Agente de Zendesk
Para comenzar con las aplicaciones de Zendesk, puede seguir sus instrucciones para Construya su primera aplicación de soporte tutorial. Vaya al directorio del proyecto y ejecute el siguiente comando.
zat newSe le pedirá alguna información como el nombre de su aplicación; la llamaremos Zendesk Video App. También le pedirá su dirección de correo electrónico y algunos otros parámetros que no afectarán a la funcionalidad. Una vez ejecutado el comando, verás que se crea la aplicación. También vamos a crear una carpeta para nuestro servidor. La estructura final del proyecto se ve así.
|--Application
|-- Server
|-- server.js
|-- Zendesk Video App
|-- manifest.json
|-- Assets
|-- iframe.html
|-- index.css
|-- index.jssNuestra aplicación estará compuesta por un marco incrustado en la interfaz de Zendesk, y tendrá un área de videochat con varias acciones disponibles. Vamos a editar el archivo iframe.html añadiendo algunos elementos de botón sencillos que permitirán al Agente mantener una videollamada con el cliente dentro del ticket. Puedes copiar y pegar el siguiente código en tu archivo iframe.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/combine/npm/@zendeskgarden/css-bedrock@7.0.21,npm/@zendeskgarden/css-utilities@4.3.0">
<link href="main.css" rel="stylesheet">
</head>
<body>
<div id="content"></div>
<button id="initiatesession" class="button" onclick="initializeSession()">Initiate Session</button>
<button id="startPublishingVideoId" class="button" onclick="startPublishingVideo()">Turn on Video </button>
<button id="startPublishingScreenId" class="button" onclick="startPublishingScreen()">Share Screen</button>
<button id="handleRecording" class="button" onclick="handleRecording()">Start Recording</button>
<div id="videos" >
<div id="publisher" ></div>
<div id="subscriber" ></div>
</div>
<script id="requester-template" type="text/x-handlebars-template">
</script>
<script src="https://cdn.jsdelivr.net/npm/handlebars@4.3.3/dist/handlebars.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js"></script>
<script src="https://static.zdassets.com/zendesk_app_framework_sdk/2.0/zaf_sdk.min.js"></script>
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
<script src="index.js"></script>
</body>
</html>
También añadiremos algo de CSS básico para los botones.
.button {
background-color: #008CBA;;
border: none;
color: black;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 12px;
}Ahora, edite el archivo main.js para crear un cliente ZAF. El cliente ZAF permite que su aplicación se comunique con el producto Zendesk anfitrión. Puede usar el cliente en sus aplicaciones para escuchar eventos, obtener o establecer propiedades o invocar acciones. En este caso, nos interesan los detalles del ticket en el que estamos trabajando. En concreto, el ID del ticket y el ID del solicitante. Una vez cumplida la promesa, podemos enviar una petición a nuestro servidor para obtener la clave API, el ID de sesión y un token para este ticket. Toda la lógica de generación de sesión vendrá de nuestro servidor. Llegaremos a eso más adelante.
$(function() {
let client = ZAFClient.init();
client.invoke('resize', { width: '100%', height: '79vh' });
videos.style.display = 'none';
client.get(['ticket.id', 'ticket.requester.id']).then(data => {
let user_id = data['ticket.requester.id']
let ticket_id = data['ticket.id'];
fetch(SERVER_BASE_URL + '/room/' + user_id + "-" + ticket_id).then(res => {
return res.json()
}).then(res => {
apiKey = res.apiKey;
sessionId = res.sessionId;
token = res.token;
}).catch(handleError);
});
});
Ahora que tenemos estos valores, podemos dejar que el Agente elija cuando iniciar la sesión de Video. Definiremos una función initializeSession que se activará cuando el Agente pulse el botón Initiate session botón . Estableceremos el display del contenedor del editor a block para hacerlo visible (ya que inicialmente está establecido a none). Iniciaremos la sesión instanciando un objeto de sesión, y luego inicializaremos el editor.
let initializeSession = () => {
session = OT.initSession(apiKey, sessionId);
// Create a publisher
publisher = OT.initPublisher('publisher', {
insertMode: 'replace',
publishVideo: false,
}, handleError);
// Connect to the session
session.connect(token, error => {
// If the connection is successful, initialize a publisher and publish to the session
if (error) {
handleError(error);
} else {
session.publish(publisher)
document.getElementById("initiatesession").style.display = "none"
}
});
}
También crearemos algunos listeners para eventos que son enviados por el objeto de sesión. Aprovecharemos las etiquetas archiveStarted y archiveSopped para controlar el estado de nuestra aplicación, es decir, para saber si estamos publicando el vídeo o si está apagado si estamos grabando.
Mostraremos un valor diferente en los botones HTML, dependiendo del estado. Por ejemplo, cuando recibamos el mensaje archiveStartedquerremos que nuestro botón muestre "Detener archivo" en lugar de "Iniciar archivo", ya que el archivo/grabación ya se ha iniciado. En la parte superior de nuestro código, hemos definido algunas variables de estado (archiving, videoy screen) que cambiarán en función de estos eventos.
También querremos suscribirnos a un flujo tan pronto como se cree, por lo que escucharemos el evento streamCreated evento.
session.on('archiveStarted', event => {
archiveID = event.id;
archiving = true
document.getElementById('handleRecording').innerHTML = 'Stop Archive';
console.log('ARCHIVE STARTED ' + archiveID);
});
session.on('archiveStopped', event => {
archiveID = event.id;
archiving = false
document.getElementById('handleRecording').innerHTML = 'Start Archive';
console.log('ARCHIVE STOPED ' + archiveID);
});
session.on("streamPropertyChanged", event => {
video = event.newValue
video ? document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video off' : document.getElementById("startPublishingVideoId").innerHTML = 'Turn on Video';
});
session.on('streamCreated', event => {
console.log('stream created' + event.stream)
session.subscribe(event.stream, 'subscriber', {
insertMode: 'append',
}, handleError);
});
La función handleError que pasamos como callback es una función que lanza una alerta si se produce un error al escuchar eventos en la sesión.
let handleError = (error) => {
if (error) {
alert(error.message);
}
}
Podemos crear una función handleRecording que determine si estamos grabando o no. Esto nos permitirá disparar una función diferente dependiendo del estado.
let handleRecording = () => {
archiving ? stopArchive() : startArchive();
}
La función StartArchive hará una petición POST a la ruta archive/start de nuestro servidor. Necesitamos pasar nuestro sessionId para que nuestro servidor sepa qué sesión está activando la grabación. Verás más adelante en el tutorial que nos referimos a la grabación y almacenamiento de la sesión. No te confundas; es el mismo concepto, pero usamos el término "archivo" internamente :)
let startArchive = () => {
console.log('start');
fetch(SERVER_BASE_URL +'/archive/start', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
'sessionId': sessionId
})
})
.then((response) => {
return response.json();
})
.then((data) => {
console.log('data from server when starting archiving', data)
})
.catch(error => console.log('errror starting archive', error))
}
En cuanto a la función StopArchive es prácticamente igual que la función StartArchive. Pero, en este caso, tenemos que pasar el archiveID que viene del archiveStarted evento.
let stopArchive = () => {
console.log('archiveID' + archiveID);
fetch(SERVER_BASE_URL + '/archive/' + archiveID + '/stop', {
method: 'post',
headers: {
'Content-type': 'application/json'
}
})
.then((response) => {
return response.json()
})
.then((data) => {
console.log('data from server when stopping archiving', data)
})
.catch(error => console.log('errror stopping archive', error))
}
Ahora tenemos que añadir soporte para los flujos de compartir pantalla. Vamos a crear una función que comprobará si ya estamos compartiendo nuestra pantalla y si no, creará un nuevo editor. Esta función actuará como un toggler para el flujo de pantalla compartida en conjunción con algunos eventos, al igual que hicimos para el archivo.
Vamos a comprobar si el navegador soporta Compartir Pantalla llamando al método OT.checkScreenSharingCapability método. Explicamos más sobre el soporte de compartir pantalla en la documentación sobre checkScreenSharingCapability callback. Para algunas versiones antiguas de navegadores, puede ser necesario instalar una extensión, pero vamos a suponer que ambos participantes utilizarán un navegador reciente en aras de la simplicidad.
Tenga en cuenta que los eventos que estamos escuchando en este caso son enviados por el objeto editor en lugar del objeto de sesión. Consulte el archivo StreamEvent para obtener más información.
const startPublishingScreen = () => {
if (screenSharing === true) {
session.unpublish(screenPublisher)
} else {
OT.checkScreenSharingCapability(response => {
if (!response.supported || response.extensionRegistered === false) {
alert('Screen share is not supported in this browser')
} else {
screenPublisher = OT.initPublisher('screen', {
videoSource: 'screen'
}, error => {
if (error) {
console.log(error)
} else {
session.publish(screenPublisher, handleError)
.on("streamCreated", event => {
if (event.stream.videoType === 'screen') {
screenSharing = true;
document.getElementById("startPublishingScreenId").innerHTML = 'stop screenShare'
}
})
.on("streamDestroyed", event => {
if (event.stream.videoType === 'screen') {
screenSharing = false
document.getElementById("startPublishingScreenId").innerHTML = 'start screenShare'
}
})
}
})
}
})
}
}
Cliente
Ahora que tenemos nuestro lado del agente en funcionamiento, tenemos que pensar en añadir la capacidad de Video al lado del cliente. El objetivo principal de este post es conseguir que el cliente final (solicitante del ticket) y el agente de soporte (asignatario del ticket) estén conectados.
Para ello, vamos a seguir guía de personalización del tema del centro de ayuda para que podamos acceder al código de la página del solicitante del ticket y construir una experiencia de cliente más rica en el Centro de ayuda.
En este caso, nos interesa personalizar las Requests pagees decir, las listas de solicitudes o tickets asignados a un usuario específico. Como se explica en el artículo enlazado más arriba, el HTML del Centro de Ayuda está contenido en plantillas editables. Vamos a editar el archivo requests_page.hbs archivo. El código va a ser muy similar al código JavaScript en el archivo main.js archivo.
En primer lugar, vamos a importar la biblioteca Opentok. Esto descargará la última versión del SDK JS.
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>Estamos añadiendo un poco de marcado básico que contendrá el editor y el suscriptor de vídeo, así como algunos botones que se encargará de la funcionalidad de nuestra aplicación. Usted habrá notado que tenemos {{assignee.avatar_url}}. Es un lenguaje de plantilla llamado Curlybars que nos permitirá interactuar con los datos del Centro de Ayuda en el contexto del ticket Zendsk.
En este ejemplo, mostramos una foto del asignatario del ticket en el botón que iniciará la videollamada. El objetivo es ofrecer una experiencia cercana al cliente. Además, para mantenerlo simple al principio, ocultaremos todos los botones excepto el que inicia la llamada. Lo haremos estableciendo la propiedad display de nuestros elementos HTML a none.
<div>
<button class="button" onclick="initializeSession()" style="position:relative">
<img src={{assignee.avatar_url}} />
<span class="tooltiptext">Discuss live with {{assignee.name}}</span>
</button>
</div>
<button id="startPublishingVideoId" class="button" onclick="toggleVideo()" style="display:none">Turn Video off</button>
<button id="handleRecording" class="button" onclick="handleRecording()" style="display:none>Start video recording</button>
<button id="startPublishingScreenId" class="button" onclick="startPublishingScreen()" style="display:none">Share your screen</button>
<div id="videos">
<div id="publisher"></div>
<div id="subscriber"></div>
</div>
Vamos a definir algunas variables que utilizaremos a lo largo del código. Como hicimos para el lado del Agente, trabajaremos con algunas variables de estado (video, archivingy screenSharing). También definiremos el endpoint de nuestro servidor.
let sessionId;
let publisher;
let archiveId;
let screenSharing = false;
let archiving = false;
let video = true;
const SERVER_BASE_URL = 'SERVER_BASE_URL';Estamos definiendo una simple función manejadora de errores que usaremos para alertar al usuario en caso de error. El único objetivo de definir esto como una función separada es limpiar un poco nuestro código.
const handleError = (error) => {
if (error) {
alert(error.message);
}
}
Estamos buscando apiKey, sessionIdy token de nuestro servidor.
fetch(SERVER_BASE_URL + '/room/' + {{request.requester.id}} + '-' +{{request.id}}).then(res => {
return res.json()
}).then(res => {
apiKey = res.apiKey;
sessionId = res.sessionId;
token = res.token;
}).catch(handleError);
A continuación, añada la siguiente función initializeSession que se activará cuando el cliente decida solicitar una videollamada con el agente de soporte. Vamos a mostrar los botones que estaban ocultos al principio, entonces estamos instanciando primero un objeto de sesión y la creación de un editor. Por último, intentaremos conectarnos a la sesión. Si la conexión tiene éxito, intentaremos publicar en la sesión, como se explicó anteriormente.
const initializeSession = () => {
document.getElementById('startPublishingVideoId').style.display = "block";
document.getElementById('handleRecording').style.display = "block";
document.getElementById('startPublishingScreenId').style.display = "block";
videos.style.display = 'block';
session = OT.initSession(apiKey, sessionId);
publisher = OT.initPublisher('publisher', {
insertMode: 'append',
width: '100%',
height: '100%',
}, handleError);
session.connect(token, error => {
if (error) {
handleError(error);
} else {
session.publish(publisher, handleError);
}
});
session.on('streamCreated', (event) => {
session.subscribe(event.stream, 'subscriber', {
insertMode: 'append',
width: '100%',
height: '100%'
}, handleError);
});
session.on('archiveStarted', event => {
archiveID = event.id;
archiving = true
document.getElementById('handleRecording').innerHTML = 'Stop Archive';
console.log('ARCHIVE STARTED ' + archiveID);
});
session.on('archiveStopped', event => {
archiveID = event.id;
archiving = false
document.getElementById('handleRecording').innerHTML = 'Start Archive';
console.log('ARCHIVE STOPED ' + archiveID);
});
session.on("streamPropertyChanged", event => {
console.log(event.newValue)
video = event.newValue
video ? document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video off' : document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video on';
});
session.on('streamCreated', event => {
session.subscribe(event.stream, 'subscriber', {
insertMode: 'append',
}, handleError);
});
}
Vamos a utilizar operadores ternarios para decidir si necesitamos encender o apagar el vídeo. La misma lógica se aplica para determinar si vamos a llamar a la función para iniciar la grabación o para detenerla.
const toggleVideo = () => {
video ? publisher.publishVideo(false) : publisher.publishVideo(true)
}
const handleRecording = () => {
archiving ? stopArchive() : startArchive();
}
En startArchive() y startArchive() tienen exactamente el mismo aspecto que las funciones main.jspor lo que las omitiremos en aras de la simplicidad. También es posible que desee sólo dar la opción de iniciar grabaciones al agente de soporte y no al cliente final, pero esto es totalmente de usted. Para hacerlo más divertido, permitiremos a ambos iniciar y detener grabaciones ya que ambos podrán recuperar la grabación después de la llamada.
Servidor
Nuestro servidor se compondrá de varias rutas para gestionar las solicitudes procedentes del Agente o del ingeniero de soporte.
Vamos a importar los módulos que vamos a utilizar para nuestra aplicación y definir algunas variables de entorno.
apiKey y apiSecret son las credenciales de la Video API que se encuentran en su panel de controlel remoteUri hace referencia al endpoint de Zendesk de tu organización en forma de https://xxxxxx.zendesk.com/. Para la autenticación de Zendesk, consulta su "Cómo puedo autenticar las solicitudes de API"ya que admiten diferentes métodos de autenticación; nosotros utilizamos el nombre de usuario y el token.
En cuanto a la autenticación con AWS, existen varios métodos soportadospero también decidimos optar por las variables de entorno. Ten en cuenta que en este caso, el SDK detecta automáticamente las credenciales de AWS establecidas como variables en tu entorno y las utiliza para las solicitudes del SDK, eliminando la necesidad de gestionar las credenciales en tu aplicación. Por eso no estamos leyendo las variables de nuestro archivo .env archivo.
const fs = require('fs');
const bodyParser = require('body-parser')
const express = require('express');
const path = require('path');
const app = express();
const _ = require('lodash');
const request = require ('request')
const ZD = require('node-zendesk');
const cors = require('cors');
const dotenv = require('dotenv')
dotenv.config();
const apiKey = process.env.apiKey
const apiSecret = process.env.apiSecret
const AWS = require('aws-sdk');
const remoteUri = process.env.remoteUri
const client = ZD.createClient({
username: process.env.username,
token: process.env.token,
remoteUri: process.env.remoteUri
});
const OpenTok = require('opentok');
const opentok = new OpenTok(apiKey, apiSecret);
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
let ticketId
const app = express()
init()Añada esto a su archivo index.js archivo.
const init = () => {
app.listen(8080, () => {
console.log('You\'re app is now ready at http://localhost:8080/');
}
La ruta que gestiona la creación de sesiones y tokens va a comprobar si ya hay una sesión creada para discutir este ticket, y si no, creará una. En caso de que no estés familiarizado con el concepto de token para la Video API, es como una llave de la sala (sesión).
Usted querría tener una solución más segura, pero decidimos hacer un poco de validación básica aquí para mantenerlo simple. En este caso, estamos recibiendo un parámetro name con el siguiente formato XXXXXX-YYYYY. ¿Recuerdas esas llamadas fetch que hicimos en ambas partes (Agente y Cliente)? Viene de ahí.
Sólo generaremos una sesión y un token si el ID del solicitante del ticket coincide con la segunda parte de nuestro :name parámetro recibido. Vamos a utilizar un paquete de Zendesk para realizar la validación. Como ejemplo, si recibimos 1222-1234comprobaremos a través de la API de Zendesk si efectivamente el ticket 1234 fue solicitado por el usuario 1222. Si no es así, devolveremos un HTTP 404.
También verá que hay cierta validación en torno al referente y al origen de la solicitud. Esto es un hack rápido hecho para actualizar el ticket sólo si la solicitud proviene del cliente, y dejar que el ingeniero de soporte sepa que el solicitante del ticket le gustaría tener una sesión de vídeo.
app.get('/room/:name', (req, res) => {
if (!req.params.name) {
res.status(402).end()
}
let roomName = req.params.name;
let sessionId;
let requesterId = roomName.split("-")[0]
ticketId = roomName.split("-")[1]
checkIfValid(ticketId, req).then(response => {
if (response && response.toString() === requesterId) {
if (req.headers.origin === endpoint && req.headers.referer.split("/")[3] === "hc") {
updateTicket(ticketId)
}
if (roomToSessionIdDictionary[roomName]) {
sessionId = roomToSessionIdDictionary[roomName];
token = opentok.generateToken(sessionId);
res.setHeader('Content-Type', 'application/json');
res.send({
apiKey: apiKey,
sessionId: sessionId,
token: token
});
} else {
giveMeSession().then(session => {
roomToSessionIdDictionary[roomName] = session.sessionId;
token = opentok.generateToken(session.sessionId);
res.setHeader('Content-Type', 'application/json');
res.send({
apiKey: apiKey,
sessionId: session.sessionId,
token: token
});
})
.catch(e => res.status(500).send({
error: 'createSession error:' + e
}))
}
} else {
res.status(404).end()
}
})
.catch((e) => {
res.status(404).end()
})
})
En una aplicación real, probablemente necesitaría almacenar los ID de sesión en su base de datos y comprobar si ya se ha creado una sesión para este ticket. Sin embargo, para este tutorial hemos decidido utilizar simplemente un diccionario que almacena los identificadores de sesión asociados a un nombre de sala. Tenga en cuenta que esto se restablecerá una vez que reinicie su servidor.
let roomToSessionIdDictionary = {};
// returns the room name, given a session ID that was associated with it
const findRoomFromSessionId = sessionId => {
return _.findKey(roomToSessionIdDictionary, value => { return value === sessionId; });
}
Como hemos mencionado, crearemos una sesión sólo si no hay ninguna sesión asociada al nombre de sala recibido. Estamos envolviendo el método basado en callback en una promesa que devolverá un objeto de sesión.
const giveMeSession = ()=>{
return new Promise((resolve, reject) => {
opentok.createSession({ mediaMode: 'routed' }, (err, session) => {
if (err) {
console.log('[Opentok - createRoutedSession] - Err', err);
reject(err);
}
resolve(session);
});
})
}
También hemos envuelto en una promesa la comprobación de Zendesk que nos permite consultar el ID del ticket que hemos recibido para poder determinar si la solicitud es legítima o no.
const checkIfValid = (ticketId, res) => {
return new Promise(
(resolve, reject) => {
client.tickets.show(ticketId, function(err, request, result){
if (err) reject(err);
resolve(result.requester_id);
})
}
);
};
Si la solicitud es válida y proviene del lado del Cliente (no del Agente), actualice el ticket para que el ingeniero de soporte reciba la notificación de que hay alguien esperando una sesión de video.
const updateTicket = (ticketId) => {
let notification = 'The requester of the ticket would like to talk to you.'
client.tickets.update(ticketId, {"ticket":{comment:{"body": notification, "public": false}}}, (err, req, res) => {
if(!err){console.log('Ticket updated')
}}
)}
Estamos definiendo las rutas para iniciar y detener el archivo. Tenga en cuenta que la ruta para detener el archivo también toma el ID de sesión. Esto es, para que nuestros servidores sepan para qué ID de sesión estás intentando parar la grabación.
app.post('/archive/start', (req, res) => {
var json = req.body;
var sessionId = json.sessionId;
opentok.startArchive(sessionId, { name: 'testSession' }, (err, archive) => {
if (err) {
console.error(err);
res.status(500).send({ error: 'startArchive error:' + err });
return;
}
res.setHeader('Content-Type', 'application/json');
res.send(archive);
});
});
app.post('/archive/:archiveId/stop', (req, res) => {
opentok.stopArchive(archiveId, function (err, archive) {
if (err) {
console.error('error in stopArchive');
console.error(err);
res.status(500).send({ error: 'stopArchive error:' + err });
return;
}
res.setHeader('Content-Type', 'application/json');
res.send(archive);
});
});
Si ejecuta su servidor, expóngalo con ngroky configure la URL de ngrok como SERVER_BASE_URL en ambos front ends (lado Cliente y Agente). Ahora tiene una sesión de Video, ¡bien hecho!
Vale, eso ha estado bien, ¡pero vayamos un paso más allá! ¿No sería genial si también pudiéramos manejar dinámicamente la grabación de la llamada y subirla a Zendesk para que tanto el ingeniero de soporte como el cliente pudieran recuperarla cuando más les convenga? ¡Hagámoslo!

Gestión de grabaciones
En primer lugar, tenemos que hacer saber a la Video API dónde queremos subir nuestra grabación de vídeo. Como vamos a usar un punto final de AWS S3, puedes seguir nuestra guía Uso del almacenamiento S3 con el archivo de la Video API de Vonage de Vonage. Una vez configurado, si tienes una sesión de video e inicias y detienes una grabación, se subirá automáticamente a tu cubo de S3.
Todos los archivos se guardan en un subdirectorio de tu bucket de S3 que tiene como nombre tu clave API de OpenTok, y cada archivo se guarda en un subdirectorio de ese, con el ID del archivo como nombre. El archivo de almacenamiento es archive.mp4.
Por ejemplo, considere un archivo con la siguiente clave de API e ID:
Clave API -- 123456
ID de archivo -- ab0baa3d-2539-43a6-be42-b41ff1488af3
El archivo de este archivo se carga en el siguiente directorio de su cubo S3:
123456/ab0baa3d-2539-43a6-be42-b41ff1488af3/archive.mp4
A continuación, necesitamos saber cuándo se ha subido el archivo a nuestro bucket de S3 para poder recuperarlo. Vamos a configurar una ruta en nuestro servidor para escuchar los eventos relacionados con el archivo. La plataforma Video API le enviará un webhook a su URL de devolución de llamada previamente configurada cuando cambie el estado de un archivo.
Ve a tu panel de control, pulsa sobre el proyecto que estés utilizando y configura la URL de tu servidor como https://YOUR_SERVER_URL/events. Como se explica en la guía de archivola plataforma Video API te enviará un estado de disponibilidad una vez que el archivo esté disponible para su descarga desde el bucket de S3. Escucharemos ese evento en nuestro servidor y lo descargaremos. Toda la lógica se manejará en el lado del servidor (server.js archivo).
app.post('/events', (req, res) => {
res.send('OK')
if(req.body.status === 'uploaded'){
let key = apiKey + "/" + req.body.id + "/archive.mp4"
downloadVideo(req.body.id + ".mp4", key)
}
})
Recuerde configurar la URL de su servidor en su cuenta de Video API. De lo contrario, no recibirás estos webhooks en tu servidor. Debería ser algo parecido a lo siguiente

Pasaremos dos variables a la función downloadVideo function; una es el nombre con el que queremos que se descargue nuestro archivo, y la otra es la Clave, para que nuestro bucket S3 sepa qué grabación estamos intentando recuperar.
La petición transmitirá los datos devueltos directamente a un objeto Stream de Node.js llamando al método createReadStream en la petición. La llamada a createReadStream devuelve el flujo HTTP sin procesar gestionado por la petición. El flujo de datos en bruto puede entonces ser canalizado a un objeto Node.js Stream. Ahora deberíamos ser capaces de descargar las grabaciones dinámicamente una vez subidas a nuestro bucket.
const downloadVideo = (name, key) => {
var fileStream = fs.createWriteStream(name);
s3 = new AWS.S3();
var s3Stream = s3.getObject({Bucket: process.env.BucketName, Key: key}).createReadStream();
s3Stream.on('error', (err) => {
console.error(err);
});
s3Stream.pipe(fileStream).on('error', (err) => {
// capture any errors that occur when writing data to the file
console.error('File Stream:', err);
}).on('close', () => {
console.log('Done.');
getToken(name)
});
}
Habrás notado que estamos llamando a una función getToken una vez que terminamos de descargar el archivo. Eso se debe al proceso de subir un archivo a Zendesk. Puedes hacer lo que quieras con el archivo en este punto, ya que ya está descargado. Sin embargo, para completar nuestro post, vamos a subir la grabación al ticket de Zendesk para que ambos participantes puedan ver la grabación después de la llamada.
Primero necesitamos obtener un token, y luego necesitamos actualizar el ticket pasando este token. Haremos la segunda parte en una función separada llamada uploadVideo.
const getToken = (archiveName) => {
client.attachments.upload(__dirname + '/' + archiveName , {binary: false, filename: archiveName}, (err, req, result) => {
if (err) {
console.log("error:", err);
}
console.log("token:", result.upload.token);
uploadVideo(result.upload.token, ticketId)
})
}
const uploadVideo = (token, ticketId) =>{
let ticket = {
"ticket":{"comment": { "body": "This is the recording of the call", "public": true, "uploads":[token]},
}};
client.tickets.update(ticketId,ticket, (err, req, res) => {
if(!err){
console.log('ticket updated with the video recording')
}
})
}
Echa un vistazo a la demo para hacerse una mejor idea de cómo funciona todo esto. Adapte este tutorial a sus necesidades, deje a sus clientes muy satisfechos y sean verdaderos defensores de la experiencia de asistencia.
El código de este proyecto se encuentra en la sección vonage-zendesk-integration GitHub.
¿Qué va a construir ahora? Cuéntanoslo.
