Tres públicos, un producto, cero supuestos compartidos.
Una red de publicidad OOH tiene tres usuarios distintos sin nada en común. Los dueños de locales quieren saber qué se está reproduciendo en sus pantallas y cuándo. Los anunciantes quieren comprar espacios, segmentar por ubicación y ver recuentos de impresiones. Las pantallas en sí no son usuarios en absoluto: son dispositivos que necesitan recibir las programaciones de forma fiable y reproducirlas sin intervención humana.
El cliente tenía un concepto funcional y una ventana de lanzamiento ajustada. Necesitaban un backend de marketplace para gestionar las aprobaciones de anunciantes, la facturación y la programación de campañas. Una consola para que operadores y dueños de locales arrastraran espacios a un calendario. Y un runtime de reproductor que pudiera ejecutarse en tablets Android detrás del mostrador de un hotel, en quioscos Linux en una estación de tránsito y en decodificadores Windows en una cadena de tiendas, sin desplegar un código base distinto en cada uno.
La opción cómoda por defecto para el reproductor era Electron. Electron empaqueta una instancia de Chromium y un runtime de Node, y se ejecuta en todas partes. También pesa cientos de megabytes, se autoactualiza de forma impredecible y añade la complejidad de un navegador entero a lo que debería ser un reproductor de medios. En hardware de quiosco con recursos limitados, la huella importaba. La historia de las actualizaciones importaba más.
Un backend de marketplace, una consola de arrastrar y soltar y un reproductor nativo que se ejecuta en cuatro sistemas operativos.
El backend es una aplicación Laravel 12 multi-tenant que gestiona todo lo que el marketplace necesita: alta y aprobaciones de anunciantes, programación de campañas con day-parting y geosegmentación, reconciliación de facturación de Stripe, subida y transcodificación de medios encolada a través de la capa de trabajos en segundo plano de Laravel, y un modelo de agregación de impresiones que alimenta los informes. Sanctum gestiona la autenticación de la API. Los activos viven en S3.
La consola del operador es una superficie de administración en Next.js 16 con un programador de arrastrar y soltar construido sobre @dnd-kit. Los dueños de locales ven sus pantallas y sus espacios. Los anunciantes ven sus campañas y su alcance. El rol de administrador lo ve todo. Las tres son la misma aplicación, restringida por rol.
El reproductor es Qt 6 y C++20. Se ejecuta en macOS, Linux, Windows y Android desde un único código base CMake con presets por plataforma. Al arrancar se conecta a Supabase mediante WebSockets y se suscribe a su canal de programación. Cuando el operador mueve un espacio en la consola, el cambio llega a la pantalla en tiempo real: sin sondeo, sin ciclo de refresco. Si la red se cae, el reproductor recurre a su programación y medios cacheados localmente. La pantalla sigue reproduciendo.
- F · 01Consola multi-tenant
- Dueños de locales, anunciantes y administrador en una sola superficie, restringida por rol. Sin portales separados, sin código base duplicado.
- F · 02Programador de arrastrar y soltar
- Espacios, geo-vallas y franjas horarias organizados en un calendario construido sobre @dnd-kit. Los operadores ven los conflictos antes de guardar.
- F · 03Sincronización del reproductor en tiempo real
- Los cambios de programación se envían a la flota de reproductores mediante WebSockets de Supabase en el momento en que el operador guarda. Sin sondeo, sin reinicio.
- F · 04Reproductor capaz de funcionar offline
- Qt Sql con SQLite cachea la programación y los medios actuales localmente. Una caída de Wi-Fi no deja la pantalla en blanco.
- F · 05Binario nativo multiplataforma
- Un código base CMake, cuatro plataformas objetivo: macOS, Linux, Windows, Android. Cada build es un binario nativo autocontenido, sin Chromium, sin V8.
- F · 06Facturación de marketplace
- Reconciliación de facturación de Stripe y agregación de impresiones en el backend de Laravel. Los anunciantes pagan por impresiones confirmadas.
La elección de Qt, y lo que costó frente al Electron por defecto.
Electron era la opción obvia. Todo el equipo sabía JavaScript. La consola de administración ya estaba en TypeScript. Un reproductor Electron habría compartido un tercio de su código base con la superficie web. Lo descartamos en la primera semana.
La realidad del despliegue fue el factor decisivo. El hardware objetivo abarcaba desde tablets Android sobre 4G hasta mini-PC Windows sobre Wi-Fi corporativo y quioscos Linux con imágenes de SO bloqueadas. La huella de Chromium de Electron —típicamente 150–300 MB instalados— era un no rotundo para los dispositivos con recursos limitados. El comportamiento de autoactualización de Chromium, que choca con las políticas de SO bloqueado de los quioscos, era un no aún más rotundo para los despliegues corporativos.
Qt 6 con C++20 da un binario nativo por plataforma, compilado a aproximadamente 15 MB en Linux y menos de 30 MB en Windows y Android. La ruta de actualización es un intercambio de archivos, orquestado por el propio reproductor. No hay motor de navegador, no hay V8, no hay broker de protocolo de Electron. La conexión WebSocket a Supabase corre a través de Qt Network; la reproducción de medios corre a través de Qt Multimedia. La caché offline usa Qt Sql con SQLite. Cada dependencia se distribuye en el binario.
Un reproductor que encaja con el hardware vence a un reproductor que encaja con el equipo.
El coste fue el cambio de contexto. El equipo ejecutó tres runtimes —PHP, TypeScript, C++— en paralelo. Las pruebas de integración entre runtimes requirieron más andamiaje del que necesitaría un stack de un solo lenguaje. La matriz de build de CMake para cuatro plataformas objetivo añadió complejidad a CI. GitHub Actions ejecuta cuatro trabajos de build del reproductor por cada push; el build de Linux basado en Docker resultó ser la línea base más fiable para la reproducibilidad.
Una plataforma OOH de tres niveles, publicada en menos de un mes.
La primera versión de las tres superficies estuvo lista para producción en 27 días: 223 commits en tres repos, CI multiplataforma en cada push, binarios del reproductor confirmados en los cuatro sistemas operativos objetivo antes de la revisión final. El backend de marketplace gestiona la reconciliación de facturación de anunciantes y la agregación de impresiones. La consola gestiona la programación sin una hoja de cálculo. El reproductor gestiona las caídas de red sin dejar la pantalla en blanco.
La plataforma está en producción. La consola del operador es el sistema de registro para las programaciones de pantallas en toda la red. La flota de reproductores recibe actualizaciones en tiempo real mediante WebSockets de Supabase: los cambios de programación hechos en la consola se propagan a las pantallas físicas sin reinicio ni sincronización manual.
La moraleja de la elección de stack es simple. Electron es la respuesta correcta cuando la comodidad del equipo y la velocidad de entrega son las restricciones determinantes. Aquí no era la respuesta correcta. Elige el runtime de reproductor que se ajusta a tu hardware, no el que se ajusta a la zona de confort de tu equipo.
Les dijimos que el reproductor tenía que funcionar en todo lo que ya habíamos comprado. Volvieron con un binario que lo hacía.
