Skip to content

Control remoto ​

Con la sesión abierta, tu app puede manejar lo que se reproduce en el TV y ver qué está pasando: posición, duración, pausa, volumen, subtítulos y audio.

Enviar un comando ​

POST /v1/sessions/commands con el sessionToken. El TV lo aplica al instante y lo muestra en pantalla (destello de pausa, "+30 segundos", barra de volumen), igual que si se usara el control remoto.

typeCamposQué hace
play—Reproduce. Si el video terminó, empieza de nuevo.
pause—Pausa.
stop—Cierra el reproductor; el TV vuelve a "Teléfono conectado".
seekposition (segundos)Va a ese punto.
skipsecondsAdelanta, o retrocede si es negativo.
volumelevel (0–100)Volumen del reproductor. Si estaba en silencio, lo quita.
mutemuted (true/false)Silencia o quita el silencio.
subtitlestrackId o nullActiva esos subtítulos; null los desactiva.
audiotrackIdCambia el idioma del audio.

Todos aceptan además castId: si el TV ya está reproduciendo otro video (alguien envió uno nuevo), el comando se rechaza con 409 cast_mismatch en vez de pausar el video equivocado.

sh
curl -X POST https://beta.playeroff.app/v1/sessions/commands \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"type": "skip", "seconds": 30}'
js
await fetch('https://beta.playeroff.app/v1/sessions/commands', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${sessionToken}` },
  body: JSON.stringify({ type: 'volume', level: 40 }),
})
dart
await http.post(
  Uri.parse('https://beta.playeroff.app/v1/sessions/commands'),
  headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $sessionToken'},
  body: jsonEncode({'type': 'subtitles', 'trackId': '54'}),
);

Respuesta 202 { "commandId": "…" }. Cuando el TV lo aplica, el siguiente estado de reproducción trae ese commandId en lastCommandId.

  • 409 nothing_playing: no hay video en el TV, se detuvo o falló. Envía uno con POST /v1/casts.
  • Un comando que no llega al TV en 10 segundos se descarta: una pausa aplicada medio minuto tarde es peor que nada.
  • Volumen: es el del reproductor, entre 0 y 100 % del volumen del TV. El volumen del TV en sí se sigue manejando con su control.

Ver qué se está reproduciendo ​

GET /v1/sessions/playback devuelve { "playback": … }, o null si el TV todavía no reprodujo nada:

json
{
  "castId": "WFSRb-51hgKoO2YGXWByaQ",
  "title": "Mi película",
  "state": "playing",
  "position": 754.2,
  "duration": 6120,
  "volume": 80,
  "muted": false,
  "subtitles": [{ "id": "54", "lang": "es", "label": "Español" }],
  "activeSubtitleId": null,
  "audioTracks": [{ "id": "en", "lang": "en", "label": "Inglés" }, { "id": "es", "lang": "es", "label": "Español" }],
  "activeAudioId": "en",
  "lastCommandId": "…",
  "updatedAt": 1800000123000
}
  • state: loading, playing, paused, buffering, ended, stopped o failed (con error).
  • position y duration en segundos; duration es null mientras se desconoce o en vivo.
  • Los id de las pistas son los que tienes que mandar en los comandos subtitles y audio. Úsalos tal cual, sin interpretarlos.
  • Mientras state sea playing, avanza position por tu cuenta (el TV informa cada 5 segundos y en cada cambio).

El estado es del TV, no de tu sesión: si te conectas mientras algo se reproduce, lo ves y lo puedes controlar.

Seguir el TV en vivo (SSE) ​

GET /v1/sessions/events con Authorization: Bearer <sessionToken> es un flujo de Server-Sent Events:

EventoDatosCuándo
playbackPlayback o nullAl conectar y en cada cambio en el TV
cast{ castId, status, error? }Cuando cambia el estado de uno de tus envíos
ended{ reason }La sesión terminó; el servidor cierra el flujo

Se permiten 2 flujos abiertos a la vez por sesión en cada servidor. Al abrir otro se cierra el más antiguo, y recibe ended con reason: superseded. También se cierra un flujo cuyo cliente deja de leer.

reason es replaced (otra persona se conectó), disconnected (alguien desconectó o el TV se cerró), expired o superseded. Salvo en superseded, que no debe reabrirse, para seguir hay que canjear un código nuevo.

Un video que lleva 30 s sin informar llega como playback con state: stopped.

EventSource del navegador no permite enviar cabeceras; desde una web usa fetch y lee el cuerpo como stream. En Flutter, http.Client().send() con la cabecera y response.stream funciona igual.

js
const res = await fetch('https://beta.playeroff.app/v1/sessions/events', {
  headers: { Authorization: `Bearer ${sessionToken}` },
})
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = ''
for (;;) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += value
  let i
  while ((i = buffer.indexOf('\n\n')) >= 0) {
    const block = buffer.slice(0, i)
    buffer = buffer.slice(i + 2)
    const event = block.match(/^event: (.*)$/m)?.[1]
    const data = block.match(/^data: (.*)$/m)?.[1]
    if (event) console.log(event, JSON.parse(data))
  }
}
dart
final req = http.Request('GET', Uri.parse('https://beta.playeroff.app/v1/sessions/events'))
  ..headers['Authorization'] = 'Bearer $sessionToken';
final res = await http.Client().send(req);
String? event;
await for (final line in res.stream.transform(utf8.decoder).transform(const LineSplitter())) {
  if (line.startsWith('event: ')) event = line.substring(7);
  if (line.startsWith('data: ') && event != null) print('$event ${line.substring(6)}');
  if (line.isEmpty) event = null;
}

Si el flujo se corta, vuelve a abrirlo: el primer evento trae el estado actual. Si no consigues mantenerlo abierto (algunas redes cortan conexiones largas), consulta GET /v1/sessions/playback cada pocos segundos.

PlayerOFF · API abierta para enviar videos a cualquier pantalla.