Como Criar um App FAST Linear para Smart TVs Samsung (Passo a Passo)

Guia prático, visual e aplicado à realidade de produção


1. O ambiente real de desenvolvimento Samsung (Tizen TV)

A Sam­sung Smart TV uti­liza o sis­tema opera­cional Tizen, e o desen­volvi­men­to ofi­cial de aplica­tivos ocorre através do Tizen Stu­dio.

Difer­ente de um app mobile ou web comum, um app de TV é um soft­ware embar­ca­do, que roda em hard­ware lim­i­ta­do, con­tro­la­do por con­t­role remo­to e otimiza­do para uso con­tín­uo (horas lig­a­do).

Ferramenta central: Tizen Studio

O Tizen Stu­dio é respon­sáv­el por:

  • Cri­ar o pro­je­to do app
  • Empa­co­tar o aplica­ti­vo para TV
  • Assi­nar dig­i­tal­mente o app
  • Exe­cu­tar no emu­lador
  • Insta­lar e depu­rar na TV real

📎 Down­load ofi­cial:
https://developer.samsung.com/smarttv/develop/tools/tizen-studio.html


2. Criando um projeto FAST TV no Tizen Studio (visual)

Ao cri­ar um novo pro­je­to, o fluxo cor­re­to é:

File → New → Tizen Project → TV → Web Application

Você escol­he:

  • Pro­file: TV
  • Tem­plate: Basic Web Appli­ca­tion

O Tizen cria auto­mati­ca­mente a estru­tu­ra mín­i­ma necessária para uma Smart TV.


3. Estrutura real de um app FAST (como ele nasce no código)

Depois de cri­a­do, o pro­je­to terá algo pare­ci­do com isso:

/fast-tv-app
 ├── index.html        ← ponto de entrada
 ├── config.xml        ← manifesto do app (obrigatório)
 ├── css/
 │    └── style.css
 ├── js/
 │    ├── app.js       ← lógica principal
 │    ├── player.js   ← controle do HLS
 │    ├── epg.js      ← guia de programação
 │    └── keys.js     ← controle remoto
 └── assets/
      └── logos/

O config.xml é o “RG” do app
É nele que você define:

  • ID do app
  • Ver­são
  • Per­mis­sões
  • Per­fil de TV
  • Ori­en­tações de tela

4. Diagrama real: como um FAST HLS funciona na prática

Antes de falar de códi­go, é essen­cial visu­alizar a arquite­tu­ra.

Arquitetura FAST Linear com HLS

Usuário
  │
Controle Remoto
  │
Samsung TV (Tizen)
  │
Tizen Web App
  │
Player AVPlay
  │
HLS (.m3u8)
  │
CDN / Encoder

Ou em camadas:

┌───────────────────────┐
│ Interface TV (UI)     │ ← foco, navegação, EPG
├───────────────────────┤
│ Player Controller     │ ← troca de canal, watchdog
├───────────────────────┤
│ AVPlay (nativo)       │ ← player da Samsung
├───────────────────────┤
│ HLS Stream (.m3u8)    │ ← live FAST
└───────────────────────┘

📌 Por isso FAST é difer­ente de VOD:
o app não escol­he o vídeo, ele entra no fluxo con­tín­uo.


5. Player HLS: por que AVPlay é a escolha certa

A Sam­sung fornece o AVPlay, um play­er nati­vo alta­mente inte­gra­do ao sis­tema da TV.

O que o AVPlay resolve melhor que <video>:

  • Reconexão de streams live
  • Con­t­role de buffer
  • Mel­hor esta­bil­i­dade em lon­gas sessões
  • Inte­gração com hard­ware da TV

📎 Doc­u­men­tação ofi­cial AVPlay:
https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/avplay-api.html


6. Experiência de TV: foco, controle remoto e zapping

TV não é touch. Tudo gira em torno de foco.

Navegação típica em um FAST:

TeclaAção
↑ ↓Lista ráp­i­da de canais
OKMostrar infor­mações do canal
← →Nave­g­ação no guia
BackFechar over­lay / sair
CH+ / CH-Zap­ping dire­to

