Player.IO - Um media player que roda no navegador, feito com Node.JS, Socket.IO e é controlado pelo seu smartphone

Player.IO - Um media player que roda no navegador, feito com Node.JS, Socket.IO e é controlado pelo seu smartphone

Se você acompanha o mundo do desenvolvimento web , provavelmente já ouviu algo sobre o Node.JS e sua incrível capacidade de rodar aplicações altamente escaláveis escritas em JavaScript no servidor. Minha idéia inicial era criar um post onde abordaria como iniciar uma aplicação utilizando o Node.JS, porém quando comecei trabalhar com ele, vi que a coisa era tão bacana que acabei criando esse experimento: Um media player que roda no seu navegador, carrega arquivos de media locais utilizando a File Access API, e você pode controlar do seu smartphone trocando dados em tempo real graças ao Socket.IO.


Antes de continuar lendo o post, você pode conferir o experimento e entender melhor como o ele se comporta. É altamente recomendado utilizar o Google Chrome e arquivos nos formatos: m4a, mp3, mp4 ou webm. Vai lá e depois volte aqui, fico aguardando…

Node.JS

Talvez você já saiba o que é, mas vou repetir a explicação aqui para quem não conhece.

O Node.JS é um interpretador de JavaScript que utiliza a engine V8 do Google (a mesma do Chrome). Ele roda no servidor, mas não confunda ele com um Apache por exemplo, porque talvez ele seja mais que isso.

Definicão oficial:

Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

Site oficial

Socket.IO

Nas especificações do HTML5 está a WebSocket API que possibilita trocar dados com um servidor e múltiplos clientes de forma assíncrona, ou seja, troca de dados em tempo real (exemplos de aplicações: chats, games), porém para essa API ainda é restrita pois nem todos os navegadores suportam WebSockets. Devido essa jovialidade do WebSocket, existem bibliotecas que fornecem a tecnologia de socket e conexão em tempo real no navegador, e uma das mais conhecidas é o Socket.IO, que inclui a biblioteca javascript no código do cliente, e roda no servidor com o node.js, dessa forma temos uma aplicação de tempo real cross-browser.

Site oficial

Player.IO

Eu acredito que o experimento, apesar de simples, tem como principal objetivo mostrar como é possível criar uma interação de usuário rica utilizando apenas HTML5 e o poder do JavaScript, e isso deixa claro que, cada vez mais, a Web é a plataforma.


A idéia é que o usuário carregue uma playlist de arquivos (áudio e vídeo) locais ou de um links diretos (ex: http://dominio.com/arquivo.mp4), após isso, abra uma página no smartphone que irá funcionar como um controle remoto. Para abrir a página o usuário tem como opção ler um QR Code ou abrir um link (que também pode ser aberto na mesma máquina). Essa página envia dados para o servidor que por sua vez se comunica com a página de origem no computador e assim é possível executar comandos remotamente, como: troca de arquivo, play/pause, aumentar e diminuir volume, entrar ou sair do modo tela cheia, etc.

A aplicação é multiusuário, isto é: várias pessoas podem usar o serviço simultaneamente (e o Node.JS suporta muitas conexões) e cada usuário pode enviar comandos de onde quer que esteja pois tudo é feito online.

Fases de Desenvolvimento

Antes de mostrar código, vou listar as fases mais importantes do desenvolvimento.

  • → Utilizar a API File Acces para acessar arquivos locais.
  • → Tratar tipos de arquivos suportados pelos navegadores.
  • → Gerar Blob URL para cada arquivo local (e também tratar suporte dos navegadores)
  • → Criar conexão com o Node.JS
  • → Implementar conexão com o Socket.IO e gerar array de sockets
  • → Servir páginas com o Node.JS utilizando o express
  • → Criar layout para página requisitada pelo express utilizando Jade
  • → Implementar evendos de servidor/cliente para troca de dados
  • → Criar novo cliente para página de controle
  • → Fazer tudo isso funcionar

Iniciando com o Node.JS e Socket.IO

Bom, já conversamos bastante, agora lets go to the code. Como o tópico principal do post é Node.JS e Socket.IO, não vou entrar em detalhes sobre a manipulação de arquivos utilizando a File Access API, gerar Blobs URLs e outras coisas que utilizei no experimento, o código está lá para quem quiser se divertir, aqui vou focar em como iniciar a aplicação com o Node.JS. Vamos lá…

O arquivo app.js é onde fica o código do servidor, que é interpretado pelo Node.JS. Nele definimos as dependências que iremos utilizar (ex: socket.io, express), configurações necessárias (como a engine de templates) e claro os eventos de servidor que serão disparados, com as operações que o Node.JS terá de fazer para a aplicação.

Criando conexão entre cliente e servidor com o Socket.IO

// File: app.js

// Criando socket que fica aguardando na porta 8080 por conexões
var io = require('socket.io').listen(8080);

// Declarando array para armazenar cada cliente
var sockets = {};

// Evento connection: quando uma nova conexão é estabeleciada
io.sockets.on('connection', function (socket) {
    // Aguarda o evento setId que será disparado pelo cliente
    socket.on('setId', function (id) {
    	// setId é responsável por gerar um identificador para cada conexão
        console.log("ID : " + id);
        // a nova conexão é armazenada no array sockets com o identificador
        sockets[id] = socket;
    });
});
// File: client.js

// Definindo id para a conexão
var id = randomId();

// Conectando ao socket utilizando host e porta definida em app.js
var socket = io.connect('http://localhost:8080');

// Emitindo evento para o servidor e passando o id como parâmetro
socket.emit('setId', id);

Bom, até agora o que aconteceu acima foi simples, criamos um socket no arquivo app.js que fica escutando conexões na porta 8080 (se você não definir uma porta pode ocorrer conflito). Declaramos um array onde cada nova conexão é armazenada, a partir do evento conection. No arquivo do cliente, é feita a conexão e emitido um evento chamado setId para o servidor, nesse evento do lado do servidor foi definida uma função anônima que recebe como parâmetro um id que serve para identificar cada conexão.


No arquivo index.html

<!-- Arquivo para conexão com o socket.io (ele é gerado automáticamente) -->
<script src="http://localhost:8080/socket.io/socket.io.js"></script>
<!-- Script do Cliente -->
<script src="client.js"></script>

Agora precisamos rodar a aplicação e testar. Você deve ter instalado o Node.JS junto do npm (gerenciador de pacotes do node). Como fazer isso é simples, e existem vários tutoriais por ai. Com o node e o npm instalados, iremos instalar as dependências:


Antes, precisamos criar o arquivo package.json responsável por gerenciar as dependências do projeto, e conter suas informações.

// File: package.json

{
  "name": "Player.IO",
  "version": "0.1.0",
  "description": "A real-time media player...",
  "repository": "https://github.com/vagnervjs/player.io.git",
  "dependencies": {
    "express": "3.x",
    "socket.io": "0.9.x", 
    "jade" : "0.28.x"
  },
  "author": "Vagner Santana"
}

Instalando as dependências:

// acessando pasta do projeto
cd player.io 
// utilize sudo Mac ou Linux
sudo npm install 

Não é nescessário passar o nome do pacote que deseja instalar para o comando npm install, pois estamos utilizando o arquivo package.json.

Com as dependências instaladas, vamos rodar o arquivo app.js:

node app.js 

Se tudo ocorreu bem, ao abrir o arquivo index.html no navegador um novo socket foi criado. Lembrando que a função randomId foi criada por mim, se estiver realizando este teste você pode atribuir à varíavel id uma string qualquer. No console do Node.JS a saída deve conter uma linha com: ID: <id que foi passado>.

Servindo páginas com Express

O Express é um framework minimalista e flexível de desenvolvimento web para o Node.JS que possui vários métodos HTTP tornando possível servir páginas a partir de requisições de usuários, tudo isso com um ótimo desempenho.

Site oficial

Iremos utilizar ele para servir a página de controle. Veja:

// File app.js

// solicitando dependência do express
var express = require("express");

// criando uma aplicação express
var app = express();

// definindo engine para as views
app.set('view engine', 'jade');

// ao receber uma requisição com o valor /mb/:id
// onde :id é uma string 
// chama uma função callback
app.get('/mb/:id', function (req, res) {
    // armazenando valor do parâmetro id
    var id = req.params.id;
    // imprimindo
    console.log("ID Mb: " + id);
    // chama uma a view (arquivo jade), passando algumas variáveis (id e title)
    res.render('mobile.jade', {id:id, title:'Player.IO | Control'});
});

// a aplicação express fica escutando na porta 8000
app.listen(8000);

Certo, neste ponto nossa aplicação já abre um socket para cada conexão, e agora acabamos de criar um servidor HTTP com o express que fica aguardando requisições na porta 8000. Lembre-se que iniciamos o socket.io e também definimos uma porta para ele, portanto a porta do express deve ser diferente da porta definida para o socket.io (no nosso caso utilizamos as portas 8000 e 8080).

Com a aplicação express criada e escutando na porta 8000, ao fazermos uma requisição do tipo localhost:8000/mb/novoId123 o express irá chamar uma função callback, que recebe os parâmetros req e res, respectivamente: request e response. O req é um objeto que contém informações sobre a requisição HTTP, já o res é o objeto responsável por enviar uma resposta HTTP, como renderizar uma página por exemplo.
Nesse callback armazenamos o valor do parâmetro id que foi passado na requisição, para isso utilizamos o método param do objeto req. É obrigatório o uso de : antes do nome do parâmetro esperado, se o paramêtro for opcional deve ser utilzado o símbolo de interrogação após o nome, ex: :id?. Com o id armazenado, a próxima operação (além de imprimir o valor de id) é chamado o método render() do objeto response. Ele é responsável por renderizar uma view utilizando a engine de templates definida acima em aap.set('view engine', 'jade'). Esse método aceita três parâmetros: o primeiro e obrigatório é o nome da view a ser renderizada, os outros dois são: opções para serem utilizadas na view, e uma função callback. Nesse caso não fiz callback.


Pronto, para o express é isso. O método get() fica encarregado de verificar qual chamada está sendo feita, e executar seu respectivo callback. Não podemos esquecer de chamar o método listen para o express funcionar, ele aceita um parâmero único que é a porta. O método listen é idêntico ao http.Server#listen(). do node.


Agora é só executar a aplicação novamente e testar.

Enviando dados do controle para o player

Agora que servimos a página (que veio da requisição /mb/id), é ela que irá enviar os comandos para o player, portanto precisamos emitir eventos para o socket.io, esses valores defini como: ação e valor para a ação (caso houver). Por exemplo: para trocar de arquivo a página de controle precisa enviar para o servidor a ação que dei nome de “change”, e o valor que é o id do arquivo escolhido. Além desses dois valores, preciso também devolver aquele id que foi passado como opção para a view, lembram ? Esse id serve para identificar para qual socket devo enviar os comandos, do contrário, minha unica opção seria enviar os dados por bradcast, o que bloquearia a idéia da aplicação ser multiusuário. Quer ver parte do código ? Lá vai

// File: mobile.jade

// Volume Up
$('#vol_up').on("tap", function() {
    $.get('/player/#{id}/volup', function() {});
});

O código acima é simples: quando houver um toque no botão de aumentar o volume (#vol_up) é chamado o método get do jQuery que faz uma requisição para localhost:8000/player/idQualquer/volup. Ou seja, uma requisição para o express. Não é preciso definir locaholst:8000 no código pois estamos trabalhando com caminho relativo, portanto ele irá utilizar o mesmo domínio e porta da página.


Mas, espera ai, onde foi definido esse caminho /player/ ? Você pode ver logo a seguir:

// File: app.js

app.get('/player/:id/:action/:val?', function (req, res) {
    var id = req.params.id;
    var action = req.params.action;
    var val = req.params.val;
    sockets[id].emit('play', {action:action, val:val});
    res.send('ok');
});

O que está acontecendo ali você já viu anteriormente, o express espera por uma requisição com o valor “player”, dois parâmetros obrigatórios (id e action) e um opcional (val). Ali pegamos os valores, e emitimos o o evento play para um único socket,. Um único socket pois a variável sckets é um array, e seus índices são os ids de cada conexão, por isso devolvemos o id como um parâmetro da requisição /player/. Emitimos junto do evento, os dados (action e val) e seus respectivos valores.


Agora, é só ir la no cliente (o player), e tratar esse evento play certo?

// File: client.js

// selecionando a tag <video> do arquivo index.html
var media = document.getElementById("player");

socket.on('play', function (data) {
	if (data.action == 'volup'){
        media.volume += 0.1;
    }
}

No lado do cliente, com o evento on() do socket, recebemos o evento emitido pelo servidor, e chamamos uma função callback. Essa função recebe como parâmetro os dados que também foram passados pelo servidor. Assim, é só tratar os dados. Foi o que fiz para verificar qual ação foi enviada, no if comparo se o valor do dado action é igual ‘volup’, se for, aumento o volume do arquivo (utilizando a Web Audio API). Isso vale para todos os outros comandos.

Pedras no caminho

Durante o desenvolvimento desse experimento fui descobrindo algumas coisas que talvez nunca iria descobrir se não fosse programando.

  • → A FullScreen API não aceita que seja disparado o evento requestFullScreen() sem a interação do usuário, ou seja, só é possível via clique. Para resolver isso, ou tentar, fiz um full screen manual, alterando o tamanho do player para o tamanho da janela.
  • → O Chrome e o Firefox são os únicos navegadores que suportão reproduzir blobs urls. Os outros navegadores, mesmo tendo suporte para blobs urls, não conseguem reproduzir um vídeo o música. Foi ai que resolvi adicionar o campo para adicionar uma media por URL, dessa forma consigo reproduzir o arquivo.
  • → Para manter o Node.JS rodando no meu servidor (em http://vagnersantana.com/player.io), estou utilizando o forever. Para instalar é só mandar o comando npm install forever e para executá-lo: forever start app.js. Assim o socket.io e o express ficaram escutando constantemente nas portas 8080 e 8000. Para visualizar quais arquivos o forever está executando use o comando forever list.

Conclusão

Tem muito mais código na aplicação toda, o post já está gigantesco, mas espero ter explicado todo o conceito de utilizar o Node.JS e o Socket.IO.


Apesar de ser um experimento simples, foi extremamente empolgante ter desenvolvido, pois só assim é possível aprender de verdade. Espero que este post ajude você de alguma forma, seja com node.js, socket.io ou express, ou seja para incentivar a desenvolver experimentos, brincar mais, fazer qualquer tipo de coisa que te faça adquirir mais conhecimento sempre, buscar mais informações, e depois, contribuir ensinando.


O código está disponível no Github, espero que você vá até lá, de um fork, envie pull requests, reportem bugs e tudo mais que tiver direito.


Referências