
Compartilhar:
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.
Adicione recursos de vídeo ao Zendesk com a Video API da Vonage
Tempo de leitura: 13 minutos
Neste tutorial, vamos adicionar funcionalidades de vídeo, compartilhamento de tela e gravação ao Zendesk usando a Video API da Vonage, para que você possa oferecer uma experiência mais rica ao cliente.
Talvez você esteja pensando que isso não é para você, já que não usa o Zendesk, mas, na verdade, existem muitos outros sistemas de tickets nos quais você poderia aplicar essas dicas. Se isso não o convenceu, vamos mostrar como lidar com gravações programaticamente e enviá-las para um ticket do Zendesk, para que ambas as partes possam baixá-las.
O Cenário
A cliente gostaria de discutir um ticket pendente com o engenheiro de suporte. Ela solicita uma videochamada com o engenheiro de suporte clicando no
Discuss Live with Javierbotão e aguarda que ele entre na chamada.

- O ticket é atualizado com um comentário interno, de modo que o engenheiro de suporte seja notificado de que o solicitante do ticket gostaria de realizar uma sessão por Video.

O engenheiro de suporte entra na sessão e analisa o ticket (não há muito o que discutir neste caso específico 😂). Eles decidem gravar a ligação e, assim que a gravação é encerrada, ela é enviada na forma de um comentário no ticket para que ambos os participantes possam baixá-la.

Se isso chamou sua atenção, por favor, continue lendo.
Arquitetura
Para apresentar uma visão geral da arquitetura dessa integração, gostaríamos de compartilhar o seguinte diagrama com vocês:

Por um lado, o cliente final solicita uma videochamada com o engenheiro de suporte por meio da página de solicitações do Zendesk. O servidor processará a solicitação e atualizará o ticket para chamar a atenção do agente. Por outro lado, o agente que utiliza o Zendesk entrará na mesma sessão para conversar ao vivo.
Pré-requisitos
Antes de começarmos, você precisará do seguinte:
Node.js instalado e alguns conhecimentos básicos de JavaScript
Um account no Zendesk com direitos de administrador
O Zendesk App Tools (ZAT) instaladas
Agente do Zendesk
Para começar a usar as Applications do Zendesk, você pode seguir o tutorial “Crie seu primeiro aplicativo de suporte” . Acesse o diretório do seu projeto e execute o comando a seguir.
zat newSerão solicitadas algumas informações, como o nome do seu aplicativo; vamos chamá-lo de Zendesk Video App. Também será solicitado seu e-mail e alguns outros parâmetros que não afetarão a funcionalidade. Assim que o comando for executado, você verá que o aplicativo foi criado. Também vamos criar uma pasta para o nosso servidor. A estrutura final do projeto fica assim.
|--Application
|-- Server
|-- server.js
|-- Zendesk Video App
|-- manifest.json
|-- Assets
|-- iframe.html
|-- index.css
|-- index.jssNosso aplicativo será composto por um quadro incorporado à interface do Zendesk e contará com uma área de chat por vídeo com várias ações disponíveis. Vamos editar o iframe.html arquivo adicionando alguns botões simples que permitirão ao agente realizar uma videochamada com o cliente diretamente no ticket. Você pode copiar e colar o código a seguir no seu 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>
Vamos adicionar também um pouco de CSS básico para os botões.
.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;
}Agora, edite o main.js arquivo que instancia um cliente ZAF. O cliente ZAF permite que seu aplicativo se comunique com o produto Zendesk principal. Você pode usar o cliente em seus aplicativos para monitorar eventos, obter ou definir propriedades ou invocar ações. Neste caso, estamos interessados nos detalhes do ticket em que estamos trabalhando. Em particular, o ID do ticket e o ID do solicitante. Assim que a promessa for cumprida, poderemos enviar uma solicitação ao nosso servidor para obter a chave da API, o ID da sessão e um token para esse ticket. Toda a lógica de geração da sessão virá do nosso servidor. Falaremos sobre isso mais adiante.
$(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);
});
});
Agora que já temos esses valores, podemos deixar que o Agente decida quando iniciar a sessão de Video. Vamos definir uma initializeSession função que será acionada assim que o Agente clicar no Initiate session botão. Vamos definir a exibição do contêiner do publisher como “block” para torná-lo visível (já que inicialmente está definido como “none”). Iniciaremos a sessão instanciando um objeto de sessão e, em seguida, inicializaremos o publisher.
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"
}
});
}
Também vamos criar alguns ouvintes para eventos que são disparados pelo objeto de sessão. Vamos utilizar o archiveStarted e archiveSopped para controlar o estado do nosso aplicativo, ou seja, para saber se estamos transmitindo o Video ou se ele está desligado durante a gravação.
Exibiremos um valor diferente nos botões HTML, dependendo do estado. Por exemplo, assim que recebermos o archiveStarted, queremos que nosso botão exiba “Parar Arquivamento” em vez de “Iniciar Arquivamento”, já que o arquivamento/gravação já foi iniciado. No início do nosso código, definimos algumas variáveis de estado (archiving, video, e screen) que mudarão de acordo com esses eventos.
Também vamos querer nos inscrever em um stream assim que ele for criado; por isso, ficaremos atentos ao 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);
});
A handleError função que estamos passando como callback é uma função que exibe um alerta caso ocorra um erro durante a escuta de eventos na sessão.
let handleError = (error) => {
if (error) {
alert(error.message);
}
}
Podemos criar uma handleRecording função que determinará se já estamos gravando ou não. Isso nos permitirá acionar uma função diferente dependendo do estado.
let handleRecording = () => {
archiving ? stopArchive() : startArchive();
}
A StartArchive função fará uma solicitação POST para a archive/start . Precisamos passar nossa sessionId para que nosso servidor saiba qual sessão está acionando a gravação. Você verá mais adiante no tutorial que nos referimos à gravação e ao armazenamento da sessão. Não se confunda; é o mesmo conceito, mas usamos o termo “arquivo” 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))
}
Quanto à StopArchive função, ela é praticamente igual à StartArchive. Mas, nesse caso, precisamos passar o archiveID que vem do 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))
}
Agora precisamos adicionar suporte para transmissões de compartilhamento de tela. Vamos criar uma função que verificará se já estamos compartilhando nossa tela e, caso contrário, criará um novo publisher. Essa função atuará como um botão de ativação/desativação para a transmissão de compartilhamento de tela em conjunto com alguns eventos, assim como fizemos para o arquivamento.
Vamos verificar se o navegador suporta o compartilhamento de tela chamando o OT.checkScreenSharingCapability método. Explicamos mais sobre a compatibilidade com o compartilhamento de tela na documentação sobre o callback checkScreenSharingCapability. Em algumas versões mais antigas de navegadores, talvez seja necessário instalar uma extensão, mas, para simplificar, vamos supor que ambos os participantes estejam usando um navegador recente.
Observe que os eventos que estamos monitorando, neste caso, são disparados pelo objeto publisher, e não pelo objeto session. Consulte o StreamEvent para obter mais informações.
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'
}
})
}
})
}
})
}
}
Lado do cliente
Agora que já temos o lado do agente em funcionamento, precisamos pensar em adicionar a funcionalidade de Video ao lado do cliente. O principal objetivo desta publicação é conectar o cliente final (autor do ticket) e o agente de suporte (responsável pelo ticket).
Para isso, vamos seguir o guia de personalização do tema da sua Central de Ajuda para que possamos ter acesso ao código da página do solicitante do ticket e criar uma experiência mais rica para o cliente na Central de Ajuda.
Nesse caso, estamos interessados em personalizar o Requests page, ou seja, as listas de solicitações ou tickets atribuídos a um usuário específico. Conforme explicado no artigo cujo link está acima, o código HTML da Central de Ajuda está contido em modelos editáveis. Vamos editar o requests_page.hbs arquivo. O código será muito semelhante ao código JavaScript no main.js arquivo.
Primeiramente, vamos importar a biblioteca Opentok. Isso fará com que a versão mais recente do SDK do JS seja baixada.
<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>Estamos adicionando uma marcação básica que conterá os Videos do editor e do assinante, além de alguns botões que controlarão as funcionalidades do nosso aplicativo. Você deve ter notado que temos {{assignee.avatar_url}}. Trata-se de uma linguagem de modelo chamada Curlybars que nos permitirá interagir com os dados da Central de Ajuda no contexto de um ticket do Zendsk.
Neste exemplo, estamos exibindo uma foto do responsável pelo ticket no botão que iniciará a videochamada. O objetivo é oferecer uma experiência mais personalizada ao cliente. Além disso, para simplificar no início, vamos ocultar todos os botões, exceto aquele que inicia a chamada. Faremos isso definindo a propriedade “display” dos nossos elementos HTML como 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 definir algumas variáveis que usaremos ao longo do código. Assim como fizemos no lado do Agente, trabalharemos com algumas variáveis de estado (video, archiving, e screenSharing). Também definiremos o endpoint do nosso servidor.
let sessionId;
let publisher;
let archiveId;
let screenSharing = false;
let archiving = false;
let video = true;
const SERVER_BASE_URL = 'SERVER_BASE_URL';Estamos definindo uma função simples de tratamento de erros que usaremos para alertar o usuário caso ocorra um erro. O único objetivo de definir isso como uma função separada é deixar nosso código um pouco mais organizado.
const handleError = (error) => {
if (error) {
alert(error.message);
}
}
Estamos carregando apiKey, sessionId, e token do nosso 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);
Em seguida, adicione a seguinte initializeSession função, que será acionada assim que o cliente decidir solicitar uma videochamada com o agente de suporte. Mostraremos os botões que estavam ocultos inicialmente; em seguida, instanciaremos um objeto de sessão e criaremos um publisher. Por fim, tentamos nos conectar à sessão. Se a conexão for bem-sucedida, tentaremos publicar na sessão, conforme explicado 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 usar operadores ternários para decidir se precisamos ligar ou desligar o Video. A mesma lógica se aplica para determinar se vamos chamar a função para iniciar a gravação ou para interrompê-la.
const toggleVideo = () => {
video ? publisher.publishVideo(false) : publisher.publishVideo(true)
}
const handleRecording = () => {
archiving ? stopArchive() : startArchive();
}
O startArchive() e startArchive() funções têm exatamente a mesma aparência que no main.js, por isso vamos omitir para simplificar. Você também pode optar por dar a opção de iniciar gravações apenas ao agente de suporte e não ao cliente final, mas isso fica totalmente a seu critério. Para tornar o processo mais interessante, permitiremos que ambos iniciem e interrompam as gravações, já que ambos poderão acessar a gravação após a chamada.
Servidor
Nosso lado do servidor será composto por várias rotas para processar as solicitações provenientes tanto do agente quanto do engenheiro de suporte.
Vamos importar os módulos que vamos usar em nosso aplicativo e definir algumas variáveis de ambiente.
apiKey e apiSecret são as credenciais da Video API encontradas no seu painel; o remoteUri faz referência ao endpoint do Zendesk da sua organização no formato https://xxxxxx.zendesk.com/. Para a autenticação no Zendesk, consulte a seção “Como posso autenticar solicitações de API”, já que eles oferecem suporte a diferentes métodos de autenticação; nós usamos nome de usuário e token.
Quanto à autenticação com a AWS, existem vários métodos suportados, mas também decidimos optar pelas variáveis de ambiente. Observe que, nesse caso, o SDK detecta automaticamente as credenciais da AWS definidas como variáveis em seu ambiente e as utiliza para as solicitações do SDK, eliminando a necessidade de gerenciar credenciais em seu aplicativo. É por isso que não estamos lendo as variáveis de nosso .env arquivo.
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()Adicione isso ao seu index.js arquivo.
const init = () => {
app.listen(8080, () => {
console.log('You\'re app is now ready at http://localhost:8080/');
}
A rota responsável pela criação de sessões e tokens verificará se já existe uma sessão criada para tratar desse ticket e, caso contrário, criará uma. Caso você não esteja familiarizado com o conceito de token da Video API, ele é como uma chave para a sala (sessão).
Você provavelmente gostaria de ter uma solução mais segura, mas decidimos fazer uma validação básica aqui para manter as coisas simples. Neste caso, estamos recebendo um name parâmetro no seguinte formato XXXXXX-YYYYY. Você se lembra daquelas chamadas `fetch` que fizemos nas duas partes (Agente e Cliente)? É daí que ele vem.
Só geraremos uma sessão e um token se o ID do solicitante do ticket corresponder à segunda parte do nosso :name parâmetro recebido. Vamos usar um pacote do Zendesk para realizar a validação. Por exemplo, se recebermos 1222-1234, verificaremos por meio da API do Zendesk se, de fato, o ticket 1234 foi solicitado pelo usuário 1222. Caso contrário, retornaremos um código de erro HTTP 404.
Você também verá que há uma validação em relação ao referer e à origem da solicitação. Trata-se de uma solução rápida criada para atualizar o ticket somente se a solicitação vier do cliente e para informar ao técnico de suporte que o solicitante do ticket gostaria de realizar uma sessão por Video.
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()
})
})
Em uma aplicação real, você provavelmente precisaria armazenar os IDs de sessão em seu banco de dados e verificar se já foi criada uma sessão para esse ticket. No entanto, decidimos simplesmente usar um dicionário que armazena os IDs de sessão associados ao nome de uma sala para este tutorial. Lembre-se de que esses dados serão reinicializados assim que você reiniciar o 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 mencionamos, criaremos uma sessão somente se não houver nenhuma sessão associada ao nome da sala recebido. Estamos envolvendo o método baseado em callback em uma promessa que retornará um objeto de sessão.
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);
});
})
}
Também encapsulamos em uma promessa a verificação do Zendesk que nos permite consultar o ID do ticket que recebemos, para que possamos determinar se a solicitação é legítima ou não.
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);
})
}
);
};
Se a solicitação for válida e vier do lado do Cliente (e não do Agente), atualize o ticket para que o engenheiro de suporte seja notificado de que há alguém aguardando uma sessão 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 definindo as rotas para iniciar e interromper o arquivamento. Observe que a rota para interromper o arquivamento também recebe o ID da sessão. Isso é para que nossos servidores saibam para qual ID de sessão você está tentando interromper a gravação.
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);
});
});
Se você estiver executando seu servidor, exponha-o com ngroke configure a URL do ngrok como SERVER_BASE_URL em ambos os front-ends (do lado do cliente e do agente). Agora você tem uma sessão de Video, muito bem!
Ok, isso foi legal, mas vamos dar mais um passo adiante! Não seria ótimo se também pudéssemos lidar dinamicamente com a gravação da chamada e enviá-la para o Zendesk, de modo que tanto o técnico de suporte quanto o cliente pudessem acessá-la quando lhes fosse mais conveniente? Vamos fazer isso!

