2.2.20. Recording Synchronization
When recording multiple video feeds that may start and stop at different times, synchronizing playback can be challenging. The SDK provides an optional feature to automatically embed UTC timestamps in recording filenames to enable precise multi-stream synchronization.
Note
This feature is optional and disabled by default to maintain backwards
compatibility. Enable it by calling settings.enableTimestampInFilename(true)
in your recording settings.
2.2.20.1. Enabling Timestamp Embedding
To use timestamp-based synchronization, enable the feature in your recording settings:
PxMedia::AVOutputFeedFile::AVOutputFeedFileSettings settings;
if (auto err = settings.destinationDirectory("/recordings")) {
// Handle error
}
if (auto err = settings.destinationFilenamePattern("camera1_%02d.mp4")) {
// Handle error
}
if (auto err = settings.enableTimestampInFilename(true)) {
// Handle error
}
auto recording = PxMedia::AVOutputFeedFile::create(
context, feedProps, settings, videoInput, audioInputs);
Warning
If you set a custom filename handler using fileOutputLocationHandler()
and it returns a non-empty filename, it overrides the built-in timestamp embedding feature.
If the handler returns an empty string, the built-in naming (including timestamp embedding
when enabled) will be used. If you need both custom naming AND timestamps, you must implement
the timestamp logic within your custom handler function.
2.2.20.2. Timestamp Embedding
When enabled, all recordings created with AVOutputFeedFile
automatically include a UTC timestamp in the filename:
Format: {prefix}_{utc_milliseconds}_{sequence}.{extension}
Example: video_1708185600000_00.mp4
- Where:
videois the base filename from the pattern1708185600000is UTC milliseconds since Unix epoch (13 digits)00is the sequence number.mp4is the file extension
Timestamp Accuracy and Multi-File Synchronization
The embedded timestamp represents the exact moment the first video frame arrives at the muxer, providing frame-accurate synchronization:
Probe accuracy: <1 millisecond (time to capture timestamp when frame arrives)
Multi-file accuracy: Frame-perfect across file splits using PTS-based calculation
Network streams: Timestamp reflects first frame arrival, not network transmission delays
Independent of delays: Works correctly even with packet loss, jitter, or buffering
How Frame-Accurate Multi-File Timing Works:
When recordings split into multiple files (e.g., 4-second segments), maintaining accurate timestamps requires special handling:
Continuous PTS Capture: A buffer probe runs on every frame, capturing both wall-clock time and GStreamer PTS (Presentation Time Stamp)
First File: Uses the buffer probe timestamp directly from the first frame
Split Files: Calculates timestamp using PTS difference:
timestamp_new_file = last_wall_clock + (pts_new - pts_last)
Frame-Perfect Accuracy: Because wall-clock is re-anchored every frame (not just once), the “last” measurement is only 1-2 frames old (~33-66ms), ensuring accurate timing
This PTS-based approach is necessary because GStreamer’s splitmuxsink resets segment timing for each new file, but PTS values remain monotonic across files. By continuously capturing wall-clock + PTS pairs, we can accurately calculate timestamps for split files using the time difference between frames.
Clock Drift:
Continuous re-anchoring eliminates clock drift accumulation within a single machine. The only source of drift is NTP clock adjustments, which are typically small and gradual. For multi-machine recording, see NTP requirements below.
2.2.20.3. Basic Usage
Configure recording settings and enable timestamp embedding:
// Configure recording with filename pattern
PxMedia::AVOutputFeedFile::AVOutputFeedFileSettings settings;
if (auto err = settings.destinationDirectory("/recordings")) {
std::cerr << "Invalid directory: " << err.message() << std::endl;
return;
}
if (auto err = settings.destinationFilenamePattern("camera1_%02d.mp4")) {
std::cerr << "Invalid filename pattern: " << err.message() << std::endl;
return;
}
constexpr int maxDurationSeconds = 300; // 5 minutes per file
if (auto err = settings.maxFileDurationS(maxDurationSeconds)) {
std::cerr << "Invalid max duration: " << err.message() << std::endl;
return;
}
if (auto err = settings.enableTimestampInFilename(true)) {
std::cerr << "Failed to enable timestamp: " << err.message() << std::endl;
return;
}
// Create recording (context, props, settings, video, audio)
auto recording =
PxMedia::AVOutputFeedFile::create(context, feedProps, settings, videoInput, audioInputs);
if (!recording) {
// Handle error...
return;
}
// With enableTimestampInFilename = true:
// Result: /recordings/camera1_1708185600000_00.mp4
// ^^^^^^^^^^^^^^^ UTC milliseconds
//
// With enableTimestampInFilename = false (default):
// Result: /recordings/camera1_00.mp4 (backwards compatible)
The SDK will automatically generate filenames like: camera1_1708185600000_00.mp4
Without timestamp embedding (backwards compatible, default):
settings.enableTimestampInFilename(false); // or omit (default)
// Generates: camera1_00.mp4, camera1_01.mp4, ...
With timestamp embedding (for synchronization):
settings.enableTimestampInFilename(true);
// Generates: camera1_1708185600000_00.mp4, camera1_1708185600000_01.mp4, ...
2.2.20.4. Parsing Timestamps
To synchronize playback, extract timestamps from filenames:
C++ Example
// Extract UTC timestamp from filename using regex
std::string filename = "camera1_1708185600000_00.mp4";
std::regex timestampRegex(R"(_(\d{13})_)");
std::smatch match;
if (std::regex_search(filename, match, timestampRegex)) {
int64_t utcMs = std::stoll(match[1].str());
// Convert to time_point
auto timePoint = std::chrono::system_clock::time_point(std::chrono::milliseconds(utcMs));
// Convert to calendar time (thread-safe)
auto timeT = std::chrono::system_clock::to_time_t(timePoint);
std::tm timeTm{};
localtime_r(&timeT, &timeTm);
std::cout << "Recording started at: " << std::put_time(&timeTm, "%c") << std::endl;
}
Python Example
import re
from pathlib import Path
def parse_timestamp(filename):
"""Extract UTC milliseconds from filename."""
match = re.search(r'_(\d{13})_', filename)
if match:
return int(match.group(1))
return None
# Find all recordings
recordings = list(Path('/recordings').glob('*.mp4'))
# Parse timestamps
files_with_times = []
for f in recordings:
utc_ms = parse_timestamp(f.name)
if utc_ms:
files_with_times.append((f, utc_ms))
# Calculate offsets relative to earliest recording
min_time = min(t for _, t in files_with_times)
offsets = {f: (t - min_time) / 1000.0 for f, t in files_with_times}
print("Playback delays (seconds):")
for f, delay in offsets.items():
print(f" {f.name}: {delay:.3f}s")
2.2.20.5. Multi-Stream Synchronization
For synchronized playback of multiple recordings:
// Track recording info (thread-safe access required)
struct RecordingInfo {
std::string filepath;
int64_t utcStartMs;
};
std::vector<RecordingInfo> recordings;
std::mutex recordingsMutex; // Protect against concurrent callback access
std::regex timestampRegex(R"(_(\d{13})_)");
// Create multiple recordings
for (size_t i = 0; i < 3; ++i) {
PxMedia::AVOutputFeedFile::AVOutputFeedFileSettings settings;
if (auto err = settings.destinationDirectory("/recordings")) {
std::cerr << "Invalid directory: " << err.message() << std::endl;
continue;
}
if (auto err =
settings.destinationFilenamePattern("feed_" + std::to_string(i) + "_%02d.mp4")) {
std::cerr << "Invalid filename pattern: " << err.message() << std::endl;
continue;
}
constexpr int maxDurationSeconds = 60; // 1 minute per file
if (auto err = settings.maxFileDurationS(maxDurationSeconds)) {
std::cerr << "Invalid max duration: " << err.message() << std::endl;
continue;
}
if (auto err = settings.enableTimestampInFilename(true)) {
std::cerr << "Failed to enable timestamp: " << err.message() << std::endl;
continue;
}
auto recording = PxMedia::AVOutputFeedFile::create(
context, feedProps, settings, videoInputs[i], audioInputs);
if (!recording) {
// Handle error...
continue;
}
// Capture filename when recording finishes (runs on SDK thread)
recording.value()->onFeedFileFinished(
[&recordings, &recordingsMutex, ×tampRegex](auto const&, string_view path)
{
std::string pathStr(path);
boost::filesystem::path filePath(pathStr);
std::string filename = filePath.filename().string();
std::smatch match;
if (std::regex_search(filename, match, timestampRegex)) {
std::lock_guard<std::mutex> lock(recordingsMutex);
recordings.push_back({pathStr, std::stoll(match[1].str())});
}
});
}
// Calculate synchronization offsets (protect read access)
{
std::lock_guard<std::mutex> lock(recordingsMutex);
if (!recordings.empty()) {
int64_t minTime = recordings[0].utcStartMs;
for (const auto& rec : recordings) {
minTime = std::min(minTime, rec.utcStartMs);
}
constexpr double millisecondsPerSecond = 1000.0;
for (const auto& rec : recordings) {
double offsetSec =
static_cast<double>(rec.utcStartMs - minTime) / millisecondsPerSecond;
std::cout << rec.filepath << ": " << offsetSec << "s offset" << std::endl;
}
}
}
- This example shows how to:
Create multiple synchronized recordings
Track when each recording finishes
Parse timestamps from filenames
Calculate time offsets between recordings
Generate synchronized playback commands
2.2.20.6. Use Cases
- Multi-Camera Surgery
Record from multiple cameras with independent start/stop times. Synchronize during post-procedure review.
- Distributed Recording
Record on different systems (synchronized via NTP). Combine recordings using UTC timestamps.
- Surgical Training
Align multiple angle recordings for training review. Navigate timeline across all synchronized views.
- Audit & Compliance
Absolute timestamps for regulatory requirements. Precise temporal correlation of events across recordings.
2.2.20.7. Technical Details
- Timestamp Source
UTC wall-clock time from
std::chrono::system_clock- Timestamp Capture Method
GStreamer pad buffer probing on the muxer’s video sink pad runs on every frame, continuously capturing wall-clock + PTS pairs. This continuous re-anchoring eliminates clock drift. First file uses the timestamp captured when the first frame arrives. Split files use PTS-based calculation for frame-accurate timing.
- Precision
Millisecond resolution (13-digit timestamp: UTC milliseconds since Unix epoch)
- Accuracy
Buffer probe captures timestamp with <1ms accuracy when frame arrives
Multi-file recordings: PTS-based calculation provides frame-perfect accuracy across splits
Timestamp reflects first frame arrival time, not file creation time
Works correctly regardless of network delays, jitter, or packet loss
System clock accuracy: typically ±1-10ms with NTP synchronization
- Network Stream Behavior
For live network streams (WebRTC, SRT), the timestamp represents when the first frame’s data actually arrives at the muxer, not when the stream connection was established. This ensures accurate synchronization even if network delays cause lag between initiating recording and receiving the first frame.
Network Time Protocol (NTP) Requirements
Warning
For distributed recording across multiple systems, all systems must be synchronized via NTP (Network Time Protocol) for accurate timestamp alignment. Without NTP, each machine’s system clock drifts independently, causing synchronization errors.
Single Machine: NTP not required for synchronization (all recordings use the same system clock), but still recommended for absolute timestamp accuracy.
- Multiple Machines:
Required: Enable NTP daemon (
ntpd,chronyd, orsystemd-timesyncd)LAN Accuracy: 1-10ms clock synchronization achievable on local networks
Monitoring: Verify NTP status with
ntpq -por equivalentProduction: Monitor NTP offset to ensure clocks remain synchronized
Example NTP Setup (Ubuntu/Debian):
# Install and enable NTP
sudo apt-get install ntp
sudo systemctl enable ntp
sudo systemctl start ntp
# Verify synchronization status
ntpq -p
# Look for '*' indicating active sync source
- Filename Safety
Timestamps are always numeric, ensuring filesystem compatibility across all platforms
File Rotation Limitation
Warning
The maxFiles()
setting is incompatible with UTC timestamp embedding.
GStreamer’s max-files feature works by wrapping sequence numbers back to 0 and
overwriting old files with the same name. However, when timestamp embedding is enabled,
each file has a unique timestamp in its name (e.g., video_1708185600000_00.mp4),
preventing overwriting:
File with sequence 00:
video_1708185600000_00.mp4(timestamp T1)After limit reached:
video_1708185603000_00.mp4(timestamp T2, different name)
Result: All files are retained regardless of the maxFiles setting.
Workaround: Disable timestamp embedding if automatic file rotation is required, or implement custom file deletion logic in your application.