Skins personalizadas e plugins

Mude a aparência do player, a lista de controles e adicione funcionalidades próprias.

A aparência do player — quais controles existem, em que ordem, com que tamanho, cor e ícones — é uma skin. Funcionalidades próprias (um formulário de leads, um botão de dúvidas, um painel de estatísticas) entram como plugins. Os dois mecanismos são os mesmos que o player usa internamente para seus recursos embutidos.

Skins personalizadas

Como escolher uma skin

A ordem de precedência é: opção do construtor, parâmetro de URL (?skin=minimal, com allowUrlParams: true), configuração do backend e, por fim, uma escolha automática por tipo de dispositivo.

new SpallaPlayer('#player', { skin: 'minimal' });
player.setSkin('tv'); // troca com o vídeo já tocando
player.skin; // 'tv'
SpallaPlayer.skins(); // lista todas as registradas, embarcadas e customizadas

Registrando uma skin própria

Uma skin é uma classe que estende SpallaPlayer.Skin, registrada antes de qualquer new SpallaPlayer(...) que vá usá-la:

class MinhaSkin extends SpallaPlayer.Skin {
  mount() {
    // roda quando a skin entra — desenhe o que for próprio dela aqui
  }
  destroy() {
    // roda quando a skin sai — desfaça o que mount() criou
  }
}

MinhaSkin.skinName = 'minha';
MinhaSkin.controls = {
  desktop: ['play_pause', 'time_and_duration', 'spacer', 'plugins', 'fullscreen'],
  mobile: ['play_pause', 'spacer', 'plugins', 'fullscreen'],
  tv: ['play_pause', 'spacer', 'plugins', 'fullscreen'],
};

SpallaPlayer.registerSkin(MinhaSkin);
new SpallaPlayer('#player', { skin: 'minha' });

Por que os campos estáticos ficam fora da classe

Atribuídos fora da declaração de propósito: a sintaxe de campo estático de classe (static skinName = 'minha' dentro do corpo) só chegou no Chromium 72, e o player precisa continuar funcionando em navegadores de TV mais antigos, que não passam por nenhum processo de build antes de carregar sua skin.

Campos estáticos da skin

CampoPara que serve
skinNameIdentificador único, usado em skin: '...' e ?skin=...
controlsLista de controles por dispositivo (desktop, mobile, tv)
overflowItens do menu "mais opções", por dispositivo
contextMenuItens do menu do botão direito
adControlsControles mostrados durante a reprodução de um anúncio
stylesCSS como string — sem import './x.css' fora de um bundler
tokensVariáveis de tema (--shaka-* e --spalla-*), sobrescritas em :root
iconsTroca de ícones: chave em MAIÚSCULAS troca um ícone nativo do player; em minúsculas, o de um botão de plugin
trickPlayConfiguração de avanço/retrocesso rápido (elements, after, sempre)
hiddenNomes de controle que nunca aparecem, mesmo disponíveis
seekBarColors / volumeBarColorsCores das barras de progresso e volume

O token plugins, citado em controls/overflow, é onde os botões dos plugins ativos entram automaticamente — sem precisar listar cada plugin por nome.

Ícones

O valor de cada entrada em icons é o atributo d de um <path> SVG, num viewBox de 24x24. Para converter um ícone de outra fonte, normalize o desenho para essa caixa antes de usar — ícones fora dela ficam desproporcionais entre si na mesma barra.

Tokens de tema

tokens sobrescreve variáveis CSS que o player já declara em :root (prefixo --shaka-) e/ou variáveis próprias (prefixo --spalla-), que herdam para qualquer elemento dentro do player:

MinhaSkin.tokens = {
  '--shaka-icon-size': '44px',
  '--shaka-font-color': '#dce6ff',
  '--shaka-bg-menu': 'rgba(16, 22, 43, 0.95)',
  '--spalla-accent': '#7ee3ff',
};

Plugins

Um plugin injeta o próprio HTML e CSS no player e se desfaz sozinho quando sai — é o mecanismo usado internamente para recursos como legendas inteligentes, formulário de leads, marca d'água e estatísticas.

Ciclo de vida

  1. O player decide, via shouldMount(context), se aquele plugin deve existir para o conteúdo atual (por exemplo, só em VOD, ou só quando uma opção foi ligada).
  2. Se sim, mount() roda uma vez — é onde o plugin desenha sua interface e se inscreve em eventos.
  3. Quando o player é destruído (ou o plugin deixa de se aplicar), o framework cuida da limpeza dos listeners registrados via this.listen(...); timers e observers criados pelo plugin devem ser liberados explicitamente por ele.

Registrando um plugin

SpallaPlayer.registerPlugin(
  'meu-plugin',
  class extends SpallaPlayer.Plugin {
    static pluginName = 'meu-plugin';
    static styles = '.meu { color: tomato }';

    static shouldMount({ config }) {
      return config.isVod; // só existe em conteúdo VOD
    }

    mount() {
      this.mountDOM('<div class="meu">olá</div>', 'overlay');
      this.listen('timeupdate', () => {
        // reage a eventos do player sem precisar desinscrever manualmente
      });
    }
  },
);
  • mountDOM(html, camada) insere o elemento numa das camadas visuais do player: background, content, controls, overlay ou modal — a ordem delas na tela é fixa, então um plugin em overlay sempre fica acima de um em content, independente da ordem em que os plugins montam.
  • this.listen(nomeDoEvento, handler) é a forma recomendada de ouvir eventos dentro de um plugin: a inscrição é cancelada automaticamente quando o plugin é desmontado.

Botão na barra de controles

Um plugin pode expor um botão na barra (que aparece onde a skin ativa citar o token plugins):

this.player.addControlButton({
  name: this.constructor.pluginName,
  label: 'Meu recurso',
  icon: 'M11 18h2v-2h-2v2zm1-16A10…',
  onClick: () => this.abrirPainel(),
});

Opções e dependências

Qualquer plugin pode ser forçado ligado ou desligado via features no construtor, independente do que shouldMount() decidiria sozinho:

new SpallaPlayer('#player', { features: { 'meu-plugin': false } });

Um plugin pode consultar outro já montado via this.player.getPlugin(nome) — trate o retorno como possivelmente undefined: a ordem e a própria existência de outro plugin dependem da configuração daquele conteúdo específico.

Exemplo completo

O repositório do player traz uma skin completa e funcional, com styles, tokens, icons e controls, e um exemplo de plugin em uso no app de React.

Nesta página