Ir al contenido

Sincronización de video multicámara con ISOBMFF

Extrae las marcas de tiempo de milisegundos de Rhombus embebidas en contenedores de video ISOBMFF para alinear fotogramas con precisión entre varias cámaras en análisis forense.

En esta página

Rhombus escribe una marca de tiempo UTC con precisión de milisegundos en cada segmento grabado .mp4/.m4v. Los campos de tiempo estándar de los contenedores ISOBMFF (ISO Base Media File Format) tienen resolución de un segundo, lo que no basta para alinear el mismo instante entre varias cámaras, audio y datos de sensores. Esta guía explica dónde se guarda la marca de tiempo y cómo leerla.

Comprender ISOBMFF y el átomo "free"

El estándar ISOBMFF (ISO/IEC 14496-12) es el formato de contenedor de .mp4, .m4v, .mov y de formatos de segmentos como MP4 fragmentado. Se compone de cajas (átomos): unidades de datos autocontenidas identificadas por un código de cuatro caracteres, como moov, mdat y free.

Las marcas de tiempo estándar de ISOBMFF, como creation_time en la caja mvhd, tienen resolución de un segundo: suficiente para la mayoría de los flujos multimedia, pero no para sincronizar varias cámaras ni para correlacionar eventos rápidos. Rhombus añade una marca de tiempo con precisión de milisegundos dentro de un átomo free, que los reproductores ignoran, así que los archivos siguen cumpliendo el estándar.

La marca de tiempo personalizada de Rhombus

Cuando Rhombus segmenta el video (y si se obtienen segmentos reales, no un flujo de transporte en vivo), se escribe una firma de metadatos personalizada en la caja free:

  1. Tipo de caja: free

    En ISOBMFF, free normalmente es una caja de relleno que contiene espacio sin usar. Rhombus la reutiliza para transportar metadatos de marca de tiempo.

  2. Firma: rhom

    Los primeros 4 bytes son la cadena ASCII rhom—que la identifica como datos específicos de Rhombus.

  3. Marca de tiempo en milisegundos

    Los siguientes 8 bytes son un entero de 64 bits que representa la hora de inicio del contenido de video, medida en milisegundos desde la época Unix (UTC).

Ejemplo de disposición binaria:

[ free box length ][ 'free' ][ 'rhom' ][ 8-byte timestamp (ms since epoch) ]

Por qué esto es importante para los desarrolladores

Esta decisión de diseño desbloquea capacidades de sincronización precisa en múltiples casos de uso:

Alineación multicámara

Alinea las transmisiones de video de diferentes cámaras con una precisión de 1 ms para un monitoreo coordinado

Fusión de sensores

Combina video con datos de sensores IoT (eventos de control de acceso, lecturas ambientales)

Precisión forense

Reconstruye eventos hasta intervalos de subsegundos en investigaciones

Menor deriva

Evita los errores que se acumulan al depender únicamente de los relojes del sistema cliente o de la sincronización NTP

Recuperar la marca de tiempo

Analizar archivos ISOBMFF

Puedes usar bibliotecas de código abierto para leer la caja free de un segmento .mp4/.m4v y comprobar la firma rhom.

Ejemplos de implementación

Python Implementationpython
import struct
import datetime

def extract_rhombus_timestamp(file_path):
    """
    Extrae la marca de tiempo con precisión de milisegundos de un segmento de video de Rhombus.

    Args:
        file_path: Ruta al archivo .mp4 o .m4v

    Returns:
        tuple: (timestamp_ms, objeto datetime) o (None, None) si no se encuentra
    """
    with open(file_path, "rb") as f:
        data = f.read()

    # Buscar la caja 'free'
    idx = data.find(b'free')
    if idx == -1:
        return None, None

    # Buscar la firma 'rhom' después de 'free'
    rhom_idx = data.find(b'rhom', idx)
    if rhom_idx == -1:
        return None, None

    # Leer los siguientes 8 bytes después de 'rhom' (entero de 64 bits big-endian)
    timestamp_bytes = data[rhom_idx + 4 : rhom_idx + 12]
    timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big")

    # Convertir a hora UTC legible para humanos
    timestamp_dt = datetime.datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc)

    return timestamp_ms, timestamp_dt

# Ejemplo de uso:
timestamp_ms, timestamp_dt = extract_rhombus_timestamp("video_segment.mp4")

if timestamp_ms:
    print(f"Timestamp (ms since epoch): {timestamp_ms}")
    print(f"UTC Time: {timestamp_dt}")
    print(f"ISO Format: {timestamp_dt.isoformat()}")
else:
    print("Rhombus timestamp not found in file")

# Salida de ejemplo:
# Timestamp (ms since epoch): 1722945678123
# UTC Time: 2024-08-06 15:21:18.123000
# ISO Format: 2024-08-06T15:21:18.123000

Uso avanzado en Python

Multi-Segment Processingpython
import os
import glob
from datetime import datetime

class RhombusTimestampExtractor:
    """Extrae y administra marcas de tiempo de múltiples segmentos de video."""

    def __init__(self):
        self.timestamps = []

    def process_directory(self, directory_path, pattern="*.mp4"):
        """Procesa todos los archivos de video en un directorio."""
        files = glob.glob(os.path.join(directory_path, pattern))

        for file_path in sorted(files):
            timestamp_ms, timestamp_dt = extract_rhombus_timestamp(file_path)

            if timestamp_ms:
                self.timestamps.append({
                    'file': os.path.basename(file_path),
                    'timestamp_ms': timestamp_ms,
                    'datetime': timestamp_dt
                })

    def get_timeline(self):
        """Obtiene una lista de segmentos ordenada cronológicamente."""
        return sorted(self.timestamps, key=lambda x: x['timestamp_ms'])

    def find_segment_at_time(self, target_datetime):
        """Encuentra el segmento de video que contiene una hora específica."""
        target_ms = int(target_datetime.timestamp() * 1000)

        for segment in self.get_timeline():
            if segment['timestamp_ms'] <= target_ms:
                closest = segment
            else:
                break

        return closest

# Ejemplo de uso
extractor = RhombusTimestampExtractor()
extractor.process_directory("/path/to/video/segments")

# Encontrar el segmento en una hora específica
target = datetime(2024, 8, 6, 15, 21, 0)
segment = extractor.find_segment_at_time(target)
print(f"Segment at {target}: {segment['file']}")

Casos de uso del mundo real

Reconstrucción de eventos multicámara

Sincroniza el material de varias cámaras para reconstruir incidentes de seguridad:

Event Timeline Reconstructionpython
from datetime import datetime, timedelta

class EventReconstructor:
    def __init__(self):
        self.camera_segments = {}  # cameraId -> lista de segmentos

    def add_camera_footage(self, camera_id, segment_files):
        """Agrega segmentos de video de una cámara específica."""
        segments = []

        for file_path in segment_files:
            timestamp_ms, timestamp_dt = extract_rhombus_timestamp(file_path)
            if timestamp_ms:
                segments.append({
                    'file': file_path,
                    'start_time': timestamp_dt,
                    'timestamp_ms': timestamp_ms
                })

        self.camera_segments[camera_id] = sorted(
            segments,
            key=lambda x: x['timestamp_ms']
        )

    def reconstruct_event(self, event_time, window_seconds=30):
        """
        Encuentra todos los segmentos de cámara dentro de la ventana de tiempo de un evento.

        Args:
            event_time: datetime del evento
            window_seconds: segundos antes/después del evento a incluir
        """
        window = timedelta(seconds=window_seconds)
        start_time = event_time - window
        end_time = event_time + window

        relevant_footage = {}

        for camera_id, segments in self.camera_segments.items():
            camera_clips = []

            for segment in segments:
                # Comprobar si el segmento se superpone con la ventana del evento
                if start_time <= segment['start_time'] <= end_time:
                    offset = (segment['start_time'] - event_time).total_seconds()
                    camera_clips.append({
                        'file': segment['file'],
                        'start_time': segment['start_time'],
                        'offset_from_event': offset
                    })

            if camera_clips:
                relevant_footage[camera_id] = camera_clips

        return relevant_footage

# Uso
reconstructor = EventReconstructor()
reconstructor.add_camera_footage('entrance', entrance_files)
reconstructor.add_camera_footage('lobby', lobby_files)
reconstructor.add_camera_footage('parking', parking_files)

# Reconstruir el evento en una hora específica
event_time = datetime(2024, 8, 6, 15, 21, 18)
footage = reconstructor.reconstruct_event(event_time, window_seconds=30)

print(f"Footage for event at {event_time}:")
for camera_id, clips in footage.items():
    print(f"\n{camera_id}:")
    for clip in clips:
        print(f"  - {clip['file']}")
        print(f"    Offset: {clip['offset_from_event']:.2f}s")

Correlación de datos de sensores

Alinea el video con eventos de control de acceso o de sensores ambientales:

Sensor-Video Correlationjavascript
class SensorVideoCorrelator {
    constructor() {
        this.videoTimestamps = [];
        this.sensorEvents = [];
    }

    /**
     * Agrega un segmento de video con su marca de tiempo
     */
    addVideoSegment(cameraId, timestamp, videoUrl) {
        this.videoTimestamps.push({
            cameraId,
            timestamp: timestamp.timestampMs,
            datetime: timestamp.utcTime,
            url: videoUrl
        });
    }

    /**
     * Agrega un evento de sensor con su marca de tiempo
     */
    addSensorEvent(sensorId, eventType, timestampMs, data) {
        this.sensorEvents.push({
            sensorId,
            eventType,
            timestamp: timestampMs,
            datetime: new Date(timestampMs),
            data
        });
    }

    /**
     * Encuentra la cobertura de video para un evento de sensor
     */
    findVideoForEvent(eventTimestampMs, cameras = null) {
        const relevantVideos = this.videoTimestamps.filter(video => {
            // El segmento de video comienza antes o en la hora del evento
            const inTimeRange = video.timestamp <= eventTimestampMs;

            // Filtrar por cámara si se especifica
            const inCameraList = !cameras || cameras.includes(video.cameraId);

            return inTimeRange && inCameraList;
        });

        // Obtener el video más reciente antes del evento para cada cámara
        const latestByCamera = {};

        relevantVideos.forEach(video => {
            if (!latestByCamera[video.cameraId] ||
                video.timestamp > latestByCamera[video.cameraId].timestamp) {
                latestByCamera[video.cameraId] = video;
            }
        });

        return Object.values(latestByCamera);
    }

    /**
     * Genera un informe de correlación
     */
    generateCorrelationReport() {
        return this.sensorEvents.map(event => {
            const videos = this.findVideoForEvent(event.timestamp);

            return {
                event: {
                    type: event.eventType,
                    sensor: event.sensorId,
                    time: event.datetime.toISOString(),
                    data: event.data
                },
                associatedVideos: videos.map(v => ({
                    camera: v.cameraId,
                    url: v.url,
                    offset: event.timestamp - v.timestamp
                }))
            };
        });
    }
}

// Ejemplo de uso
const correlator = new SensorVideoCorrelator();

// Agregar un evento de acceso a una puerta
correlator.addSensorEvent(
    'door-entrance-1',
    'ACCESS_GRANTED',
    1722945678123,
    { userId: 'user123', cardId: '12345' }
);

// Agregar el video correspondiente
correlator.addVideoSegment(
    'camera-entrance',
    { timestampMs: 1722945670000, utcTime: new Date(1722945670000) },
    'https://media.rhombussystems.com/segment1.mp4'
);

// Generar informe
const report = correlator.generateCorrelationReport();
console.log(JSON.stringify(report, null, 2));

Mejores prácticas para la integración

Validar la firma

Confirma siempre la etiqueta rhom antes de interpretar los bytes siguientes como una marca de tiempo. Esto evita la mala interpretación de datos no relacionados.

python
def is_valid_rhombus_timestamp(data, rhom_index):
    # Verificar la firma 'rhom'
    if data[rhom_index:rhom_index + 4] != b'rhom':
        return False

    # Verificar que haya datos suficientes para la marca de tiempo
    if len(data) < rhom_index + 12:
        return False

    # Extraer y validar el rango de la marca de tiempo
    timestamp_bytes = data[rhom_index + 4:rhom_index + 12]
    timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big")

    # Verificación de coherencia: la marca de tiempo debe ser razonable
    # (entre 2015 y 2050)
    min_timestamp = 1420070400000  # 1 de enero de 2015
    max_timestamp = 2524608000000  # 1 de enero de 2050

    return min_timestamp <= timestamp_ms <= max_timestamp
Manejar las zonas horarias correctamente

La marca de tiempo está basada en UTC. Conviértela apropiadamente si tu aplicación necesita la hora local.

python
import datetime as dt
import pytz

def convert_to_local_time(timestamp_ms, timezone='America/New_York'):
    # Crear un datetime UTC
    utc_dt = dt.datetime.fromtimestamp(timestamp_ms / 1000.0, tz=dt.timezone.utc)
    utc_dt = pytz.utc.localize(utc_dt)

    # Convertir a la zona horaria local
    local_tz = pytz.timezone(timezone)
    local_dt = utc_dt.astimezone(local_tz)

    return local_dt

# Uso
timestamp_ms = 1722945678123
local_time = convert_to_local_time(timestamp_ms, 'America/Los_Angeles')
print(f"Local time: {local_time}")
Usar como reloj de referencia

Combina la marca de tiempo de inicio del segmento con las marcas de tiempo de los fotogramas para una alineación con precisión de fotograma.

javascript
class FrameTimestampCalculator {
    constructor(segmentStartMs, frameRate) {
        this.segmentStartMs = segmentStartMs;
        this.frameRate = frameRate;
        this.frameDurationMs = 1000 / frameRate;
    }

    /**
     * Calcula la marca de tiempo absoluta para un fotograma específico
     */
    getFrameTimestamp(frameNumber) {
        const offsetMs = frameNumber * this.frameDurationMs;
        return this.segmentStartMs + offsetMs;
    }

    /**
     * Encuentra el número de fotograma para una marca de tiempo específica
     */
    getFrameAtTimestamp(targetTimestampMs) {
        const offsetMs = targetTimestampMs - this.segmentStartMs;
        return Math.floor(offsetMs / this.frameDurationMs);
    }
}

// Uso
const segmentTimestamp = 1722945678123;
const calculator = new FrameTimestampCalculator(segmentTimestamp, 30);

// Obtener la marca de tiempo del fotograma 150
const frameTime = calculator.getFrameTimestamp(150);
console.log('Frame 150 timestamp:', new Date(frameTime));

// Encontrar el fotograma en una hora específica
const targetTime = 1722945683123;
const frameNum = calculator.getFrameAtTimestamp(targetTime);
console.log('Frame at target time:', frameNum);
Versiona para prepararte a futuro

Almacena tu lógica de análisis de forma modular por si Rhombus agrega nuevos formatos de metadatos.

python
class RhombusMetadataParser:
    """Analizador extensible para los formatos de metadatos de Rhombus."""

    VERSION = "1.0"

    def __init__(self):
        self.parsers = {
            b'rhom': self._parse_v1_timestamp
        }

    def parse(self, file_path):
        """Analiza los metadatos de un archivo de video."""
        with open(file_path, "rb") as f:
            data = f.read()

        # Buscar la caja 'free'
        free_idx = data.find(b'free')
        if free_idx == -1:
            return None

        # Comprobar todas las firmas conocidas
        for signature, parser_func in self.parsers.items():
            sig_idx = data.find(signature, free_idx)
            if sig_idx != -1:
                return parser_func(data, sig_idx)

        return None

    def _parse_v1_timestamp(self, data, rhom_idx):
        """Analiza el formato de marca de tiempo v1."""
        timestamp_bytes = data[rhom_idx + 4:rhom_idx + 12]
        timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big")

        return {
            'version': 1,
            'type': 'timestamp',
            'timestamp_ms': timestamp_ms,
            'datetime': datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc)
        }

    def add_parser(self, signature, parser_func):
        """Agrega un analizador personalizado para nuevos formatos de metadatos."""
        self.parsers[signature] = parser_func

# Uso
parser = RhombusMetadataParser()
metadata = parser.parse("video_segment.mp4")

if metadata:
    print(f"Version: {metadata['version']}")
    print(f"Type: {metadata['type']}")
    print(f"Timestamp: {metadata['datetime']}")

Consideraciones de rendimiento

Lectura eficiente de archivos

Para archivos grandes, lee solo la parte del encabezado en lugar del archivo completo

Estrategia de caché

Almacena en caché las marcas de tiempo extraídas para evitar volver a analizar los mismos archivos

Procesamiento por lotes

Procesa varios archivos en paralelo al construir líneas de tiempo

Administración de memoria

Usa analizadores de streaming para archivos de video muy grandes

Lectura optimizada de archivos

Optimized Parserpython
def extract_rhombus_timestamp_optimized(file_path, max_search_bytes=100_000):
    """
    Versión optimizada que lee solo el comienzo del archivo.
    La mayoría de los metadatos están en la primera parte de los archivos MP4.
    """
    with open(file_path, "rb") as f:
        # Leer solo la parte del encabezado
        data = f.read(max_search_bytes)

    # Buscar la firma 'rhom'
    rhom_idx = data.find(b'rhom')
    if rhom_idx == -1:
        return None, None

    # Verificar que tenemos datos suficientes para la marca de tiempo
    if len(data) < rhom_idx + 12:
        return None, None

    timestamp_bytes = data[rhom_idx + 4:rhom_idx + 12]
    timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big")
    timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc)

    return timestamp_ms, timestamp_dt

Resumen

Cada segmento ISOBMFF lleva en su átomo free una marca de tiempo UTC con precisión de milisegundos. Las herramientas de video estándar ignoran ese átomo, así que los segmentos se reproducen en cualquier reproductor, y tu código puede leerlo para alinear eventos entre flujos con una precisión muy inferior a un segundo.

Consejo

Próximos pasos para los desarrolladores:

  • Experimenta con la biblioteca ISOBMFF de GitHub para analizar segmentos de Rhombus
  • Usa el visor en línea MP4Box.js para inspeccionar visualmente las estructuras de cajas
  • Incorpora la extracción de marcas de tiempo en tu canalización de ingesta para conjuntos de datos multiorigen perfectamente sincronizados

Recursos adicionales

Soporte

¿Necesitas ayuda con la extracción de marcas de tiempo o la sincronización de video?

  • Correo electrónico: support@rhombus.com
  • Documentación: Explora nuestra referencia completa de la API