Especificación del Formato .ovm
El formato .ovm (Object Video Media) es un estándar multimedia abierto diseñado para vídeo interactivo y multilingüe en la web. Combina contenedores de vídeo acelerados por hardware nativo (MP4 y WebM) con demultiplexación de metadatos a latencia cero mediante peticiones HTTP por rangos (Range requests) y capas de seguimiento temporal continuo a 60 FPS.
Arquitectura del Formato
+-------------------------------------------------------------------------+
| Capa +1: Capa Dinámica Interactiva de Vídeo |
| - Renderizada en tiempo real sobre el lienzo del vídeo en Shadow DOM |
| - Subtítulos multilingües, botones hotspot dinámicos, tarjetas activas |
| - Interpolación matemática de seguimiento temporal (tracking) a 60 FPS |
+-------------------------------------------------------------------------+
| Capa 0: Separador (simbólica, no almacena nada) |
+-------------------------------------------------------------------------+
| Capa −1: Fotogramas de vídeo horneados |
| - Contenedor estándar MP4 (H.264/HEVC/AV1) o WebM (VP9/AV1) |
| - Sin decodificación en JS: el navegador lo gestiona todo |
| - Reproducción instantánea: no requiere descargar el archivo completo |
+-------------------------------------------------------------------------+Las arquitecturas tradicionales de vídeo interactivo obligan a descargar el archivo completo, usar demultiplexores pesados en WebAssembly o alojar archivos JSON externos que se pierden al mover el vídeo. .ovm resuelve esto integrando los metadatos dentro del contenedor y permitiendo su extracción instantánea.
Protocolo de Demuxing a Latencia Cero (HTTP Range Requests)
Para iniciar la reproducción inmediatamente sin esperar la descarga de megabytes de vídeo, el reproductor <ovm-player> aplica una estrategia de tres niveles:
[Petición video.ovm]
|
v
Nivel 1: Sonda de Cabecera (Range: bytes=0-262143)
|
+---> ¿Firma "OVM_Metadata" presente?
|-- SÍ --> Extraer Base64 -> Recortar padding -> Descomprimir Deflate crudo
|-- NO --> Pasar a Nivel 2
|
v
Nivel 2: Sonda de Cola (Range: bytes=-65536)
|
+---> ¿"ovm_version" o "slots" en los últimos 64 KB?
|-- SÍ --> Parsear JSON balanceado mediante contador de llaves
|-- NO --> Pasar a Nivel 3
|
v
Nivel 3: Fallback Sidecar (Petición video.ovm.json)
|
+---> El servidor no soporta HTTP 206 -> Descargar JSON complementarioNivel 1: Petición de Cabecera (Primeros 256 KB)
El reproductor emite la solicitud inicial:
GET /media/demo.ovm HTTP/1.1
Range: bytes=0-262143- Examina los primeros 262.144 bytes buscando la firma ASCII
OVM_Metadata(ometadata.ovmdata). - Extrae la secuencia continua en Base64 coincidente con
/[A-Za-z0-9+/=]{100,}/. - Recorta iterativamente los bytes sobrantes de padding y descomprime mediante
DecompressionStream('deflate-raw').
Nivel 2: Petición de Cola (Últimos 64 KB)
Si el software de codificación colocó los metadatos al final del archivo contenedor:
GET /media/demo.ovm HTTP/1.1
Range: bytes=-65536- Escanea los últimos 65.536 bytes buscando
{seguido deovm_versionoslots. - Extrae el objeto JSON balanceado con un analizador de profundidad que respeta cadenas y caracteres de escape.
Nivel 3: Fallback Sidecar
Si el servidor web devuelve HTTP 200 ignorando la cabecera Range (servidor sin soporte de contenido parcial), el cliente solicita automáticamente ${src}.json.
Interpolación de Seguimiento Temporal a 60 FPS
Los elementos interactivos en .ovm siguen objetos en movimiento dentro de la escena mediante fotogramas clave cuadriláteros almacenados en slot.tracking_data:
// Formato: [t, x1, y1, x2, y2, x3, y3, x4, y4]
tracking_data: [
[0.00, 0.20, 0.30, 0.40, 0.30, 0.40, 0.50, 0.20, 0.50],
[1.50, 0.35, 0.32, 0.55, 0.32, 0.55, 0.52, 0.35, 0.52]
]Donde:
t: Marca de tiempo del vídeo en segundos.(x1,y1)a(x4,y4): Coordenadas normalizadas ($0.0 \le x, y \le 1.0$) de los 4 vértices del polígono que delimita al objeto.
Algoritmo de Interpolación Lineal (LERP)
En cada ciclo de refresco de pantalla durante la reproducción:
- Se localizan los fotogramas clave delimitadores $K_{\text{antes}}[0] \le t$ y $K_{\text{después}}[0] > t$.
- Se calcula el progreso relativo normalizado: $$p = \operatorname{clamp}\left(\frac{t - t_0}{t_1 - t_0}, 0, 1\right)$$
- Se interpolan linealmente las 8 coordenadas del cuadrilátero: $$\operatorname{coord}[i] = K_{\text{antes}}[i] + \left(K_{\text{después}}[i] - K_{\text{antes}}[i]\right) \times p$$
- Se calcula el punto central del polígono: $$\operatorname{left} = \left(\frac{x_1 + x_3}{2}\right) \times 100%$$ $$\operatorname{top} = \left(\frac{y_1 + y_3}{2}\right) \times 100%$$
- Se posiciona el elemento mediante CSS
transform: translate(-50%, -50%).
Esquema Zod Oficial (@oim-media/core)
import { z } from "zod";
import { slotSchema, zoneSchema } from "@oim-media/core";
export const OvmMetadataSchema = z.object({
ovm_version: z.string().optional(), // Versión de formato ("1.0")
version: z.string().optional(),
ovm_id: z.string().max(128).optional(), // Identificador global UUID
image_hash: z.string().max(64).optional(), // Hash SHA-256 de la pista de vídeo
immutable_lock: z.boolean().optional(), // Candado contra modificaciones
metadata: z.object({
fps: z.number().min(1).max(120).optional(), // Tasa de cuadros por segundo
resolution: z.tuple([z.number(), z.number()]).optional(), // [Ancho, Alto]
}).optional(),
zones: z.array(zoneSchema).optional(), // Catálogo de zonas
slots: z.array(slotSchema), // Slots con seguimiento
signature: z.string().optional(), // Firma criptográfica ECDSA
key_version: z.string().optional(),
timestamp: z.string().datetime({ offset: true }).optional(),
}).passthrough();Ejemplo Completo del Contrato JSON
{
"ovm_version": "1.0",
"ovm_id": "ovm_98a7b6c5d4e3",
"metadata": {
"fps": 60,
"resolution": [1920, 1080]
},
"slots": [
{
"id": "etiqueta_ponente",
"name": "Rótulo de Ponente",
"type": "text",
"content": {
"es": "Dra. Elena Vance — Investigadora Principal",
"en": "Dr. Elena Vance — Lead Researcher"
},
"style": {
"color": "#ffffff",
"fontSize": "16px",
"font": "Inter"
},
"tracking_data": [
[0.0, 0.40, 0.20, 0.60, 0.20, 0.60, 0.35, 0.40, 0.35],
[2.5, 0.45, 0.22, 0.65, 0.22, 0.65, 0.37, 0.45, 0.37],
[5.0, 0.50, 0.25, 0.70, 0.25, 0.70, 0.40, 0.50, 0.40]
]
}
],
"signature": "MEUCIQ...firma_digital_ecdsa_64_bytes...",
"key_version": "v1",
"timestamp": "2026-09-03T03:10:00.000000+00:00"
}Implementación del Motor de Demuxing en <ovm-player>
async function extractOvmMetadata(src) {
// 1. Sonda de cabecera (Primeros 256 KB)
try {
const resp = await fetch(src, { headers: { Range: "bytes=0-262143" } });
if (resp.ok || resp.status === 206) {
const buffer = await resp.arrayBuffer();
const text = new TextDecoder().decode(new Uint8Array(buffer));
let idx = text.indexOf("OVM_Metadata");
if (idx === -1) idx = text.indexOf("metadata.ovmdata");
if (idx !== -1) {
const match = text.substring(idx).match(/[A-Za-z0-9+/=]{100,}/);
if (match) {
const b64 = match[0];
for (let i = b64.length; i >= 10; i--) {
let cand = b64.substring(0, i);
if (cand.length % 4 !== 0) cand += "=".repeat(4 - (cand.length % 4));
try {
const bytes = Uint8Array.from(atob(cand), (c) => c.charCodeAt(0));
const ds = new DecompressionStream("deflate-raw");
const writer = ds.writable.getWriter();
writer.write(bytes);
writer.close();
const plain = await new Response(ds.readable).text();
const parsed = JSON.parse(plain);
if (parsed.slots || parsed.elements) return parsed;
} catch (_) {}
}
}
}
}
} catch (_) {}
// 2. Sonda de cola de archivo (Últimos 64 KB)
try {
const resp = await fetch(src, { headers: { Range: "bytes=-65536" } });
if (resp.ok || resp.status === 206) {
const text = await resp.text();
for (let i = 0; i < text.length; i++) {
if (text[i] === "{" && (text.includes("ovm_version") || text.includes("slots"))) {
let depth = 0, inStr = false;
for (let j = i; j < text.length; j++) {
if (text[j] === '"' && text[j - 1] !== "\\") inStr = !inStr;
if (!inStr) {
if (text[j] === "{") depth++;
else if (text[j] === "}" && --depth === 0) {
return JSON.parse(text.substring(i, j + 1));
}
}
}
}
}
}
} catch (_) {}
// 3. Fallback a archivo sidecar .json
const sidecar = await fetch(src + ".json");
if (sidecar.ok) return await sidecar.json();
throw new Error("No se encontraron metadatos OVM");
}