> ## 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.

# PyTrickle reference

> PyTrickle is the Python framework for building BYOC containers on Livepeer. Covers FrameProcessor API, StreamServer, TrickleClient, and VideoFrame/AudioFrame interfaces.

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>;
};

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 CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

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>;
};

<CenteredContainer preset="readable90">
  <Tip>PyTrickle is the integration layer between your AI model and the Livepeer network. You implement one class (`FrameProcessor`). PyTrickle handles streaming, encoding, decoding, the REST API contract, and GPU memory management.</Tip>
</CenteredContainer>

<CustomDivider />

PyTrickle is a Python framework for real-time video and audio streaming over the trickle protocol. It is the canonical way to implement BYOC containers on Livepeer. PyTrickle reached production use in Phase 4 (January 2026) and is maintained at [https://github.com/livepeer/pytrickle](https://github.com/livepeer/pytrickle).

<Note>
  PyTrickle is early-stage software (3 stars, 20 open issues as of April 2026). The API is stable enough for production use (Embody SPE and Streamplace use it) but the project is under active development. Check the GitHub repository for the latest API changes before building against it.
</Note>

<CustomDivider />

## Installation

```bash icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
pip install git+https://github.com/livepeer/pytrickle.git
```

**Requirements:**

* Python 3.8 or later
* PyTorch (for GPU tensor support)
* FFmpeg (for encoding/decoding)
* NVIDIA GPU recommended for inference workloads

<CustomDivider />

## FrameProcessor

`FrameProcessor` is the base class you subclass to implement your AI model. Override the async methods for the workload types your container handles.

```python icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
from pytrickle import FrameProcessor
from pytrickle.frames import VideoFrame, AudioFrame
from typing import Optional, List
import torch

class MyProcessor(FrameProcessor):

    async def initialize(self):
        """Called once on startup. Load your model here."""
        self.model = load_model()  # your model loading logic

    async def process_video_async(self, frame: VideoFrame) -> Optional[VideoFrame]:
        """
        Called once per video frame.

        Args:
            frame: VideoFrame containing a PyTorch tensor (H, W, C) and metadata

        Returns:
            Processed VideoFrame, or None to drop the frame
        """
        tensor = frame.tensor  # torch.Tensor, shape (H, W, C), dtype uint8

        with torch.no_grad():
            processed = self.model(tensor)

        return frame.replace_tensor(processed)

    async def process_audio_async(self, frame: AudioFrame) -> Optional[List[AudioFrame]]:
        """
        Called once per audio frame.

        Returns:
            List of AudioFrames to output, or None to drop
        """
        return [frame]  # pass through

    def update_params(self, params: dict):
        """
        Called when the gateway or client sends updated parameters mid-stream.
        Implement to support dynamic model configuration.
        """
        pass
```

<CustomDivider />

## VideoFrame

`VideoFrame` wraps a decoded video frame as a PyTorch tensor with metadata.

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

  <tbody>
    <TableRow>
      <TableCell>`tensor`</TableCell>
      <TableCell>`torch.Tensor`</TableCell>
      <TableCell>Frame pixel data, shape `(H, W, C)`, dtype `uint8`, values 0-255, RGB channel order</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`pts`</TableCell>
      <TableCell>`float`</TableCell>
      <TableCell>Presentation timestamp in seconds</TableCell>
    </TableRow>

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

    <TableRow>
      <TableCell>`height`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Frame height in pixels</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

**Key methods:**

```python icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
# Replace the tensor while keeping metadata
new_frame = frame.replace_tensor(processed_tensor)

# Move tensor to GPU
frame_gpu = frame.to('cuda')

# Move tensor to CPU
frame_cpu = frame.to('cpu')
```

<CustomDivider />

## AudioFrame

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

  <tbody>
    <TableRow>
      <TableCell>`tensor`</TableCell>
      <TableCell>`torch.Tensor`</TableCell>
      <TableCell>Audio samples, shape `(channels, samples)`, dtype `float32`, values -1.0 to 1.0</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`sample_rate`</TableCell>
      <TableCell>`int`</TableCell>
      <TableCell>Audio sample rate in Hz (e.g. 44100, 48000)</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>`pts`</TableCell>
      <TableCell>`float`</TableCell>
      <TableCell>Presentation timestamp in seconds</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

<CustomDivider />

## StreamServer

`StreamServer` wraps your `FrameProcessor` with the REST API contract required by the Livepeer gateway. You do not implement the endpoints manually.

```python icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
from pytrickle import StreamServer

server = StreamServer(
    frame_processor=MyProcessor(),
    port=8000,
    capability_name='live-video-to-video',  # pipeline type identifier
    host='0.0.0.0',
    target_fps=24,        # output frame rate (1-60)
    max_queue_size=30,    # frames to buffer before dropping
)

# Run the server (blocks until stopped)
import asyncio
asyncio.run(server.run_forever())
```

`StreamServer` automatically exposes four endpoints on the configured port:

| Endpoint             | Method | Description                                                        |
| -------------------- | ------ | ------------------------------------------------------------------ |
| `/api/stream/start`  | POST   | Start a session; receives `subscribe_url`, `publish_url`, `params` |
| `/api/stream/params` | POST   | Update parameters mid-stream                                       |
| `/api/stream/status` | GET    | Returns current session status                                     |
| `/api/stream/stop`   | POST   | Stop the current session                                           |

<CustomDivider />

## TrickleClient

For direct trickle protocol connections without the REST API layer:

```python icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
from pytrickle import TrickleClient

async def stream_frames():
    async with TrickleClient(
        subscribe_url='http://trickle-server/input-stream',
        publish_url='http://trickle-server/output-stream',
    ) as client:
        async for frame in client.video_frames():
            processed = await my_model(frame)
            await client.publish_video_frame(processed)
```

<CustomDivider />

## Built-in monitoring

`FrameProcessor` exposes metrics via `get_metrics()`:

```python icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
metrics = processor.get_metrics()
# {
#   'fps': 24.1,
#   'latency_ms': 42.3,
#   'gpu_memory_mb': 4096,
#   'frames_processed': 10234,
#   'frames_dropped': 3,
#   'error_count': 0,
# }
```

These metrics are also exposed on the `/api/stream/status` endpoint when running via `StreamServer`.

<CustomDivider />

## Related pages

<CardGroup cols={2}>
  <Card title="BYOC Guide" icon="box" href="/v2/developers/build/compute/byoc/overview">
    Full BYOC walkthrough: implementing FrameProcessor, building a container, and deploying to the network.
  </Card>

  <Card title="Build with ComfyStream" icon="camera" href="/v2/developers/build/ai-and-agents/realtime-ai/comfystream/overview">
    ComfyStream uses PyTrickle internally -- use it if your model is a ComfyUI workflow.
  </Card>
</CardGroup>
