# O player nas TVs

## Estratégia

Um único player HTML5 roda em todas as marcas. Isso não é atalho — é como o
mercado funciona: apps de webOS e Tizen **são** aplicações web empacotadas, e
Android TV usa WebView. Um player bem escrito é a base comum das três.

```
                    ┌──────────────────────┐
                    │  player.js (ES5)     │
                    └──────────┬───────────┘
        ┌──────────────┬───────┴────────┬──────────────┐
        ▼              ▼                ▼              ▼
   LG webOS       Samsung Tizen     Android TV     Navegador
   (.ipk)          (.wgt)            (.apk)        (URL direta)
```

O player é ES5 puro, com `XMLHttpRequest` em vez de `fetch` — firmwares de TV
de 2016–2018 ainda estão em campo e não têm `fetch` nem arrow functions.

---

## Ordem de recomendação

| Cenário | Recomendação | Por quê |
|---|---|---|
| Piloto, poucas telas | **Navegador da TV** com a URL | Zero instalação; funciona hoje |
| Produção, escala | **Android Box** (~R$ 200) + APK | Autostart confiável, controle total, independe da marca da TV |
| TVs LG já instaladas | **App webOS** | Abre sozinho no boot |
| TVs Samsung corporativas | **Tizen** ou MagicINFO | Modo quiosque nativo |

A opção mais estável em escala **não** é o app nativo da TV — é um Android Box
ou Raspberry Pi em modo quiosque ligado no HDMI. Firmware de TV atualiza sozinho
e quebra app; um box você controla.

---

## 1. Navegador (imediato)

1. Abra o navegador da TV
2. Acesse `https://tv.seudominio.com.br/player`
3. Digite o código de 6 dígitos com os números do controle
4. Pressione **OK**

O token fica em `localStorage` — a TV não pede o código de novo, mesmo após
desligar. Para desconectar, use **Reparear** no painel.

**Limitação:** o navegador não abre sozinho ao ligar a TV. Aceitável em telas
que ficam permanentemente ligadas; incômodo se houver queda de energia.

---

## 2. Android TV / TCL — APK

Melhor equilíbrio entre esforço e confiabilidade. O APK é um WebView em tela
cheia com autostart no boot.

