> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Frame Processor Reference

> Reference for the PyTrickle FrameProcessor API: VideoFrame, AudioFrame, StreamServer configuration, and the update_params protocol.

export const TableCell = ({children, align = "left", header = false, style = {}, className = "", ...rest}) => {
  const Component = header ? "th" : "td";
  return <Component className={className} style={{
    padding: "0.75rem 1rem",
    textAlign: align,
    border: header ? "none" : "1px solid var(--lp-color-border-default)",
    ...style
  }} {...rest}>
      {children}
    </Component>;
};

export const TableRow = ({children, header = false, hover = false, style = {}, className = "", ...rest}) => {
  const rowId = `table-row-${Math.random().toString(36).substr(2, 9)}`;
  return <>
      {hover && <style>{`
          #${rowId}:hover {
            background-color: var(--lp-color-bg-card);
          }
        `}</style>}
      <tr id={rowId} className={className} style={{
    ...header && ({
      backgroundColor: "var(--lp-color-accent-strong)",
      color: "var(--lp-color-on-accent)",
      fontWeight: "bold"
    }),
    ...style
  }} {...rest}>
        {children}
      </tr>
    </>;
};

export const StyledTable = ({children, variant = "default", style = {}, className = "", ...rest}) => {
  const wrapperVariants = {
    default: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "var(--lp-color-bg-card)",
      overflow: "hidden"
    },
    bordered: {
      border: "2px solid var(--lp-color-accent)",
      backgroundColor: "var(--lp-color-bg-page)",
      overflow: "hidden"
    },
    minimal: {
      border: "none",
      backgroundColor: "transparent",
      overflow: "visible"
    }
  };
  return <div data-docs-styled-table-shell className={className} style={{
    width: "100%",
    padding: 0,
    margin: 0,
    ...wrapperVariants[variant],
    ...style
  }} {...rest}>
      <table data-docs-styled-table style={{
    width: "100%",
    borderCollapse: "collapse",
    borderSpacing: 0,
    margin: 0,
    backgroundColor: "transparent"
  }}>
        {children}
      </table>
    </div>;
};

