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

# Transcoding Direct Quickstart

> Run a Livepeer gateway and orchestrator locally in fifteen minutes. RTMP in, HLS out, no ETH, no GPU.

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 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>Run the Gateway and Orchestrator locally. Publish RTMP, pull HLS, fifteen minutes. CPU only. No ETH, no GPU, no network registration.</Tip>
</CenteredContainer>

<CustomDivider />

By the end of this quickstart you'll have a Livepeer Gateway and Orchestrator running on one machine, transcoded a test stream end-to-end, and verified an HLS playlist with three renditions (240p, 360p, 720p). The path uses `-network offchain` mode, which exercises the full job lifecycle without any on-chain dependencies. Once this works, you understand the network-direct video path; on-chain production setup uses the same architecture with payment envelopes attached.

Personas 2 and 4 both start here. Persona 2 (video platform builders) takes this forward into VOD, webhooks, and player integration. Persona 4 (live-video-first builders) tunes for sub-three-second latency. Operators running Gateways in production should follow the <LinkArrow href="" label="" newline={false} /> in the Gateways tab.

<CustomDivider />

## Required Tools

Three things on one Linux amd64 machine:

* `go-livepeer` binary or Docker
* `ffmpeg` for the test stream
* Three free terminals

No Arbitrum wallet, no API keys, no GPU.

<Tabs>
  <Tab title="Binary">
    Download the latest `go-livepeer` release for Linux amd64:

    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    # Check https://github.com/livepeer/go-livepeer/releases for the latest version
    curl -LO https://github.com/livepeer/go-livepeer/releases/download/v0.8.10/livepeer-linux-amd64.tar.gz
    tar -xzf livepeer-linux-amd64.tar.gz
    chmod +x livepeer livepeer_cli
    ./livepeer -version
    ```
  </Tab>

  <Tab title="Docker">
    Pull the image:

    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    docker pull livepeer/go-livepeer:master
    ```

    Every `./livepeer` command below has a Docker equivalent: replace `./livepeer` with `docker run --rm -v ~/.lpData:/root/.lpData --network host livepeer/go-livepeer:master`. The flags are identical.
  </Tab>
</Tabs>

Install `ffmpeg`:

```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
# Ubuntu / Debian
sudo apt install ffmpeg

# macOS
brew install ffmpeg
```

<Note>
  The `-gateway` flag replaces the older `-broadcaster` flag. If you see `-broadcaster` in community guides, substitute `-gateway`. The old flag still works in some builds and is deprecated.
</Note>

<CustomDivider />

## Orchestrator Setup

<Steps>
  <Step title="Run go-livepeer in orchestrator mode">
    First terminal:

    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    ./livepeer \
      -orchestrator \
      -transcoder \
      -network offchain \
      -serviceAddr 127.0.0.1:8936 \
      -cliAddr 127.0.0.1:7936 \
      -datadir ~/.lpData-orch
    ```

    | Flag                          | Effect                                                   |
    | ----------------------------- | -------------------------------------------------------- |
    | `-orchestrator`               | Accept jobs from Gateways                                |
    | `-transcoder`                 | Attach a local transcoder; CPU transcoding via `libx264` |
    | `-network offchain`           | Local job routing, no Ethereum dependency                |
    | `-serviceAddr 127.0.0.1:8936` | gRPC address the Gateway connects to                     |
    | `-cliAddr 127.0.0.1:7936`     | CLI management port                                      |
    | `-datadir ~/.lpData-orch`     | Data directory, kept separate from the Gateway           |
  </Step>

  <Step title="Confirm it's listening">
    Expected log lines:

    ```
    I0000 Starting Livepeer node
    I0000 Orchestrator registered with service address 127.0.0.1:8936
    I0000 Listening for jobs
    ```

    Leave this terminal running.
  </Step>
</Steps>

<Note>
  `-orchestrator` and `-transcoder` together run in one process. In production these are typically split: Orchestrator on a small VPS handling routing and payments, transcoder on a GPU machine. Tutorial 3 in the Gateways tab covers the split.
</Note>

<CustomDivider />

## Gateway Setup

<Steps>
  <Step title="Run go-livepeer in gateway mode">
    Second terminal:

    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    ./livepeer \
      -gateway \
      -network offchain \
      -orchAddr 127.0.0.1:8936 \
      -rtmpAddr 127.0.0.1:1935 \
      -httpAddr 127.0.0.1:8935 \
      -cliAddr 127.0.0.1:5935 \
      -datadir ~/.lpData-gw
    ```

    | Flag                       | Effect                                     |
    | -------------------------- | ------------------------------------------ |
    | `-gateway`                 | Accept RTMP streams from publishers        |
    | `-network offchain`        | Matches the Orchestrator                   |
    | `-orchAddr 127.0.0.1:8936` | Point at the local Orchestrator            |
    | `-rtmpAddr 127.0.0.1:1935` | RTMP ingest port                           |
    | `-httpAddr 127.0.0.1:8935` | HTTP port the HLS playlist is pulled from  |
    | `-cliAddr 127.0.0.1:5935`  | CLI management port                        |
    | `-datadir ~/.lpData-gw`    | Data directory, separate from Orchestrator |
  </Step>

  <Step title="Confirm it's listening">
    Expected log lines:

    ```
    I0000 Starting Livepeer node
    I0000 RTMP Server listening on rtmp://127.0.0.1:1935
    I0000 HTTP Server listening on http://127.0.0.1:8935
    I0000 Registered orchestrator: 127.0.0.1:8936
    ```
  </Step>