**`AndroidManifest.xml`**

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="br.com.seudominio.signage">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>

    <uses-feature android:name="android.software.leanback" android:required="false"/>
    <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>

    <application
        android:label="Signage TV"
        android:banner="@drawable/banner"
        android:usesCleartextTraffic="false">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:screenOrientation="landscape"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:theme="@android:style/Theme.NoTitleBar.Fullscreen">

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <receiver android:name=".BootReceiver" android:exported="true">
            <intent-filter android:priority="1000">
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>
```

**`MainActivity.java`**

```java
package br.com.seudominio.signage;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends Activity {

    // Aponte para a instalação do cliente.
    private static final String PLAYER_URL = "https://tv.seudominio.com.br/player";

    private WebView web;

    @Override
    protected void onCreate(Bundle saved) {
        super.onCreate(saved);

        // Tela nunca apaga: é um painel, não um celular.
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        web = new WebView(this);
        WebSettings s = web.getSettings();

        s.setJavaScriptEnabled(true);
        s.setDomStorageEnabled(true);          // localStorage guarda o token
        s.setMediaPlaybackRequiresUserGesture(false);  // sem isto, nada toca sozinho
        s.setCacheMode(WebSettings.LOAD_DEFAULT);
        s.setAllowFileAccess(false);
        s.setAllowContentAccess(false);

        web.setWebViewClient(new WebViewClient());
        web.setBackgroundColor(0xFF000000);

        setContentView(web);
        hideSystemUi();

        web.loadUrl(PLAYER_URL);
    }

    private void hideSystemUi() {
        getWindow().getDecorView().setSystemUiVisibility(
              View.SYSTEM_UI_FLAG_FULLSCREEN
            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    }

    @Override
    public void onBackPressed() {
        // Ignora o botão Voltar: o operador não deve conseguir sair do player.
    }

    @Override
    protected void onResume() {
        super.onResume();
        hideSystemUi();
        web.onResume();
    }
}
```

**`BootReceiver.java`**

```java
package br.com.seudominio.signage;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class BootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context ctx, Intent intent) {
        if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            return;
        }
        Intent i = new Intent(ctx, MainActivity.class);
        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        ctx.startActivity(i);
    }
}
```

**Instalar:**

```bash
./gradlew assembleRelease
adb connect 192.168.1.50:5555
adb install -r app/build/outputs/apk/release/app-release.apk
```

---

## 3. LG webOS

```bash
npm install -g @webos-tools/cli
```

**`appinfo.json`**

```json
{
  "id": "br.com.seudominio.signage",
  "version": "1.0.0",
  "vendor": "Sua Empresa",
  "type": "web",
  "main": "index.html",
  "title": "Signage TV",
  "icon": "icon.png",
  "largeIcon": "icon-large.png",
  "bgColor": "#000000",
  "disableBackHistoryAPI": true,
  "handlesRelaunch": true
}
```

**`index.html`** — apenas redireciona para o player hospedado. Assim, atualizar
o player não exige reempacotar e reinstalar em cada TV:

```html
<!DOCTYPE html>
<html><head><meta charset="utf-8">
<script>location.replace('https://tv.seudominio.com.br/player');</script>
</head><body style="background:#000"></body></html>
```

```bash
ares-package .
ares-setup-device            # cadastre o IP da TV
ares-install ./br.com.seudominio.signage_1.0.0_all.ipk -d minhaTV
ares-launch br.com.seudominio.signage -d minhaTV
```

> Para instalação permanente em ambiente comercial, a LG exige o programa
> **webOS Signage** (SoC) ou distribuição pela Content Store. Modo desenvolvedor
> expira em ~50 horas e serve só para testes.

---

## 4. Samsung Tizen

Requer **Tizen Studio** e um certificado de desenvolvedor Samsung.

**`config.xml`**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<widget xmlns="http://www.w3.org/ns/widgets"
        xmlns:tizen="http://tizen.org/ns/widgets"
        id="http://seudominio.com.br/signage" version="1.0.0">

    <tizen:application id="SignageTV0.SignageTV"
                       package="SignageTV0" required_version="3.0"/>
    <content src="index.html"/>
    <name>Signage TV</name>

    <tizen:privilege name="http://tizen.org/privilege/internet"/>
    <tizen:privilege name="http://tizen.org/privilege/application.launch"/>

    <access origin="https://tv.seudominio.com.br" subdomains="false"/>
    <tizen:profile name="tv-samsung"/>
</widget>
```

`index.html` idêntico ao do webOS.

```bash
tizen build-web -- .
tizen package -t wgt -o . -- .buildResult
tizen install -n SignageTV.wgt -t "UE55TU7000"
```

Em parque corporativo, o caminho suportado é o **Samsung MagicINFO**, que aponta
para a URL do player em modo quiosque — sem empacotar nada.

---

## 5. Câmeras RTSP

**Nenhum navegador de TV reproduz RTSP.** É preciso converter para HLS.

```
Câmera IP ──RTSP──▶ MediaMTX / ffmpeg ──HLS──▶ TV (<video>)
```

### MediaMTX (recomendado)

Um binário, um arquivo de configuração, sem transcodificação — o custo de CPU é
próximo de zero porque o vídeo é apenas reempacotado.

```yaml
# mediamtx.yml
hlsAddress: :8888
hlsVariant: lowLatency
hlsSegmentCount: 7
hlsSegmentDuration: 1s
hlsAlwaysRemux: yes

paths:
  recepcao-loja1:
    source: rtsp://usuario:senha@192.168.1.50:554/onvif1
    sourceProtocol: tcp
    sourceOnDemand: no        # 'yes' economiza banda, mas atrasa o 1º frame
```