Manuseio de gravações
Primeiro, precisamos informar à Video API para onde queremos que nossa gravação de vídeo seja enviada. Como vamos usar um endpoint do AWS S3, você pode seguir nosso guia “Usando o armazenamento S3 com o arquivamento da Video API da Vonage” . Depois de configurado, se você tiver uma sessão de Video e iniciar e interromper uma gravação, ela será automaticamente enviada para o seu bucket do S3.
Todos os arquivos são salvos em um subdiretório do seu bucket do S3 cujo nome corresponde à sua chave de API do OpenTok, e cada arquivo é salvo em um subdiretório desse, cujo nome corresponde ao ID do arquivo. O arquivo é archive.mp4.
Por exemplo, considere um arquivo com a seguinte chave de API e ID:
Chave da API -- 123456
ID do arquivo -- ab0baa3d-2539-43a6-be42-b41ff1488af3
O arquivo deste repositório é enviado para o seguinte diretório do seu bucket do S3:
123456/ab0baa3d-2539-43a6-be42-b41ff1488af3/archive.mp4
Em seguida, precisamos saber quando o arquivo foi enviado para o nosso bucket do S3 para que possamos recuperá-lo. Vamos configurar uma rota em nosso servidor para monitorar eventos relacionados ao arquivo. A Video API enviará um webhook para a URL de retorno de chamada que você configurou anteriormente quando o status de um arquivo for alterado.
Acesse seu painel, clique no projeto que você está usando e configure a URL do seu servidor para https://YOUR_SERVER_URL/events. Conforme explicado no guia de arquivamento, a plataforma da Video API enviará uma notificação de disponibilidade assim que o arquivo estiver pronto para download do bucket do S3. Vamos monitorar esse evento em nosso servidor e fazer o download. Toda a lógica será processada no lado do servidor (server.js arquivo).
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)
}
})
Lembre-se de configurar a URL do seu servidor no seu Account da Video API. Caso contrário, você não receberá esses webhooks no seu servidor. Deve ficar mais ou menos assim:

Vamos passar duas variáveis para a downloadVideo função; uma é o nome com o qual queremos que nosso arquivo seja baixado, e a outra é a chave, para que nosso bucket do S3 saiba qual gravação estamos tentando recuperar.
A solicitação transmitirá os dados retornados diretamente para um objeto Stream do Node.js, chamando o createReadStream método na solicitação. A chamada de createReadStream retorna o fluxo HTTP bruto gerenciado pela solicitação. O fluxo de dados brutos pode então ser canalizado para um objeto Stream do Node.js. Agora devemos conseguir baixar as gravações dinamicamente assim que forem enviadas para o nosso 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)
});
}
Você deve ter notado que estamos chamando uma getToken função assim que terminarmos de baixar o arquivo. Isso se deve ao processo de envio de um arquivo para o Zendesk. Você poderia fazer o que quisesse com o arquivo neste momento, já que ele já foi baixado. No entanto, para concluir nossa postagem, vamos enviar a gravação para o ticket do Zendesk para que ambos os participantes possam assistir à gravação após a chamada.
Primeiro, precisamos obter um token e, em seguida, atualizar o ticket passando esse token. Faremos a segunda parte em uma função separada chamada 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')
}
})
}
Confira a demonstração para ter uma ideia melhor de como tudo isso funciona. Adapte este tutorial às suas necessidades, deixe seus clientes super satisfeitos e transforme-os em verdadeiros defensores da experiência de suporte.
Encontre o código deste projeto no repositório no repositório do GitHub.
O que você vai criar a seguir? Conte para a gente!