Regra de ouro:

Se o foco se perder, o app é reprova­do (pelo usuário ou pela loja).


7. Guia de Programação (EPG) integrado ao HLS

Mes­mo sendo live, o FAST pre­cisa de con­tex­to.

EPG mínimo (Now / Next)

  • Ago­ra: o que está pas­san­do
  • A seguir: próx­i­mo pro­gra­ma
  • Bar­ra de pro­gres­so basea­da no tem­po

EPG completo (nível TV profissional)

  • Grade ver­ti­cal (canais)
  • Lin­ha do tem­po hor­i­zon­tal
  • Nave­g­ação flu­i­da com setas

Tec­ni­ca­mente, o EPG não vem do HLS
Ele vem de uma API sep­a­ra­da, sin­croniza­da pelo reló­gio.


8. Testes reais: emulador vs TV física

Emulador (Tizen Emulator)

Serve para:

  • Tes­tar lay­out
  • Nave­g­ação
  • Fluxo de telas
  • Debug rápi­do

TV real (obrigatório)

Só na TV você val­i­da:

  • Per­for­mance real
  • Zap­ping rápi­do
  • Reconexão de HLS
  • Com­por­ta­men­to do con­t­role físi­co

FAST que não roda 6h seguidas na TV real não está pron­to.


9. Assinatura e publicação (o passo final)

Antes de pub­licar, o app pre­cisa ser assi­na­do dig­i­tal­mente.

Depois dis­so, você pub­li­ca via Sam­sung Sell­er Office.

📎 Por­tal ofi­cial de pub­li­cação:
https://developer.samsung.com/tv-seller-office


10. O que diferencia um FAST amador de um FAST profissional

AmadorProfis­sion­al
Tela pre­ta ao fal­harFall­back automáti­co
Zap­ping lentoTro­ca instan­tânea
Sem EPGGuia com­ple­to
Tra­va após horasWatch­dog ati­vo
Só 1 streamPri­ma­ry + Back­up

Conclusão (visão de produto, não só código)

Desen­volver um app FAST HLS para Sam­sung não é só tocar um vídeo.
É con­stru­ir um canal de TV dig­i­tal, com:

  • Engen­haria de stream­ing
  • UX de sala de estar
  • Resil­iên­cia de pro­dução
  • Per­for­mance de hard­ware embar­ca­do

Quan­do bem feito, o resul­ta­do é:

Uma TV que o usuário esquece que é um app.

A seguir vai um mod­e­lo real­ista e pron­to pra virar pro­je­to de app FAST (HLS) para Sam­sung Tizen Web App, com:

  • Estru­tu­ra de pas­tas
  • Códi­go de ver­dade: PlayerAdapter (AVPlay), ZappingController, EPGService
  • Mod­e­lo de JSON: canais + EPG (Now/Next e grade com­ple­ta)
  • Teclas do con­t­role e fluxo típi­co de TV

Obser­vação: eu vou te dar um esquele­to funcional/implementável. Depen­den­do do Tizen/ano do device, alguns detal­h­es de evento/estado do AVPlay vari­am um pouco, mas a arquite­tu­ra (que é o que impor­ta) fica igual.


1) Estrutura de projeto (Tizen Web App)

fast-samsung-tizen/
 ├── index.html
 ├── config.xml
 ├── css/
 │   └── style.css
 ├── js/
 │   ├── main.js              # bootstrap
 │   ├── core/
 │   │   ├── eventBus.js
 │   │   ├── logger.js
 │   │   └── storage.js
 │   ├── player/
 │   │   ├── PlayerAdapter.js # interface
 │   │   ├── AVPlayAdapter.js # implementação samsung
 │   │   ├── ZappingController.js
 │   │   └── Watchdog.js
 │   ├── data/
 │   │   ├── ChannelService.js
 │   │   └── EPGService.js
 │   └── ui/
 │       ├── FocusManager.js
 │       ├── overlays.js
 │       └── screens.js
 └── assets/
     └── logos/

2) index.html (inclui AVPlay e webapis)

Em Tizen/Samsung, nor­mal­mente você inclui webapis.js pra aces­sar as APIs de pro­du­to (AVPlay etc.)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>FAST TV</title>
  <meta name="viewport" content="width=1920,height=1080" />
  <link rel="stylesheet" href="css/style.css" />
</head>

<body>
  <!-- AVPlay costuma ser instanciado via object -->
  <object id="avplay"
          type="application/avplayer"
          style="position:absolute; left:0; top:0; width:100%; height:100%;">
  </object>

  <div id="overlay" class="overlay hidden">
    <div id="overlayChannel"></div>
    <div id="overlayNow"></div>
    <div id="overlayNext"></div>
    <div id="overlayMsg"></div>
  </div>

  <!-- Samsung/Tizen product api -->
  <script src="$WEBAPIS/webapis/webapis.js"></script>

  <script type="module" src="js/main.js"></script>
</body>
</html>

3) PlayerAdapter (interface) — padrão “camada de abstração”

// js/player/PlayerAdapter.js
export class PlayerAdapter {
  async open(url) { throw new Error("not implemented"); }
  play() { throw new Error("not implemented"); }
  pause() { throw new Error("not implemented"); }
  stop() { throw new Error("not implemented"); }
  close() { throw new Error("not implemented"); }
  setListener(listener) { throw new Error("not implemented"); }
  getState() { return "UNKNOWN"; }
  getPlaybackTimeMs() { return 0; }
}

4) AVPlayAdapter (Samsung) — implementação real

// js/player/AVPlayAdapter.js
import { log } from "../core/logger.js";
import { PlayerAdapter } from "./PlayerAdapter.js";

export class AVPlayAdapter extends PlayerAdapter {
  constructor(objectId = "avplay") {
    super();
    this.obj = document.getElementById(objectId);
    this.listener = null;
    this.state = "IDLE";
    this.lastTimeMs = 0;
  }

  setListener(listener) {
    this.listener = listener;
  }

  async open(url) {
    log("AVPlay open:", url);
    this.state = "OPENING";
    this._notify("state", { state: this.state });

    try {
      // webapis.avplay é o principal
      webapis.avplay.close(); // garante limpeza
      webapis.avplay.open(url);

      // listener nativo
      webapis.avplay.setListener({
        onbufferingstart: () => this._onBuffering(true),
        onbufferingcomplete: () => this._onBuffering(false),
        onbufferingprogress: (p) => this._notify("buffer", { progress: p }),
        oncurrentplaytime: (ms) => {
          this.lastTimeMs = ms;
          this._notify("time", { ms });
        },
        onevent: (eventType, eventData) => {
          this._notify("event", { eventType, eventData });
        },
        onstreamcompleted: () => {
          this.state = "ENDED";
          this._notify("ended", {});
        },
        onerror: (err) => {
          this.state = "ERROR";
          this._notify("error", { err });
        }
      });

      // display full screen
      webapis.avplay.setDisplayRect(0, 0, 1920, 1080);

      // Prepare
      await new Promise((resolve, reject) => {
        webapis.avplay.prepareAsync(
          () => resolve(),
          (e) => reject(e)
        );
      });

      this.state = "READY";
      this._notify("state", { state: this.state });

    } catch (e) {
      log("AVPlay open failed:", e);
      this.state = "ERROR";
      this._notify("error", { err: e });
      throw e;
    }
  }

  play() {
    try {
      webapis.avplay.play();
      this.state = "PLAYING";
      this._notify("state", { state: this.state });
    } catch (e) {
      this.state = "ERROR";
      this._notify("error", { err: e });
    }
  }

  pause() {
    try {
      webapis.avplay.pause();
      this.state = "PAUSED";
      this._notify("state", { state: this.state });
    } catch (e) {
      this._notify("error", { err: e });
    }
  }

  stop() {
    try {
      webapis.avplay.stop();
      this.state = "STOPPED";
      this._notify("state", { state: this.state });
    } catch (e) {
      this._notify("error", { err: e });
    }
  }

  close() {
    try {
      webapis.avplay.close();
      this.state = "IDLE";
      this._notify("state", { state: this.state });
    } catch (e) {
      // ignora
    }
  }

  getState() { return this.state; }
  getPlaybackTimeMs() { return this.lastTimeMs; }

  _onBuffering(isBuffering) {
    this._notify("buffering", { isBuffering });
  }

  _notify(type, payload) {
    if (this.listener && typeof this.listener === "function") {
      this.listener(type, payload);
    }
  }
}

5) ZappingController (troca de canal com debounce + cancelamento + fallback)

// js/player/ZappingController.js
import { log } from "../core/logger.js";

export class ZappingController {
  constructor({ player, channelService, epgService, overlay }) {
    this.player = player;
    this.channelService = channelService;
    this.epgService = epgService;
    this.overlay = overlay;

    this.currentIndex = 0;
    this.tuneToken = 0;
    this.debounceTimer = null;

    // políticas
    this.STARTUP_TIMEOUT_MS = 10000;
  }

  getCurrentChannel() {
    return this.channelService.getChannels()[this.currentIndex];
  }

  zap(delta) {
    const channels = this.channelService.getChannels();
    this.currentIndex = (this.currentIndex + delta + channels.length) % channels.length;
    const ch = channels[this.currentIndex];

    // overlay aparece imediato (sensação TV)
    this.overlay.showChannel(ch);

    // debounce pra não matar a TV com 20 zaps
    clearTimeout(this.debounceTimer);
    this.debounceTimer = setTimeout(() => this.tune(ch), 200);
  }

  async tune(channel) {
    const token = ++this.tuneToken;
    log("Tune channel:", channel.id, "token:", token);

    // mostra now/next rápido se tiver cache
    const nowNext = this.epgService.getNowNextCached(channel.id);
    if (nowNext) this.overlay.showNowNext(nowNext);

    // tenta tocar primary e depois backup
    const attempts = [
      { url: channel.stream.primary, label: "primary" },
      ...(channel.stream.backup ? [{ url: channel.stream.backup, label: "backup" }] : [])
    ];

    for (let i = 0; i < attempts.length; i++) {
      if (token !== this.tuneToken) return; // cancelado por novo zap

      const { url, label } = attempts[i];
      this.overlay.setMessage(`Carregando (${label})...`);

      try {
        await this._openPlayWithTimeout(url, token);
        this.overlay.setMessage(""); // limpa
        return;
      } catch (e) {
        log("Tune failed", label, e);
      }
    }

    // se tudo falhar: fallback global (safe channel)
    const safe = this.channelService.getSafeChannel();
    if (safe && token === this.tuneToken) {
      this.overlay.setMessage("Sem sinal. Indo para canal seguro...");
      try {
        await this._openPlayWithTimeout(safe.stream.primary, token);
      } catch (e) {
        this.overlay.setMessage("Falha geral de reprodução.");
      }
    }
  }

  async _openPlayWithTimeout(url, token) {
    const timeout = new Promise((_, reject) =>
      setTimeout(() => reject(new Error("startup_timeout")), this.STARTUP_TIMEOUT_MS)
    );

    const openAndPlay = (async () => {
      await this.player.open(url);
      if (token !== this.tuneToken) return;
      this.player.play();
    })();

    return Promise.race([openAndPlay, timeout]);
  }
}

6) EPGService (Now/Next + Grade) com cache e refresh

// js/data/EPGService.js
import { log } from "../core/logger.js";

export class EPGService {
  constructor({ epgUrl }) {
    this.epgUrl = epgUrl;
    this.cache = new Map(); // channelId -> { now, next, schedule[] }
    this.lastFetch = 0;
    this.MIN_REFRESH_MS = 60_000; // 1 min
  }

  getNowNextCached(channelId) {
    const c = this.cache.get(channelId);
    return c ? { now: c.now, next: c.next } : null;
  }

  async refreshIfNeeded() {
    const now = Date.now();
    if (now - this.lastFetch < this.MIN_REFRESH_MS) return;

    this.lastFetch = now;
    await this.fetchAll();
  }