```bash
wget https://github.com/bluenviron/mediamtx/releases/latest/download/mediamtx_linux_amd64.tar.gz
tar xzf mediamtx_linux_amd64.tar.gz
./mediamtx
```

No painel, cadastre a câmera com modo **MediaMTX** e a URL HLS resultante:

```
https://stream.seudominio.com.br/recepcao-loja1/index.m3u8
```

> Publique o MediaMTX atrás de um proxy reverso com TLS. Uma página HTTPS não
> carrega mídia HTTP — o navegador bloqueia como conteúdo misto.

### ffmpeg (alternativa)

O painel gera o comando pronto quando o modo é **ffmpeg**. Rode como serviço:

```ini
# /etc/systemd/system/cam-recepcao.service
[Unit]
Description=Relay HLS - Recepcao
After=network-online.target

[Service]
ExecStart=/usr/bin/ffmpeg -rtsp_transport tcp -i "rtsp://..." -c:v copy -an \
          -f hls -hls_time 2 -hls_list_size 6 \
          -hls_flags delete_segments+append_list \
          /var/www/hls/recepcao/index.m3u8
Restart=always
RestartSec=5
User=www-data

[Install]
WantedBy=multi-user.target
```

`-c copy` mantém o stream original. Só transcodifique (`-c:v libx264`) se a
câmera entregar H.265 — muitas TVs não decodificam H.265 via HLS.

---

## 6. Atalhos no controle remoto

| Tecla | Ação |
|---|---|
| **0–9** | Digita o código de pareamento |
| **OK** | Confirma o código |
| **Voltar** | Apaga um dígito / fecha o diagnóstico |
| **→** | Pula para o próximo conteúdo |
| **8888** | Abre o painel de diagnóstico (TV já pareada) |

O diagnóstico mostra versão do player, nome da TV, itens carregados, versão do
conteúdo e estado da conexão. É a primeira coisa a olhar em chamado de suporte.

---

## 7. Formatos recomendados

| Tipo | Formato | Observação |
|---|---|---|
| Vídeo | **H.264 (baseline/main) + AAC, MP4** | Compatibilidade máxima. Evite H.265 |
| Resolução | 1920×1080 | 4K só se todas as telas suportarem — arquivo 4× maior |
| Bitrate | 4–8 Mbps | Acima disso, TVs antigas engasgam |
| Imagem | JPEG ou PNG, 1920×1080 | Acima de 4K é desperdício de banda |
| Áudio | AAC 128 kbps | Deixe mudo se a tela não tem caixa |

```bash
# Receita segura para qualquer TV:
ffmpeg -i entrada.mov -c:v libx264 -profile:v main -level 4.0 \
       -pix_fmt yuv420p -b:v 6M -c:a aac -b:a 128k \
       -movflags +faststart saida.mp4
```

`-movflags +faststart` move o índice para o início do arquivo — sem isso, a TV
baixa o vídeo inteiro antes de começar a tocar.

---

## 8. Diagnóstico

| Sintoma | Verificar |
|---|---|
| Tela preta, sem overlay | JavaScript desativado ou firmware muito antigo |
| "Sem conteúdo programado" | Playlist padrão vazia ou nenhum agendamento em vigor no horário |
| Vídeo não toca, imagem sim | Codec incompatível — recodifique em H.264 |
| Câmera não aparece | HLS fora do ar (use **Testar** no cadastro) ou conteúdo misto HTTP/HTTPS |
| YouTube não carrega | Rede corporativa bloqueando `youtube.com` |
| TV cai como offline | Verifique `DEVICE_HEARTBEAT_SEC` e se o proxy da rede não corta requisições longas |
| Código de pareamento recusado | Expirou. Gere um novo no painel |
