Cameras
Multi-Camera Video Synchronization with ISOBMFF
Extract Rhombus millisecond timestamps embedded in ISOBMFF video containers to align frames precisely across multiple cameras for forensic analysis.
On this page
- Understanding ISOBMFF and the "free" atom
- The Rhombus custom timestamp
- Why this is important for developers
- Retrieving the timestamp
- Parsing ISOBMFF files
- Implementation examples
- Real-world use cases
- Multi-camera event reconstruction
- Sensor data correlation
- Best practices for integration
- Performance considerations
- Optimized file reading
- Summary
- Additional resources
- Support
Rhombus writes a millisecond-precision UTC timestamp into every recorded .mp4/.m4v segment. The standard timing fields in ISOBMFF (ISO Base Media File Format) containers have one-second resolution, which isn't enough to line up the same moment across several cameras, audio and sensor data. This guide explains where the timestamp is stored and how to read it.
Understanding ISOBMFF and the "free" atom
The ISOBMFF standard (ISO/IEC 14496-12) is the container format underlying .mp4, .m4v, .mov, and segment formats such as fragmented MP4. It is built from boxes (atoms): self-contained data units identified by a four-character code, such as moov, mdat and free.
Standard timestamps in ISOBMFF, such as creation_time in the mvhd box, have one-second resolution: enough for most media workflows, but not for multi-camera synchronization or correlating fast events. Rhombus adds a millisecond-precision timestamp inside a free atom, which players ignore, so the files remain standard-compliant.
The Rhombus custom timestamp
When Rhombus segments video (and if pulling actual segments, not a live transport stream), a custom metadata signature is written into the free box:
- Box Type: free
In ISOBMFF,
freeis normally a placeholder box containing unused space. Rhombus repurposes this to carry timestamp metadata. - Signature: rhom
The first 4 bytes are the ASCII string
rhom—identifying it as Rhombus-specific data. - Millisecond Timestamp
The next 8 bytes are a 64-bit integer representing the start time of the video content, measured in milliseconds since the Unix epoch (UTC).
Binary layout example:
[ free box length ][ 'free' ][ 'rhom' ][ 8-byte timestamp (ms since epoch) ]Why this is important for developers
This design choice unlocks precise synchronization capabilities across multiple use cases:
Align video feeds from different cameras to within 1 ms for coordinated monitoring
Merge video with IoT sensor data (access control events, environmental readings)
Reconstruct events down to sub-second intervals in investigations
Avoid errors that accumulate when relying solely on client system clocks or NTP sync
Retrieving the timestamp
Parsing ISOBMFF files
You can use open-source libraries to read the free box from an .mp4/.m4v segment and check for the rhom signature.
Professional library for parsing ISO Base Media File Format
Online tool for visually inspecting MP4 box structures
Implementation examples
import struct
import datetime
def extract_rhombus_timestamp(file_path):
"""
Extract millisecond-precision timestamp from Rhombus video segment.
Args:
file_path: Path to the .mp4 or .m4v file
Returns:
tuple: (timestamp_ms, datetime object) or (None, None) if not found
"""
with open(file_path, "rb") as f:
data = f.read()
# Find the 'free' box
idx = data.find(b'free')
if idx == -1:
return None, None
# Search for 'rhom' signature after 'free'
rhom_idx = data.find(b'rhom', idx)
if rhom_idx == -1:
return None, None
# Read the next 8 bytes after 'rhom' (big-endian 64-bit integer)
timestamp_bytes = data[rhom_idx + 4 : rhom_idx + 12]
timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big")
# Convert to human-readable UTC time
timestamp_dt = datetime.datetime.fromtimestamp(timestamp_ms / 1000.0, tz=datetime.timezone.utc)
return timestamp_ms, timestamp_dt
# Example usage:
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")
# Sample output:
# Timestamp (ms since epoch): 1722945678123
# UTC Time: 2024-08-06 15:21:18.123000
# ISO Format: 2024-08-06T15:21:18.123000Advanced Python Usage
import os
import glob
from datetime import datetime
class RhombusTimestampExtractor:
"""Extract and manage timestamps from multiple video segments."""
def __init__(self):
self.timestamps = []
def process_directory(self, directory_path, pattern="*.mp4"):
"""Process all video files in a directory."""
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):
"""Get chronologically sorted list of segments."""
return sorted(self.timestamps, key=lambda x: x['timestamp_ms'])
def find_segment_at_time(self, target_datetime):
"""Find the video segment containing a specific time."""
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
# Usage example
extractor = RhombusTimestampExtractor()
extractor.process_directory("/path/to/video/segments")
# Find segment at specific time
target = datetime(2024, 8, 6, 15, 21, 0)
segment = extractor.find_segment_at_time(target)
print(f"Segment at {target}: {segment['file']}")/**
* Extract Rhombus timestamp from video segment ArrayBuffer
* @param {ArrayBuffer} arrayBuffer - The video file as ArrayBuffer
* @returns {Object|null} Object with timestampMs and utcTime, or null if not found
*/
function extractRhombusTimestamp(arrayBuffer) {
const data = new Uint8Array(arrayBuffer);
// Find 'free' box
const freePattern = new Uint8Array([0x66, 0x72, 0x65, 0x65]); // 'free'
let freeIndex = -1;
for (let i = 0; i <= data.length - 4; i++) {
if (data.subarray(i, i + 4).every((val, idx) => val === freePattern[idx])) {
freeIndex = i;
break;
}
}
if (freeIndex === -1) return null;
// Find 'rhom' signature
const rhomPattern = new Uint8Array([0x72, 0x68, 0x6f, 0x6d]); // 'rhom'
let rhomIndex = -1;
for (let i = freeIndex; i <= data.length - 4; i++) {
if (data.subarray(i, i + 4).every((val, idx) => val === rhomPattern[idx])) {
rhomIndex = i;
break;
}
}
if (rhomIndex === -1) return null;
// Extract 8-byte timestamp (big-endian)
const timestampBytes = data.subarray(rhomIndex + 4, rhomIndex + 12);
let timestamp = 0;
for (let i = 0; i < 8; i++) {
timestamp = timestamp * 256 + timestampBytes[i];
}
return {
timestampMs: timestamp,
utcTime: new Date(timestamp),
isoString: new Date(timestamp).toISOString()
};
}
// Example usage with File API
async function processVideoFile(file) {
const arrayBuffer = await file.arrayBuffer();
const result = extractRhombusTimestamp(arrayBuffer);
if (result) {
console.log('Timestamp (ms):', result.timestampMs);
console.log('UTC Time:', result.utcTime);
console.log('ISO String:', result.isoString);
} else {
console.log('Rhombus timestamp not found');
}
}
// Usage with input element
document.querySelector('#fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
await processVideoFile(file);
}
});Advanced JavaScript Usage
class RhombusVideoSync {
constructor() {
this.cameras = new Map();
}
/**
* Add a camera's video segment with its timestamp
*/
async addCameraSegment(cameraId, videoFile) {
const arrayBuffer = await videoFile.arrayBuffer();
const timestamp = extractRhombusTimestamp(arrayBuffer);
if (timestamp) {
if (!this.cameras.has(cameraId)) {
this.cameras.set(cameraId, []);
}
this.cameras.get(cameraId).push({
timestamp: timestamp.timestampMs,
datetime: timestamp.utcTime,
file: videoFile
});
}
}
/**
* Find segments from all cameras at a specific time
*/
findSegmentsAtTime(targetTime) {
const targetMs = targetTime.getTime();
const results = {};
for (const [cameraId, segments] of this.cameras.entries()) {
// Find closest segment before or at target time
const sorted = segments.sort((a, b) => a.timestamp - b.timestamp);
const segment = sorted.find(s => s.timestamp <= targetMs);
if (segment) {
results[cameraId] = {
timestamp: segment.timestamp,
datetime: segment.datetime,
offset: targetMs - segment.timestamp
};
}
}
return results;
}
/**
* Get time range covered by all cameras
*/
getCommonTimeRange() {
let earliestEnd = Infinity;
let latestStart = 0;
for (const segments of this.cameras.values()) {
if (segments.length === 0) continue;
const start = Math.min(...segments.map(s => s.timestamp));
const end = Math.max(...segments.map(s => s.timestamp));
latestStart = Math.max(latestStart, start);
earliestEnd = Math.min(earliestEnd, end);
}
return {
start: new Date(latestStart),
end: new Date(earliestEnd),
duration: earliestEnd - latestStart
};
}
}
// Usage
const sync = new RhombusVideoSync();
await sync.addCameraSegment('camera1', file1);
await sync.addCameraSegment('camera2', file2);
await sync.addCameraSegment('camera3', file3);
// Find what was happening across all cameras at a specific time
const target = new Date('2024-08-06T15:21:18.123Z');
const segments = sync.findSegmentsAtTime(target);
console.log('Synchronized segments:', segments);#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
#include <chrono>
#include <optional>
#include <algorithm>
struct RhombusTimestamp {
uint64_t timestampMs;
std::chrono::system_clock::time_point utcTime;
// Convert to ISO 8601 string
std::string toISO8601() const {
auto timeT = std::chrono::system_clock::to_time_t(utcTime);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
utcTime.time_since_epoch()
).count() % 1000;
char buffer[32];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S",
std::gmtime(&timeT));
return std::string(buffer) + "." +
std::to_string(ms) + "Z";
}
};
/**
* Extract Rhombus timestamp from video file
*/
std::optional<RhombusTimestamp> extractRhombusTimestamp(
const std::string& filePath
) {
std::ifstream file(filePath, std::ios::binary);
if (!file) {
return std::nullopt;
}
// Read entire file into memory
std::vector<uint8_t> data(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>()
);
// Find 'free' box
const std::vector<uint8_t> freePattern = {0x66, 0x72, 0x65, 0x65};
auto freeIt = std::search(
data.begin(), data.end(),
freePattern.begin(), freePattern.end()
);
if (freeIt == data.end()) {
return std::nullopt;
}
// Find 'rhom' signature
const std::vector<uint8_t> rhomPattern = {0x72, 0x68, 0x6f, 0x6d};
auto rhomIt = std::search(
freeIt, data.end(),
rhomPattern.begin(), rhomPattern.end()
);
if (rhomIt == data.end()) {
return std::nullopt;
}
// Extract 8-byte timestamp (big-endian)
uint64_t timestamp = 0;
for (int i = 0; i < 8; i++) {
timestamp = (timestamp << 8) | *(rhomIt + 4 + i);
}
// Convert to time_point
auto utcTime = std::chrono::system_clock::from_time_t(
timestamp / 1000
);
utcTime += std::chrono::milliseconds(timestamp % 1000);
return RhombusTimestamp{timestamp, utcTime};
}
// Example usage
int main() {
auto result = extractRhombusTimestamp("video_segment.mp4");
if (result) {
std::cout << "Timestamp (ms): " << result->timestampMs << std::endl;
std::cout << "ISO 8601: " << result->toISO8601() << std::endl;
} else {
std::cout << "Rhombus timestamp not found" << std::endl;
}
return 0;
}Advanced C++ Usage
#include <filesystem>
#include <map>
class RhombusTimeline {
private:
struct Segment {
std::filesystem::path filePath;
RhombusTimestamp timestamp;
};
std::vector<Segment> segments;
public:
/**
* Load all video segments from a directory
*/
void loadDirectory(const std::filesystem::path& directory) {
for (const auto& entry :
std::filesystem::directory_iterator(directory)) {
if (entry.path().extension() == ".mp4" ||
entry.path().extension() == ".m4v") {
auto timestamp = extractRhombusTimestamp(
entry.path().string()
);
if (timestamp) {
segments.push_back({entry.path(), *timestamp});
}
}
}
// Sort by timestamp
std::sort(segments.begin(), segments.end(),
[](const Segment& a, const Segment& b) {
return a.timestamp.timestampMs < b.timestamp.timestampMs;
}
);
}
/**
* Find segment containing a specific time
*/
std::optional<Segment> findSegmentAtTime(
const std::chrono::system_clock::time_point& targetTime
) const {
auto targetMs = std::chrono::duration_cast<
std::chrono::milliseconds
>(targetTime.time_since_epoch()).count();
Segment* closest = nullptr;
for (const auto& segment : segments) {
if (segment.timestamp.timestampMs <= targetMs) {
closest = const_cast<Segment*>(&segment);
} else {
break;
}
}
return closest ? std::optional<Segment>(*closest) : std::nullopt;
}
/**
* Get chronological list of all segments
*/
const std::vector<Segment>& getTimeline() const {
return segments;
}
};Real-world use cases
Multi-camera event reconstruction
Synchronize footage from multiple cameras to reconstruct security incidents:
from datetime import datetime, timedelta
class EventReconstructor:
def __init__(self):
self.camera_segments = {} # cameraId -> list of segments
def add_camera_footage(self, camera_id, segment_files):
"""Add video segments from a specific camera."""
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):
"""
Find all camera segments within time window of an event.
Args:
event_time: datetime of the event
window_seconds: seconds before/after event to include
"""
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:
# Check if segment overlaps with event window
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
# Usage
reconstructor = EventReconstructor()
reconstructor.add_camera_footage('entrance', entrance_files)
reconstructor.add_camera_footage('lobby', lobby_files)
reconstructor.add_camera_footage('parking', parking_files)
# Reconstruct event at specific time
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")Sensor data correlation
Align video with access control or environmental sensor events:
class SensorVideoCorrelator {
constructor() {
this.videoTimestamps = [];
this.sensorEvents = [];
}
/**
* Add video segment with its timestamp
*/
addVideoSegment(cameraId, timestamp, videoUrl) {
this.videoTimestamps.push({
cameraId,
timestamp: timestamp.timestampMs,
datetime: timestamp.utcTime,
url: videoUrl
});
}
/**
* Add sensor event with timestamp
*/
addSensorEvent(sensorId, eventType, timestampMs, data) {
this.sensorEvents.push({
sensorId,
eventType,
timestamp: timestampMs,
datetime: new Date(timestampMs),
data
});
}
/**
* Find video coverage for a sensor event
*/
findVideoForEvent(eventTimestampMs, cameras = null) {
const relevantVideos = this.videoTimestamps.filter(video => {
// Video segment starts before or at event time
const inTimeRange = video.timestamp <= eventTimestampMs;
// Filter by camera if specified
const inCameraList = !cameras || cameras.includes(video.cameraId);
return inTimeRange && inCameraList;
});
// Get the most recent video before the event for each camera
const latestByCamera = {};
relevantVideos.forEach(video => {
if (!latestByCamera[video.cameraId] ||
video.timestamp > latestByCamera[video.cameraId].timestamp) {
latestByCamera[video.cameraId] = video;
}
});
return Object.values(latestByCamera);
}
/**
* Generate correlation report
*/
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
}))
};
});
}
}
// Usage example
const correlator = new SensorVideoCorrelator();
// Add door access event
correlator.addSensorEvent(
'door-entrance-1',
'ACCESS_GRANTED',
1722945678123,
{ userId: 'user123', cardId: '12345' }
);
// Add corresponding video
correlator.addVideoSegment(
'camera-entrance',
{ timestampMs: 1722945670000, utcTime: new Date(1722945670000) },
'https://media.rhombussystems.com/segment1.mp4'
);
// Generate report
const report = correlator.generateCorrelationReport();
console.log(JSON.stringify(report, null, 2));Best practices for integration
Validate the Signature
Always confirm the rhom tag before interpreting the following bytes as a timestamp. This prevents misinterpretation of unrelated data.
def is_valid_rhombus_timestamp(data, rhom_index):
# Verify 'rhom' signature
if data[rhom_index:rhom_index + 4] != b'rhom':
return False
# Verify sufficient data for timestamp
if len(data) < rhom_index + 12:
return False
# Extract and validate timestamp range
timestamp_bytes = data[rhom_index + 4:rhom_index + 12]
timestamp_ms = int.from_bytes(timestamp_bytes, byteorder="big")
# Sanity check: timestamp should be reasonable
# (between 2015 and 2050)
min_timestamp = 1420070400000 # Jan 1, 2015
max_timestamp = 2524608000000 # Jan 1, 2050
return min_timestamp <= timestamp_ms <= max_timestampHandle Time Zones Correctly
The timestamp is UTC-based. Convert it appropriately if your application needs local time.
import datetime as dt
import pytz
def convert_to_local_time(timestamp_ms, timezone='America/New_York'):
# Create UTC datetime
utc_dt = dt.datetime.fromtimestamp(timestamp_ms / 1000.0, tz=dt.timezone.utc)
utc_dt = pytz.utc.localize(utc_dt)
# Convert to local timezone
local_tz = pytz.timezone(timezone)
local_dt = utc_dt.astimezone(local_tz)
return local_dt
# Usage
timestamp_ms = 1722945678123
local_time = convert_to_local_time(timestamp_ms, 'America/Los_Angeles')
print(f"Local time: {local_time}")Use as Reference Clock
Combine the segment start timestamp with frame timestamps for frame-accurate alignment.
class FrameTimestampCalculator {
constructor(segmentStartMs, frameRate) {
this.segmentStartMs = segmentStartMs;
this.frameRate = frameRate;
this.frameDurationMs = 1000 / frameRate;
}
/**
* Calculate absolute timestamp for a specific frame
*/
getFrameTimestamp(frameNumber) {
const offsetMs = frameNumber * this.frameDurationMs;
return this.segmentStartMs + offsetMs;
}
/**
* Find frame number for a specific timestamp
*/
getFrameAtTimestamp(targetTimestampMs) {
const offsetMs = targetTimestampMs - this.segmentStartMs;
return Math.floor(offsetMs / this.frameDurationMs);
}
}
// Usage
const segmentTimestamp = 1722945678123;
const calculator = new FrameTimestampCalculator(segmentTimestamp, 30);
// Get timestamp of frame 150
const frameTime = calculator.getFrameTimestamp(150);
console.log('Frame 150 timestamp:', new Date(frameTime));
// Find frame at specific time
const targetTime = 1722945683123;
const frameNum = calculator.getFrameAtTimestamp(targetTime);
console.log('Frame at target time:', frameNum);Version for Future-Proofing
Store your parsing logic in a modular way in case Rhombus adds new metadata formats.
class RhombusMetadataParser:
"""Extensible parser for Rhombus metadata formats."""
VERSION = "1.0"
def __init__(self):
self.parsers = {
b'rhom': self._parse_v1_timestamp
}
def parse(self, file_path):
"""Parse metadata from video file."""
with open(file_path, "rb") as f:
data = f.read()
# Find 'free' box
free_idx = data.find(b'free')
if free_idx == -1:
return None
# Check all known signatures
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):
"""Parse v1 timestamp format."""
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):
"""Add custom parser for new metadata formats."""
self.parsers[signature] = parser_func
# Usage
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']}")Performance considerations
For large files, read only the header portion instead of the entire file
Cache extracted timestamps to avoid re-parsing the same files
Process multiple files in parallel when building timelines
Use streaming parsers for very large video files
Optimized file reading
def extract_rhombus_timestamp_optimized(file_path, max_search_bytes=100_000):
"""
Optimized version that reads only the beginning of the file.
Most metadata is in the first portion of MP4 files.
"""
with open(file_path, "rb") as f:
# Read only header portion
data = f.read(max_search_bytes)
# Search for 'rhom' signature
rhom_idx = data.find(b'rhom')
if rhom_idx == -1:
return None, None
# Verify we have enough data for timestamp
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_dtSummary
Each ISOBMFF segment carries a millisecond-precision UTC timestamp in its free atom. Standard video tools ignore the atom, so the segments play anywhere, and your code can read it to align events across streams to well under a second.
Next Steps for Developers:
- Experiment with the ISOBMFF GitHub library to parse Rhombus segments
- Use the MP4Box.js online viewer to visually inspect box structures
- Incorporate timestamp extraction into your ingest pipeline for perfectly synchronized multi-source datasets
Additional resources
Learn how to implement live video streaming with DashJS
Explore camera and media API endpoints as well as other video options
Read the official ISO Base Media File Format spec
Contact Rhombus Support for help with your integration.
Support
Need assistance with timestamp extraction or video synchronization?
- Email: support@rhombus.com
- Documentation: Browse our complete API reference
This advanced implementation guide is regularly updated to reflect the latest best practices for working with Rhombus video segments.