</Steps>

<Note>
  `-orchAddr` accepts a comma-separated list. Off-chain mode requires an explicit list because there's no on-chain registry. On-chain mode removes this flag; the registry supplies Orchestrators automatically.
</Note>

<CustomDivider />

## Stream Publishing

Third terminal. Pick one option below.

<Tabs>
  <Tab title="Test pattern">
    No source file needed; `ffmpeg` generates one:

    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    ffmpeg \
      -f lavfi \
      -i testsrc=duration=60:size=1280x720:rate=30 \
      -c:v libx264 \
      -b:v 2000k \
      -f flv \
      rtmp://127.0.0.1:1935/live/test_stream
    ```
  </Tab>

  <Tab title="Sample file">
    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    ffmpeg \
      -re \
      -i /path/to/video.mp4 \
      -c:v libx264 \
      -b:v 2000k \
      -f flv \
      rtmp://127.0.0.1:1935/live/test_stream
    ```

    The `-re` flag reads input at native frame rate, simulating a live stream. Skip it and `ffmpeg` pushes the file as fast as possible, which overwhelms the Orchestrator buffer.
  </Tab>
</Tabs>

The stream key (`test_stream`) is arbitrary in off-chain mode. The Gateway accepts any key.

Expected `ffmpeg` output:

```
Output #0, flv, to 'rtmp://127.0.0.1:1935/live/test_stream':
  Stream #0:0: Video: h264, yuv420p, 1280x720, 2000 kb/s, 30 fps
frame=   30 fps= 30 q=23.0 size=     320kB time=00:00:01.00 bitrate=2621.4kbits/s
```

<CustomDivider />

## Output Verification

<Steps>
  <Step title="Pull the HLS playlist">
    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    curl http://127.0.0.1:8935/stream/test_stream.m3u8
    ```

    Expected response:

    ```
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=400000,RESOLUTION=426x240
    test_stream_240p0.m3u8
    #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=800000,RESOLUTION=640x360
    test_stream_360p0.m3u8
    #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=2000000,RESOLUTION=1280x720
    test_stream_720p0.m3u8
    ```

    Three renditions confirm the Orchestrator successfully transcoded the source. 240p, 360p, and 720p are the default profile.
  </Step>

  <Step title="Play it (optional)">
    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    ffplay http://127.0.0.1:8935/stream/test_stream.m3u8
    ```

    Or paste the URL into VLC, OBS, or any HLS-capable player.
  </Step>

  <Step title="Watch the gateway logs">
    The Gateway terminal shows the job flow:

    ```
    I0000 Received RTMP stream: test_stream
    I0000 Starting session with orchestrator 127.0.0.1:8936
    I0000 Segment 0 sent to orchestrator
    I0000 Segment 0 transcoded, 3 renditions received
    I0000 Segment 1 sent to orchestrator
    ```

    Every two-second chunk goes to the Orchestrator, comes back transcoded, and joins the HLS playlist.
  </Step>
