API de control de ReproductorUA

Con esta API, un guion, un atajo de AutoHotkey, un botón de Stream Deck o un complemento de un lector de pantalla pueden preguntar qué suena y mandar al reproductor con respuesta, sin robarle el foco y sin que hable si no se pide. Es la misma que usa el acompañante de ReproductorUA para NVDA.

Versión de la API: 1.1 (api.version). Solo se añaden métodos y campos en versiones menores; quitar o cambiar algo sería la 2.0.

Activarla

Viene apagada. Se enciende en Preferencias, General, Control desde otros programas, con la casilla «Permitir que otros programas de tu usuario manejen el reproductor» (o con la acción «Permitir el control desde otros programas» del buscador de acciones). En la misma página:

OpciónDe fábricaQué hace
Dejar que manden, además de preguntarSíSin marcar, los métodos que cambian algo contestan con el error -32002
Incluir las rutas de los archivos en las respuestasNoAñade ruta al estado, a la lista y a los eventos
Dejar que hagan hablar al reproductorNoPermite decir.texto
Dejar que ejecuten cualquier acciónNoPermite en acciones.ejecutar salir y las acciones que abren diálogos
Decir cuándo se conecta o se desconecta un programaNoSe dice sin interrumpir

Al pie de la página están el estado («activo, 2 programas conectados»), el botón para copiar el nombre de la tubería y el que abre este documento.

Conectarse

Formato

Un objeto JSON-RPC 2.0 por línea, en UTF-8, terminado en \n (también un lote: una lista en una línea).

→ {"jsonrpc": "2.0", "id": 1, "method": "audio.volumen", "params": {"valor": 40}}
← {"jsonrpc":"2.0","id":1,"result":{"volumen":40,"mudo":false}}

Sin id es una notificación: se hace y no se contesta. Una línea no puede pasar de 1 MB.

Métodos

MétodoParámetrosResultadoCambia algo
api.version—{api, programa, pid}No
api.metodos—lista de {nombre, descripcion, parametros, escribe}No
api.eventos—los nombres de los eventosNo
estado.obtener—{estado, medio: {titulo, artista, album, remoto, tiene_video, ruta?} o null, posicion, duracion, restante, volumen, mudo, velocidad, lista: {indice, total, aleatorio, repetir}, capitulo: {indice, titulo} o null, bucle: {a, b}, apagado}No
estado.frase{tipo}: tiempo, restante, total, completo, estado, titulo, volumen o elemento{texto}: lo que diría el reproductor, sin decirloNo
reproduccion.alternar / .reproducir / .pausar / .detener / .siguiente / .anterior—{estado, indice}Sí
reproduccion.saltar{segundos} (negativo, atrás){posicion}Sí
reproduccion.ir_a{segundos} o {porcentaje}{posicion}Sí
reproduccion.velocidad{valor} (0,25 a 4) o {cambio}{velocidad}Sí
audio.volumen{valor} (0 a 100) o {cambio}{volumen, mudo}Sí
audio.silencio{activo?} (sin él, alterna){mudo}Sí
lista.obtener{desde: 0, cuantos: 100} (hasta 1000){total, actual, desde, elementos: [{indice, titulo, artista, duracion, desactivado, ruta?}]}No
lista.reproducir{indice} (desde 0){indice}Sí
lista.abrir{rutas: [...], modo: "agregar" | "reemplazar" | "agregar_siguiente", reproducir: true}{aceptadas, no_existen}Sí
acciones.listar{categoria?}[{id, nombre, categoria, teclas, disponible, marcada, interactiva}]: interactiva es true en las que abren un diálogo o solo van por teclado, y acciones.ejecutar las rechaza con -32005 salvo con control.permitir_todas_las_acciones marcadaNo
acciones.ejecutar{id}{ejecutada}Sí
ordenes.ejecutar{linea}: una orden con nombre y sus argumentos, sin la barra{correcto, mensaje, detalle}Sí
decir.texto{texto, interrumpir?} (hasta 2000 caracteres){}Sí
ventana.traer—{traida}Sí
eventos.suscribir{eventos: [...]} (o ["*"]){suscritos}No
eventos.cancelar{eventos?} (sin él, todos){suscritos}No
ext.<extensión>.<nombre>los que diga la extensiónlo que devuelvasegún la extensión

api.metodos da la lista viva, con los de las extensiones activas.

Eventos

Tras eventos.suscribir, llegan notificaciones por la misma conexión, sin preguntar:

{"jsonrpc":"2.0","method":"evento","params":{"nombre":"estado","datos":{"estado":"pausado"},"momento":1789650000.123}}
EventoCuándoDatos
medio.abriendoEmpieza a abrirse algo{titulo, artista, album, duracion, remoto, tiene_video, indice, ruta?}
medio.abiertoYa suena (o está listo)igual que medio.abriendo
medio.cerradoSe cierra el medio{}
estadoReproduciendo, pausado, detenido, terminado, error...{estado}
volumenCambia el volumen o el silencio{volumen, mudo}
velocidadCambia la velocidad{velocidad}
saltoUn salto o un «ir a»{posicion, duracion}
posicionMientras suena, como mucho una vez por segundo{posicion, duracion}
capituloCambia el capítulo{indice, titulo, inicio, fin}
pistaCambia la pista de audio o de subtítulos{tipo, nombre}
subtituloCambia el subtítulo que se ve{texto, inicio, fin, imagen}
bucleBucle A-B{fase: "a" | "ab" | "no" | "invalido", a, b}
retardoRetardo de audio o de subtítulos{tipo, valor}
dispositivoCambia la salida de sonido{indice, nombre}
grabacionEmpieza o acaba una grabación{activa, ruta, cortada}
apagadoSe programa, avisa o cancela el temporizador de apagado{programado, tipo, minutos, aviso, quedan}
errorAlgo no se ha podido abrir o reproducir{mensaje}
lista.cambiadaSe añade, quita, mueve o cambia algo de la lista{cambio: "contenido" | "elementos", indices, total, actual}
lista.terminadaSe acaba la lista{}
marcadorSe pasa escuchando por un marcador (se diga o no){ruta, indice, segundos, nombre, nota}