export const CenteredContainer = ({children, maxWidth = "800px", padding = "0", preset = "default", width = "", minWidth = "", marginRight = "", marginBottom = "", textAlign = "", style = {}, className = "", ...rest}) => {
  const presets = {
    default: {},
    fitContent: {
      width: "fit-content",
      minWidth: "fit-content"
    },
    readable70: {
      width: "70%",
      minWidth: "fit-content"
    },
    readable80: {
      width: "80%",
      minWidth: "fit-content"
    },
    readable90: {
      width: "90%"
    },
    wide900: {
      maxWidth: "900px"
    }
  };
  const presetStyle = presets[preset] || presets.default;
  return <div className={className} style={{
    maxWidth: presetStyle.maxWidth || maxWidth,
    margin: "0 auto",
    padding: padding,
    ...presetStyle.width ? {
      width: presetStyle.width
    } : {},
    ...presetStyle.minWidth ? {
      minWidth: presetStyle.minWidth
    } : {},
    ...width ? {
      width
    } : {},
    ...minWidth ? {
      minWidth
    } : {},
    ...marginRight ? {
      marginRight
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...textAlign ? {
      textAlign
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

<CenteredContainer preset="readable90">
  <Tip>All methods on FrameProcessor are async. The SDK calls them from an asyncio event loop; blocking calls in process\_video\_async will stall the pipeline. Run blocking model inference in a thread pool using asyncio.to\_thread.</Tip>
</CenteredContainer>

***

PyTrickle's processing surface is the `FrameProcessor` base class. Subclass it, implement the async methods below, and pass an instance to `StreamServer`. The SDK manages connection lifecycle, decoding, encoding, and transport.

<CustomDivider />

## FrameProcessor

### Initialize Method

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
async def initialize(self) -> None
```

Called once before the first frame is delivered. Load AI models, allocate CUDA tensors, or warm up any resources that must be ready before processing begins. Raises are propagated and halt startup.

***

### Process Video Async Method

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
async def process_video_async(self, frame: VideoFrame) -> Optional[VideoFrame]
```

Called for each decoded video frame. Return a `VideoFrame` to include it in the output stream, or `None` to drop the frame.

Blocking operations (model inference on CPU, disk I/O) stall the processing loop and increase output latency. Run them in a thread pool:

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
import asyncio

async def process_video_async(self, frame: VideoFrame) -> Optional[VideoFrame]:
    tensor = frame.tensor.clone()
    result_tensor = await asyncio.to_thread(self._run_model, tensor)
    return frame.replace_tensor(result_tensor)

def _run_model(self, tensor):
    # Blocking model inference here
    return self.model(tensor)
```

***

### Process Audio Async Method

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
async def process_audio_async(self, frame: AudioFrame) -> Optional[List[AudioFrame]]
```

Called for each decoded audio frame. Return a list of `AudioFrame` objects (pass-through returns `[frame]`), or `None` to drop the frame. The SDK automatically detects and converts between mono, stereo, and multi-channel formats.

***

### Update Params Method

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
def update_params(self, params: dict) -> None
```

Called when the Livepeer Orchestrator forwards a parameter update from the Gateway. `params` is a plain dict; the schema is defined by your service. Updates are applied immediately to the running processor without interrupting the stream.

Example:

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
def update_params(self, params: dict):
    if "prompt" in params:
        self.prompt = params["prompt"]
    if "strength" in params:
        self.strength = float(params["strength"])
```

<CustomDivider />

## VideoFrame

Represents a single decoded video frame.

<StyledTable variant="bordered">
  <thead>
    <TableRow header>
      <TableCell header>Attribute / Method</TableCell>
      <TableCell header>Type</TableCell>
      <TableCell header>Description</TableCell>
    </TableRow>
  </thead>

  <tbody>
    <TableRow>
      <TableCell>`frame.tensor`</TableCell>
      <TableCell>`torch.Tensor`</TableCell>
      <TableCell>CPU tensor, shape \[H, W, C], float32, values in \[0, 1], RGB channel order</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`frame.pts`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Presentation timestamp in stream time base units</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`frame.width`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Frame width in pixels</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`frame.height`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Frame height in pixels</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`frame.replace_tensor(tensor)`</TableCell>
      <TableCell>`VideoFrame`</TableCell>
      <TableCell>Returns a new VideoFrame with the given tensor and the same metadata</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

<CustomDivider />

## AudioFrame

Represents a decoded audio buffer.

<StyledTable variant="bordered">
  <thead>
    <TableRow header>
      <TableCell header>Attribute</TableCell>
      <TableCell header>Type</TableCell>
      <TableCell header>Description</TableCell>
    </TableRow>
  </thead>

  <tbody>
    <TableRow>
      <TableCell>`frame.tensor`</TableCell>
      <TableCell>`torch.Tensor`</TableCell>
      <TableCell>Audio samples as a float32 tensor, shape \[channels, samples]</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`frame.sample_rate`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Sample rate in Hz</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`frame.pts`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Presentation timestamp</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

<CustomDivider />

## StreamServer

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
StreamServer(
    frame_processor: FrameProcessor,
    port: int = 8000,
    capability_name: str = "custom-processor",
)
```

<StyledTable variant="bordered">
  <thead>
    <TableRow header>
      <TableCell header>Parameter</TableCell>
      <TableCell header>Type</TableCell>
      <TableCell header>Description</TableCell>
    </TableRow>
  </thead>

  <tbody>
    <TableRow>
      <TableCell>`frame_processor`</TableCell>
      <TableCell>`FrameProcessor`</TableCell>
      <TableCell>The processor instance to call for each frame. Must be started before passing to StreamServer.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`port`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>HTTP port to listen on. Default: 8000.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`capability_name`</TableCell>
      <TableCell>`str`</TableCell>
      <TableCell>Name advertised to the Orchestrator as the BYOC capability identifier.</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

### HTTP endpoints

<StyledTable variant="bordered">
  <thead>
    <TableRow header>
      <TableCell header>Endpoint</TableCell>
      <TableCell header>Method</TableCell>
      <TableCell header>Purpose</TableCell>
    </TableRow>
  </thead>

  <tbody>
    <TableRow>
      <TableCell>`/api/stream/start`</TableCell>
      <TableCell>POST</TableCell>
      <TableCell>Start a processing session. Body: `subscribe_url`, `publish_url`, `gateway_request_id`, `params`.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`/api/stream/stop`</TableCell>
      <TableCell>POST</TableCell>
      <TableCell>Stop the active session. Body: `gateway_request_id`.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`/api/stream/update`</TableCell>
      <TableCell>POST</TableCell>
      <TableCell>Send parameter updates. Body: any JSON object; forwarded to `update_params`.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`/api/stream/status`</TableCell>
      <TableCell>GET</TableCell>
      <TableCell>Returns current session status and frame counts.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`/health`</TableCell>
      <TableCell>GET</TableCell>
      <TableCell>Returns `{"status": "ok"}`. Required for BYOC registration.</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

<CustomDivider />

## Framerate and Throughput

The `max_framerate` parameter in the `/api/stream/start` request body controls the maximum number of frames passed to `process_video_async` per second. Frames arriving faster than `max_framerate` are dropped before the processor sees them. This prevents backpressure from accumulating when model inference is slower than the input frame rate.

Set `max_framerate` to the throughput your model can sustain. For StreamDiffusion workflows on an RTX 4090, this is typically 15-30 FPS depending on resolution. For lighter processing, match the input stream frame rate.

<CustomDivider />

The [PyTrickle quickstart](/v2/developers/build/ai-and-agents/realtime-ai/pytrickle/pytrickle-quickstart) walks through a complete FrameProcessor from scratch. The [PyTrickle reference](/v2/developers/resources/reference/pytrickle-reference) covers the full API surface.

## Related Pages

<CardGroup cols={2}>
  <Card title="PyTrickle Quickstart" icon="bolt" href="/v2/developers/build/ai-and-agents/realtime-ai/pytrickle/pytrickle-quickstart" arrow horizontal>
    Working example of a FrameProcessor running against a local test stream.
  </Card>

  <Card title="ComfyStream as BYOC" icon="box" href="/v2/developers/build/ai-and-agents/realtime-ai/comfystream/comfystream-as-byoc" arrow horizontal>
    How to register a trickle service with a Livepeer Orchestrator.
  </Card>
</CardGroup>