</Steps>

<CustomDivider />

## Job Lifecycle

```
ffmpeg
  +-> RTMP publish to Gateway port 1935
        +-> Gateway creates a session, selects Orchestrator (127.0.0.1:8936)
              +-> Gateway splits stream into ~2s segments
                    +-> Each segment sent to Orchestrator via gRPC
                          +-> Orchestrator transcodes to 240p / 360p / 720p (CPU)
                                +-> Transcoded segments returned to Gateway
                                      +-> Gateway assembles HLS playlist + segments
                                            +-> curl / ffplay pulls the HLS
```

The full job lifecycle ran. The only piece off-chain mode skips is the payment envelope; segment routing, transcoding, and delivery are identical to a production Gateway.

Four ideas underpin the completed run.

**Session.** The Gateway creates a session per stream key. The session persists until the publisher disconnects or the Gateway shuts down.

**Segment.** A two-second chunk of video. The Gateway sends segments to the Orchestrator independently and in order.

**Off-chain routing.** Without `-network offchain`, the Gateway needs an Arbitrum RPC and a funded PM deposit. Off-chain mode keeps the routing logic intact and leaves the payment envelope empty.

**Transcoding profiles.** The Orchestrator applied the default 240p, 360p, and 720p at 30fps. Customise via `transcodingOptions.json` passed to the Gateway.

<CustomDivider />

## Common Errors

<AccordionGroup>
  <Accordion title="No orchestrators found or connection refused on 8936">
    Start the Orchestrator before the Gateway. Confirm the `Listening for jobs` log line in terminal 1, then start the Gateway in terminal 2.
  </Accordion>

  <Accordion title="ffmpeg exits with Connection refused">
    The Gateway's RTMP server isn't running, or the port is wrong. Confirm the Gateway terminal shows `RTMP Server listening on rtmp://127.0.0.1:1935`. If `-rtmpAddr` was changed, update the `ffmpeg` command to match.
  </Accordion>

  <Accordion title="HLS playlist returns 404">
    Wait three to five seconds after `ffmpeg` starts. The Gateway only creates the playlist once it has received and forwarded at least one segment. If it persists, check the stream key matches between `ffmpeg` and `curl`.
  </Accordion>

  <Accordion title="Playlist exists but has zero segments">
    The Gateway received the stream and the Orchestrator failed to transcode. Check the Orchestrator terminal for errors. Common cause: `libx264` missing from the `go-livepeer` build. Switch to the Docker path.
  </Accordion>

  <Accordion title="Port conflict: address already in use">
    Check what's holding the port:

    ```bash theme={"theme":{"light":"github-light","dark":"dark-plus"}}
    lsof -i :1935   # RTMP
    lsof -i :8935   # Gateway HTTP
    lsof -i :8936   # Orchestrator gRPC
    ```

    Kill the conflicting process, or change `-rtmpAddr`, `-httpAddr`, and `-serviceAddr` to free ports.
  </Accordion>
</AccordionGroup>

<CustomDivider />

## Next Steps

<CardGroup cols={2}>
  <Card title="Ingest and Playback" icon="play" href="/v2/developers/build/video/ingest-and-playback">
    RTMP, WHIP, and WebRTC ingest; HLS, LL-HLS, and DASH playback.
  </Card>

  <Card title="Codec Support" icon="layer-group" href="/v2/developers/build/video/codec-support">
    H.264, H.265, AV1, transcoding profiles, custom `transcodingOptions.json`.
  </Card>

  <Card title="VOD and Recording" icon="bookmark" href="/v2/developers/build/video/vod-and-recording">
    Stream recording, archive playback, asset management.
  </Card>

  <Card title="Low-Latency Live Streaming" icon="bolt" href="/v2/developers/build/tutorials/low-latency-live-streaming-app">
    Sub-3s glass-to-glass with WebRTC ingest and LL-HLS.
  </Card>
</CardGroup>

For production Gateway operation, including on-chain mode and GPU transcoding, follow the <LinkArrow href="" label="" newline={false} />.
