Encoder Packet Timing in OBS Studio: Understanding CTS, FER, FERC, and PIR Timestamps

OBS Studio tracks four nanosecond-precision timestamps—CTS, FER, FERC, and PIR—to measure rendering latency, encoder pipeline delay, and end-to-end frame delivery time.

Encoder packet timing in OBS Studio provides granular visibility into the video pipeline through the encoder_packet_time structure defined in libobs/obs-encoder.h. The obsproject/obs-studio repository implements a four-point timestamp system that captures frame composition, encode request issuance, encode completion, and packet interleaving events. These nanosecond-resolution measurements enable developers to calculate precise latency metrics and diagnose pipeline stalls in custom output plugins.

The Four Timestamp Fields

OBS Studio records a high-resolution timeline for every video frame using four distinct events. Each field uses the monotonic nanosecond timebase provided by os_gettime_ns().

CTS (Composition Timestamp)

CTS marks the moment the raw frame is rendered and copied into the GPU texture queue. This timestamp is captured from the texture-frame timestamp (tf.timestamp) immediately before the encode request is issued. CTS serves as the baseline for all latency calculations, representing when the frame first entered the OBS rendering pipeline.

FER (Frame Encode Request)

FER records the time the encode request is issued to the encoder, such as when encode_texture2 is invoked. In libobs/obs-video-gpu-encode.c, this is captured as fer_ts right before the encoder callback is invoked. FER separates the rendering phase from the encoding phase, allowing developers to isolate GPU render time from encoder initialization overhead.

FERC (Frame Encode Request Complete)

FERC indicates when the encoder finishes processing the request, either returning a packet or failing. The timestamp is filled immediately after the encoder call returns—ept->ferc = os_gettime_ns() on success, or 0 on error (see lines 73‑78 of obs-video-gpu-encode.c). The difference FERC − FER yields the actual encode duration, including any internal queuing delay when the encoder operates asynchronously.

PIR (Packet Interleave Request)

PIR captures the moment the encoded packet is queued for interleaving into the output stream. This timestamp is set later in the output pipeline—typically in the MP4 muxer or RTMP output—when the packet is passed to obs_output. Together with CTS, the difference PIR − CTS represents the full end‑to‑end latency experienced by the viewer, from frame render to muxer ingestion.

Structure Definition and Data Flow

The timing data is stored in a dedicated structure that travels with the encoder packet through the pipeline.

/* libobs/obs-encoder.h – definition of the timing struct */
struct encoder_packet_time {
    int64_t pts;      /* PTS used to match frames with packets */
    uint64_t ccts;    /* Composition timestamp – when the frame was rendered */
    uint64_t fer;     /* Frame Encode Request – when encode was started */
    uint64_t ferc;    /* Frame Encode Request Complete – when encode finished */
    uint64_t pir;     /* Packet Interleave Request – when packet entered the muxer */
};

During GPU‑accelerated encoding, the core populates the fields sequentially:

/* libobs/obs-video-gpu-encode.c – enqueue timing data */
if (tf.timestamp) {
    struct encoder_packet_time *ept = da_push_back_new(encoder->encoder_packet_times);
    ept->pts = encoder->cur_pts;      // PTS for this frame
    ept->cts = tf.timestamp;          // CTS (render time)
    ept->fer = fer_ts;                // FER (encode request)
    if (success)
        ept->ferc = os_gettime_ns(); // FERC (encode complete)
    else
        ept->ferc = 0;                // error case
    /* PIR will be filled later when the packet is handed to the output */
}

Accessing Timing Data in Plugins

Custom output plugins can inspect these timestamps via the packet callback API defined in libobs/obs-output.h.

static void my_output_packet_cb(void *data,
                                struct encoder_packet *pkt,
                                struct encoder_packet_time *pkt_time,
                                void *param)
{
    blog(LOG_INFO,
         "Frame PTS=%" PRId64 " CTS=%" PRIu64 " FER=%" PRIu64
         " FERC=%" PRIu64 " PIR=%" PRIu64,
         pkt_time->pts, pkt_time->cts, pkt_time->fer,
         pkt_time->ferc, pkt_time->pir);
    /* Your output logic … */
}

Register the callback when initializing your output:

obs_output_set_video_encoder(output, encoder);
obs_output_set_video_callback(output,
    (obs_output_video_cb)my_output_packet_cb,
    NULL, NULL);

Calculating Latency Metrics

The nanosecond timestamps enable precise latency calculations without floating‑point drift.

End‑to‑end latency (render to muxer):

int64_t latency_us = (int64_t)((pkt_time->pir - pkt_time->cts) / 1000);
blog(LOG_INFO, "Observed latency: %"PRId64" µs", latency_us);

Encoder pipeline duration (request to completion):

uint64_t encode_duration_ns = pkt_time->ferc - pkt_time->fer;

Render‑to‑encode delay (composition to request):

uint64_t render_delay_ns = pkt_time->fer - pkt_time->cts;

Summary

  • CTS marks when a frame is rendered and copied to the GPU texture queue, serving as the latency baseline.
  • FER captures the moment the encode request is issued to the encoder, separating render time from encode initialization.
  • FERC records when the encoder finishes processing, enabling calculation of actual encode duration including internal queuing.
  • PIR indicates when the packet enters the muxer, allowing calculation of total end‑to‑end latency when compared with CTS.
  • All timestamps use the nanosecond timebase os_gettime_ns() defined in libobs/obs-encoder.h and populated in libobs/obs-video-gpu-encode.c.

Frequently Asked Questions

What is the difference between FER and FERC in OBS Studio encoder timing?

FER (Frame Encode Request) marks the exact moment OBS issues the encode command to the encoder, while FERC (Frame Encode Request Complete) records when the encoder returns the processed packet or fails. The difference between these two timestamps represents the actual time the encoder spent processing the frame, including any internal queuing delays within the encoder itself.

How do I calculate total streaming latency using OBS encoder packet timestamps?

To calculate end-to-end latency, subtract the CTS (Composition Timestamp) from the PIR (Packet Interleave Request). CTS represents when the frame was first rendered, and PIR marks when the encoded packet entered the muxer. The formula (pir - cts) / 1000 yields the total latency in microseconds, covering rendering, encoding, and pipeline queuing time.

Where are encoder packet timestamps stored in the OBS Studio source code?

The timestamp structure is defined in libobs/obs-encoder.h as struct encoder_packet_time, which contains fields for PTS, CTS, FER, FERC, and PIR. The population logic resides in libobs/obs-video-gpu-encode.c, where the core captures CTS from texture frames, FER before encoder invocation, and FERC upon encoder completion. Output modules such as the MP4 muxer later populate the PIR field when packets enter the interleaving stage.

Can custom OBS plugins access encoder timing data for latency monitoring?

Yes, custom output plugins can access these timestamps through the packet callback API defined in libobs/obs-output.h. When registering a video callback with obs_output_set_video_callback, the callback receives a struct encoder_packet_time * parameter containing all four timestamps. This allows plugins to log latency metrics, embed timing metadata into custom containers, or implement adaptive bitrate algorithms based on real-time encoder performance data.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →