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ón | De fábrica | Qué hace |
|---|---|---|
| Dejar que manden, además de preguntar | Sí | Sin marcar, los métodos que cambian algo contestan con el error -32002 |
| Incluir las rutas de los archivos en las respuestas | No | Añade ruta al estado, a la lista y a los eventos |
| Dejar que hagan hablar al reproductor | No | Permite decir.texto |
| Dejar que ejecuten cualquier acción | No | Permite en acciones.ejecutar salir y las acciones que abren diálogos |
| Decir cuándo se conecta o se desconecta un programa | No | Se 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
- Tubería con nombre de Windows:
\\.\pipe\ReproductorUA-<huella>-control. La huella sale de la carpeta de datos, así que dos copias portables en carpetas distintas no se pisan. - Mientras está abierta existe
data\control_local.jsoncontuberia,pid,version_apiyprograma: un cliente que conoce la carpeta del portable lee de ahí el nombre. Se borra al cerrarla. - Solo pueden conectarse los programas de tu usuario (y los administradores) a integridad media: ni otros usuarios, ni la pantalla de bloqueo, ni procesos aislados de un navegador. No se abre nada en la red.
- Hasta 8 conexiones a la vez.
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étodo | Parámetros | Resultado | Cambia algo |
|---|---|---|---|
api.version | — | {api, programa, pid} | No |
api.metodos | — | lista de {nombre, descripcion, parametros, escribe} | No |
api.eventos | — | los nombres de los eventos | No |
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 decirlo | No |
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 marcada | No |
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ón | lo que devuelva | segú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}}
| Evento | Cuándo | Datos |
|---|---|---|
medio.abriendo | Empieza a abrirse algo | {titulo, artista, album, duracion, remoto, tiene_video, indice, ruta?} |
medio.abierto | Ya suena (o está listo) | igual que medio.abriendo |
medio.cerrado | Se cierra el medio | {} |
estado | Reproduciendo, pausado, detenido, terminado, error... | {estado} |
volumen | Cambia el volumen o el silencio | {volumen, mudo} |
velocidad | Cambia la velocidad | {velocidad} |
salto | Un salto o un «ir a» | {posicion, duracion} |
posicion | Mientras suena, como mucho una vez por segundo | {posicion, duracion} |
capitulo | Cambia el capítulo | {indice, titulo, inicio, fin} |
pista | Cambia la pista de audio o de subtítulos | {tipo, nombre} |
subtitulo | Cambia el subtítulo que se ve | {texto, inicio, fin, imagen} |
bucle | Bucle A-B | {fase: "a" | "ab" | "no" | "invalido", a, b} |
retardo | Retardo de audio o de subtítulos | {tipo, valor} |
dispositivo | Cambia la salida de sonido | {indice, nombre} |
grabacion | Empieza o acaba una grabación | {activa, ruta, cortada} |
apagado | Se programa, avisa o cancela el temporizador de apagado | {programado, tipo, minutos, aviso, quedan} |
error | Algo no se ha podido abrir o reproducir | {mensaje} |
lista.cambiada | Se añade, quita, mueve o cambia algo de la lista | {cambio: "contenido" | "elementos", indices, total, actual} |
lista.terminada | Se acaba la lista | {} |
marcador | Se 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ódigo | Qué pasa |
|---|---|
| -32700 | JSON mal formado |
| -32600 | Petición no válida (o línea demasiado larga) |
| -32601 | No existe el método |
| -32602 | Parámetros no válidos |
| -32603 | Error interno (el detalle queda en el registro del reproductor) |
| -32001 | No hay nada abierto |
| -32002 | No permitido: los mandos, hablar u otra cosa están desactivados en Preferencias |
| -32003 | El programa está ocupado (la interfaz no contestó en 5 segundos) |
| -32004 | No existe: el elemento, la acción o las rutas (data.no_existen) |
| -32005 | La 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
- Imprime el resultado en JSON y sale con 0.
- Con un error del reproductor, lo imprime por la salida de errores y sale con 2.
- Sin copia abierta o con el control desactivado, sale con 3 y lo explica.
eventos.escucharimprime un evento por línea hasta que se cierra el reproductor o se pulsa Control+C.--espera segundoscambia cuánto se espera a una tubería ocupada y--datos carpetahabla con la copia de otra carpeta de datos.
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
- Nada de esta API ejecuta programas, escribe archivos donde diga el cliente ni cambia opciones en bloque.
- Las rutas no salen si no se permite en Preferencias.
- Lo único que se escribe es
data\control_local.json, que se borra al cerrar. - Cualquier programa de tu usuario ya podía mandar órdenes con
ReproductorUA.exe /reproductor-pausa; lo nuevo es leer el estado y la lista, y por eso viene todo apagado.