Si se pide un evento que esta versión no tiene, eventos.suscribir contesta -32602 con data.disponibles (la lista de los que sí hay) y no suscribe a ninguno: un cliente pensado para varias versiones puede repetir la petición solo con los que aparecen ahí (el acompañante de NVDA lo hace así). Si un cliente no lee, se le guardan hasta 500 eventos; los más viejos se tiran y, cuando vuelva a leer, recibe {"jsonrpc":"2.0","method":"eventos.perdidos","params":{"cuantos":12}}. La suscripción acaba al cerrar la conexión.

Errores

Siempre JSON-RPC, con code, message en español y, a veces, data:

CódigoQué pasa
-32700JSON mal formado
-32600Petición no válida (o línea demasiado larga)
-32601No existe el método
-32602Parámetros no válidos
-32603Error interno (el detalle queda en el registro del reproductor)
-32001No hay nada abierto
-32002No permitido: los mandos, hablar u otra cosa están desactivados en Preferencias
-32003El programa está ocupado (la interfaz no contestó en 5 segundos)
-32004No existe: el elemento, la acción o las rutas (data.no_existen)
-32005La acción no está disponible ahora, o solo se puede hacer con el teclado

Desde la línea de órdenes

Para guiones que no hablan tuberías, ReproductorUA-consola.exe (la de consola; ReproductorUA.exe no tiene dónde imprimir):

ReproductorUA-consola.exe --api estado.obtener
ReproductorUA-consola.exe --api audio.volumen "{\"valor\": 40}"
ReproductorUA-consola.exe --api lista.abrir --parametros-archivo peticion.json
ReproductorUA-consola.exe --api eventos.escuchar estado,medio.abierto

Desde el código fuente: venv\Scripts\python.exe src\main.py --api estado.obtener.

Ejemplos

Python (pywin32)

import json
import win32file

tuberia = json.load(open(r"C:\ReproductorUA\data\control_local.json", encoding="utf-8"))["tuberia"]
manejador = win32file.CreateFile(tuberia, win32file.GENERIC_READ | win32file.GENERIC_WRITE, 0, None,
                                 win32file.OPEN_EXISTING, 0, None)
win32file.WriteFile(manejador, b'{"jsonrpc":"2.0","id":1,"method":"estado.obtener"}\n')
_codigo, datos = win32file.ReadFile(manejador, 65536)
print(json.loads(datos.split(b"\n")[0])["result"]["medio"])

PowerShell

$datos = Get-Content "C:\ReproductorUA\data\control_local.json" | ConvertFrom-Json
$nombre = $datos.tuberia -replace '^\\\\\.\\pipe\\', ''
$tuberia = New-Object System.IO.Pipes.NamedPipeClientStream('.', $nombre, 'InOut')
$tuberia.Connect(3000)
$escritor = New-Object System.IO.StreamWriter($tuberia); $escritor.AutoFlush = $true
$lector = New-Object System.IO.StreamReader($tuberia)
$escritor.WriteLine('{"jsonrpc":"2.0","id":1,"method":"reproduccion.alternar"}')
$lector.ReadLine() | ConvertFrom-Json
$tuberia.Dispose()

AutoHotkey (por la línea de órdenes)

^!p::
{
    RunWait('"C:\ReproductorUA\ReproductorUA-consola.exe" --api reproduccion.alternar', , "Hide")
}

Para leer la respuesta, ComObject("WScript.Shell").Exec(...) y .StdOut.ReadAll().

Node.js (por ejemplo, un complemento de Stream Deck)

const net = require("net");
const fs = require("fs");
const { tuberia } = JSON.parse(fs.readFileSync("C:\\ReproductorUA\\data\\control_local.json", "utf8"));
const cliente = net.connect(tuberia, () => {
  cliente.write(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eventos.suscribir", params: { eventos: ["medio.abierto"] } }) + "\n");
});
let pendiente = "";
cliente.on("data", (trozo) => {
  pendiente += trozo;
  let fin;
  while ((fin = pendiente.indexOf("\n")) >= 0) {
    const mensaje = JSON.parse(pendiente.slice(0, fin));
    pendiente = pendiente.slice(fin + 1);
    if (mensaje.method === "evento") console.log("Suena:", mensaje.params.datos.titulo);
  }
});

Métodos propios desde una extensión

Una extensión (Herramientas, Extensiones) puede ofrecer sus métodos:

def activar(api):
    api.registrar_metodo_control("marcar", lambda parametros: {"hecho": True},
                                 "Pone un marcador donde suena", escribe=True)

Se piden como ext.<archivo de la extensión>.marcar y desaparecen al desactivar la extensión. La función se ejecuta en el hilo de la interfaz y devuelve datos que se puedan pasar a JSON; para contestar con un error, lanza ErrorControl (de plataforma.api) con su código.

Seguridad