
Compartilhar:
Richard é engenheiro sênior de suporte à Video API na Vonage, com mais de 25 anos de experiência em funções relacionadas a produtos, suporte e engenharia nos setores de manufatura, telecomunicações e redes.
Como criar uma tela verde virtual usando a Video API da Vonage
Tempo de leitura: 10 minutos
Introdução
Este artigo descreve como usar a Video API da Vonage para sobrepor os apresentadores ao conteúdo compartilhado, utilizando a substituição de plano de fundo e o canvas do HTML5 para criar uma tela verde virtual.
Você pode encontrar o código em GitHub.
Presentation Example
Usando telas verdes virtuais para economizar espaço na área de trabalho
Ao fazer apresentações on-line, os apresentadores e participantes precisam dividir o espaço disponível na área de trabalho entre suas apresentações e outros recursos multimídia. Isso muitas vezes pode tornar o conteúdo pequeno demais para ser lido por muitos participantes. No exemplo acima, utilizamos os recursos da Video API da Vonage para criar uma tela verde virtual, de modo que o apresentador possa ser exibido sobre a apresentação. Isso permite que a apresentação em si assuma o protagonismo, de forma que todos possam vê-la por inteiro.
Alguns obstáculos iniciais
O principal problema para conseguir algo assim é que a Comunicação em Tempo Real na Web (WebRTC) e os codecs de vídeo geralmente não oferecem suporte à transparência. Enviar vídeo transparente pela Internet não vai funcionar. No entanto, a Video API da Vonage oferece alguns recursos que podem ajudar, e um deles é a Substituição de Fundo. Saiba mais sobre filtros e efeitos em nossa documentação.
O que faremos neste tutorial é usar o recurso de Substituição de Fundo com uma imagem de fundo de cor única para criar uma tela verde virtual. Isso permite que nossos Video Transformers façam o que for necessário para garantir uma detecção e substituição precisas do fundo. Usaremos uma imagem pequena (16x16) com a cor sólida RGB(61,180,60). Essa imagem está incluída no repositório do GitHub deste tutorial, juntamente com todo o código necessário para executar o exemplo.
greenscale backgroundSeguindo o processo abaixo, podemos remover o fundo completamente para que, ao ser renderizado, o Video possa ser exibido sobre mídias existentes, páginas da web ou outros Videos.
Application Workflow
Pré-requisitos
Antes de começarmos, dê uma olhada no repositório do GitHub para que você possa baixar o código e executar os exemplos.
Você vai precisar de:
Alguma experiência com JavaScript e HTML5 Canvas.
Alguma experiência prévia com a Video API da Vonage é preferível, mas não essencial.
Este projeto inclui uma aplicação básica de Video, mas ela está fora do escopo deste tutorial. Você pode encontrar mais exemplos em nosso Repositório de Exemplos de Video na Web.
Você precisará de uma câmera e de um microfone que estejam funcionando para executar o exemplo.
Uma chave da API de vídeo da Vonage, um ID de sessão e um token. Você pode criá-los no painel de controle ou por meio do Video API Playground. Você pode acessá-los por meio da sua conta de API online.
Visão geral do aplicativo
O aplicativo de exemplo é composto por vários componentes. Uma página inicial (app.html) com um formulário que ajuda a identificar usuários e funções para uso no aplicativo. A página principal do aplicativo (liveroom.html) carrega o SDK do Video em JavaScript e executa nosso código JavaScript. Neste exemplo, o código é carregado como módulos ES6.
Preparar a sala e a sessão
Quando a liveroom.html página é carregada, o nome do usuário, o nome da sala e a função são incluídos na URL como parâmetros de URL. Esses dados são coletados e passados para o Liveroom construtor da classe para criar a sala.
let urlRoomName = new URLSearchParams(window.location.search).get('roomName')
let urlUserName = new URLSearchParams(window.location.search).get('userName')
let urlUserRole = new URLSearchParams(window.location.search).get('userRole')Depois de termos os parâmetros e de verificarmos se estão válidos e preenchidos, podemos criar a sala.
const liveroom = new Liveroom(urlRoomName, urlUserName, urlUserRole)O Liveroom objeto fará primeiro uma chamada para buscar as credenciais de sessão de um servidor. Para simplificar, configuramos isso para obter um config.json arquivo da pasta atual que contenha credenciais de sessão válidas. Você precisará inserir seu ID de sessão, chave de API e token nesse arquivo antes de executar o exemplo.
// config.json
{
"apiKey": "your api key",
"sessionId": "your session id",
"token": "your token"
} Liveroom também instancia um Display objeto.
this.display = new Display()A Display classe gerencia a largura e a altura da área de exibição e adiciona um ouvinte para garantir que esses atributos sejam atualizados quando a janela do aplicativo for redimensionada. O código dessa classe está no display.js arquivo.
Conectar a sessão da Video API da Vonage
Assim que o aplicativo tiver as credenciais, a Liveroom classe criará um objeto Video que gerenciará a mídia e se conectará à sessão do Vonage.
this.getVideoCredentials(this.roomName)
.then(()=>{
this.video = new Video(this.sessionCredentials, {})
return this.video.connectSession()
})
.then(()=>{
console.log(`User Role: ${this.userRole}`)
if(this.userRole == 'composer'){
this.display.enableComposerMode()
this.composer = true
}
if(this.userRole != 'viewer'){
this.video.publishCamera(this.userRole)
}
if(this.userRole == 'presenter'){
this.video.publishScreen()
}
this.updateDisplay(this.video.participants, this.video.mainstage)
})
.catch((error)=>{
console.log(error)
})Estamos usando promessas para garantir que nada seja iniciado até que todas as informações e objetos necessários estejam disponíveis. Primeiro, as credenciais são coletadas e passadas para o construtor da classe Video. Em seguida, este chama a connectSession() função, que conecta o SDK do Video em JavaScript ao ID da sessão. Feito isso, ele inicia as funções-membro apropriadas com base na função do usuário que foi passada.
Enviar e receber arquivos de mídia
Como mencionamos anteriormente, não é possível enviar transparência via WebRTC; portanto, precisamos substituir o fundo pelo efeito de tela verde. Isso é feito pela função publishCamera() função. Essa função cria um novo objeto CameraPublisher, que utiliza as seguintes configurações para criar o publisher:
let publisherOptions = {
showControls: false,
videoFilter: {
type: "backgroundReplacement",
backgroundImgUrl: "/images/greenscreen.png" // r:61 g:180 b:60 #3db43c
}
}Isso instrui o SDK a substituir o plano de fundo pela nossa imagem greenscreen.png. Como ela será esticada e redimensionada e tem apenas uma cor, o arquivo tem apenas 16x16 pixels.
Ao criarmos o publisher, passamos o objeto DOM HTML ao qual queremos que o Video seja anexado. Nesse caso, há um objeto DIV que faz parte da CameraPublisher classe. Esse objeto DIV fica oculto com CSS display: none; , pois conterá o Video não processado.
Também criamos um objeto HTML5 Canvas para receber a saída do Video com transparência. Esse também é um membro do objeto e se chama outputCanvas.
this.publisher = OT.initPublisher(this.publisherDiv, publisherOptions, (event)=>{
this.videoElement = this.publisherDiv.querySelector("video")
this.videoElement.onloadeddata = (event)=>{
console.log("Video Loaded Data")
this.renderer = new VideoRenderer(this.videoElement, this.outputCanvas)
this.renderer.processFrames()
this.session.publish(this.publisher)
}
})Neste código, também passamos uma função para o callback da OT.initPublisher() função. Quando isso for executado, sabemos que o elemento de Video terá sido adicionado ao nosso DIV. Isso significa que podemos usar a querySelector() função para localizá-lo e adicionar outra função de retorno de chamada. Ela é executada quando a mídia de Video estiver pronta e cria a VideoRenderer instância.
Assim que tivermos o elemento de vídeo e uma tela (canvas) para gravar, esse código cria um novo objeto VideoRenderer.
Para receber mídia, seguimos o mesmo processo na CameraSubscriber() classe, criando outra instância da VideoRenderer classe para renderizar o Video recebido.
Renderizar o Video
A classe VideoRenderer é a peça final para que isso funcione. Ela recebe um elemento de entrada <video> e um elemento de saída <canvas> HTML5 de saída. Quando ativada, ela copia o quadro de vídeo do elemento Video e o insere, com um fundo verde, em um CANVAS interno (“builderCanvas”), permitindo que acessemos os dados de pixels de cada quadro de Video.
Vamos examinar mais de perto o que o VideoRenderer faz e como ele funciona.
constructor(sourcevideo, outputcanvas){
let randomIcon = (Math.floor(Math.random()*14)+1) + '.png'
this.imageIcon = new Image()
this.imageIcon.src = /images/${randomIcon}
console.log('Random icon for user: ', this.imageIcon.src)
this.targetFPS = 30
this.videoElement = sourcevideo
this.width = sourcevideo.videoWidth
this.height = sourcevideo.videoHeight
this.outputCanvas = outputcanvas
this.builderCanvas = document.createElement("canvas")
this.enabled = false
}O construtor da classe configura nossa tela do builder e também contém algumas funções para substituir o feed de Video por uma imagem aleatória caso o Video esteja desativado.
A seguir, temos duas funções curtas que servem apenas para ativar e desativar o renderizador de Video. Isso ajuda a economizar recursos para os participantes que não estão sendo exibidos ou que estão com o áudio desativado.
enable(){
if(!this.enabled){
this.enabled = true
this.processFrames()
}
}
disable(){
this.enabled = false
}Em seguida, temos a processFrames() função. Ela é chamada pela primeira vez quando o renderizador é ativado e, em seguida, usa requestAnimationFrame() para se chamar novamente. Solicitar um quadro de animação dessa forma reduz o uso de recursos quando o quadro não está sendo exibido.
A função `processFrames` será encerrada se o renderizador não estiver ativado. Isso desativa o renderizador completamente.
processFrames() {
if(!this.enabled) return
}Em seguida, capture a largura e a altura do elemento de Video para garantir que todas as nossas telas tenham as mesmas dimensões. Isso também cria uma variável `sourceImage` como um ponteiro para o elemento de Video. Se o Video não estiver habilitado, a variável `sourceImage` é definida como o `imageIcon` selecionado pelo construtor.
this.width = this.videoElement.videoWidth
this.height = this.videoElement.videoHeight
let sourceImage = this.videoElement
if(!this.videoElement.srcObject.getVideoTracks()[0].enabled){
sourceImage = this.imageIcon
this.width = sourceImage.width
this.height = sourceImage.height
}A largura da tela para o construtor é definida, e o contexto da tela é criado. Esse contexto builderCtx permite que leiamos e gravemos na tela. Primeiro, a limpamos e, em seguida, desenhamos nossa imagem de origem (sourceImage) na tela do construtor (builderCanvas).
this.builderCanvas.width = this.width
this.builderCanvas.height = this.height
let builderCtx = this.builderCanvas.getContext("2d")
builderCtx.clearRect(0,0,this.width, this.height)
builderCtx.drawImage(sourceImage, 0, 0, this.width, this.height)Com o builderCanvas e context configuração, passamos então para o outputCanvas. Verificamos se a largura e a altura correspondem à nossa mídia de origem.
NOTA: Isso não altera a largura e a altura de exibição no CSS. Apenas altera a largura e a altura da tela interna. O DOM dimensionará a tela interna para que ela se adapte ao nó externo.
this.outputCanvas.width = this.width
this.outputCanvas.height = this.height
let outputCtx = this.outputCanvas.getContext("2d")
Agora já temos nossos contextos de construtor e de saída definidos. Para continuar, criamos um ImageData objeto a partir do contexto do construtor. Isso concede acesso aos dados no nível do pixel dentro da tela por meio da ImageData.data matriz. Trata-se de uma matriz plana e unidimensional que contém os atributos vermelho, verde, azul e alfa de cada pixel, apresentados sequencialmente.
var imgdata = builderCtx.getImageData(0, 0, this.width, this.height);
var pix = imgdata.data;Nossa função precisa percorrer todos esses pixels da matriz e avaliar cada um deles para determinar se corresponde ao verde do nosso plano de fundo. Para isso, utilizamos a adjustPixel() função, que abordaremos em breve.
for (var i = 0, n = pix.length; i < n; i += 4) {
let r = pix[i]
let g = pix[i+1]
let b = pix[i+2]
let a = pix[i+3]
let newColor = this.adjustPixel(r,g,b,a)
pix[i] = newColor.r
pix[i+1] = newColor.g
pix[i+2] = newColor.b
pix[i+3] = newColor.a
}Assim que todos os pixels em nosso imgdata forem atualizados, limpamos o contexto do construtor e colocamos o imgdata de volta para a tela. Podemos então desenhar a partir dessa tela oculta para o nosso outputCanvas . Com requestAnimationFrame(), essa função será executada entre 30 e 60 vezes por segundo.
builderCtx.clearRect(0,0,this.width,this.height)
builderCtx.putImageData(imgdata, 0, 0)
outputCtx.drawImage(this.builderCanvas, 0, 0, this.width, this.height)
this.animationFrameId = window.requestAnimationFrame(()=>{this.processFrames()})
} Definir opacidade do pixel
A adjustPixel() função da VideoRenderer classe aceita uma cor com valores independentes de vermelho, verde, azul e alfa. Em seguida, ela retorna um objeto com quatro membros: r, g, b e a. A primeira linha cria um output objeto.
adjustPixel(r,g,b,a){
let c = {r: r, g: g, b: b, a: a}
}A função, então, calcula a média dos valores de vermelho e azul e a armazena como rb.
let rb = (r + b) / 2A próxima linha de código determina se o pixel deve ser transparente ou não. Primeiro, ela usa a withinRange() função para verificar se a diferença entre o vermelho e o azul é menor que 40. Se o vermelho e o azul tiverem valores diferentes, a cor não será verde.
A segunda parte do raciocínio verifica se o valor do verde é pelo menos 20 maior do que a média do vermelho e do azul. Isso abrange uma série de tons e matizes de verde que melhor correspondem à nossa imagem original de 16x16.
if((this.withinRange(r, b, 40) && g - rb > 20)){
c.a = 0
}
return c
}
withinRange(val1, val2, range){
let diff = val1 - val2
if(diff < 0) diff = diff * -1
if(diff < range){
return true
} else {
return false
}
} Vários alto-falantes e fade out
Na captura de tela abaixo, você pode ver um exemplo de como vários participantes foram acomodados, com o som daqueles que não estão falando no momento sendo atenuado e todos mantidos alinhados no canto inferior esquerdo. Neste momento, há três participantes na chamada, mas apenas uma pessoa pode falar por vez.
Multiple Speaker
Ao silenciar e remover os participantes inativos, é possível aumentar o número de participantes e a resolução da tela compartilhada ou da apresentação sem comprometer a qualidade da experiência.
Executar o exemplo
Você precisa inserir sua chave de API, ID de sessão e token no arquivo config.json fornecido; depois, poderá hospedar esses arquivos no seu servidor web preferido. Se estiver usando o Node, você pode executar os seguintes comandos a partir da raiz do repositório clonado:
npm install
npm run serveEm seguida, basta abrir seu navegador e acessar http://127.0.0.1:3000. Quando você falar, o microfone deve captar sua voz e ativar a transmissão de Video.
Conclusão
É isso aí para este tutorial. Seja ao apresentar uma apresentação de slides, editar uma planilha ou assistir a um Video, a imagem da sua câmera só será exibida quando você estiver falando e não ocupará espaço adicional na tela. Espero que tenham achado interessante. Outras implementações e melhorias poderiam incluir:
Fundos de Video para editores
Applications para assistir em grupo
O código completo desta postagem pode ser encontrado no GitHub.
Tem alguma dúvida ou quer compartilhar o que está criando?
Inscreva-se no Boletim Informativo para Desenvolvedores
Siga-nos no X (antigo Twitter) para ficar por dentro das novidades
Assista aos tutoriais no nosso canal do YouTube
Conecte-se conosco na página de desenvolvedores da Vonage no LinkedIn
Fique conectado e acompanhe as últimas notícias, dicas e eventos para desenvolvedores.