  async fetchAll() {
    log("Fetching EPG:", this.epgUrl);
    const res = await fetch(this.epgUrl, { cache: "no-store" });
    const data = await res.json();

    // data.channels: array
    for (const ch of data.channels) {
      this.cache.set(ch.channelId, {
        now: ch.now,
        next: ch.next,
        schedule: ch.schedule || []
      });
    }
  }
}

7) Watchdog (antitela-preta)

// js/player/Watchdog.js
export class Watchdog {
  constructor({ player, onStall }) {
    this.player = player;
    this.onStall = onStall;
    this.lastTime = 0;
    this.lastTick = Date.now();
    this.STALL_MS = 12000;
    this.timer = null;
  }

  start() {
    this.stop();
    this.timer = setInterval(() => this.tick(), 3000);
  }

  stop() {
    if (this.timer) clearInterval(this.timer);
    this.timer = null;
  }

  tick() {
    const t = this.player.getPlaybackTimeMs();
    const now = Date.now();

    // se o tempo não evoluiu por muito tempo
    if (t === this.lastTime && now - this.lastTick > this.STALL_MS) {
      this.onStall?.();
      this.lastTick = now;
    }

    if (t !== this.lastTime) {
      this.lastTime = t;
      this.lastTick = now;
    }
  }
}

8) Overlay (UI mínima “TV-like”)

// js/ui/overlays.js
export class Overlay {
  constructor() {
    this.el = document.getElementById("overlay");
    this.ch = document.getElementById("overlayChannel");
    this.now = document.getElementById("overlayNow");
    this.next = document.getElementById("overlayNext");
    this.msg = document.getElementById("overlayMsg");
    this.hideTimer = null;
  }

  showChannel(channel) {
    this.el.classList.remove("hidden");
    this.ch.textContent = `${channel.number ?? ""} ${channel.name}`;
    this._autoHide();
  }

  showNowNext({ now, next }) {
    if (now) this.now.textContent = `Agora: ${now.title}`;
    if (next) this.next.textContent = `A seguir: ${next.title}`;
    this._autoHide();
  }

  setMessage(text) {
    this.msg.textContent = text || "";
    if (text) this.el.classList.remove("hidden");
    this._autoHide();
  }

  _autoHide() {
    clearTimeout(this.hideTimer);
    this.hideTimer = setTimeout(() => {
      this.el.classList.add("hidden");
    }, 4000);
  }
}

9) main.js (bootstrap + teclas + fluxo)

// js/main.js
import { AVPlayAdapter } from "./player/AVPlayAdapter.js";
import { ZappingController } from "./player/ZappingController.js";
import { Watchdog } from "./player/Watchdog.js";
import { ChannelService } from "./data/ChannelService.js";
import { EPGService } from "./data/EPGService.js";
import { Overlay } from "./ui/overlays.js";

async function init() {
  const overlay = new Overlay();

  const channelService = new ChannelService({ channelsUrl: "assets/channels.json" });
  await channelService.load();

  const epgService = new EPGService({ epgUrl: "assets/epg.json" });
  await epgService.fetchAll();

  const player = new AVPlayAdapter("avplay");
  player.setListener((type, payload) => {
    // aqui você conecta logs, buffering UI, etc.
    // console.log(type, payload);
  });

  const zapping = new ZappingController({
    player,
    channelService,
    epgService,
    overlay
  });

  const watchdog = new Watchdog({
    player,
    onStall: () => {
      overlay.setMessage("Reconectando…");
      zapping.tune(zapping.getCurrentChannel());
    }
  });
  watchdog.start();

  // iniciar no canal 0
  await zapping.tune(zapping.getCurrentChannel());

  // teclas
  document.addEventListener("keydown", (e) => {
    switch (e.keyCode) {
      case 38: // up
        zapping.zap(-1);
        break;
      case 40: // down
        zapping.zap(+1);
        break;
      case 13: // enter
        overlay.showChannel(zapping.getCurrentChannel());
        break;
      case 10009: // Tizen back (muitos devices)
      case 27: // esc
        // aqui você fecha overlays, abre modal "sair", etc.
        overlay.setMessage("Pressione Back novamente para sair");
        break;
    }
  });

  // refresh de EPG em background
  setInterval(() => epgService.refreshIfNeeded(), 30_000);
}

