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:
  • video is the base filename from the pattern

  • 1708185600000 is UTC milliseconds since Unix epoch (13 digits)

  • 00 is the sequence number

  • .mp4 is 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:

  1. Continuous PTS Capture: A buffer probe runs on every frame, capturing both wall-clock time and GStreamer PTS (Presentation Time Stamp)

  2. First File: Uses the buffer probe timestamp directly from the first frame

  3. Split Files: Calculates timestamp using PTS difference:

    timestamp_new_file = last_wall_clock + (pts_new - pts_last)
    
  4. 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, &timestampRegex](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:
  1. Create multiple synchronized recordings

  2. Track when each recording finishes

  3. Parse timestamps from filenames

  4. Calculate time offsets between recordings

  5. 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, or systemd-timesyncd)

  • LAN Accuracy: 1-10ms clock synchronization achievable on local networks

  • Monitoring: Verify NTP status with ntpq -p or equivalent

  • Production: 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.