> ## 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 data channels

> Read and write non-media data (text, JSON, binary) alongside video/audio in PyTrickle real-time sessions.

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

<CenteredContainer preset="readable90">
  <Tip>Data channels use the same TrickleSubscriber as media channels but carry text, JSON, or binary payloads. Subscribe to job.events\_url for pipeline data output.</Tip>
</CenteredContainer>

<CustomDivider />

PyTrickle data channels extend real-time sessions with non-media output. A pipeline can produce video on the media channel and structured data (transcriptions, labels, metrics) on a data channel simultaneously.

<CustomDivider />

## Reading data channel output

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
from pytrickle import TrickleSubscriber
import json

# Subscribe to the data output channel
data_sub = TrickleSubscriber(url=job.events_url)

async for segment in data_sub:
    payload = json.loads(segment.data.decode('utf-8'))

    if payload.get('type') == 'transcription':
        print(f"[{payload['timestamp']:.1f}s] {payload['text']}")
    elif payload.get('type') == 'detection':
        for box in payload['boxes']:
            print(f"  {box['label']}: ({box['x']}, {box['y']}, {box['w']}, {box['h']})")
```

The `events_url` is available on `LiveVideoToVideo` when the pipeline declares data output support. If the pipeline does not produce data output, `events_url` is None.

<CustomDivider />

## Writing data from a FrameProcessor

Inside a BYOC container, write data channel output through the `StreamServer`'s data publish interface:

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
from pytrickle import FrameProcessor, VideoFrame

class TranscriptionPipeline(FrameProcessor):
    async def process_frame(self, frame: VideoFrame) -> VideoFrame:
        # Process video as normal
        result = self.model(frame.data)

        # Write transcription to data channel
        await self.publish_data({
            'type': 'transcription',
            'text': result.text,
            'timestamp': frame.pts_time,
            'confidence': result.confidence,
        })

        return VideoFrame(data=result.video, pts=frame.pts)
```

Data channel segments are independent of media segments. They can be published at any rate and do not need to align with video frame boundaries.

<CustomDivider />

The [data channels concept](/v2/developers/guides/transport/trickle-protocol) page covers the trickle data channel architecture. The [FrameProcessor reference](/v2/developers/build/ai-and-agents/realtime-ai/pytrickle/frame-processor) covers the full processing API.