init();

10) ChannelService + JSON de canais (modelo pronto)

ChannelService.js

// js/data/ChannelService.js
export class ChannelService {
  constructor({ channelsUrl }) {
    this.channelsUrl = channelsUrl;
    this.channels = [];
    this.safeChannelId = null;
  }

  async load() {
    const res = await fetch(this.channelsUrl, { cache: "no-store" });
    const data = await res.json();
    this.channels = data.channels;
    this.safeChannelId = data.safeChannelId;
  }

  getChannels() { return this.channels; }

  getSafeChannel() {
    return this.channels.find(c => c.id === this.safeChannelId) || null;
  }
}

📦 assets/channels.json (modelo de canais)

{
  "safeChannelId": "safe",
  "channels": [
    {
      "id": "news24",
      "number": 1,
      "name": "NEWS 24",
      "logo": "assets/logos/news24.png",
      "category": "Notícias",
      "stream": {
        "primary": "https://seu-cdn.com/news24/master.m3u8",
        "backup": "https://backup-cdn.com/news24/master.m3u8"
      }
    },
    {
      "id": "movies",
      "number": 2,
      "name": "FILMES",
      "logo": "assets/logos/movies.png",
      "category": "Filmes",
      "stream": {
        "primary": "https://seu-cdn.com/movies/master.m3u8",
        "backup": null
      }
    },
    {
      "id": "safe",
      "number": 999,
      "name": "CANAL SEGURO",
      "logo": "assets/logos/safe.png",
      "category": "Sistema",
      "stream": {
        "primary": "https://seu-cdn.com/safe/master.m3u8",
        "backup": null
      }
    }
  ]
}

11) 📦 assets/epg.json (Now/Next + grade completa)

Esse mod­e­lo fun­ciona bem em FAST:

{
  "generatedAt": "2026-02-02T12:00:00-03:00",
  "timezone": "America/Sao_Paulo",
  "channels": [
    {
      "channelId": "news24",
      "now": {
        "title": "Jornal da Hora",
        "start": "2026-02-02T11:30:00-03:00",
        "end": "2026-02-02T12:00:00-03:00",
        "rating": "L",
        "image": "https://seu-cdn.com/epg/news24/jornal.png"
      },
      "next": {
        "title": "Plantão ao Vivo",
        "start": "2026-02-02T12:00:00-03:00",
        "end": "2026-02-02T12:30:00-03:00",
        "rating": "L",
        "image": "https://seu-cdn.com/epg/news24/plantao.png"
      },
      "schedule": [
        {
          "title": "Jornal da Hora",
          "start": "2026-02-02T11:30:00-03:00",
          "end": "2026-02-02T12:00:00-03:00",
          "description": "Resumo das principais notícias."
        },
        {
          "title": "Plantão ao Vivo",
          "start": "2026-02-02T12:00:00-03:00",
          "end": "2026-02-02T12:30:00-03:00",
          "description": "Cobertura em tempo real."
        }
      ]
    },
    {
      "channelId": "movies",
      "now": {
        "title": "Sessão Aventura",
        "start": "2026-02-02T11:00:00-03:00",
        "end": "2026-02-02T12:40:00-03:00",
        "rating": "12"
      },
      "next": {
        "title": "Clássicos da Noite",
        "start": "2026-02-02T12:40:00-03:00",
        "end": "2026-02-02T14:10:00-03:00",
        "rating": "12"
      },
      "schedule": []
    }
  ]
}

Leia tam­bém:

Como Gan­har Din­heiro com Stream­ing e Smart TVs em 2026 (FAST TV, Apps e Anún­cios)

Como Cri­ar Ren­da Dig­i­tal Escaláv­el: Do App Sim­ples aos Mod­e­los com IA e FAST TV

Como Gan­har Din­heiro com Stream­ing e Smart TVs em 2026 (FAST TV, Apps e Anún­cios)

Posts Similares